Skip to content

索引的合理设置


在软件开发的世界里,数据库索引常被视作提升查询速度的“灵丹妙药”。许多开发者一遇到慢查询,第一反应就是:“加个索引试试!”于是,字段上索引如雨后春笋般冒出——姓名、状态、创建时间、甚至逻辑删除标志(如 is_deleted = 0)……仿佛索引越多,系统就越快。

然而,现实往往令人意外:系统不仅没变快,反而越来越慢,磁盘空间疯狂增长,写入操作频频超时,甚至出现主从延迟加剧、CPU 使用率飙升等问题。这究竟是为什么?

问题的根源在于:索引是一把锋利的双刃剑——用得好,如虎添翼;用得滥,反噬系统。它并非“越多越好”,而是需要在查询加速与系统开销之间取得精妙的平衡。

下面将深入探讨数据库索引的本质,揭开它作为“双刃剑”的真相,并提供一套科学、实用、可落地的索引设计方法论

索引确实能加速查询,但它并非免费午餐。每一次索引的创建,都伴随着以下五大隐性成本,这些成本在高并发、大数据量场景下尤为致命:

每当执行 INSERTUPDATEDELETE 操作时,数据库不仅要修改数据本身,还必须同步更新所有相关索引

  • 5个索引?更新5次。
  • 20个索引?更新20次!

在 InnoDB 引擎中,每个索引本质上是一棵 B+ 树。写入时,数据库需要对每棵树进行插入、分裂或合并操作,这些操作涉及磁盘 I/O 和 CPU 计算。
在高并发写入场景下(如订单创建、日志写入、用户行为埋点),这种开销可能让系统吞吐量下降 50% 甚至更多
更严重的是,索引越多,事务提交(commit)的延迟越高,直接影响用户体验和系统 SLA。

📌 案例:某电商平台在商品表上为 statuscategory_idcreate_timeis_deleted 等字段单独建了 8 个单列索引。结果在大促期间,订单写入延迟从 10ms 暴涨至 300ms,最终不得不紧急下线部分冗余索引。

每个索引都是对数据的冗余存储。对于大表而言,索引总大小甚至可能超过原始数据本身
例如:

  • 一张 100GB 的用户行为日志表;
  • 若为 user_idaction_typetimestampdevice_id 等字段分别建立单列索引;
  • 再加上几个 (user_id, timestamp)(action_type, is_deleted) 等复合索引;
  • 索引总大小轻松突破 150GB,甚至达到 200GB

这不仅浪费昂贵的 SSD 存储资源,还带来连锁反应:

  • 备份时间翻倍:全量备份从 2 小时延长到 4 小时;
  • 恢复时间不可控:灾难恢复窗口大幅延长;
  • 云数据库成本飙升:按存储计费的云服务(如 AWS RDS、阿里云 RDS)账单显著增加。

MySQL 的查询优化器(Query Optimizer)需要在多个索引间权衡执行计划。索引越多,评估路径越复杂,反而可能选中次优甚至错误的执行计划,导致查询更慢。

具体表现包括:

  • 索引合并(Index Merge)失效:优化器本可使用多个索引合并扫描,但因成本估算偏差而放弃;
  • 错误选择低选择性索引:例如对 is_deleted(只有 0/1 两个值)建索引,优化器误判其“高效”,导致全索引扫描;
  • 统计信息过期:索引过多导致 ANALYZE TABLE 频繁失效,优化器基于陈旧统计做出错误决策。

💡 提示:可通过 EXPLAIN FORMAT=JSON 查看优化器为何选择某个索引,以及是否考虑了其他候选索引。

InnoDB 的 Buffer Pool 是数据库性能的核心缓存区,既要缓存数据页(Data Pages),也要缓存索引页(Index Pages)。
当索引数量激增时,大量 Buffer Pool 空间被低效或冷门索引占据,导致:

  • 热点业务数据无法常驻内存;
  • 频繁触发磁盘读取(Disk I/O);
  • 查询响应时间波动剧烈,尤其在缓存未命中时。

在内存有限的实例上(如 16GB RAM 的云数据库),这个问题尤为突出。一个被过度索引的表,可能让整个实例的缓存命中率从 99% 跌至 80% 以下

