本文是英文贴的翻译,能够直接查看英文原文。
选择正确的数据模型正是使用Cassandra最困难的一部分。若是诸位有相关开发经验,就会发现CQL虽然看起来很熟悉,可是使用起来却彻底不一样。
本文将说明设计Cassandra schema的基本准则。在项目中遵照它们不只有立刻可见的益处,并且能够保证在从此扩展节点时Cassandra的性能保持线性增加。node
有关系型数据库经验的开发者常常会将以前的经验带入Cassandra。为了不在这些并不重要的事(对Cassandra而言)上耗费时间,先指出一些无效准则:sql
虽然在Cassandra中写数据并不彻底免费,可是很是很是廉价。Cassandra天生就是为了高速率的写而优化设计的。在Cassandra中几乎老是应该使用冗余的写来提升读取效率。请记住,读比写昂贵不少而且更难以调优。数据库
别惧怕,Cassandra所作的事情就是复制数据。磁盘空间一般是最廉价的资源(比起CPU、memory、磁盘IO、网络等等),整个Cassandra正是架构在这个事实之上。你经常须要复制数据来提高读的效率。
另外Cassandra没有JOINs语句(固然,在分布式设计中你不会真的想使用它!)网络
你的数据模型应遵循以下两个高层级目标:
1. 让数据在集群(cluster)中尽量的均匀分布
2. 从尽量少的分区(partition)中查找数据
上面的两条是最最重要的准则。你应该首选要学会在项目中如何作好这两点。架构
通常来讲,但愿集群中的各个节点数据量尽量相等。在Cassandra中,数据经过分区键(partition key)来分区存储。分区键即主键(PRIMARY KEY)的第一个元素。
分区键决定了数据存放在哪一个node上。因此数据可以均匀分布的关键就在于:选择一个好的主键。分布式
分区包含的是有着相同分区键的数据行。当你发起一个查询时,请从尽量少的分区中读取数据。
这条准则的重要性在于各个分区可能位于不一样的节点。协调者(coordinator)将向各个节点发起不一样命令。这将带来额外的负荷与延迟。
即便所要查询的分区都位于一个节点,跨分区的查询也要昂贵许多。post
既然应该从尽量少的分区中查询数据,那为什么不将全部数据都存到一个分区中?由于这违反了第一条准则。
能够看到这两条准则会相互矛盾,因此实际项目中经常须要权衡。性能
实现最小化分区查找的方法就是使数据模型彻底切合你的查询。不要从关系、对象等等来倒推模型,从查询入手!
第一步. 肯定真正须要支持什么查询
试着决定你真正须要支持的查询。尽可能考虑那些甚至一开始没想到的需求。例如:
* 按照属性分组
* 按照属性排序
* 根据条件过滤
* 在结果集中去重
为了追求效率,查询条件的变化常常致使数据模型的变化。
第二步. 尽量的让你的查询只读取一个分区
实践中这经常意味着你须要为每个查询单独创建一张表。换种说法,每张表意味着为你不一样的查询事先准备好了答案。若是须要不一样的答案,那就创建不一样的表。这就是对读的优化。
永远记住,数据复制是常规操做。你的许多表可能包含重复的数据。学习
用一些小例子来看看上面这些准则的最佳实践。优化
最顶层的需求是“咱们有一些用户,而且须要查询他们”。设计步骤以下:
1. 细化查询
好比须要按用户名或者email地址查询。每一种查询方式都须要获得用户的全部详细信息。
2. 按每次只查一个分区的目标来建表
既然两种查询都须要获得用户的全信息,那就建两张表:
CREATE TABLE users_by_username ( username text PRIMARY KEY, email text, age int ) CREATE TABLE users_by_email ( email text PRIMARY KEY, username text, age int )
回顾一下上面提到的两条准则:
数据均匀分布?数据将按照不一样的用户进行分区,知足。
最小化分区读?每一个查询只读一个分区,知足。
如今假设咱们须要按照上面提到的无效目标进行优化,数据模型将会变成这样:
CREATE TABLE users ( id uuid PRIMARY KEY, username text, email text, age int ) CREATE TABLE users_by_username ( username text PRIMARY KEY, id uuid ) CREATE TABLE users_by_email ( email text PRIMARY KEY, id uuid )
这种数据模型确实是均匀分布的,可是它的缺点是须要查询两个分区。:(
如今来改变一下示例一中的顶层需求:用户是按组进行划分的,咱们须要按照组来查找用户
1. 细化查询条件
咱们须要查找特定组中的全部用户信息,不关心排序。
2. 按每次只查一个分区的目标来建表
如何将一个组放到一个分区中 ? 这须要用到复合主键:
CREATE TABLE groups ( groupname text, username text, email text, age int, PRIMARY KEY (groupname, username) )
这里的主键包含组名和用户名:组名做为分区键,用户名做为聚合键(clustering key)。这样一个组就彻底属于同一个分区。
在特定组中,数据以用户名为序。查询语句很简单:
SELECT * FROM groups WHERE groupname = ?
很容易看出这种设计知足第二条准则。可是并非那么知足第一条。若是咱们有着大量的小组数量,每一个里面有着少许百用户,这时数据才会均匀分布。
可是若是仅仅有着一个组,里面包含着大量用户,那么这个组所在的节点(或该节点的复制组)将会承担全部负载。
若是须要将数据更加均匀的分布,咱们须要使用一些策略。首先须要在主键中增长一列,使用联合分区键:
CREATE TABLE groups ( groupname text, username text, email text, age int, hash_prefix int, PRIMARY KEY ((groupname, hash_prefix), username) )
新增长的hash_prefix是用户名的hash的前缀。好比,能够是hash对4取模的第一个字节。hash_prefix和groupname组成了联合分区键。这致使了一个组的全部用户会分布在四个分区中。
咱们的数据变得更均匀了,可是如今一个查询将要跨越四个分区。这就上文所提到矛盾的例子。你须要从你的具体使用环境中找到平衡。
若是查询十分频繁,并且你的每一个分组不是特别大,将对4取模改成对2取模多是一个好的选择。
反过来讲,若是查询不频繁,而且要查询的分组会特别大,将4改成10将会更合适。
在这个例子中,咱们在每一个分区中复制了相同用户的全部信息。你可能会尝试着这样作来减小数据复制:
CREATE TABLE users ( id uuid PRIMARY KEY, username text, email text, age int ) CREATE TABLE groups ( groupname text, user_id uuid, PRIMARY KEY (groupname, user_id) )
这种作法中,若是说一个分组有着1000个用户,那么查询时咱们就要读取1001个分区!若是读取频繁,那么这种作法是极端不可取的。
另外一方面,若是按组读取的频率极低,可是更新用户信息(用户名)特别频繁,此时这种作法倒还有可取之处。
记住设计数据模型时将读/写频率考虑进去!
如今在上个例子中加入入组时间,一次读取x个新入组用户。新表以下:
CREATE TABLE group_join_dates ( groupname text, joined timeuuid, username text, email text, age int, PRIMARY KEY (groupname, joined) )
这里使用了timeuuid(相似于timestamp,可是两条记录不会相同)做为聚合列。在一个组(分区)中,数据按照用户入组时间排列。查询语句以下:
SELECT * FROM group_join_dates WHERE groupname = ? ORDER BY joined DESC LIMIT ?
这样作能够有效的保证查询效率。可是老是使用ORDER BY并非最高效的。更高效的作法表结构以下:
CREATE TABLE group_join_dates ( groupname text, joined timeuuid, username text, email text, age int, PRIMARY KEY (groupname, joined) ) WITH CLUSTERING ORDER BY (joined DESC)
下面是能为查询效率带来那么一点轻微提高的查询:
SELECT * FROM group_join_dates WHERE groupname = ? LIMIT ?
前一个例子中,由于在组变得过大时数据将不会均匀分布,因此咱们用某种随机方式来进行分区(用户名hash)。在这个例子中,咱们能够利用时间段来进行分区。好比:
CREATE TABLE group_join_dates ( groupname text, joined timeuuid, join_date text, username text, email text, age int, PRIMARY KEY ((groupname, join_date), joined) ) WITH CLUSTERING ORDER BY (joined DESC)
此次利用了入组时间做为复合分区键,每一天将开始一个新的分区。当查询x个新用户的时候,咱们首先查询当天的分区,而后是昨天,直到结果集包含x个用户。这样在获得最终结果前可能要查询多个分区。
为了最小化查询分区数量,应该尝试选择一个合适的时间范围,使得你的查询只会查到一个或两个分区。好比说业务上常常要查的是10个最新的用户,同时每一个组天天会增长三我的左右。
合适的作法是将4天做为一个时间段,而不是一天。能够截取时间戳到合适的长度。如:
now = time() four_days = 4 * 24 * 60 * 60 shard_id = now - (now % four_days)
本文的基本原则适用于目前全部的Cassandra版本,极可能也一样适用于将来的版本。其余的一些数据模型面临的问题(如tombstone),固然也须要考虑,但不一样的是它们不太可能出如今Cassandra的将来版本中。
还有Cassandra的其余一些特性(如 集合、用户自定义类型、 静态列)也有助于减小查询分区数目。设计时请别忘了它们。
想更深刻的学习,请看这里。祝各位好运!!