第四章:Cassandra查询语言--Cassandra:The Definitive Guide 2nd Edition

在本章中,您将了解Cassandra的数据模型以及Cassandra查询语言(CQL)如何实现该数据模型。我们将展示CQL如何支持Cassandra的设计目标并查看一些一般行为特征。

对于来自关系世界的开发人员和管理员来说,Cassandra数据模型最初很难理解。一些术语,例如“键空间”,是全新的,有些术语,例如“列”,存在于两个世界中,但含义略有不同。 CQL的语法在很多方面类似于SQL,但有一些重要的区别。对于那些熟悉NoSQL技术(如Dynamo或Bigtable)的人来说,它也可能令人困惑,因为尽管Cassandra可能基于这些技术,但它自己的数据模型却有很大不同。

因此,在本章中,我们从关系数据库术语开始,介绍Cassandra对世界的看法。在此过程中,我们将更熟悉CQL并了解它如何实现此数据模型。

关系数据模型

在关系数据库中,我们拥有数据库本身,它是可能对应于单个应用程序的最外层容器。数据库包含表。表具有名称并包含一个或多个列,这些列也具有名称。当我们向表中添加数据时,我们为每个定义的列指定一个值;如果我们没有特定列的值,我们使用null。这个新条目向表中添加一行,如果我们知道行的唯一标识符(主键),或者使用表示行可能遇到的某些条件的SQL语句,我们稍后可以读取该行。如果我们想要更新表中的值,我们可以更新所有行或只更新其中一些,具体取决于我们在SQL语句的“where”子句中使用的过滤器。

既然我们已经进行了这次审核,那么我们就可以从Cassandra的相似性和差异性的角度来看待它们。

Cassandra的数据模型

在本节中,我们将采用自下而上的方法来理解Cassandra的数据模型。

您可能想要使用的最简单的数据存储可能是数组或列表。它如图4-1所示。
在这里插入图片描述

如果您持久保存此列表,则可以稍后查询,但您必须检查每个值以了解它所代表的内容,或者始终将每个值存储在列表中的相同位置,然后从外部维护有关哪个单元格的文档。 数组包含哪些值。 这意味着您可能必须提供空的占位符值(空值),以便在您没有可选属性(例如传真号码或公寓号码)的值时保留阵列的预定大小。 数组是一个明显有用的数据结构,但在语义上并不丰富。

所以我们想在这个列表中添加第二个维度:匹配值的名称。 我们将为每个单元格命名,现在我们有了一个地图结构,如图4-2所示。

在这里插入图片描述

这是一个改进,因为我们可以知道我们的值的名称。因此,如果我们确定我们的地图将保存用户信息,我们可以使用列名,如first_name,last_name,phone,email等。这是一个更丰富的结构。

但到目前为止我们构建的结构只有在我们有一个给定实体的实例时才有效,例如单个人,用户,酒店或推文。如果我们想要存储具有相同结构的多个实体,它不会给我们太多,这当然是我们想要做的。没有什么可以统一一些名称/值对的集合,也没有办法重复相同的列名。因此,我们需要一些能够将一些列值组合在一个明显可寻址的组中的东西。我们需要一个键来引用一组应该作为集合处理的列。我们需要行。然后,如果我们得到一行,我们可以同时获得单个实体的所有名称/值对,或者只获取我们感兴趣的名称的值。我们可以调用这些名称/值对列。我们可以调用每个包含一些列行的独立实体。并且每行的唯一标识符可以称为行键或主键。图4-3显示了一个简单行的内容:主键,它本身是一列或多列,以及其他列。

在这里插入图片描述

Cassandra将表定义为关联相似数据的逻辑分区。例如,我们可能有一个用户表,一个酒店表,一个地址簿表等。通过这种方式,Cassandra表类似于关系世界中的表。

现在,每次存储新实体时,我们都不需要为每列存储值。也许我们不知道给定实体的每列的值。例如,有些人有第二个电话号码,有些人没有,在Cassandra支持的在线表格中,可能有一些字段是可选的,有些字段是必需的。没关系。而不是为那些我们不知道的值存储null,这会浪费空间,我们根本不会为该行存储该列。所以现在我们有一个稀疏的多维数组结构,如图4-4所示。

在传统关系数据库中设计表时,通常会处理“实体”或描述特定名词(酒店,用户,产品等)的属性集。没有考虑行本身的大小,因为一旦你确定了你的表所代表的名词,行大小就不可协商了。但是,当您使用Cassandra时,实际上您决定要确定行的大小:它们可以是宽或粗,取决于行所包含的列数。

