Apache Cassandra 二级索引

FreeGuideOnline 9阅读 2026-07-12

Apache Cassandra 二级索引完全指南

二级索引是 Cassandra 查询模型中一个非常有用的特性,它允许你对非主键列进行查询。但它的行为与传统关系型数据库中的索引完全不同,理解其内部机制与适用场景是高效使用 Cassandra 的关键。本教程将从概念到实践,带你全面掌握二级索引。


1. 什么是二级索引?

在 Cassandra 中,主键是数据存取的核心。所有数据的分布与查询都必须围绕PRIMARY KEY展开。当你需要按某个非主键列(例如用户的邮箱、产品的类别)进行查询时,就无法直接依赖分区键和聚簇键。此时,二级索引提供了另一种查询路径。

一级索引:指基于表的主键(PRIMARY KEY)进行的查询,这是 Cassandra 中最快、最直接的查询方式。

二级索引:指在非主键列上额外构建的索引结构,允许你用WHERE子句对这些列进行过滤。Cassandra 通过后台维护的隐藏索引表来实现这一功能。


2. 二级索引的工作原理

理解原理能帮助你避免性能陷阱。

Cassandra 的二级索引不是全局保存的,而是本地化于每个节点。这意味着每个节点只维护自己存储的数据部分的索引。

  • 当你创建一个二级索引后,Cassandra 会在后台自动构建一个不可见的索引表。
  • 该索引表以被索引列的值作为分区键,以原表行的主键值作为聚簇列。
  • 查询时,协调节点会将请求发送到集群中的所有节点(除非分区键也被指定),每个节点在其本地索引中查找匹配值,然后回传对应的主键,最后协调节点根据主键去获取实际数据。

示例:假设有一张用户表:

CREATE TABLE users (
    user_id UUID PRIMARY KEY,
    email TEXT,
    name TEXT,
    city TEXT
);

当你在 city 列上创建二级索引后,内部大致会生成如下逻辑结构:

索引表:city_idx
------------------
city (分区键) | user_id (聚簇列)
------------------
'London'      | uuid1
'London'      | uuid2
'Paris'       | uuid3

查询 SELECT * FROM users WHERE city='London'; 时,协调节点会向所有节点广播,每个节点在本地索引中找到 London 对应的 user_id,然后读取 users 表返回结果。

关键启示:由于索引是本地化的,查询通常需要扫描所有节点,导致较高的延迟和资源消耗。这与基于主键的点查询(只命中一个节点)截然不同。


3. 二级索引的语法与操作

3.1 创建二级索引

在表定义之后,使用 CREATE INDEX 语句。

语法

CREATE INDEX [IF NOT EXISTS] <index_name>
ON <keyspace>.<table_name> ( <column_name> );

示例

CREATE INDEX IF NOT EXISTS idx_users_city ON my_keyspace.users (city);

如果想自定义索引名(推荐),直接指定:

CREATE INDEX city_idx ON users (city);

Cassandra 3.4+ 以及 DSE 5.1+ 支持 SASI 索引(SSTable Attached Secondary Index),这是一种更强大的全局视野索引,后文会专门介绍。但默认的仍是传统二级索引。

3.2 使用索引查询

索引创建成功后,就可以在查询的 WHERE 子句中使用该列。

基本查询

SELECT * FROM users WHERE city = 'London';

必须注意

  • 只有当被索引列在 WHERE 子句中单独以相等条件出现,或与分区键组合使用时,索引才会被触发。
  • 对于范围查询、CONTAINSLIKE 等操作,传统二级索引往往不支持或性能极差(SASI 索引除外)。

合法查询示例

-- 假设 users 表的分区键是 (user_id),city 建了索引
SELECT * FROM users WHERE city = 'Paris';         -- 生效
SELECT * FROM users WHERE user_id = some_uuid AND city = 'Paris';  -- 生效

不合法或低效查询

SELECT * FROM users WHERE city > 'M';             -- 可能被拒绝或全表扫描
SELECT * FROM users WHERE city IN ('London','Paris'); -- 某些版本可能支持,但实为多次索引查找

3.3 删除索引

DROP INDEX IF EXISTS my_keyspace.idx_users_city;

删除索引后,对应列上的查询将不再生效。


4. 何时适合使用二级索引

二级索引并非毒药,但务必严格评估以下场景:

适用场景

  • 低基数列:被索引列的基数不能太高。例如“用户性别”、“国家代码”、“订单状态”(只有几个取值)。高基数列(如邮箱、用户名)会导致索引表极度膨胀,并产生大量宽分区。
  • 表中的分区数量非常巨大,但经常需要跨分区进行小范围条件筛选,且筛选条件的重复度较高。
  • 异步性、最终一致性可接受的查询:二级索引查询通常不是高度一致性的,因为在主表数据更新时,索引更新是异步的。
  • 配合分区键使用:在查询中同时提供分区键和被索引列,这样协调节点可以直接定位到特定分区所在的节点,避免广播到全集群。这是最佳实践