索引泛滥直接拉高系统运维成本:

  • DDL 操作耗时剧增ALTER TABLE ADD COLUMNMODIFY COLUMN 需要重建整张表及其所有索引。100GB 表 + 10 个索引,重建可能耗时数小时;
  • 在线 DDL 风险加大:即使使用 ALGORITHM=INPLACE,索引越多,锁表时间越长,主从延迟风险越高;
  • 备份与恢复变慢:物理备份(如 XtraBackup)和逻辑备份(如 mysqldump)都因数据量增大而效率下降;
  • 索引碎片累积:频繁更新导致 B+ 树节点分裂,产生大量碎片,需定期执行 OPTIMIZE TABLE(或 ALTER TABLE ... ENGINE=InnoDB)来整理,而该操作本身又是一次昂贵的重建。

⚠️ 现实困境:很多团队因“不敢动索引”而陷入恶性循环——索引越来越多,系统越来越慢,但又因担心影响线上业务而不敢清理。

要科学使用索引,必须先理解它的底层机制。在主流关系型数据库(如 MySQL 的 InnoDB 引擎)中,索引并非魔法,而是一种精心设计的数据结构——B+ 树(B+ Tree)

  • 叶子节点存储实际数据(或主键):在 InnoDB 中,主键索引(聚簇索引)的叶子节点直接包含整行数据;而二级索引(非聚簇索引)的叶子节点只存储主键值。
  • 非叶子节点仅作导航:它们保存索引键的分界值,用于快速定位目标区间。
  • 所有叶子节点通过指针串联:这使得范围查询(如 WHERE create_time BETWEEN ...)极其高效,只需一次树遍历 + 顺序扫描。

B+ 树高度通常只有 34 层,即使亿级数据,也能在 **34 次磁盘 I/O 内定位到目标**。

当查询所需的所有字段都包含在某个索引中时,数据库无需回表查询主键索引,直接从索引中返回结果——这就是覆盖索引(Covering Index)

-- 假设有一个复合索引 idx_user_time_status (user_id, create_time, status)
SELECT user_id, create_time, status
FROM orders
WHERE user_id = 123 AND create_time > '2024-01-01';

此查询完全命中覆盖索引,零回表、零随机 I/O,性能极佳。但如果查询 SELECT user_id, amount,而 amount 不在索引中,则必须回表查主键索引,性能大打折扣。

💡 设计建议:对高频查询,尽量将 SELECT 字段纳入复合索引,实现覆盖。

复合索引 (A, B, C) 并非对任意包含 A/B/C 的查询都有效,它遵循最左前缀匹配原则

查询条件是否命中索引
WHERE A = 1
WHERE A = 1 AND B = 2
WHERE A = 1 AND B = 2 AND C = 3
WHERE B = 2❌(跳过 A)
WHERE C = 3
WHERE B = 2 AND C = 3
WHERE A = 1 AND C = 3⚠️ 仅用到 A,C 无法利用索引
  1. 复合索引的字段顺序至关重要!应将高选择性、高频过滤、等值查询的字段放前面,范围查询字段放最后。
  2. 复合索引已包含单列索引的功能,通常可以删除单列索引,但在极端性能或覆盖索引场景下仍可能有用,删除前建议用 EXPLAIN 验证查询是否正常走复合索引。

盲目建索引是灾难,但完全不用索引更是低效。以下是经过大量生产验证的五大索引设计原则,助你精准发力:

3.1. 只给“高频 + 高选择性”字段建索引

Section titled “3.1. 只给“高频 + 高选择性”字段建索引”
  • 高频:该字段频繁出现在 WHEREJOINORDER BY 中;
  • 高选择性:字段值分布广,重复率低(如 user_id),而非只有几个值(如 genderis_deleted)。

如何衡量选择性?
选择性 = COUNT(DISTINCT col) / COUNT(*)
越接近 1,选择性越高。低于 0.01(1%)的字段通常不值得单独建索引。

  • 一个 (user_id, status, create_time) 索引,可同时支持:
    • WHERE user_id = ?
    • WHERE user_id = ? AND status = ?
    • WHERE user_id = ? AND status = ? ORDER BY create_time
  • 而三个单列索引不仅占用更多空间,还可能触发低效的 Index Merge。

80% 的查询可通过 2~3 个精心设计的复合索引覆盖。

3.3. 避免为逻辑删除字段单独建索引

Section titled “3.3. 避免为逻辑删除字段单独建索引”

is_deleted = 0 这类字段通常只有两个值,选择性极低。单独建索引几乎无效,反而拖累写入。

正确做法:将其作为复合索引的最后一列,用于过滤已删除数据:

-- 好:(user_id, is_deleted) — 先按 user_id 定位,再过滤 is_deleted
-- 坏:单独建 is_deleted 索引

3.4. 警惕“隐式类型转换”导致索引失效

Section titled “3.4. 警惕“隐式类型转换”导致索引失效”
-- user_id 是 INT 类型
SELECT * FROM users WHERE user_id = '123'; -- 字符串 '123'

看似无害,但 MySQL 会将 user_id 转为字符串比较,导致索引失效,全表扫描!

解决方案

  • 应用层确保传参类型一致;
  • 开启 sql_mode=STRICT_TRANS_TABLES 避免隐式转换;
  • 使用 EXPLAIN 验证执行计划。

3.5. 定期审计与清理“僵尸索引”

Section titled “3.5. 定期审计与清理“僵尸索引””

很多索引上线后从未被使用,却持续消耗资源。这类“僵尸索引”必须清理。

如何识别?

  • MySQL 5.6+:启用 performance_schema,查询 table_io_waits_summary_by_index_usage
SELECT
object_schema AS '数据库',
object_name AS '表名',
index_name AS '无用索引'
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL AND count_star = 0;
  • 或使用 pt-index-usage(Percona Toolkit 工具)分析慢日志;

  • 云数据库(如阿里云、AWS)通常提供“未使用索引”监控面板。

操作建议:每季度进行一次索引健康检查,下线 30 天内零使用的索引。

4. 实战从慢查询到高效索引的完整闭环

Section titled “4. 实战从慢查询到高效索引的完整闭环”

假设你收到一条慢查询告警:

SELECT order_id, amount, status
FROM orders
WHERE user_id = 10086
AND create_time > '2024-06-01'
AND is_deleted = 0
ORDER BY create_time DESC
LIMIT 20;

Step 1:分析执行计划

EXPLAIN FORMAT=JSON
SELECT ... ; -- 发现全表扫描,未命中任何索引

Step 2:评估字段特性

  • user_id:高选择性(亿级用户中唯一)
  • create_time:范围查询,需排序
  • is_deleted:低选择性,但用于过滤软删除

Step 3:设计复合索引

ALTER TABLE orders
ADD INDEX idx_user_time_del (user_id, is_deleted, create_time);
  • 为什么 is_deleted 在中间?因为它是等值条件(=0),而 create_time 是范围(>)和排序字段,必须放最后。
  • 此索引支持覆盖查询(若 order_id, amount, status 也在索引中,可进一步优化)。

Step 4:验证效果

  • 执行时间从 2.3s → 15ms;
  • EXPLAIN 显示 type=ref, Extra=Using index condition; Using filesort(若需消除 filesort,可考虑 (user_id, is_deleted, create_time DESC),但 MySQL 8.0+ 才支持降序索引)。

Step 5:监控与迭代

  • 观察 7 天,确认无副作用;
  • 将该模式推广到同类查询(如 refund_orders 表)。

数据库性能优化从来不是“加索引”这么简单。它是一门平衡的艺术——在查询速度、写入成本、存储开销与运维复杂度之间寻找最优解。切记好的索引,是沉默的加速器;坏的索引,是隐形的性能杀手。

索引问题往往具有滞后性和隐蔽性——系统上线时风平浪静,三个月后却因索引膨胀导致主从延迟、CPU 飙升。主动监控与周期治理,是避免“索引雪崩”的关键防线

要有效治理索引,必须从以下三个维度建立监控体系:

维度关注点工具/方法
使用率哪些索引从未被查询使用?performance_schema.table_io_waits_summary_by_index_usage
效率哪些索引命中率低、扫描行数多?慢查询日志 + EXPLAIN 分析
资源消耗索引占用多少存储?写入开销多大?INFORMATION_SCHEMA.STATISTICS + SHOW TABLE STATUS

场景:某订单表有 12 个索引,但业务迭代后,部分索引已无用。

Step 1:启用 Performance Schema(MySQL 5.6+)

-- 确保开启
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES'
WHERE NAME = 'wait/io/table/sql/handler';
-- 查看索引使用统计
SELECT
OBJECT_SCHEMA AS db,
OBJECT_NAME AS table_name,
INDEX_NAME AS index_name,
COUNT_FETCH AS reads,
COUNT_INSERT + COUNT_UPDATE + COUNT_DELETE AS writes
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'your_db'
AND INDEX_NAME IS NOT NULL
ORDER BY reads ASC;
  • reads = 0writes > 0纯成本,无收益,可下线;

  • reads 极低(如 < 10 次/天),但 writes 极高 → 性价比极低,评估是否保留。