宽行表示具有批次(可能是数万甚至数百万)列的行。通常,与这么多列一起使用的行数较少。相反,你可以更接近关系模型,在那里定义较少数量的列并使用许多不同的行 - 这就是瘦模型。我们已经在图4-4中看到了一个瘦小的模型。

在这里插入图片描述

Cassandra使用称为复合键(或复合键)的特殊主键来表示宽行,也称为分区。 组合键由分区键和一组可选的聚类列组成。 分区键用于确定存储行的节点,并且本身可以包含多个列。 聚类列用于控制数据在分区中的存储方式。 Cassandra还支持另一个称为静态列的构造,该构造用于存储不属于主键但由分区中的每一行共享的数据。

图4-5显示了每个分区如何由分区键唯一标识,以及如何使用群集键唯一标识分区中的行。

在这里插入图片描述

在本章中,我们将关注由单个列组成的简单主键。在这些情况下,主键和分区键是相同的,因为我们没有聚类列。我们将在第5章中研究更复杂的主键。

综上所述,我们有基本的Cassandra数据结构:

列,是名称/值对
该行,是主键引用的列的容器
该表是行的容器
键空间,是表的容器
集群,它是跨越一个或多个节点的密钥空间的容器

这是查看Cassandra数据模型的自下而上的方法。现在我们已经了解了基本术语,让我们更详细地研究每个结构。

集群

如前所述,Cassandra数据库专门设计为分布在多个一起运行的计算机上,这些计算机作为单个实例出现给最终用户。因此,Cassandra中最外层的结构是集群,有时称为环,因为Cassandra通过将数据排列成环来将数据分配给集群中的节点。

Keyspaces

集群是密钥空间的容器。密钥空间是Cassandra中数据的最外层容器,与关系数据库紧密对应。与数据库是关系模型中表的容器的方式相同,键空间是Cassandra数据模型中表的容器。与关系数据库一样,键空间具有一个名称和一组定义键空间范围行为的属性。

因为我们目前专注于数据模型,所以我们将留下有关设置和配置群集和密钥空间的问题,直到稍后。 我们将在第7章中研究这些主题。

表是有序行集合的容器,每个行本身都是有序的列集合。 排序由列确定,列被标识为键。 我们很快就会看到Cassandra如何使用主键之外的其他键。

将数据写入Cassandra中的表时,可以指定一列或多列的值。 该值集合称为行。 您指定的值中至少有一个必须是主键,用作该行的唯一标识符。

让我们回到我们在前一章中创建的用户表。 记住我们如何编写一行数据,然后使用cqlsh中的SELECT命令读取它:

cqlsh:my_keyspace> SELECT * FROM user WHERE first_name='Bill';

 first_name | last_name
------------+-----------
       Bill |    Nguyen

(1 rows)

你会在最后一行注意到shell告诉我们返回了一行。 事实证明,这是由first_name“Bill”标识的行。 这是标识此行的主键。

数据访问需要主键

这是一个重要的细节–CQL中的SELECT,INSERT,UPDATE和DELETE命令都按行进行操作。

如前所述,当我们向表中添加新行时,我们不需要为每列包含值。 让我们使用ALTER TABLE命令使用我们的用户表对此进行测试,然后使用DESCRIBE TABLE命令查看结果:

cqlsh:my_keyspace> ALTER TABLE user ADD title text;
cqlsh:my_keyspace> DESCRIBE TABLE user;

CREATE TABLE my_keyspace.user (
    first_name text PRIMARY KEY,
    last_name text,
    title text
) ...

我们看到标题栏已添加。 请注意,我们缩短了输出以省略各种表设置。 您将在第7章中了解有关这些设置以及如何配置它们的更多信息。

现在,让我们写几行,为每个行填充不同的列,并查看结果:

cqlsh:my_keyspace> INSERT INTO user (first_name, last_name, title) 
  VALUES ('Bill', 'Nguyen', 'Mr.');
cqlsh:my_keyspace> INSERT INTO user (first_name, last_name) VALUES 
  ('Mary', 'Rodriguez');
cqlsh:my_keyspace> SELECT * FROM user;

 first_name | last_name | title
------------+-----------+-------
       Mary | Rodriguez |  null
       Bill |    Nguyen |   Mr.