避免场景

  • 高基数列(如 user_idemailtimestamp),应使用专门设计的查询表或物化视图。
  • 返回数百万行的查询,索引不适合大规模扫描。
  • 频繁更新的列,索引维护开销大。
  • 低延迟、高并发生产环境,不当的索引查询会导致压力扩散到整个集群。

黄金法则:在创建二级索引前,先问自己:“是否可以通过反范式设计一张专门的查询表来解决?” 多数情况下,手动设计查询表比二级索引更高效可靠。


5. 二级索引的性能考量与陷阱

5.1 全集群扫描问题

除非在查询中同时提供了分区键,否则使用二级索引的查询会变成全集群扫描。协调节点向所有节点发送请求,网络开销和数据扫描量可能呈线性增长。

解决方案:尽量在查询中加入partition key。例如:

-- users 表分区键为 (country)
-- city 上建索引
SELECT * FROM users WHERE country = 'UK' AND city = 'London';

这样请求只会路由到包含 country='UK' 的节点,索引扫描大大缩小。

5.2 索引列更新开销

Cassandra 的写入路径已经很快,但二级索引会增加额外的写入负担。每次更新被索引列的值,都会导致索引表的隐式删除+新增操作,造成写入放大。

5.3 内存与磁盘膨胀

每个索引会占用额外的磁盘空间和内存(用于缓存)。过多的索引会显著增加存储成本,并降低节点性能。

5.4 索引状态与重建

在节点故障恢复、数据量增长后,索引可能会出现不一致。可以通过重新重建索引来修复:

nodetool rebuild_index <keyspace> <table> <index_name>

6. SASI 索引:现代二级索引的进化

从 Cassandra 3.4 开始(以及 DataStax Enterprise 5.1),引入了 SASI 索引,它解决了传统二级索引的很多痛点。

特点

  • 全局分布:SASI 索引不再只本地化,它维护了基于 B+ 树的全局索引结构,可以更快地定位数据所在的节点。
  • 支持范围查询LIKE 前缀匹配、CONTAINS 等高级操作。
  • 内存/磁盘效率更高:SASI 基于 SSTable 构建,随 Compaction 增量更新。

创建 SASI 索引(使用 CUSTOM 关键字):

CREATE CUSTOM INDEX sasi_users_email ON users (email)
USING 'org.apache.cassandra.index.sasi.SASIIndex';

执行高级查询

SELECT * FROM users WHERE email LIKE 'john%';     -- 前缀查询
SELECT * FROM users WHERE age >= 18 AND age <= 30; -- 范围查询

注意:SASI 虽然强大,但仍然有维护成本和性能开销,不是银弹。其底层仍可能涉及多节点读取,只是比传统索引高效很多。在开源 Cassandra 中,SASI 一度因维护问题被标记为实验性,使用前请确认你的版本和兼容性。


7. 设计与操作最佳实践

  1. 永远先分析数据模型与查询模式。二级索引是弥补模型漏洞的工具,而非首选方案。
  2. 严格控制索引数量,每张表尽量不超过 1-2 个二级索引。
  3. 将查询限制为“分区键 + 索引列” 的组合以消除全集群扫描。
  4. 使用自定义索引名,便于运维管理。
  5. 监控索引带来的延迟与 IO 开销。可使用 nodetool tablestats 查看索引请求情况。
  6. 对于频繁变化的列,考虑使用物化视图或手动维护的表,而不是二级索引。
  7. 在生产环境使用前,务必进行充分的压力测试,模拟真实数据量与并发。

8. 常见问题排查

现象 可能原因 解决思路
带索引的查询极慢 高基数索引导致扫描大量分区,或索引列值分布不均 重新设计查询表;或添加分区键限定范围
索引未被使用 查询条件不符合索引使用规则(如使用范围、不等于) 检查执行计划:TRACING ON 然后执行查询
索引表空间激增 索引列基数高或更新频繁 删除不必要索引,评估物化视图
部分节点索引不一致 节点曾故障,索引未正确重建 执行 nodetool rebuild_index

9. 实战演练

假设你有一个电商订单表:

CREATE TABLE orders (
    order_id UUID,
    customer_id UUID,
    status TEXT,
    order_date DATE,
    total DECIMAL,
    PRIMARY KEY ( (customer_id), order_date, order_id )
);

如果要查询所有状态为 'SHIPPED' 的订单,显然我们无法只用主键高效完成。此时评估:status 基数值较小(几种状态),适合二级索引。

创建索引:

CREATE INDEX idx_orders_status ON orders (status);

查询时注意尽可能带上 customer_id

SELECT * FROM orders WHERE customer_id = ? AND status = 'SHIPPED';