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)。

关联