(2 rows)

现在我们已经更多地了解了表的结构并完成了一些数据建模,让我们深入研究一下列。

列是Cassandra数据模型中最基本的数据结构单元。到目前为止,我们已经看到一列包含名称和值。在定义列时,我们将每个值约束为特定类型。我们想深入研究每列可用的各种类型,但首先让我们看一下我们尚未讨论过的列的其他一些属性:时间戳和生存时间。这些属性是了解Cassandra如何利用时间保持数据最新的关键。

时间戳

每次将数据写入Cassandra时,都会为更新的每个列值生成时间戳。在内部,Cassandra使用这些时间戳来解决对相同值进行的任何冲突更改。通常,最后一个时间戳获胜。

让我们通过将writetime()函数添加到SELECT命令来查看为以前的写入生成的时间戳。我们将在lastname列上执行此操作,并为上下文包含其他几个值:

cqlsh:my_keyspace> SELECT first_name, last_name, 
  writetime(last_name) FROM user;
 
 first_name | last_name | writetime(last_name)
------------+-----------+----------------------
       Mary | Rodriguez |     1434591198790252
       Bill |    Nguyen |     1434591198798235

(2 rows)

我们可能期望如果我们在first_name上请求时间戳,我们会得到类似的结果。 但是,事实证明我们不允许在主键列上询问时间戳:

cqlsh:my_keyspace> SELECT WRITETIME(first_name) FROM user;
InvalidRequest: code=2200 [Invalid query] message="Cannot use 
  selection function writeTime on PRIMARY KEY part first_name"

Cassandra还允许我们指定执行写入时要使用的时间戳。 为此,我们将首次使用CQL UPDATE命令。 我们将使用可选的USING TIMESTAMP选项手动设置时间戳(请注意,时间戳必须晚于SELECT命令中的时间戳,否则将忽略UPDATE):

cqlsh:my_keyspace> UPDATE user USING TIMESTAMP 1434373756626000 
  SET last_name = 'Boateng' WHERE first_name = 'Mary' ;
cqlsh:my_keyspace> SELECT first_name, last_name, 
  WRITETIME(last_name) FROM user WHERE first_name = 'Mary';
 
 first_name | last_name   | writetime(last_name)
------------+-------------+---------------------
  Mary      | Boateng     | 1434373756626000
 
(1 rows)

此语句具有将姓氏列添加到由主键“Mary”标识的行,并将时间戳设置为我们提供的值的效果。

使用时间戳

写入不需要设置时间戳。此功能通常用于写入,其中担心某些写入可能导致新数据被陈旧数据覆盖。这是高级行为,应谨慎使用。

目前还没有办法将writetime()生成的时间戳转换为cqlsh中更友好的格式。

生存时间(TTL)

Cassandra提供的一个非常强大的功能是能够使不再需要的数据到期。此过期非常灵活,可以在各个列值的级别上运行。生存时间(或TTL)是Cassandra为每个列值存储的值,用于指示保留值的时间。

TTL值默认为null,表示写入的数据不会过期。让我们通过将CTL()函数添加到cqlsh中的SELECT命令来显示这一点,以查看Mary的姓氏的TTL值:

cqlsh:my_keyspace> SELECT first_name, last_name, TTL(last_name) 
  FROM user WHERE first_name = 'Mary';

 first_name | last_name | ttl(last_name)
------------+-----------+----------------
       Mary |   Boateng |           null

(1 rows)

现在让我们通过在UPDATE命令中添加USING TTL选项,将姓氏列上的TTL设置为一小时(3,600秒):

cqlsh:my_keyspace> UPDATE user USING TTL 3600 SET last_name = 
  'McDonald' WHERE first_name = 'Mary' ;
cqlsh:my_keyspace> SELECT first_name, last_name, TTL(last_name) 
  FROM user WHERE first_name = 'Mary';                                                                              

 first_name | last_name   | ttl(last_name)
------------+-------------+---------------
  Mary      |  McDonald   |           3588

(1 rows)

如您所见,时钟已经在倒数我们的TTL,反映了输入第二个命令所需的几秒钟。如果我们在一小时内再次运行此命令,则Mary的last_name将设置为null。我们还可以使用相同的USING TTL选项在INSERTS上设置TTL。

使用TTL

请记住,TTL存储在每列级别。目前没有直接在行级设置TTL的机制。与时间戳一样,无法获取或设置主键列的TTL值,并且只有在为列提供值时才能为列设置TTL。

