我给表建了 a、b、c 的联合索引,以为查这三列里哪一个都能走索引,结果按 b 单独查时全表扫描慢成狗,我对着最左前缀排查了大半天的复盘

我给一张大表建了 (a,b,c) 的联合索引,想当然以为查 a、查 b、查 c 都能用上它,结果上线后按 b 单独过滤的查询慢得要命,EXPLAIN 一看 type=ALL、key=NULL,索引完全没生效、走的全表扫描。我对着这个"明明建了索引却用不上"的现象排查了大半天,才彻底搞懂联合索引的最左前缀原则:(a,b,c) 这个索引,是先按 a 排序、a 相同再按 b、b 相同再按 c,就像通讯录先按姓再按名排;所以只有从最左列 a 开始、连续地匹配前缀(查 a、a+b、a+b+c)才能走索引,一旦跳过最左列 a 只查 b 或 c,索引就完全用不上;而且范围查询(b>)还会截断它之后列(c)的索引使用。这篇从联合索引的存储与排序原理、最左前缀讲起,到按查询模式设计索引(等值列在前/范围列在后/覆盖索引/高频独立查询单独建)的正解、索引失效的八大常见原因(列上函数运算、隐式转换、like前缀%、OR非索引列等)、命中速查表与索引设计权衡、决策图与几条铁律,以及附上一组可直接跑的 EXPLAIN 验证 SQL。核心领悟:建了索引≠用上索引,工具生效有前提,优化要懂条件、更要用 EXPLAIN 验证它真的生效了。

我建了 a、b、c 的联合索引,以为查这三列里哪个都能走索引,结果按 b 单独查时索引完全没用上、全表扫描慢成狗,我对着最左前缀排查了大半天的复盘

这是一个让我对数据库"联合索引最左前缀"刻骨铭心的故事。我有张大表,经常用 abc 三个列做查询,为了加速,我建了一个联合索引 (a, b, c)。我当时很满意,心想"这下查 a、b、c 哪个都快了"。可上线后,有些查询慢得离谱。我把慢 SQL 拎出来 EXPLAIN 一看,大跌眼镜:那些只用 a 查的,确实走了索引、很快;可那些只用 b(或只用 c)查的,EXPLAIN 显示 type=ALL(全表扫描)、key=NULL(没用上任何索引)!我明明建了包含 bc 的联合索引,为什么单独查 bc 时,它视而不见?

我顺着"索引没用上"的线索深挖,才终于揭开真相,补上了我对数据库索引一个最核心的认知漏洞:问题的核心,是联合索引的"最左前缀(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)=2026amount+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 验证它真用上了。

我立下的几条规矩

这场"索引用不上"的事故,换来了我做数据库优化时,刻进骨子里的几条铁律:

  1. 联合索引遵循最左前缀。(a,b,c) 只有从 a 开始连续用前缀才走索引;跳过最左列 a 就用不上。
  2. 等值列放前,范围列放后。范围查询会截断其后列的索引使用;把范围列放最右最大化利用。
  3. 索引为查询模式而设计。先分析高频查询的条件/排序组合,再决定建什么索引、列怎么排。
  4. 高频"只按某列"查就单独建索引。别指望联合索引服务跳过最左列的查询。
  5. 让索引列保持"裸"。别在列上套函数/运算、别隐式类型转换、别 like '%前缀',否则索引失效。
  6. EXPLAIN 是检验索引的唯一标准。看 type/key/rows;别凭"我建了索引"就以为它用上了。
  7. 别滥建索引。索引用写慢+存储换读快;按需精简,联合索引常比多个单列更划算。

附:用 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),keyidx_abctyperef(走了索引);只查 b 或 c 的,keyNULLtypeALL(全表扫,露馅);范围查询 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我把一个 Integer 赋值给 int 居然抛了空指针,那行代码里根本没有方法调用、找不到 null 是怎么来的,我对着自动拆箱排查了大半天的复盘

2026-6-2 5:20:13

技术教程

我的服务高峰期突然报 Cannot assign requested address,明明机器很闲、连接也没泄漏,我对着几万个 TIME_WAIT 排查了大半天的复盘

2026-6-2 5:33:53

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索