我建了 a、b、c 的联合索引,以为查这三列里哪个都能走索引,结果按 b 单独查时索引完全没用上、全表扫描慢成狗,我对着最左前缀排查了大半天的复盘
这是一个让我对数据库"联合索引最左前缀"刻骨铭心的故事。我有张大表,经常用 a、b、c 三个列做查询,为了加速,我建了一个联合索引 (a, b, c)。我当时很满意,心想"这下查 a、b、c 哪个都快了"。可上线后,有些查询慢得离谱。我把慢 SQL 拎出来 EXPLAIN 一看,大跌眼镜:那些只用 a 查的,确实走了索引、很快;可那些只用 b(或只用 c)查的,EXPLAIN 显示 type=ALL(全表扫描)、key=NULL(没用上任何索引)!我明明建了包含 b 和 c 的联合索引,为什么单独查 b、c 时,它视而不见?
我顺着"索引没用上"的线索深挖,才终于揭开真相,补上了我对数据库索引一个最核心的认知漏洞:问题的核心,是联合索引的"最左前缀(leftmost prefix)原则"。我一直想当然地以为,"联合索引 (a,b,c),就是分别给 a、b、c 都建了索引,查哪个都能用";可真相是:联合索引,不是"三个独立的索引",而是一个"把 (a, b, c) 这三列按顺序拼接起来,作为一个整体来排序"的索引。它就像一本"先按姓氏、再按名字、最后按出生日期"排序的通讯录——整本书,是先按姓氏排好序的;只有姓氏相同的人之间,才再按名字排序;只有姓名都相同的,才再按出生日期排。所以:这本"通讯录",对"按姓氏查"(用 a)、"按姓氏+名字查"(用 a+b)、"按姓氏+名字+生日查"(用 a+b+c),都非常高效(因为它就是这么排序的,能快速定位);可如果你"只按名字查"(只用 b)、或"只按生日查"(只用 c)——对不起,整本通讯录,并不是按名字(或生日)排序的!你只能从头到尾翻一遍(全表扫描),索引帮不上忙。这就是"最左前缀":联合索引,只有当查询条件从最左边的列(a)开始、连续地用到它的前缀(a、或 a+b、或 a+b+c)时,才能利用;一旦跳过了最左列 a(只用 b 或 c),整个联合索引就用不上了。我这才痛彻地明白:联合索引,不是"列的简单集合",而是"有严格顺序的有序结构";它的列顺序,至关重要;能不能用上它,取决于你的查询条件,是否匹配它的"最左前缀"。设计联合索引,绝不能随便把几个列堆在一起,而要精心设计列的顺序(把最常用、最适合等值匹配的列放最左边),并让查询条件去匹配这个前缀。
故障现场:跳过最左列,联合索引用不上
我把这个"索引视而不见"的现场,摊开给你看:
-- ✗ 灾难: 联合索引(a,b,c), 跳过最左列 a 查询, 索引用不上
CREATE INDEX idx_abc ON t (a, b, c); -- 联合索引(a, b, c)
-- ✓ 能用上索引的(从最左 a 开始, 连续前缀):
SELECT * FROM t WHERE a = 1; -- ✓ 用 a(最左前缀)
SELECT * FROM t WHERE a = 1 AND b = 2; -- ✓ 用 a, b
SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3; -- ✓ 用 a, b, c(全用上)
-- ✗ 用不上索引的(跳过了最左列 a):
SELECT * FROM t WHERE b = 2; -- ✗ 没有 a! 全表扫描(type=ALL, key=NULL)
SELECT * FROM t WHERE c = 3; -- ✗ 没有 a! 全表扫描
SELECT * FROM t WHERE b = 2 AND c = 3; -- ✗ 没有 a! 全表扫描
-- △ 部分用上(用了 a, 但 b 断了):
SELECT * FROM t WHERE a = 1 AND c = 3; -- △ 只能用到 a(b 缺失, c 用不上索引部分)
-- 为什么? 联合索引按 (a,b,c) 顺序"拼接排序", 像通讯录:
-- 先按 a 排序; a 相同的再按 b 排; a,b 相同的再按 c 排。
-- - 按 a 查 / a+b 查 / a+b+c 查: 走索引(它就是这么排的, 能定位)。
-- - 只按 b 查 / 只按 c 查: 整个索引不是按 b/c 排的 → 用不上 → 全表扫。
-- → 这就是"最左前缀原则": 必须从最左列开始、连续地用前缀。
-- 另一个坑: 范围查询会"截断"后续列的索引使用
SELECT * FROM t WHERE a = 1 AND b > 2 AND c = 3;
-- △ a 用上、b(范围)用上, 但 c 用不上索引!
-- 因为 b 是范围(>), b 相同的范围内 c 才有序; 范围之后的列, 索引失效。
-- 用 EXPLAIN 验证:
EXPLAIN SELECT * FROM t WHERE b = 2;
-- type=ALL(全表扫描), key=NULL(没用索引) → 露馅。
-- 根因: 联合索引按列顺序拼接排序、遵循最左前缀; 跳过最左列就用不上;
-- 范围查询会截断其后列的索引使用。
看着 EXPLAIN 里那个 type=ALL, key=NULL,我才算彻底想明白了根源。问题的核心,是联合索引的最左前缀原则:(a,b,c) 是按这个顺序拼接排序的,像通讯录先按 a 排、a 相同再按 b 排、a,b 相同再按 c 排。所以:按 a / a+b / a+b+c 查能走索引(它就是这么排的、能定位);只按 b 查、只按 c 查,整个索引不是按 b/c 排的,用不上、全表扫——这就是"最左前缀":必须从最左列开始、连续地用前缀。还有个范围查询的坑:a = 1 AND b > 2 AND c = 3,a 用上、b(范围)用上,但 c 用不上索引——因为 b 是范围,b 相同的范围内 c 才有序,范围之后的列索引失效。用 EXPLAIN 一看 type=ALL, key=NULL 就露馅了。归根结底:联合索引按列顺序拼接排序、遵循最左前缀;跳过最左列就用不上;范围查询会截断其后列的索引使用——这,就是根源。
第一件事:搞懂联合索引与最左前缀
定位到根源,我必须把"联合索引、最左前缀、索引为什么能加速"从根上彻底搞清楚:
联合索引 = 多列按顺序拼接的有序结构; 遵循最左前缀, 跳过最左列就失效
# 索引为什么能加速?
# - 索引是"排好序的数据结构"(B+树), 像字典/通讯录的有序排列。
# - 有序 → 能"二分查找"快速定位, 不用全表扫描。
# 联合索引(a,b,c)是怎么排序的?
# - 不是给 a、b、c 各建一个索引!
# - 而是把 (a,b,c) 当一个整体: 先按 a 排, a 相同按 b 排, a,b 相同按 c 排。
# - 类比: 先按姓、再按名、最后按生日排序的通讯录。
# 最左前缀原则(核心):
# - 只有查询从"最左列"开始、用"连续的前缀", 才能利用索引。
# - 能用: a / a,b / a,b,c。
# - 不能用: b / c / b,c(跳过了最左的 a)。
# - 部分用: a,c(只用到 a, b 断了 c 接不上)。
# 范围查询的"截断":
# - 遇到范围(> < between like'前缀%'), 该列能用, 但它"之后"的列用不上。
# - 所以: 等值列放前面, 范围列放最后, 能最大化索引利用。
# 其他让索引失效的常见情况:
# - 在列上用函数/运算: WHERE func(a)=... / a+1=... → 失效(见隐式转换篇)。
# - 隐式类型转换: 字符串列传数字 → 失效。
# - like '%xxx'(前缀模糊)→ 失效; like 'xxx%'(后缀模糊)可用。
# - OR 连接非索引列 → 可能失效。
# 怎么验证? EXPLAIN:
# - type: ALL=全表扫(差), ref/range=用了索引(好), const=最优。
# - key: 实际用的索引(NULL=没用上)。
# - rows: 预估扫描行数(越小越好)。
# 关键认知: 联合索引的"列顺序"决定一切; 查询要匹配"最左前缀"才走索引。
# 核心: 联合索引是多列按序拼接的有序结构, 遵循最左前缀(从最左列连续用前缀);
# 跳过最左列/范围列之后的列用不上; 等值列放前、范围列放后, 用 EXPLAIN 验证。
原理终于清晰了。索引为什么能加速?——索引是"排好序的数据结构"(B+树),有序就能二分查找快速定位、不用全表扫。联合索引 (a,b,c) 怎么排序?不是给 a、b、c 各建一个索引,而是把 (a,b,c) 当一个整体:先按 a 排、a 相同按 b 排、a,b 相同按 c 排(像先按姓、再按名、最后按生日排的通讯录)。最左前缀原则(核心):只有查询从"最左列"开始、用"连续的前缀"才能利用索引——能用 a/a,b/a,b,c;不能用 b/c/b,c(跳过了最左的 a);部分用 a,c(只用到 a)。范围查询的截断:遇到范围(> < between like'前缀%'),该列能用,但它"之后"的列用不上;所以等值列放前面、范围列放最后。其他让索引失效的:列上用函数/运算、隐式类型转换、like '%xxx' 前缀模糊、OR 连非索引列。怎么验证?EXPLAIN:type(ALL=全表扫差、ref/range=用了索引好)、key(实际用的索引、NULL=没用上)、rows(预估扫描行数)。由此,我刻下一个关键认知:联合索引的"列顺序"决定一切;查询要匹配"最左前缀"才走索引。归根结底:联合索引是多列按序拼接的有序结构,遵循最左前缀(从最左列连续用前缀);跳过最左列/范围列之后的列用不上;等值列放前、范围列放后,用 EXPLAIN 验证。
第二件事:正解——按最左前缀设计索引和查询
搞懂了原理,正解就清晰了:按"查询模式"精心设计联合索引的列顺序(高频等值列放左、范围列放右),让查询匹配最左前缀;用 EXPLAIN 验证。
-- ✓ 正解一: 按"实际查询模式"设计联合索引的列顺序
-- 假设最常见的查询是: WHERE status=? AND user_id=? AND created_at > ?
-- 原则: 等值匹配的列放左边, 范围列放最右边
CREATE INDEX idx_q ON orders (status, user_id, created_at);
-- ✓ status、user_id 是等值(=)放前面, created_at 是范围(>)放最后
-- → WHERE status=1 AND user_id=2 AND created_at>'...' 能最大化用上索引
-- ✓ 正解二: 让查询条件"从最左列开始、连续"
SELECT * FROM orders WHERE status=1; -- ✓ 用 status
SELECT * FROM orders WHERE status=1 AND user_id=2; -- ✓ 用 status, user_id
-- ✗ 别写 WHERE user_id=2(跳过了最左的 status)→ 用不上
-- ✓ 正解三: 如果确实要"只按 b 查", 单独为 b 建索引(按需)
-- 若 "WHERE user_id=?" 是高频查询, 给 user_id 单独建索引:
CREATE INDEX idx_user ON orders (user_id);
-- → 别指望靠 (status,user_id,created_at) 这个联合索引来服务"只查 user_id"。
-- ✓ 正解四: 覆盖索引(进阶)—— 索引里包含查询要的所有列, 免回表
CREATE INDEX idx_cover ON orders (status, user_id, amount);
SELECT amount FROM orders WHERE status=1 AND user_id=2;
-- ✓ 要查的 amount 也在索引里 → 直接从索引取, 不用回表查数据行(更快)。
-- ✓ 正解五: 范围列放最后
-- ✗ CREATE INDEX (created_at, status, user_id); -- 范围列 created_at 在前, 截断后面
-- ✓ CREATE INDEX (status, user_id, created_at); -- 等值在前、范围在后
-- ✓ 正解六: 用 EXPLAIN 验证索引是否真的用上了
EXPLAIN SELECT ... ;
-- 看 type(别是 ALL)、key(别是 NULL)、rows(别太大)。
-- 设计要点:
-- - 按"高频查询的条件组合"决定建哪些索引、列怎么排。
-- - 等值列在前、范围列在后; 区分度高的列靠前。
-- - 别为每种查询都建索引(索引也有写入/存储开销), 按需、合并。
-- 核心: 按查询模式设计联合索引列顺序(等值在前范围在后), 查询匹配最左前缀;
-- 高频独立查询单独建索引; 善用覆盖索引免回表; 用 EXPLAIN 验证。
修复的方向,是"让索引的设计,去贴合真实的查询模式"。正解一,按"实际查询模式"设计列顺序:核心原则是"等值匹配的列放左边、范围列放最右边"(比如最常见查询是 status=? AND user_id=? AND created_at > ?,就建 (status, user_id, created_at)——两个等值列在前、范围列在后,能最大化利用)。正解二,让查询条件"从最左列开始、连续"(别写跳过最左列的条件)。正解三,确实要"只按 b 查"就给 b 单独建索引(别指望联合索引服务这种查询)。正解四,覆盖索引(进阶):索引里包含查询要的所有列,直接从索引取、不用回表查数据行,更快。正解五,范围列放最后(避免截断后面的列);正解六,用 EXPLAIN 验证(看 type 别是 ALL、key 别是 NULL)。设计要点:按高频查询的条件组合决定建哪些索引、列怎么排;等值列在前、范围列在后、区分度高的列靠前;别为每种查询都建索引(有写入/存储开销),按需合并。归根结底:按查询模式设计联合索引列顺序(等值在前范围在后),查询匹配最左前缀;高频独立查询单独建索引;善用覆盖索引免回表;用 EXPLAIN 验证。
第三件事:索引失效的其他常见原因
这次踩坑后,我把"明明建了索引却用不上"的其他常见原因,系统梳理了一遍:
索引失效的常见原因(明明建了却用不上)
# 1. 不满足最左前缀(本文)
# - 联合索引(a,b,c), 查询没从 a 开始 → 用不上。
# 2. 在索引列上做运算/函数
# WHERE YEAR(created_at) = 2026 -- ✗ 列上套函数, 失效
# WHERE amount + 10 > 100 -- ✗ 列上运算, 失效
# → 改写成 created_at >= '2026-01-01' AND < '2027-01-01'(列保持"裸")。
# 3. 隐式类型转换(见隐式转换篇)
# phone 是 varchar, WHERE phone = 13800138000(传数字)→ 失效。
# → 类型要匹配, 字符串列传字符串。
# 4. like 前缀模糊
# WHERE name LIKE '%张' -- ✗ 以 % 开头, 失效(无法用有序索引定位)
# WHERE name LIKE '张%' -- ✓ 后缀模糊可用。
# 5. OR 连接了非索引列
# WHERE a = 1 OR b = 2 -- 若 b 没索引, 可能整体走全表。
# → OR 的每个条件都要能用索引; 或改用 UNION。
# 6. 范围查询后的列
# (a,b,c)索引, WHERE a=1 AND b>2 AND c=3 → c 用不上(b 是范围)。
# 7. 不等于 / NOT IN / IS NOT NULL(部分情况)
# != / <> / NOT IN 常用不上索引(看优化器和数据分布)。
# 8. 优化器"主动放弃"索引
# - 当它估算"走索引还不如全表扫"(如要返回大部分行)时, 会放弃索引。
# 排查: EXPLAIN 看 type/key/rows; 用不上就对照上面逐条查。
# 核心: 索引失效常因 不满足最左前缀、列上函数运算、隐式转换、like前缀%、
# OR非索引列、范围后列; 用 EXPLAIN 定位, 让索引列保持"裸"且匹配前缀。
原来"建了索引却用不上"的原因,五花八门。不满足最左前缀(本文);在索引列上做运算/函数(YEAR(created_at)=2026、amount+10>100 都失效,要改写成让列保持"裸");隐式类型转换(varchar 列传数字失效);like 前缀模糊('%张' 失效、'张%' 可用);OR 连接非索引列(可能整体走全表);范围查询后的列(范围之后的列用不上);不等于/NOT IN(常用不上);优化器主动放弃索引(估算走索引不如全表扫时)。它们的共同启示是:"建了索引" ≠ "查询一定能用上索引";能不能用上,取决于你的查询写法是否"对索引友好"(满足最左前缀、列保持裸、类型匹配、避免前缀模糊)。排查靠 EXPLAIN。归根结底:索引失效常因不满足最左前缀、列上函数运算、隐式转换、like 前缀 %、OR 非索引列、范围后列;用 EXPLAIN 定位,让索引列保持"裸"且匹配前缀。
下面这张图,是这次"索引用不上"的成因与解法:
第四件事:联合索引能否命中速查
这次踩坑后,我把"什么查询能命中联合索引 (a,b,c)"整理成一张速查表,以后写查询前对一对。
| 查询条件 | 能用到索引的列 | 说明 |
|---|---|---|
| a=1 | a | ✓ 最左前缀 |
| a=1 AND b=2 | a, b | ✓ 连续前缀 |
| a=1 AND b=2 AND c=3 | a, b, c | ✓ 全用上 |
| b=2 / c=3 / b=2 AND c=3 | 无(全表扫) | ✗ 跳过了最左列 a |
| a=1 AND c=3 | a | △ b 断了, c 用不上 |
| a=1 AND b>2 AND c=3 | a, b | △ b 范围, 截断 c |
| a=1 ORDER BY b | a + b排序 | ✓ 排序也能用前缀(免filesort) |
这张表,把"能不能命中"讲清了。规律就一条:从最左列 a 开始、连续地匹配前缀,就能用上对应的列;一旦跳过 a(只用 b/c),整个索引就用不上;中间断了(a 后直接 c)或遇到范围(b>),后面的列就接不上了。还有个常被忽略的好处:联合索引不仅能加速 WHERE 过滤,还能加速 ORDER BY 排序——a=1 ORDER BY b 能利用索引里 b 的有序性、免去 filesort。它给我的启发是:用好联合索引,关键是"让你的查询(包括 WHERE 和 ORDER BY)的列,去匹配索引的最左前缀";而这要求你在建索引时,就预判好"这张表最常被怎么查",并据此精心排列索引的列顺序。索引设计,本质是一种"为高频查询量身定制有序结构"的工程;建得好,事半功倍;建不好(列顺序乱、不匹配查询),建了也白建。
第五件事:索引设计的几个权衡
这次踩坑也让我意识到,索引不是"建得越多越好"。我把索引设计的几个权衡梳理了一下。
| 权衡维度 | 多建索引 | 少建/合理建索引 |
|---|---|---|
| 查询速度 | 更多查询能走索引(快) | 没覆盖的查询慢 |
| 写入速度 | 每次增删改都要维护索引(慢) | 写入更快 |
| 存储空间 | 索引占额外空间 | 省空间 |
| 设计成本 | 索引多了难管理/可能冗余 | 精简好维护 |
| 联合 vs 多个单列 | 多个单列灵活但各自局限 | 联合索引能服务一组前缀查询 |
这张表,让我对"索引设计"有了更全面的认识——它是一门取舍的艺术,而非"能加速就多建"。索引的本质权衡是:它用"更慢的写入 + 更多的存储",换"更快的查询"。所以:不能为了查询快,就无脑乱建索引——每个索引都会拖慢增删改(因为每次写都要维护索引)、占额外存储、还增加管理负担(冗余/重复的索引);而联合索引,一个就能服务"一组最左前缀查询",往往比建多个单列索引更划算。它给我的最大启发是:索引设计,要从"整体"和"权衡"的视角去做:分析真实的高频查询模式,用尽量少、且精心设计列顺序的索引,去覆盖它们;既不能"建得太少"(查询慢),也不能"建得太多太乱"(写入慢、空间浪费、难维护)。好的索引设计,是在"读性能、写性能、存储、维护成本"之间,找到那个最适合你业务读写特征的平衡点——而这,需要你真正理解索引的原理(如最左前缀),并对自己的查询模式了如指掌。
第六件事:要建/查一个索引时,我现在会怎么决策
现在,每当我要建索引或写查询,脑子里都会过一遍这张决策图——核心两问:这表最常被怎么查?我的查询匹配索引前缀吗?
这张图的灵魂,是从"查询模式"出发去设计索引。第一步,先分析"这表最常被怎么查"(哪些列组合做条件/排序)——索引是为查询服务的,得先懂查询。然后建联合索引:等值匹配的列放左边、范围列放最右边、区分度高的列靠前;有高频"只按某列"查的就给它单独建索引。写查询时:让条件从最左列连续匹配前缀、让索引列保持"裸"(别套函数/运算/隐式转换)。最后两步,是我以前最缺的:用 EXPLAIN 验证(type 不是 ALL、key 不是 NULL、rows 小);别滥建索引,权衡读快 vs 写慢/存储。这套判断,让我建索引/写查询时,不再"建了就以为快了"——核心始终是:索引为查询而生,让查询匹配最左前缀,并用 EXPLAIN 验证它真用上了。
我立下的几条规矩
这场"索引用不上"的事故,换来了我做数据库优化时,刻进骨子里的几条铁律:
- 联合索引遵循最左前缀。(a,b,c) 只有从 a 开始连续用前缀才走索引;跳过最左列 a 就用不上。
- 等值列放前,范围列放后。范围查询会截断其后列的索引使用;把范围列放最右最大化利用。
- 索引为查询模式而设计。先分析高频查询的条件/排序组合,再决定建什么索引、列怎么排。
- 高频"只按某列"查就单独建索引。别指望联合索引服务跳过最左列的查询。
- 让索引列保持"裸"。别在列上套函数/运算、别隐式类型转换、别 like '%前缀',否则索引失效。
- EXPLAIN 是检验索引的唯一标准。看 type/key/rows;别凭"我建了索引"就以为它用上了。
- 别滥建索引。索引用写慢+存储换读快;按需精简,联合索引常比多个单列更划算。
附:用 EXPLAIN 亲手验证索引有没有走
口说无凭。下面这组 SQL,建表、建联合索引、用 EXPLAIN 亲眼看哪些查询走了索引、哪些全表扫,跑一遍就懂:
-- 建表 + 联合索引(a, b, c)
CREATE TABLE t (
id INT PRIMARY KEY AUTO_INCREMENT,
a INT, b INT, c INT, val VARCHAR(50)
);
CREATE INDEX idx_abc ON t (a, b, c);
-- (插入足够多的数据, 否则优化器可能因表小而直接全表扫)
-- ✓ 走索引(最左前缀): 看 EXPLAIN 的 key=idx_abc, type=ref
EXPLAIN SELECT * FROM t WHERE a = 1;
EXPLAIN SELECT * FROM t WHERE a = 1 AND b = 2;
EXPLAIN SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3;
-- → key: idx_abc, type: ref(用了索引), rows: 小
-- ✗ 用不上(跳过最左列 a): key=NULL, type=ALL
EXPLAIN SELECT * FROM t WHERE b = 2;
EXPLAIN SELECT * FROM t WHERE c = 3;
-- → key: NULL(没用索引), type: ALL(全表扫描), rows: 全表行数
-- △ 范围截断: a,b 用上, c 用不上(看 key_len 比全用上时短)
EXPLAIN SELECT * FROM t WHERE a = 1 AND b > 2 AND c = 3;
-- → key: idx_abc, 但 key_len 显示只用到了 a,b 的部分
-- ✗ 列上套函数: 失效
EXPLAIN SELECT * FROM t WHERE a + 1 = 2; -- key=NULL
EXPLAIN SELECT * FROM t WHERE a = 1; -- ✓ 对比: 这个走索引
-- EXPLAIN 关键字段怎么看:
-- type: ALL(全表扫,差) < index < range < ref < eq_ref < const(最优)
-- key: 实际使用的索引名(NULL = 没用上索引)
-- key_len: 用到了索引的多少字节(能推断用到了联合索引的哪几列)
-- rows: 预估扫描行数(越小越好)
-- Extra: "Using index"=覆盖索引(好); "Using filesort"=额外排序(可优化)
-- 核心: 建完索引别想当然, 用 EXPLAIN 逐个查询验证 —— 看 key(用没用上)、
-- type(扫描方式)、key_len(用到几列)、rows(扫多少); 眼见为实。
这组 SQL,把"索引到底有没有走"这件事,从"猜"变成了"看"。建好 (a,b,c) 索引后,用 EXPLAIN 逐个验证:从 a 开始的查询(a / a+b / a+b+c),key 是 idx_abc、type 是 ref(走了索引);只查 b 或 c 的,key 是 NULL、type 是 ALL(全表扫,露馅);范围查询 a=1 AND b>2 AND c=3,看 key_len 比全用上时短(说明 c 没用上)。而读懂 EXPLAIN 的几个关键字段是基本功:type(ALL 全表扫最差 → const 最优)、key(实际用的索引、NULL=没用上)、key_len(用到索引几个字节、能推断用到联合索引的哪几列)、rows(预估扫描行数、越小越好)、Extra(Using index=覆盖索引好、Using filesort=额外排序可优化)。这,正是我想用这组 SQL,留给每一个做数据库优化的人的最后一课:数据库优化,最忌讳"凭感觉、想当然"("我建了索引应该快了吧");而 EXPLAIN,就是那个能让你从"想当然"走向"看得见"的照妖镜——它把"这条查询到底怎么执行、用没用上索引、要扫多少行"清清楚楚地摊在你面前。养成"优化前先 EXPLAIN 看现状、优化后再 EXPLAIN 验效果"的习惯,你的每一次优化,才是有的放矢、且能被验证的。用数据说话,而不是用感觉优化——这,是数据库优化的第一原则。
写在最后
回头看,这场由"联合索引最左前缀"引发的、索引视而不见的事故,真正教给我的,是一个比"记住最左前缀"本身更深的道理:很多工具/技术,它能"提供能力",是有"前提条件"的;而"拥有这个能力"和"满足条件、真正用上这个能力",是两回事;如果你只知道"它能做什么",却不懂"它在什么条件下才能做",你就会误以为"我有了它",实则"它根本没生效"。我"建了联合索引",以为就拥有了对 a、b、c 的加速能力;却不知道这份能力,有"最左前缀"这个严格的前提——于是,我那个"只查 b"的查询,明明守着一个包含 b 的索引,却得不到它的任何加速,白白全表扫描。这让我深刻地领悟到:用任何工具/优化手段,不能停留在"我用了它"的表面满足上,而要深入理解"它在什么条件下才真正生效",并主动去验证"它有没有真的生效"(对索引而言, 就是 EXPLAIN)。很多"性能优化做了却没效果"的困惑,根源都在于:优化的"前提条件"没满足,或者根本没去验证它有没有生效。不仅要"用上"工具,更要懂它生效的条件、并验证它真的生效了——这,是我用一次"索引用不上"的事故,换来的、关于数据库、也关于"如何让优化真正落地"的、最朴素也最深刻的领悟。如果这篇复盘,能让你在下一次建完索引后,顺手用 EXPLAIN 验证一下查询是否真的走了它,那我对着那个被无视的索引熬的这大半天,就值了。
—— 别看了 · 2026