如果我们想在整行中设置TTL,我们必须在INSERT或UPDATE命令中为每个非主键列提供一个值。

CQL类型

现在我们已经深入探讨了Cassandra如何表示包含基于时间的元数据的列,让我们看一下我们可用于我们的值的各种类型。

正如我们在目前的探索中看到的那样,我们表中的每一列都是指定的类型。到目前为止,我们只使用了varchar类型,但是在CQL中我们还有很多其他选项,所以让我们来探索它们。

CQL支持一组灵活的数据类型,包括简单的字符和数字类型,集合和用户定义的类型。我们将描述这些数据类型,并提供一些示例,说明如何使用它们来帮助您学习如何为数据模型做出正确的选择。

数字数据类型

CQL支持您期望的数字类型,包括整数和浮点数。 这些类型与Java和其他语言中的标准类型类似:

  • INT
    • 32位有符号整数(如Java中所示)
  • BIGINT
    • 64位有符号长整数(相当于Java long)
  • SMALLINT
    • 一个16位有符号整数(相当于Java短)
  • TINYINT
    • 一个8位有符号整数(如Java中所示)
  • varint
    • 变量精度有符号整数(相当于java.math.BigInteger)
  • 浮动
    • 32位IEEE-754浮点(如Java中所示)
    • 64位IEEE-754浮点(如Java中所示)
  • 十进制
    • 变精度十进制(相当于java.math.BigDecimal)

其他整数类型

在Cassandra 2.2版本中添加了smallint和tinyint类型。

虽然枚举类型在许多语言中很常见,但在CQL中没有直接的等价物。 通常的做法是将枚举值存储为字符串。 例如,使用Enum.name()方法将枚举值转换为String以便将Cassandra作为文本写入,并使用Enum.valueOf()方法将文本转换回枚举值。

文本数据类型

CQL提供了两种用于表示文本的数据类型,其中一种我们已经对已经(文本)进行了相当多的使用:

  • text,varchar
    • UTF-8字符串的同义词
  • ASCII
    • ASCII字符串

UTF-8是最新且广泛使用的文本标准并支持国际化,因此我们建议在为新数据构建表时使用ascii上的文本。 如果您要处理ASCII格式的旧数据,则ascii类型最有用。

在cqlsh中设置Locale

默认情况下,cqlsh使用反斜杠转义输出控件和其他不可打印的字符。 您可以通过在运行该工具之前通过’$ LANG’环境变量设置语言环境来控制cqlsh如何显示非ASCII字符。 有关更多信息,请参阅cqlsh命令HELP TEXT_OUTPUT。

时间和身份数据类型

行和分区等数据元素的标识在任何数据模型中都很重要,以便能够访问数据。 Cassandra提供了几种在定义唯一分区键时非常有用的类型。 我们花点时间(双关语)来深入研究这些:

  • 时间戳

    虽然我们之前已经注意到每列都有一个时间戳,指示上次修改的时间,但您也可以使用时间戳作为列本身的值。 时间可以编码为64位有符号整数,但使用几种支持的ISO 8601日期格式之一输入时间戳通常更有用。 例如:

2015-06-15 20:05-0700
2015-06-15 20:05:07-0700
2015-06-15 20:05:07.013-0700
2015-06-15T20:05-0700
2015-06-15T20:05:07-0700
2015-06-15T20:05:07.013+-0700

最佳做法是始终提供时区而不是依赖于操作系统时区配置。

  • date,time

    通过Cassandra 2.1发布的只有时间戳类型来表示时间,包括日期和时间。 2.2版本引入了允许这些日期和时间独立表示的日期和时间类型;也就是说,没有时间的日期,以及没有参考特定日期的时间。与时间戳一样,这些类型支持ISO 8601格式。

    虽然Java 8中提供了新的java.time类型,但日期类型映射到Cassandra中的自定义类型,以保持与旧JDK的兼容性。时间类型映射到Java long,表示自午夜以来的纳秒数。

  • UUID

    通用唯一标识符(UUID)是128位值,其中位符合几种类型中的一种,其中最常用的类型称为类型1和类型4.CQL uuid类型是类型4 UUID,其中完全基于随机数。 UUID通常表示为以十六进制数字划分的短划线序列。例如:

1a6300ca-0572-4736-a393-c0b7229e193e

