最近在Cassandra的使用過程中, 發(fā)現(xiàn)Cassandra的查詢操作異常緩慢(花費了700~900ms), 經(jīng)過排查后發(fā)現(xiàn)是使用了Secondary Index的原因.本文整理了primary key 和 Secondary Index在Cassandra中的存儲方式, 也解釋了為什么使用Secondary Index查詢會非常緩慢.
Primary Key
Cassandra的Primary Key由Partition Key和Clustering Key兩部分組成. 先看只有Partition Key的情況.
例如, 如下建表語句:
CREATE TABLE sample (
id text,
name text,
PRIMARY KEY (id)
);
該表的primary key就是其partition key. 在Cassandra中, 每一個partition key對應(yīng)著一行, 所有的column都存儲在該行中.如下圖所示:
+-------------+----------+-----------+------------+
| partition | column | column | column |
| key 1 | 1 | 2 | 3 |
+-------------+----------+-----------+------------+
| partition | column | column | column |
| key 2 | 1 | 2 | 3 |
+-------------+----------+-----------+------------+
例如, 在sample中插入若干記錄, 查詢該表:
SELECT * FROM sample;
得到如下結(jié)果:
id | name
-----+-------
id3 | name3
id1 | name1
id2 | name2
然后, 再用Cassandra-Cli去查詢, 得到如下結(jié)果:
RowKey: id3
=> (name=, value=, timestamp=1442491586006000)
=> (name=name, value=6e616d6533, timestamp=1442491586006000)
-------------------
RowKey: id1
=> (name=, value=, timestamp=1442491594600000)
=> (name=name, value=6e616d6531, timestamp=1442491594600000)
-------------------
RowKey: id2
=> (name=, value=, timestamp=1442491578108000)
=> (name=name, value=6e616d6532, timestamp=1442491578108000)
3 Rows Returned.
可以看到, 每一個partition key對應(yīng)著一個RowKey, 所有的column都存放在這一行上.
Clustering Key
Cassandra中所有的數(shù)據(jù)都只能根據(jù)Primary Key中的字段來排序, 因此, 如果想根據(jù)某個column來排序, 必須將改column加到Primary key中, 如 primary key (id, c1, c2 ,c3), 其中id時partition key, c1, c2 ,c3是Clustering Key.(如果想用id和c1作為partition key, 只需添加括號: primary key ((id, c1), c2 ,c3)).
創(chuàng)建表:
CREATE TABLE sample3 (
id text,
gmt_create bigint,
name text,
score text,
PRIMARY KEY ((id), gmt_create)
);
在該表中插入若干紀(jì)錄后, 使用select *查詢該表, 得到如下紀(jì)錄:
id | gmt_create | name | score
-----+------------+-------+--------
id3 | 1925 | name3 | score3
id3 | 1926 | name4 | score4
id1 | 1923 | name1 | score1
id2 | 1924 | name2 | score2
可以看到cql表中有四條記錄, 其中id=id3有兩條記錄, 那么是否意味著cassandra中有四行來保存這些記錄呢?用Cssandra-Cli來查詢:
RowKey: id3
=> (name=1925:, value=, timestamp=1442494070376000)
=> (name=1925:name, value=6e616d6533, timestamp=1442494070376000)
=> (name=1925:score, value=73636f726533, timestamp=1442494070376000)
=> (name=1926:, value=, timestamp=1442494335821000)
=> (name=1926:name, value=6e616d6534, timestamp=1442494335821000)
=> (name=1926:score, value=73636f726534, timestamp=1442494335821000)
-------------------
RowKey: id1
=> (name=1923:, value=, timestamp=1442494039343000)
=> (name=1923:name, value=6e616d6531, timestamp=1442494039343000)
=> (name=1923:score, value=73636f726531, timestamp=1442494039343000)
-------------------
RowKey: id2
=> (name=1924:, value=, timestamp=1442494054817000)
=> (name=1924:name, value=6e616d6532, timestamp=1442494054817000)
=> (name=1924:score, value=73636f726532, timestamp=1442494054817000)
3 Rows Returned.
可以看到, cassandra中只用3行來保存數(shù)據(jù), 其中id=id3到兩條記錄被保存在同一行上. 事實上, 當(dāng)存在clustering key時, 當(dāng)partition key相同而clustering key不同時, 其紀(jì)錄是保存在同一行上的, 而每一個column的name責(zé)由clustering的值 + ":" + 原來的column name構(gòu)成(如sample3中, column score的name = 1923:score), sample3在cassandra中的存儲結(jié)構(gòu)如下圖所示:
+-------------+----------------+-------------------+----------------------+
| | name = 1925: | name=1925:name | name=1925:score |
| | value = | value=6e616d6533 | value=73636f726533 |
| | timestamp | timestamp | timestamp |
| id 3 +----------------+-------------------+----------------------+
| | name = 1926: | name=1926:name | name=1926:score |
| | value= | value=6e616d6534 | value=73636f726534 |
| | timestamp | timestamp | timestamp |
+-------------+----------------+-------------------+----------------------+
| | name = 1923: | name=1923:name | name=1923:score |
| id 1 | value = | value=6e616d6531 | value=73636f726531 |
| | timestamp | timestamp | timestamp |
+-------------+----------------+-------------------+----------------------+
| | name = 1924: | name=1924:name | name=1924:score |
| id 2 | value = | value=6e616d6532 | value=73636f726532 |
| | timestamp | timestamp | timestamp |
+-------------+----------------+-------------------+----------------------+
Secondary Index
從cassandra 0.7開始支持Secondary Index. 例如, 在sample3上創(chuàng)建Secondary Index:
create index idx_score on sample3(score);
而Secondary Index的原理類似于創(chuàng)建一張新的表, 該表的primary key就是索引的列.因此, 若原sample表中有數(shù)據(jù):
{
"id1": {
"name" : "jim"
"score": "60"
},
"id2": {
"name" : "kate"
"score": "60"
}
}
索引表的內(nèi)部結(jié)構(gòu)類似于如下:
{
"60": {
"id1": data
"id2": data
}
}
與原表的不同之處在于: 原表的數(shù)據(jù)按照partition key分發(fā)到不同的節(jié)點進行存儲, 當(dāng)cassandra進行讀取時, 任意一個節(jié)點返回數(shù)據(jù)即可(取決于讀取時的一致性要求), 而secondary index 表則會存儲自己節(jié)點上的數(shù)據(jù), 因此, 按照secondary index進行讀取時, 需要等待每一個節(jié)點讀取完畢, 這個過程相比于primary key讀取的過程會比較緩慢.(當(dāng)并發(fā)非常高時, 就會造成大量的random I/O, 導(dǎo)致查詢非常緩慢.)
因此, 當(dāng)secondary index的column是類似于'國家'這樣有許多重復(fù)紀(jì)錄的字段時, 讀取時相當(dāng)高效的(因為n個節(jié)點都進行一個磁盤搜素, 時間復(fù)雜度O(n), 然而查詢到的紀(jì)錄數(shù)有可能是x條, 大多數(shù)情況下x>n, 平均下來時非常高效的), 但是當(dāng)secondary index的column是類似于email這樣幾乎每個人唯一的字段時, 是非常低效的, 因為按照email進行查詢是, 每一次查詢幾乎只能命中一條紀(jì)錄, 而secondary index搜索的時間復(fù)雜度則是O(n)。