cassandra数据作为一个非常类似于关系型数据库的nosql,其在数据表的设计上于关系型数据库有着一些不同的地方。本文尽个人所理解的内容做简单的剖析。
Primary Key
关系型数据的Primary Key主要是作为数据的唯一性标识用的,对具体业务并没有太大的帮助,比如大量使用的自增id作为唯一主键,而这个id实际上并不是业务的数据,只是作为一个技术上保证唯一性标识的手段而已。
cassandra的Primary key也要保证数据的唯一性标识,但是其更大的作用是服务于查询,也就是说cassandra的主键更多是面向于业务来设计的,值得注意的是cassandra的表的primary key并不能动态改变,所以很多设计者犯得一个错误就是要查询的字段并没有在primary key中导致只能开启全表扫描查询,这是一种不可取的办法,所以在设计之初就应该避免这种情况。
复合主键
cassandra主键包含了两个部分,partition key和cluster key。在说明这两个key之前需要简单的介绍一下cassandra的一些基本特性,在关系数据库的结构中当数据量增多的时候回导致单个表的数据查询性能下降,设计者也缺少好的策略去自动分割这些大量的数据。
cassandra则是采用partition来对于数据进行划分保存,在良好设计的情况下cassandra就会按照定义的策略自动对数据进行分区,而partition key就是cassandra分区的依据,通过一个一致性hash函数计算这个partition key的token进而直接找到其所在的分区。所以需要注意的是数据一旦写入分区就不能update partition key了,当你可以通过删除然后再添加来实现,同时这样附带了一个隐藏的问题,如果你的partition key有两个字段,当你在查询的时候你要么不指定分区,要么就必须明确指定分区。
表设计
在这里以datastax的一个demo来展开说明。
race_year int, #比赛的年份
race_name text, #比赛的名称
cyclist_name text, #选手名称
rank int, #名次
如果是在mysql中这是一个非常简单表,直接添加一个自增的主键id,然后把这四个字段加进去就可以了。
在cassandra中这个表的设计则变得有点复杂了,下面我们将参照不同的设计来分析所能应付的不同的业务。
在这之前我们要首先确保数据的唯一性,在某一年的某一场比赛中的第几名应该是可以确定到唯一的一条数据的。
假如表数据如下
CREATETABLE
rank_by_year_and_name (
race_year
int
,
race_name
text
,
cyclist_name
text
,
rank
int
,
PRIMARYKEY
((race_year),race_name,rank));
race_year作为唯一的partition key,race_name和rank作为cluster key,我们可以说这个表将会按照每一个年份自动切分数据,而每一个分区内都会按照比赛名和名称做排序。
接下来我们做一些简单的练习。
INSERT INTO rank_by_year_and_name(race_year, race_name, rank, cyclist_name)VALUES (2018, '青海自行车赛', 2,'李明');
INSERT INTO rank_by_year_and_name(race_year, race_name, rank, cyclist_name)VALUES (2018, '青海自行车赛', 1,'李明');
INSERT INTO rank_by_year_and_name(race_year, race_name, rank, cyclist_name)VALUES (2018, '青海自行车赛', 2,'李明');
INSERT INTO rank_by_year_and_name(race_year, race_name, rank, cyclist_name)VALUES (2018, '青海自行车赛', 1,'李明');
INSERT INTO rank_by_year_and_name(race_year, race_name, rank, cyclist_name)VALUES (2019, '青海自行车赛', 1,'李明');
UPDATE rank_by_year_and_name SETcyclist_name = '博尔特'WHERErace_year = 223 ANDrace_name = '环法自行车赛' AND rank = 1 IF EXISTS;
select * from rank_by_year_and_name;
select * from rank_by_year_and_name where race_year=2018;
select * from rank_by_year_and_name where race_name='青海自行车赛';
select * from rank_by_year_and_name where rank=1;
select * from rank_by_year_and_name where race_name='青海自行车赛' and rank=1 and race_year=2018;
select * from rank_by_year_and_name where race_name='青海自行车赛' and rank=1 and race_year>=2018;
select * from rank_by_year_and_name where race_year=2018 and race_name='青海自行车赛' and rank>0;
select * from rank_by_year_and_name where race_year=2018 and rank>0;
select * from rank_by_year_and_name where race_name='青海自行车赛' and race_year in (2018,2019);
下面选择几个特别的查询做特殊说明
- 重复插入不提示
第二句INSERT INTO rank_by_year_and_name(race_year, race_name, rank, cyclist_name)VALUES (2018, '青海自行车赛', 2,'李明');
可以看到当数据重复的时候cassandra不做任何提示.
- upsert当where条件不存在直接插入数据.
UPDATE rank_by_year_and_name SETcyclist_name = '博尔特'WHERErace_year = 222 ANDrace_name = '环法自行车赛' ANDrank = 1;
使用if exists可以防止update的时候变成插入
UPDATE rank_by_year_and_name SETcyclist_name = '博尔特'WHERErace_year = 223 ANDrace_name = '环法自行车赛' ANDrank = 1 IF EXISTS;
- 不指定partition key无法查询
select * from rank_by_year_and_name where race_name='青海自行车赛';
select * from rank_by_year_and_name where rank=1;
这样就意味着说我们查询只能在指定的partition key上操作,某种程度上这就是直接避免了关系型数据库中的全表扫描。
- Partition key的范围查询
select * from rank_by_year_and_name where race_name='青海自行车赛' and rank=1 and race_year>=2018;
select * from rank_by_year_and_name where race_name='青海自行车赛' and race_year in (2018,2019);
如果需要对多个分区进行操作,cassandra partition key不支持 >=这操作,尽管是int类型,而只能使用in查询。原因很简单,partition key是用于一致性hash计算得到token从而获取分区的,而hash是分散的所以不会随着partition key有大于小于的关系,但是in当然是可以了。
- Cluster key的顺序
select * from rank_by_year_and_name where race_year=2018 and rank>0;
select * from rank_by_year_and_name where race_year=2018 and race_name='青海自行车赛' ;
第二句可以执行,第一句错误,原因在cluster key是排序的,是有层级的,所以只能一层层的搜索。
总结
Cassandra在查询的时候尽可能避免不明确的查询,查询必须制定partition key,如果想要在多个partition key查询,就要使用in来匹配多个,对于cluster key,层级关系也是很明确。Cassandra这样设计目的在于减少大面积的不确定的查询,在tb甚至更多的数据中做模糊查询可能会到来非常恶劣的效果。
非常差的表设计
CREATE TABLE rank_by_year_and_name ( race_year int, race_name text, cyclist_name text, rank int, PRIMARY KEY ((race_year)));
INSERT INTO rank_by_year_and_name(race_year, race_name, rank, cyclist_name)VALUES (2018, '青海自行车赛', 2,'李明');INSERT INTO rank_by_year_and_name(race_year, race_name, rank, cyclist_name)VALUES (2018, '青海自行车赛', 1,'李明');
上面的表问题在无法保证数据的唯一,数据会被覆盖,所以存在明显缺陷。
CREATE TABLE rank_by_year_and_name ( race_year int, race_name text, cyclist_name text, rank int, id int,PRIMARY KEY ((id)));
这个表几乎是直接挪用关系型数据库的表。
但是结果是由于要查询的字段都不在primary key,导致只能按照主键id查询,几乎等同于废表一张。
接下来可能你会天真说,那好我把所有的字段都放到primary key里。
CREATE TABLE rank_by_year_and_name ( race_year int, race_name text, cyclist_name text, rank int, id int,PRIMARY KEY ((id)),race_year,race_name,cyclist_name,rank);
但是别着急当你需要查询的需要制定一个partition key,而这个上面的表是一个id,当你按照某一个年份查询的时候你只能使用in对所有的partition key做扫描。这很明显又是一个非常差的选择。
而且通过上面两个反面的例子,大家应该更能理解cassandra和关系型数据库在表设计上的差异,虽然不是十万百千里那么用,五万四千里还是有的,如果你直接套路关系型数据库的设计思路,那么得到的结果就是比用关系型数据库还要差。