uuid类型通常用作代理键,可以单独使用,也可以与其他值组合使用。

由于UUID的长度有限,因此并不能绝对保证它们是唯一的。但是,大多数操作系统和编程语言都提供了生成提供足够唯一性的ID的实用程序,而cqlsh也是如此。您可以通过uuid()函数获取Type 4 UUID值,并在INSERT或UPDATE中使用此值。
  • timeuuid

    这是Type 1 UUID,它基于计算机的MAC地址,系统时间和用于防止重复的序列号。此类型经常用作无冲突时间戳。 cqlsh提供了几个与timeuuid类型交互的便利函数:now(),dateOf()和unixTimestampOf()。

    这些便利功能的可用性是timeuuid比uuid更频繁使用的一个原因。

在前面的示例的基础上,我们可能会确定我们要为每个用户分配一个唯一的ID,因为first_name可能不是我们用户表的唯一键。毕竟,我们很可能会在某些时候遇到具有相同名字的用户。如果我们从头开始,我们可能已经选择将此标识符作为主键,但是现在我们将其添加为另一列。

主键是永远的

创建表后,无法修改主键,因为它控制数据在群集中的分布方式,更重要的是,它如何存储在磁盘上。

让我们使用uuid添加标识符:

cqlsh:my_keyspace> ALTER TABLE user ADD id uuid;

接下来,我们将使用uuid()函数为Mary插入一个ID,然后查看结果:

cqlsh:my_keyspace> UPDATE user SET id = uuid() WHERE first_name = 
  'Mary';
cqlsh:my_keyspace> SELECT first_name, id FROM user WHERE 
  first_name = 'Mary';

 first_name | id
------------+--------------------------------------
       Mary | e43abc5d-6650-4d13-867a-70cbad7feda9 

(1 rows)

请注意,id是UUID格式。

现在我们有了一个更强大的表设计,当我们了解更多类型时,我们可以扩展更多列。

其他简单数据类型

CQL提供了其他几种简单的数据类型,这些数据类型不能很好地归入我们已经看过的类别之一:

  • 布尔

    • 这是一个简单的真/假值。 cqlsh在接受这些值时不区分大小写,但输出True或False。
  • BLOB

    • 二进制大对象(blob)是任意字节数组的口语计算术语。 CQL blob类型对于存储媒体或其他二进制文件类型很有用。 Cassandra不验证或检查blob中的字节。 CQL将数据表示为十六进制数字 - 例如,0x00000ab83cf0。 如果要将任意文本数据编码到blob中,可以使用textAsBlob()函数来指定条目的值。 有关更多信息,请参阅cqlsh帮助函数HELP BLOB_INPUT。
  • INET

    • 此类型表示IPv4或IPv6 Internet地址。 cqlsh接受用于定义IPv4地址的任何合法格式,包括包含十进制,八进制或十六进制值的点线或非点线表示。但是,这些值使用cqlsh输出中的点分十进制格式表示 - 例如,192.0.2.235。

    • IPv6地址表示为由四个十六进制数字组成的八组,以冒号分隔 - 例如,2001:0db8:85a3:0000:0000:8a2e:0370:7334。 IPv6规范允许折叠连续的零十六进制值,因此当使用SELECT:2001:db8:85a3:a :: 8a2e:370:7334读取时,前面的值呈现如下。

  • 计数器

    • 计数器数据类型提供64位有符号整数,其值不能直接设置,而只能递增或递减。 Cassandra是为数不多的跨数据中心提供无竞争增量的数据库之一。计数器经常用于跟踪统计数据,例如页面查看次数,推文数,日志消息等。计数器类型有一些特殊限制。它不能用作主键的一部分。如果使用计数器,则除主键列之外的所有列都必须是计数器。

关于计数器的警告

请记住:增量和减量运算符不是幂等的。没有直接复位计数器的操作,但您可以通过读取计数器值并按该值递减来近似复位。不幸的是,这并不能保证完美地工作,因为在阅读和写作之间的其他地方可能已经改变了计数器。

集合

假设我们想扩展用户表以支持多个电子邮件地址。一种方法是创建其他列,如email2,email3等。虽然这是一种可行的方法,但它不能很好地扩展,并且可能会导致大量的返工。将电子邮件地址作为一个组或“集合”处理起来要简单得多.CQL提供了三种集合类型来帮助我们解决这些问题:集合,列表和映射。我们现在来看看它们中的每一个:

  • set

    • set数据类型存储元素集合。元素是无序的,但cqlsh按排序顺序返回元素。例如,文本值按字母顺序返回。集合可以包含我们之前评论过的简单类型以及用户定义的类型(我们将暂时讨论)甚至其他集合。使用set的一个优点是无需先读取内容即可插入其他项目。

    让我们修改我们的用户表以添加一组电子邮件地址:

