注意:由于原问题涉及到Cassandra,因此本答案将重点讨论它。尽管Cassandra和其他非关系型(NoSQL)数据存储通常共享类似的概念,但不能假定这里详细介绍的想法适用于其他非关系型数据存储。
最好的方法是记住,按照查询构建数据模型是标准实践(对于Cassandra)。主要区别在于,RDBMS表的构建主要关注数据的高效存储。在Cassandra(非关系型)世界中,主要关注点转向了你想要查询的样子。通常,这可能意味着以多种方式键入相同的冗余数据……这很好。这在DataStax文档
比较Cassandra数据模型和关系数据库中有解释。
假设我有以下用户表:
CREATE TABLE users (
username TEXT,
firstname TEXT,
lastname TEXT,
phone TEXT,
PRIMARY KEY (username));
插入一些样本数据后,该表格如下:
username | firstname | lastname | phone
------------------------------------------------
mreynolds | Malcolm | Reynolds | 111-555-1234
jcobb | Jayne | Cobb | 111-555-3464
sbook | Derial | Book | 111-555-2349
stam | Simon | Tam | 111-555-8899
users
表将允许我通过username
查询我的用户,因为这是我们的PRIMARY KEY。但是,如果我们想按电话号码查询数据怎么办?您可能会想在phone
上添加一个二级索引,但是phone
的基数可能与username
一样高。解决此问题的正确方法是创建一个新表,以允许您通过电话查询。注意:此模型假定username
和phone
都是唯一的。
CREATE TABLE usersbyphone (
phone TEXT,
username TEXT,
firstname TEXT,
lastname TEXT,
PRIMARY KEY (phone));
假设我们的下一个条目是针对用户“霍班·沃什伯恩”和“佐伊·沃什伯恩”的。由于他们已婚,他们将拥有相同的(家庭)电话号码。Cassandra不允许条目共享主键,并将覆盖(以最后一个条目为准)。因此,我们需要更改
usersbyphone
表上的主键,如下所示:
PRIMARY KEY (phone,username));
这里,phone
是我们的分区键(决定存储此行的分区的键),而 username
是我们的聚簇键(决定磁盘上排序顺序的键)。将这两个键组合在一起作为主键将确保唯一性。这将使我们能够像这样选择我们的 usersbyphone
表:
SELECT username, firstname, lastnamea, phone FROM usersbyphone;
username | firstname | lastname | phone
hwashburne| Hoban | Washburne| 111-555-1212
jcobb | Jayne | Cobb | 111-555-3464
mreynolds | Malcolm | Reynolds | 111-555-1234
sbook | Derial | Book | 111-555-2349
stam | Simon | Tam | 111-555-8899
zwashburne| Zoe | Washburne| 111-555-1212
重要的是要注意,
usersbyphone
表并不会
替换 users
表...它与之相辅相成。你将需要对该数据集进行一些查询,这些查询需要由
users
表提供服务,而另一些则需要由
usersbyphone
表提供服务。缺点是,保持两个表同步是在数据库外部完成的,通常是通过应用程序代码完成的。这是一个例子,展示了在非关系型范式中您的思考必须有所不同。
正如 DataStax MVP John Berryman 所解释的那样(
Understanding How CQL3 Maps To Cassandra’s Internal Data Structure),在底层,Casssandra 将以类似于以下结构存储我们的
users
数据:
RowKey:mreynolds
=> (column=, value=, timestamp=1374546754299000)
=> (column=firstname, value=Malcolm, timestamp=1374546754299000)
=> (column=lastname, value=Reynolds, timestamp=1374546754299000)
=> (column=phone, value=111-555-1234, timestamp=1374546754299000)
------------------------------------------------------
RowKey:hwashburne
=> (column=, value=, timestamp=1374546757815000)
=> (column=firstname, value=Hoban, timestamp=1374546757815000)
=> (column=lastname, value=Washburne, timestamp=1374546757815000)
=> (column=phone, value=111-555-1212, timestamp=1374546757815000)
------------------------------------------------------
RowKey:zwashburne
=> (column=, value=, timestamp=1374546761055000)
=> (column=firstname, value=Zoe, timestamp=1374546761055000)
=> (column=lastname, value=Washburne, timestamp=1374546761055000)
=> (column=phone, value=111-555-1212, timestamp=1374546761055000)
“地图中的地图”概念(在eBay文章中提到)在这里确实发挥了作用。我们的“usersbyphone”表格将与我们的“users”表格略有不同:
RowKey:111-555-1234
=> (column=mreynolds, value=, timestamp=1374546754299000)
=> (column=mreynolds:firstname, value=Malcolm, timestamp=1374546754299000)
=> (column=mreynolds:lastname, value=Reynolds, timestamp=1374546754299000)
------------------------------------------------------
RowKey:111-555-1212
=> (column=hwashburne, value=, timestamp=1374546757815000)
=> (column=hwashburne:firstname, value=Hoban, timestamp=1374546757815000)
=> (column=hwashburne:lastname, value=Washburne, timestamp=1374546757815000)
=> (column=zwashburne, value=, timestamp=1374546761055000)
=> (column=zwashburne:firstname, value=Zoe, timestamp=1374546761055000)
=> (column=zwashburne:lastname, value=Washburne, timestamp=1374546761055000)
由于主键结构,可以注意到 Wash(hwashburne)和 Zoe(zwashburne)实际上存储在同一行中。这种结构允许我们快速查询所有共享相同电话号码的用户。
总之:
- 表(列族)应以最有效的方式用于查询数据。 RDBMS 鼓励使用规范化来最有效地存储数据。
- 使用复合键,Cassandra 将类似的数据组合在附近的行中。
- Cassandra 利用磁盘(聚类)排序顺序来优化操作。
- Cassandra 主键始终是唯一的。对具有相同主键的数据进行不同的写入将互相覆盖(最后的写入获胜)。
- CQL(Cassandra 查询语言)是 SQL 的有限子集。虽然 CQL 提供了与 RDBMS 背景相似的语法,但重要的是要记住许多 SQL 关键字和概念在 CQL 中不存在。