⚠️ 注意:重启 MySQL 后统计清零,建议持续观察 7~30 天。

Step 2:使用 Percona Toolkit(推荐)

Terminal window
# 分析慢查询日志,统计索引使用情况
pt-index-usage \
--host=your-db \
--user=monitor \
--password=xxx \
/var/log/mysql/slow.log
  • 输出示例
Unused indexes:
your_db.orders.idx_old_status
your_db.users.idx_gender

Step 3:安全下线流程

  1. 标记为“待观察”:先在监控系统中标记,不立即删除;

  2. 灰度验证:在测试环境模拟流量,确认无查询依赖;

  3. 低峰期执行:使用 ALGORITHM=INPLACE, LOCK=NONE(MySQL 5.6+)在线删除;

    ALTER TABLE orders DROP INDEX idx_old_status, ALGORITHM=INPLACE, LOCK=NONE;
  4. 回滚预案:保留 DDL 脚本,确保 5 分钟内可重建。

5.3. 自动化治理构建索引健康度评分

Section titled “5.3. 自动化治理构建索引健康度评分”

进阶团队可构建“索引健康度”指标,纳入 DevOps 流程:

# 伪代码:索引健康度 = 使用频率 × 选择性 / (存储成本 + 写入开销)
def calculate_index_health(index):
read_ratio = index.reads / total_queries
selectivity = distinct_values / total_rows
storage_cost = index.size_mb / table.size_mb
write_overhead = index.writes / table.total_writes
health = (read_ratio * selectivity) / (storage_cost + write_overhead + 0.01)
return health
  • 健康度 < 0.1 → 红色预警,建议下线;
  • 健康度 0.1~1.0 → 黄色,需人工复核;
  • 健康度 > 1.0 → 绿色,高效索引。

将该评分集成到 CI/CD 流水线,新索引上线前自动评估风险。

6. 高并发场景下的索引避坑指南

Section titled “6. 高并发场景下的索引避坑指南”

在高并发、高吞吐的系统中(如电商大促、支付系统、实时推荐),索引设计稍有不慎,就会引发雪崩式故障。以下是五大高频陷阱及应对策略。

6.1. 高频更新字段建索引导致锁竞争加剧

Section titled “6.1. 高频更新字段建索引导致锁竞争加剧”

场景:为 last_login_time 建索引,每用户登录即更新。

问题

  • 每次更新需修改 B+ 树,可能引发页分裂(Page Split)

  • InnoDB 的 Change Buffer 虽可缓冲二级索引变更,但在高并发下仍会频繁 merge,导致 I/O spike;

  • 更严重的是,热点页(Hot Page)竞争:大量线程争抢同一索引页的 latch,CPU sys% 飙升。

解决方案

  • 避免为高频更新字段单独建索引

  • 若必须查询,考虑:

    • 异步写入日志表 + 离线分析;

    • 使用 Redis 缓存最新登录时间;

    • 或将该字段放入复合索引末尾(减少更新频率)。

6.2. 范围查询放复合索引前面导致索引失效

Section titled “6.2. 范围查询放复合索引前面导致索引失效”

错误示例

-- 索引:(create_time, user_id)
SELECT * FROM logs
WHERE create_time > '2024-06-01' AND user_id = 123;

问题

  • 虽然 user_id 是等值条件,但因 create_time(范围)在前,无法利用 user_id 的过滤能力

  • 优化器只能按 create_time 扫描大量数据,再过滤 user_id,效率极低。

正确做法

-- 索引:(user_id, create_time)
-- 等值字段在前,范围字段在后

口诀等值优先,范围靠后,排序最后

6.3. ORDER BY与索引方向不一致退为filesort

Section titled “6.3. ORDER BY与索引方向不一致退为filesort”

场景

-- 索引:(user_id, create_time) -- 默认升序
SELECT * FROM orders
WHERE user_id = 123
ORDER BY create_time DESC
LIMIT 10;

问题

  • MySQL 5.7 及以下:无法利用索引逆序扫描,必须 Using filesort

  • filesort 在高并发下极易成为瓶颈(内存排序 or 磁盘排序)。

解决方案

  • MySQL 8.0+:创建显式降序索引;

    CREATE INDEX idx_user_time_desc ON orders (user_id, create_time DESC);
  • MySQL 5.7 及以下

    • 接受 filesort(若数据量小);

    • 或应用层缓存最新 N 条记录;

    • 或冗余存储“倒序时间戳”(如 reverse_ts = MAX_TS - create_time)。