cqlsh:my_keyspace> ALTER TABLE user ADD emails set<text>;

然后我们将为Mary添加一个电子邮件地址并检查它是否已成功添加:

cqlsh:my_keyspace> UPDATE user SET emails = { 
  'mary@example.com' } WHERE first_name = 'Mary';
cqlsh:my_keyspace> SELECT emails FROM user WHERE first_name = 
  'Mary';
 
 emails
----------------------
 {'mary@example.com'}
 
(1 rows)

请注意,在添加第一个电子邮件地址时,我们替换了该集合的先前内容,在本例中为null。 我们可以稍后添加另一个电子邮件地址,而不使用连接替换整个集合:

cqlsh:my_keyspace> UPDATE user SET emails = emails + { 
  'mary.mcdonald.AZ@gmail.com' } WHERE first_name = 'Mary';
cqlsh:my_keyspace> SELECT emails FROM user WHERE first_name = 
  'Mary';
 
 emails
---------------------------------------------------
 {'mary.mcdonald.AZ@gmail.com', 'mary@example.com'}
 
(1 rows)

其他设定操作

我们还可以使用减法运算符清除集合中的项目:SET emails = emails - {‘mary@example.com’}。

或者,我们可以使用空集符号清除整个集合:SET emails = {}。

  • list

    • 列表数据类型包含有序的元素列表。 默认情况下,值按插入顺序存储。 让我们修改我们的用户表以添加电话号码列表:
cqlsh:my_keyspace> ALTER TABLE user ADD 
  phone_numbers list<text>;

然后我们将为Mary添加一个电话号码并检查它是否已成功添加:

cqlsh:my_keyspace> UPDATE user SET phone_numbers = [ 
  '1-800-999-9999' ] WHERE first_name = 'Mary';
cqlsh:my_keyspace> SELECT phone_numbers FROM user WHERE 
  first_name = 'Mary';
 
 phone_numbers
--------------------
 ['1-800-999-9999']
 
(1 rows)

让我们通过附加它来添加第二个数字:

cqlsh:my_keyspace> UPDATE user SET phone_numbers = 
  phone_numbers + [ '480-111-1111' ] WHERE first_name = 'Mary';
cqlsh:my_keyspace> SELECT phone_numbers FROM user WHERE 
  first_name = 'Mary';
 
 phone_numbers
------------------------------------
 ['1-800-999-9999', '480-111-1111']
 
(1 rows)

我们现在添加的第二个数字显示在列表的末尾。

注意

我们也可以通过颠倒我们的值的顺序将数字添加到列表的前面:SET phone_numbers = [‘4801234567’] + phone_numbers。

当我们通过索引引用它时,我们可以替换列表中的单个项目:

cqlsh:my_keyspace> UPDATE user SET phone_numbers[1] = 
  '480-111-1111' WHERE first_name = 'Mary';

与集合一样,我们也可以使用减法运算符来删除与指定值匹配的项目:

cqlsh:my_keyspace> UPDATE user SET phone_numbers = 
  phone_numbers - [ '480-111-1111' ] WHERE first_name = 'Mary';

最后,我们可以使用索引直接删除特定项目:

cqlsh:my_keyspace> DELETE phone_numbers[0] from user WHERE 
  first_name = 'Mary';

  • map

    地图数据类型包含键/值对的集合。 键和值可以是除计数器之外的任何类型。 让我们通过使用地图来存储有关用户登录的信息来尝试这一点。 我们将创建一个列来跟踪以秒为单位的登录会话时间,并以timeuuid为关键:

cqlsh:my_keyspace> ALTER TABLE user ADD 
  login_sessions map<timeuuid, int>; 

然后我们将为Mary添加几个登录会话并查看结果:

cqlsh:my_keyspace> UPDATE user SET login_sessions = 
  { now(): 13, now(): 18} WHERE first_name = 'Mary';
cqlsh:my_keyspace> SELECT login_sessions FROM user WHERE 
  first_name = 'Mary';
 
 login_sessions
