19.9 二级索引与分区
一句话
数据按 A 分区、却要按 B 查询,就必须建二级索引;而索引本身也要分区——本地索引读放大(scatter-gather),全局索引写放大(跨分区更新),没有两全,只有取舍。
两种索引分区
| 类型 | 索引存在哪 | 查 B 的代价 | 写 A 的代价 |
|---|---|---|---|
| 本地索引(local index) | 与数据同分区 | 查 N 个分区再合并(scatter-gather) | 低(同分区写) |
| 全局索引(global index) | 按索引键单独分区 | 一次查询命中一个索引分区 | 高(写要更新另一个分区,通常异步) |
| 维度 | 本地索引 | 全局索引 |
|---|---|---|
| 读延迟 | 随分片数线性增加(合并 N 份) | 低(点查) |
| 写延迟 | 低 | 高(跨分区/两阶段) |
| 一致性 | 易保证(同事务) | 通常最终一致(异步回填) |
| 唯一约束 | 无法做全局唯一 | 可以做(但要跨分区事务或专用服务) |
| 典型实现 | HBase/ Cassandra 的本地二级索引 | 全局索引表、ES 同步 |
一致性:索引滞后是常态
| 方案 | 滞后 | 风险 |
|---|---|---|
| 同事务写索引(本地) | 无 | 写放大、跨分区不可用 |
| 异步回填(CDC/消息) | 毫秒–秒 | 读到 “数据已改但索引未更新” |
| 双写 + 对账 | 取决于对账周期 | 漏写/重复写 |
工程上的常见做法是 “写主表 + 发变更事件 → 索引消费”,并接受短暂滞后;查询侧用 “版本号/时间戳” 处理 “索引命中但数据已删”。
范围查询:本地索引的痛
按 B 的范围查询(WHERE b BETWEEN x AND y)在本地索引下必须打所有分区,再把结果按 B 归并排序。分片数 100 时,一次查询可能变成 100 次子查询 + 归并;延迟由最慢分片决定(长尾放大)。
缓解:路由表预计算(哪些分区的 B 落在区间内)、按 B 做二次分片、或把这类查询交给专门的检索系统(见 45.1 倒排索引与分词)。
代码:全局索引表的读写
-- 索引表按 order_no 哈希分区,指向数据分区与主键
CREATE TABLE idx_order_no (
order_no VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL, -- 用于回表定位分区
order_id BIGINT NOT NULL,
PRIMARY KEY (order_no)
);
-- 写:与主表放在同一逻辑事务里(同库),或用 CDC 异步补
BEGIN;
INSERT INTO orders(user_id, order_id, ...) VALUES (...);
INSERT INTO idx_order_no(order_no, user_id, order_id) VALUES (...);
COMMIT;
-- 读:先查索引,再回表(两步,但都是点查)
SELECT user_id, order_id FROM idx_order_no WHERE order_no = ?;
反模式
| 反模式 | 后果 |
|---|---|
| 用本地索引实现 “全局唯一” | 无法保证:两个分区可同时插入同一个值 |
| 全局索引强一致双写 | 跨分区事务,写放大与阻塞 |
| 索引异步更新却不监控滞后 | 搜索结果缺数据,用户投诉 |
| 索引表也按数据分区 | 查询仍要 scatter-gather,白建 |
| 二级索引字段基数很低 | 索引分区倾斜,热点重现 |
| 全局索引与主表不在同一事务 | 读写交叉时出现“查得到但不存在” | | 索引字段频繁更新 | 写放大 + 索引碎片,查询反而变慢 |
三条铁律
🔑 本地索引读贵、全局索引写贵;按 “读多写少还是写多读少” 选。
🔑 全局唯一必须走全局索引或专用服务,本地索引给不了。
🔑 异步索引要监控滞后,并定义 “可接受的陈旧窗口”。
自检
Q1 按 user_id 分区,却要按 order_no 精确查询,怎么设计?
▸ 答案:建全局索引:order_no → user_id(或直接 → 分区+主键),按 order_no 哈希分区;查询先查索引拿定位,再回表。写入时要在同一逻辑事务里更新索引(或用 CDC 异步补偿),接受短暂不一致。
Q2 为什么本地索引不能保证全局唯一?
▸ 答案:因为唯一性检查要在全局范围内判断 “这个值是否已存在”,而本地索引只能看到本分区。两个分区同时插入相同值,各自检查都通过——约束被打破。要全局唯一,必须把 “判断” 收敛到单点(全局索引分区/专用服务/共识)。
Q3 100 个分片,一次范围查询变 100 次子查询,怎么优化?
▸ 答案:三条路:① 预计算路由:维护 “B 的区间 → 分区” 映射,只查相关分区;② 按 B 重排(CQRS/物化视图),把范围查询变成一次点查;③ 把这类查询交给搜索引擎(倒排 + 排序),主库只负责写。
Q4 索引异步更新导致 “查到已删除的数据”,怎么处理?
▸ 答案:查询侧做二次校验:索引返回候选集后,回主表确认存在性与版本;或用软删除 + 版本号,索引滞后时返回 “已失效”。彻底方案是缩短滞后(同步/近实时)并监控。
Q5 什么时候可以不建二级索引?
▸ 答案:当查询模式只按分区键(点查 + 按分区键范围)时;或者该查询频率极低、可以离线跑(全扫描 + 批处理)。二级索引是 “为查询模式付出的写放大”,没有对应查询就不该建。
Q7 索引表与主表同库同事务,为什么还会有 “查到但不存在”?
▸ 答案:因为读可能发生在索引已提交、主表却因主从延迟尚未可见的时刻(读到从库)。同库同事务只保证原子性,不保证 “读路径看到的是同一时刻”。要消除它,需要读主库、或用快照读(见 25.2 MVCC)。