6.4. IN 查询过多值导致优化器放弃索引

Section titled “6.4. IN 查询过多值导致优化器放弃索引”

场景

SELECT * FROM products WHERE category_id IN (1,2,3,...,500);

问题

  • IN 列表过长(如 > 100 项),MySQL 可能认为“全表扫描比多次索引查找更快”,主动放弃索引

  • 尤其当 category_id 选择性不高时,更易发生。

解决方案

  • 控制 IN 列表长度(建议 ≤ 50);

  • 改用 临时表 JOIN

    CREATE TEMPORARY TABLE tmp_cats (id INT PRIMARY KEY);
    INSERT INTO tmp_cats VALUES (1),(2),...,(500);
    SELECT p.* FROM products p JOIN tmp_cats t ON p.category_id = t.id;
  • 或使用 应用层分批查询 + 合并结果

现象:主库写入正常,但从库延迟持续增长。

根因

  • 从库单线程回放 binlog(即使 MySQL 5.7+ 有并行复制,仍受限于事务依赖);

  • 主库上一个 INSERT 触发 10 个索引更新 → 从库需顺序执行 10 次索引维护;

  • 索引越多,从库回放越慢

缓解措施

  • 精简非必要索引,尤其从库不需要的分析型索引;

  • 对从库关闭部分二级索引(需业务允许);

  • 升级到 MySQL 8.0,利用 WriteSet 并行复制 提升回放吞吐。

MySQL 提供 Performance Schema 监控索引使用情况:

SELECT
object_schema AS '数据库',
object_name AS '表名',
index_name AS '无用索引'
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL AND count_star = 0;

当然可以!以下是紧接前文的第七章,聚焦分库分表、分布式数据库等现代架构下的索引新挑战与应对策略,延续实战风格,提供可直接落地的技术方案。

7. 从单机到分布式聚焦分库分表下的索引新范式

Section titled “7. 从单机到分布式聚焦分库分表下的索引新范式”

当业务规模突破单机数据库瓶颈,走向分库分表(Sharding)或分布式数据库(如 TiDB、OceanBase、CockroachDB),传统索引设计逻辑将面临根本性重构。许多在单机上“正确”的做法,在分布式环境下反而成为性能陷阱。

本章将揭示三大核心挑战,并提供经过亿级流量验证的索引设计范式。

7.1. 全局查询失效——跨分片索引的天然缺陷

Section titled “7.1. 全局查询失效——跨分片索引的天然缺陷”

在分库分表架构中(如使用 ShardingSphere、MyCat 或自研中间件),数据按分片键(如 user_id)分散到多个物理库/表。

问题

若查询条件不包含分片键,例如:

SELECT * FROM orders
WHERE create_time > '2024-06-01' AND status = 'paid';

而分片键是 user_id,则系统必须广播查询到所有分片(即“全路由”),再合并结果。

即使每个分片都有 (create_time, status) 索引,整体性能仍极差:

  • 100 个分片 → 100 次并行查询;

  • 每个分片返回 1 万行 → 应用层需合并 100 万行;

  • 网络、内存、GC 压力剧增。

解决方案:构建“二级索引路由表”

思路:将非分片键的查询条件,映射回分片键,避免全路由。

实现方式

  1. 冗余索引表(Index Table)
    创建一张全局索引表,记录 (create_time, status) → user_id 的映射:

    CREATE TABLE order_time_status_idx (
    create_time DATETIME,
    status VARCHAR(20),
    user_id BIGINT,
    order_id BIGINT,
    PRIMARY KEY (create_time, status, user_id)
    ) ENGINE=InnoDB;
    • 写入订单时,同步写入此索引表;
    • 查询时,先查索引表获取 user_id 列表,再按 user_id 精准路由到对应分片。
  2. 使用 Elasticsearch / Doris 做异构索引

    • 将订单数据实时同步到 ES;

    • 复杂查询走 ES,结果返回 order_id

    • 再回 MySQL 获取完整数据(或直接从 ES 返回)。

适用场景

  • 查询频率高、但无法按分片键过滤的场景(如运营后台、风控系统);
  • 对数据一致性要求可接受秒级延迟。

7.2. 复合索引顺序与分片键的协同设计

Section titled “7.2. 复合索引顺序与分片键的协同设计”