-----------------------------------------------
 {6061b850-14f8-11e5-899a-a9fac1d00bce: 13, 
  6061b851-14f8-11e5-899a-a9fac1d00bce: 18}
 
(1 rows)

我们还可以使用其键引用地图中的单个项目。

在我们需要在单个列中存储可变数量的元素的情况下,集合类型非常有用。

用户定义的类型

现在我们可能决定需要跟踪用户的物理地址。 我们可以使用单个文本列来存储这些值,但这会给应用程序解析地址的各个组件带来负担。 如果我们能够定义一个存储地址的结构以保持不同组件的完整性,那就更好了。

幸运的是,Cassandra为我们提供了一种定义自己类型的方法。 然后,我们可以创建这些用户定义类型(UDT)的列。 让我们创建自己的地址类型,在命令中插入一些换行符以提高可读性:

cqlsh:my_keyspace> CREATE TYPE address (
               ...   street text,
               ...   city text,
               ...   state text,
               ...   zip_code int);

UDT的范围由定义它的键空间确定。 我们可以编写CREATE TYPE my_keyspace.address。 如果运行命令DESCRIBE KEYSPACE my_keyspace,您将看到地址类型是键空间定义的一部分。

现在我们已经定义了地址类型,我们将尝试在用户表中使用它,但是我们立即遇到了一个问题:

cqlsh:my_keyspace> ALTER TABLE user ADD 
  addresses map<text, address>;
InvalidRequest: code=2200 [Invalid query] message="Non-frozen 
  collections are not allowed inside collections: map<text, 
  address>"

这里发生了什么?事实证明,用户定义的数据类型被视为集合,因为它的实现类似于集合,列表或映射。

Freezing Collections

2.2之前的Cassandra版本不完全支持集合的嵌套。具体而言,尚不支持访问嵌套集合的各个属性的能力,因为嵌套集合由实现序列化为单个对象。

冻结是Cassandra社区作为前向兼容机制引入的一个概念。现在,您可以将集合标记为已冻结,从而将集合嵌套在另一个集合中。将来,当完全支持嵌套集合时,将有一种“解冻”嵌套集合的机制,允许访问各个属性。

如果冻结,您还可以将集合用作主键。

既然我们已经花了很长时间来讨论冻结和嵌套表,那么让我们回到修改我们的表,这次将地址标记为冻结:

cqlsh:my_keyspace> ALTER TABLE user ADD addresses map<text, 
  frozen<address>>;

现在让我们为Mary添加一个家庭住址:

cqlsh:my_keyspace> UPDATE user SET addresses = addresses + 
  {'home': { street: '7712 E. Broadway', city: 'Tucson', 
  state: 'AZ', zip_code: 85715} } WHERE first_name = 'Mary';

现在我们已经完成了各种类型的学习,让我们退后一步,通过描述my_keyspace来查看我们目前创建的表格:

cqlsh:my_keyspace> DESCRIBE KEYSPACE my_keyspace ;

CREATE KEYSPACE my_keyspace WITH replication = {'class': 
  'SimpleStrategy', 'replication_factor': '1'} AND 
  durable_writes = true;

CREATE TYPE my_keyspace.address (
    street text,
    city text,
    state text,
    zip_code int
);

