4. 索引优化策略:最左前缀原则、覆盖索引、索引下推、索引选择性
索引是数据库性能调优的核心武器,但错误地使用索引反而会拖垮查询。本章深入剖析四个关键索引优化策略,帮助你在实际场景中精准设计索引,避免“索引失效”陷阱。
4.1 最左前缀原则(Leftmost Prefix Principle)
最左前缀原则是联合索引(Composite Index)使用的核心规则。它规定:查询条件必须从联合索引的最左列开始,并且不能跳过中间的列,否则索引无法被完整使用。
核心定义: 对于联合索引 (a, b, c),查询条件必须包含 a 列,且遵循从左到右的匹配顺序。跳过的列会导致后续列索引失效。
4.1.1 联合索引匹配规则表
| 查询条件 | 索引使用情况 | 说明 |
|---|---|---|
WHERE a = 1 |
使用索引 a 列 | 完全匹配最左列 |
WHERE a = 1 AND b = 2 |
使用索引 a、b 列 | 连续匹配前两列 |
WHERE a = 1 AND b = 2 AND c = 3 |
使用索引全部三列 | 完全匹配所有列 |
WHERE b = 2 |
不使用索引 | 跳过了最左列 a |
WHERE a = 1 AND c = 3 |
仅使用索引 a 列 | 跳过了 b 列,c 列索引失效 |
WHERE b = 2 AND c = 3 |
不使用索引 | 跳过了最左列 a |
实战建议: 设计联合索引时,将等值查询的列放在最左侧,范围查询(如 >、<、BETWEEN)的列放在中间,排序的列放在最后。这样可以最大化索引利用率。
4.1.2 最左前缀原则的 SQL 示例
-- 创建联合索引
CREATE INDEX idx_abc ON orders (status, create_time, user_id);
-- ✅ 有效使用索引(完全匹配最左前缀)
SELECT * FROM orders WHERE status = 'PAID' AND create_time > '2024-01-01';
-- ✅ 有效使用索引(等值 + 范围 + 排序)
SELECT * FROM orders WHERE status = 'PAID' AND create_time > '2024-01-01' ORDER BY user_id;
-- ❌ 索引失效(跳过了 status 列)
SELECT * FROM orders WHERE create_time > '2024-01-01';
-- ❌ 索引部分失效(跳过了 create_time 列)
SELECT * FROM orders WHERE status = 'PAID' AND user_id = 1001;
4.2 覆盖索引(Covering Index)
覆盖索引是指查询所需的所有列都包含在索引中,无需回表(回到聚簇索引或数据行)获取数据。这是减少磁盘 I/O 和提升查询性能的利器。
性能收益: 覆盖索引可以避免回表操作,将查询完全在索引 B+ 树上完成,通常能减少 70%-90% 的磁盘 I/O。
4.2.1 覆盖索引 vs 非覆盖索引对比
| 对比维度 | 覆盖索引 | 非覆盖索引 |
|---|---|---|
| 查询过程 | 索引扫描 → 直接返回数据 | 索引扫描 → 回表查询 → 返回数据 |
| 磁盘 I/O | 仅索引页 I/O | 索引页 I/O + 数据页 I/O |
| 适用场景 | 高频查询、统计类查询 | 需要查询大量非索引列 |
| 索引设计 | 需要包含 SELECT 和 WHERE 中的所有列 | 只需包含 WHERE 条件列 |
-- 示例:覆盖索引设计
-- 表结构:orders (id, order_no, status, amount, create_time)
-- ❌ 非覆盖索引查询(需要回表)
CREATE INDEX idx_status ON orders (status);
SELECT order_no, amount FROM orders WHERE status = 'PAID';
-- 过程:索引找到 status='PAID' 的 id → 回表读取 order_no, amount
-- ✅ 覆盖索引查询(无需回表)
CREATE INDEX idx_status_covering ON orders (status, order_no, amount);
SELECT order_no, amount FROM orders WHERE status = 'PAID';
-- 过程:索引直接包含所有查询列,无需回表
优化技巧: 对于高频的 COUNT、SUM 等聚合查询,可以设计覆盖索引来避免全表扫描。例如:
SELECT COUNT(*) FROM orders WHERE status = 'PAID',如果索引包含 status 列,则直接在索引上完成计数。
4.3 索引下推(Index Condition Pushdown, ICP)
索引下推是 MySQL 5.6 引入的优化特性。它允许在索引遍历过程中,提前过滤不符合条件的记录,减少回表次数。ICP 对联合索引中无法使用最左前缀的列尤其有效。
工作原理: 在没有 ICP 时,存储引擎根据索引找到记录后,立即回表获取完整行数据,再由 Server 层进行 WHERE 条件过滤。启用 ICP 后,存储引擎在索引遍历阶段就应用部分 WHERE 条件,提前过滤掉不满足条件的记录。
4.3.1 ICP 优化效果对比
| 场景 | 无 ICP | 启用 ICP |
|---|---|---|
| 索引扫描行数 | 1000 行 | 1000 行 |
| 回表次数 | 1000 次(全部回表) | 200 次(仅满足条件的行回表) |
| Server 层过滤 | 200 行(回表后过滤) | 200 行(索引层已过滤) |
| 总 I/O 次数 | 1000 次回表 I/O | 200 次回表 I/O |
-- 示例:索引下推生效场景
-- 联合索引 (status, create_time)
-- 查询:status = 'PAID' AND create_time LIKE '2024-01%'
-- 无 ICP 时:
-- 1. 存储引擎通过 status='PAID' 找到 1000 条记录
-- 2. 全部回表获取完整行
-- 3. Server 层过滤 create_time LIKE '2024-01%',最终返回 200 条
-- 启用 ICP 时:
-- 1. 存储引擎通过 status='PAID' 找到 1000 条记录
-- 2. 在索引层直接过滤 create_time LIKE '2024-01%',仅 200 条满足
-- 3. 只对 200 条记录回表
注意事项: ICP 仅适用于 InnoDB 和 MyISAM 存储引擎,且只能用于二级索引(非聚簇索引)。对于主键索引或全表扫描,ICP 不会生效。可以通过
EXPLAIN 查看 Extra 列是否包含 Using index condition 来确认 ICP 是否启用。
4.4 索引选择性(Index Selectivity)
索引选择性是指索引列中不同值的数量与总行数的比值。选择性越高,索引过滤效果越好,查询效率越高。
计算公式: 选择性 = 基数(Cardinality)/ 总行数(Rows)
选择性越接近 1,索引效果越好;选择性越低,索引效果越差,甚至不如全表扫描。
选择性越接近 1,索引效果越好;选择性越低,索引效果越差,甚至不如全表扫描。
4.4.1 选择性评估表
| 选择性范围 | 评估等级 | 示例列 | 建议 |
|---|---|---|---|
| 0.9 - 1.0 | 优秀 | 身份证号、订单号、邮箱 | 非常适合建立索引 |
| 0.5 - 0.9 | 良好 | 用户名、手机号 | 适合建立索引 |
| 0.1 - 0.5 | 一般 | 状态(如 PAID/UNPAID) | 可考虑联合索引 |
| < 0.1 | 差 | 性别(男/女)、是否删除 | 不建议单独建索引 |
-- 查看索引选择性
SELECT
COUNT(DISTINCT column_name) AS cardinality,
COUNT(*) AS total_rows,
COUNT(DISTINCT column_name) / COUNT(*) AS selectivity
FROM table_name;
-- 示例:检查 status 列的选择性
SELECT
COUNT(DISTINCT status) AS cardinality, -- 假设只有 3 种状态
COUNT(*) AS total_rows, -- 假设 100 万行
COUNT(DISTINCT status) / COUNT(*) AS selectivity -- 0.000003,极低
FROM orders;
联合索引选择性优化: 将选择性高的列放在联合索引的左侧。例如索引 (user_id, status),user_id 选择性高,可以快速过滤大部分数据,status 选择性低但作为辅助过滤。避免将低选择性列单独建索引。
4.5 四种策略综合对比与选择
| 策略 | 核心目标 | 适用场景 | 性能收益 |
|---|---|---|---|
| 最左前缀原则 | 联合索引设计规范 | 多条件组合查询 | 避免索引失效 |
| 覆盖索引 | 消除回表 I/O | 高频查询、统计查询 | 减少 70%-90% I/O |
| 索引下推 | 减少回表次数 | 联合索引中无法完全匹配的列 | 减少 50%-80% 回表 |
| 索引选择性 | 评估索引过滤效果 | 索引列选择与排序 | 提升索引效率 10-100 倍 |
常见误区: 不要为了覆盖索引而盲目添加过多列到索引中。索引列越多,索引体积越大,写入和更新性能会下降。建议覆盖索引只包含查询最频繁的 3-5 列。
4.6 索引优化策略流程图
4.7 实战案例:订单查询优化
假设有一个订单表 orders,包含 1000 万行数据,常见查询如下:
-- 原始查询(性能差,全表扫描)
SELECT order_no, amount, status
FROM orders
WHERE status = 'PAID' AND create_time > '2024-06-01'
ORDER BY amount DESC
LIMIT 100;
-- 优化后的索引设计
-- 1. 分析查询列:order_no, amount, status(SELECT 列)
-- 2. 分析条件列:status(等值),create_time(范围)
-- 3. 分析排序列:amount(排序)
-- 最优联合索引(覆盖索引 + 最左前缀)
CREATE INDEX idx_covering ON orders (status, create_time, amount, order_no);
-- 优化效果:
-- ✅ 最左前缀:status 等值匹配 → create_time 范围匹配
-- ✅ 覆盖索引:所有查询列都在索引中,无需回表
-- ✅ 索引选择性:status 选择性低但放在最左,create_time 选择性高
-- ✅ 排序优化:amount 在索引中,避免 filesort
优化前后性能对比:
- 优化前:全表扫描 1000 万行,耗时 12.3 秒
- 优化后:索引范围扫描 5000 行,耗时 0.02 秒
- 性能提升:615 倍
- 优化前:全表扫描 1000 万行,耗时 12.3 秒
- 优化后:索引范围扫描 5000 行,耗时 0.02 秒
- 性能提升:615 倍
4.8 索引优化检查清单
- 最左前缀检查: 联合索引是否从最左列开始匹配?是否跳过了中间列?
- 覆盖索引检查: SELECT 和 WHERE 中的所有列是否都在索引中?能否消除回表?
- 索引下推检查: 对于无法完全匹配的联合索引列,是否启用了 ICP?EXPLAIN 是否显示
Using index condition? - 选择性检查: 索引列的选择性是否大于 0.1?低选择性列是否避免了单独建索引?
- 冗余索引检查: 是否存在重复或重叠的索引?例如 (a, b) 和 (a) 同时存在时,(a) 是冗余的。
- 排序优化检查: ORDER BY 列是否在索引中?是否避免了 filesort?
日常巡检命令: 使用
EXPLAIN 分析慢查询,重点关注 type(是否为 range/ref)、Extra(是否包含 Using index、Using index condition、Using filesort)和 rows(扫描行数是否合理)。