Apache Cassandra 二级索引
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子句中单独以相等条件出现,或与分区键组合使用时,索引才会被触发。 - 对于范围查询、
CONTAINS、LIKE等操作,传统二级索引往往不支持或性能极差(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_id、email、timestamp),应使用专门设计的查询表或物化视图。 - 返回数百万行的查询,索引不适合大规模扫描。
- 频繁更新的列,索引维护开销大。
- 低延迟、高并发生产环境,不当的索引查询会导致压力扩散到整个集群。
黄金法则:在创建二级索引前,先问自己:“是否可以通过反范式设计一张专门的查询表来解决?” 多数情况下,手动设计查询表比二级索引更高效可靠。
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 个二级索引。
- 将查询限制为“分区键 + 索引列” 的组合以消除全集群扫描。
- 使用自定义索引名,便于运维管理。
- 监控索引带来的延迟与 IO 开销。可使用
nodetool tablestats查看索引请求情况。 - 对于频繁变化的列,考虑使用物化视图或手动维护的表,而不是二级索引。
- 在生产环境使用前,务必进行充分的压力测试,模拟真实数据量与并发。
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';