CREATE TABLE my_keyspace.user (
    first_name text PRIMARY KEY,
    addresses map<text, frozen<address>>,
    emails set<text>,
    id uuid,
    last_name text,
    login_sessions map<timeuuid, int>,
    phone_numbers list<text>,
    title text
) WITH bloom_filter_fp_chance = 0.01
    AND caching = '{'keys':'ALL', 'rows_per_partition':'NONE'}'
    AND comment = ''
    AND compaction = {'min_threshold': '4', 'class': 
      'org.apache.cassandra.db.compaction.
      SizeTieredCompactionStrategy', 'max_threshold': '32'}
    AND compression = {'sstable_compression': 
      'org.apache.cassandra.io.compress.LZ4Compressor'}
    AND dclocal_read_repair_chance = 0.1
    AND default_time_to_live = 0
    AND gc_grace_seconds = 864000
    AND max_index_interval = 2048
    AND memtable_flush_period_in_ms = 0
    AND min_index_interval = 128
    AND read_repair_chance = 0.0
    AND speculative_retry = '99.0PERCENTILE';

二级索引

如果您尝试查询Cassandra表中不属于主键的列,您很快就会意识到这是不允许的。 例如,请考虑上一章中的用户表,该表使用first_name作为主键。 尝试按last_name查询会产生以下输出:

cqlsh:my_keyspace> SELECT * FROM user WHERE last_name = 'Nguyen';
InvalidRequest: code=2200 [Invalid query] message="No supported 
  secondary index found for the non primary key columns restrictions"

正如错误消息指示我们,我们需要为last_name列创建二级索引。 辅助索引是不属于主键的列的索引:

cqlsh:my_keyspace> CREATE INDEX ON user ( last_name );

我们还可以使用语法CREATE INDEX ON为索引提供可选名称…如果没有指定名称,cqlsh会根据表格

_ 自动创建名称_idx。 例如,我们可以使用DESCRIBE KEYSPACE学习我们刚刚创建的索引的名称:

cqlsh:my_keyspace> DESCRIBE KEYSPACE;
...
CREATE INDEX user_last_name_idx ON my_keyspace.user (last_name);

现在我们已经创建了索引,我们的查询将按预期工作:

cqlsh:my_keyspace> SELECT * FROM user WHERE last_name = 'Nguyen';

 first_name | last_name
------------+-----------
       Bill |    Nguyen

(1 rows)

我们不仅仅限于仅基于简单类型列的索引。 也可以创建基于集合中的值的索引。 例如,我们可能希望能够根据用户地址,电子邮件或电话号码进行搜索,我们分别使用map,set和list实现:

cqlsh:my_keyspace> CREATE INDEX ON user ( addresses );
cqlsh:my_keyspace> CREATE INDEX ON user ( emails );
cqlsh:my_keyspace> CREATE INDEX ON user ( phone_numbers );

请注意,对于地图,我们可以选择索引键(通过语法KEYS(地址))或值(默认值)或两者(在Cassandra 2.2或更高版本中)。

最后,我们可以使用DROP INDEX命令删除索引:

cqlsh:my_keyspace> DROP INDEX user_last_name_idx;

二级指数陷阱

由于Cassandra跨多个节点分区数据,因此每个节点必须根据其拥有的分区中存储的数据维护自己的二级索引副本。出于这个原因,涉及二级索引的查询通常涉及更多节点,这使得它们显得更加昂贵。

对于几种特定情况,不建议使用二级索引:

具有高基数的列。例如,对user.addresses列进行索引可能非常昂贵,因为绝大多数地址都是唯一的。
数据基数非常低的列。例如,对user.title列进行索引以支持对用户表中每个“Mrs.”的查询没有多大意义,因为这会导致索引中存在大量行。
经常更新或删除的列。如果删除的数据(逻辑删除)的数量比压缩过程可以处理的更快,那么构建在这些列上的索引可能会产生错误。

为了获得最佳读取性能,非规范化表格设计或物化视图通常优于使用二级索引。我们将在第5章中了解有关这些内容的更多信息。但是,二级索引可以是支持初始数据模型设计中未考虑的查询的有用方法。

SASI:新的二级指数实施

Cassandra 3.4版本包括二级索引的替代实现,称为SSTable Attached Secondary Index(SASI)。 SASI由Apple开发,并作为Cassandra二级索引API的开源实现发布。顾名思义,SASI索引是作为每个SSTable文件的一部分计算和存储的,与原始的Cassandra实现不同,后者将索引存储在单独的“隐藏”表中。

SASI实现与传统的二级索引一起存在,您可以使用CQL CREATE CUSTOM INDEX命令创建SASI索引:

CREATE CUSTOM INDEX user_last_name_sasi_idx ON user (last_name) 
  USING 'org.apache.cassandra.index.sasi.SASIIndex';

SASI索引确实提供了超越传统二级索引实现的功能,例如能够对索引列执行不等式(大于或小于)搜索。您还可以使用新的CQL LIKE关键字对索引列进行文本搜索。例如,您可以使用以下查询来查找姓氏以“N”开头的用户:

SELECT * FROM user WHERE last_name LIKE'N%';

虽然SASI索引通过消除从其他表读取的需要确实比传统索引执行得更好,但它们仍然需要从非规范化设计中读取更多节点。

总结

在本章中,我们快速浏览了Cassandra的集群,键空间,表,键,行和列的数据模型。 在这个过程中,我们学习了很多CQL语法,并获得了使用cqlsh中的表和列的更多经验。 如果您对深入了解CQL感兴趣,可以阅读完整的语言规范。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值