在分库分表中,分片键必须作为复合索引的最左列,否则无法发挥局部索引优势。

错误示例

-- 分片键:user_id
-- 索引:(create_time, user_id)

即使查询包含 user_id,因 create_time 在前,无法利用分片局部性,仍可能扫描整个分片。

正确做法

-- 索引:(user_id, create_time, status) ✅
  • 先按 user_id 定位到具体分片和索引子树;

  • 再在该分片内高效过滤 create_timestatus

黄金法则
所有高频查询的复合索引,必须以分片键开头
若存在多个分片维度(如 user_id + region_id),则需按业务主路径设计主分片键。

7.3. 分布式数据库中的索引成本被放大

Section titled “7.3. 分布式数据库中的索引成本被放大”

在 TiDB、CockroachDB 等 NewSQL 数据库中,数据以 Region 形式分布在多个节点,每个索引都是一张独立的 KV 表

隐性成本

  • 每个索引都会产生额外的 Raft 日志、副本同步、Region Split 开销;

  • 索引越多,写入放大(Write Amplification)越严重

  • 热点索引可能导致 Region Leader 倾斜,引发单节点 CPU 瓶颈。

优化策略

  1. 严格控制索引数量
    TiDB 官方建议:单表索引 ≤ 5 个,复合索引字段 ≤ 3 个。

  2. 优先使用聚簇索引(Clustered Index)
    TiDB 5.0+ 支持聚簇索引(类似 InnoDB 主键聚簇):

    CREATE TABLE orders (
    user_id BIGINT,
    order_id BIGINT,
    ...
    PRIMARY KEY (user_id, order_id)
    ) CLUSTERED;
    • 将高频查询字段作为主键的一部分,避免回表;

    • 减少二级索引数量。

  3. 利用 TiDB 的 Index Merge 与 Point Get 优化

    • IN 查询,TiDB 可自动转换为多个 Point Get(高效);

    • 但需确保 IN 列表中的值能命中索引前缀。

  4. 监控索引写入延迟

    通过 Grafana 面板观察:

    • raftstore.apply log duration

    • scheduler.pending commands

    • 若某索引对应 Region 延迟高,考虑下线或重构。

7.4. 电商订单系统的分布式索引演进

Section titled “7.4. 电商订单系统的分布式索引演进”

背景:某平台日订单量 5000 万,采用 1024 分片,分片键 user_id % 1024

初期问题

  • 运营需按 create_time 查询“昨日所有支付订单”;

  • 全路由查询,耗时 30s+,拖垮数据库。

演进路径

阶段方案效果
V1强制要求所有查询带 user_id业务不可接受
V2为每个分片建 (create_time, status) 索引全路由仍慢,写入延迟高
V3引入 ES 异构索引查询降至 200ms,但数据延迟 10s
V4构建轻量级索引表 + Binlog 同步查询 80ms,延迟 < 1s,成本可控

V4 架构细节

  • 使用 Canal / Flink CDC 捕获 MySQL Binlog;

  • 写入 Kafka → Flink 聚合 → 写入 order_global_idx 表(TiDB 集群);

  • 查询流程:

    List<Long> orderIds = globalIndexMapper.findByTimeAndStatus(...);
    Map<ShardKey, List<Long>> routed = routeByUserId(orderIds); // 按 user_id 路由
    List<Order> results = parallelQuery(routed); // 并行查各分片

成果:全局查询 P99 < 150ms,写入吞吐提升 40%,索引存储减少 60%。

在分布式时代,索引的角色正在从“单机查询加速器”演变为“数据访问的智能路由图”。它不再只是 B+ 树,而是:

  • 分片系统的导航坐标

  • 异构存储的连接桥梁

  • 成本与性能的动态平衡器

索引设计自查清单(可直接用于 Code Review)

  1. 是否包含分片键(分布式场景)?
  2. 是否遵循“等值在前,范围在后”?
  3. 是否实现覆盖索引,避免回表?
  4. 选择性是否 > 1%?
  5. 是否被慢查询或业务高频使用?
  6. 是否有监控证明其价值?
  7. 删除后是否有回滚预案?

至此,完成了从单机索引原理高并发避坑分布式治理的完整闭环。

如需继续深入,可拓展以下方向:

  • 《索引与缓存的协同设计:Redis + MySQL 联合优化》
  • 《向量索引入门:AI 时代数据库的新战场》
  • 《云原生数据库(PolarDB、Aurora)的索引自动优化实践》