我的查询明明在手机号字段上建了索引,却慢得像在全表扫描,EXPLAIN 一看果然没走索引,折腾半天发现罪魁祸首是一个隐式类型转换的深度复盘
这是一个让我对"索引失效"彻底长记性的故事。我有一张几百万行的用户表,经常要按手机号查用户。我当然知道要建索引——我早早就在 phone 字段上,建了一个索引。可上线后,这条"按手机号查用户"的 SQL,慢得令人发指:一次查询动辄几百毫秒、上千毫秒,数据库 CPU 也被它拉得老高。我百思不得其解:我明明建了索引啊,怎么会这么慢?这查询效率,跟没有索引、全表扫描有什么区别?
我一度怀疑是不是索引建坏了、是不是要重建索引、是不是该加内存加配置。但在折腾这些之前,我做了一件本该第一时间就做的事:给这条慢 SQL,加上 EXPLAIN,看看它到底有没有走索引。这一看,真相大白:EXPLAIN 的结果里,type 那一列,赫然写着 ALL——这正是"全表扫描"的意思!key 那一列(实际用到的索引),是 NULL——它根本没用上我辛辛苦苦建的那个手机号索引!可索引明明在啊,为什么不走?我又对着 SQL 反复看,终于发现了那个被我忽略的、致命的细节:我的 phone 字段,在表里的类型是 varchar(字符串);可我在 SQL 的 WHERE 条件里,写的却是 WHERE phone = 13800138000——我把手机号,当成一个数字传进去了!而当 MySQL 遇到"一个字符串列,要和一个数字比较"时,根据它的类型转换规则,它会把字符串那一边,转换成数字——也就是说,它会对每一行的 phone 值,都执行一次 CAST(phone AS 数字),再去和 13800138000 比。而这个"对索引列套了一层函数/转换"的操作,会让 MySQL 没法使用这个列上的索引(因为索引存的是原始的字符串值,不是转换后的数字),于是,它只能退化成全表扫描,挨个把每一行都拿出来转换、比较。我的索引,就这样,被一个我自己写的、不起眼的"隐式类型转换",给彻底废掉了。
故障现场:一个数字字面量,废掉了整个索引
我把这个"索引失效"的现场,用代码摊开给你看:
-- 表结构: phone 是 varchar 类型, 且建了索引
CREATE TABLE users (
id BIGINT PRIMARY KEY,
phone VARCHAR(20), -- ← 字符串类型!
name VARCHAR(50)
);
CREATE INDEX idx_phone ON users (phone); -- 在 phone 上建了索引
-- ✗ 灾难写法: 把手机号当数字传(漏了引号)
EXPLAIN SELECT * FROM users WHERE phone = 13800138000;
-- ↑ 数字字面量, 没加引号!
-- EXPLAIN 结果:
-- type: ALL ← 全表扫描!
-- key: NULL ← 没用任何索引!
-- rows: 5000000 ← 扫了全表 500 万行!
-- 为什么? 因为 phone(字符串) = 13800138000(数字), 类型不一致,
-- MySQL 的规则: 字符串和数字比较时, 把"字符串"转成"数字"。
-- → 等价于: WHERE CAST(phone AS DECIMAL) = 13800138000
-- → 对索引列 phone 套了一层 CAST, 索引存的是原始字符串, 用不上了!
-- → 只能全表扫描, 逐行 CAST 再比较。
-- ✓ 正确写法: 手机号是字符串, 就传字符串(加引号)
EXPLAIN SELECT * FROM users WHERE phone = '13800138000';
-- ↑ 字符串字面量, 加了引号!
-- EXPLAIN 结果:
-- type: ref ← 走索引了!
-- key: idx_phone ← 用上了 phone 索引!
-- rows: 1 ← 只需定位 1 行!
看着这两条只差一对引号、性能却天壤之别的 SQL,我才算真正理解了这个坑。问题的核心,是一个我完全没在意的隐式类型转换:我的 phone 列是 varchar(字符串)类型,但我在 WHERE 条件里,给它一个数字字面量 13800138000(漏了引号)。当 MySQL 比较"一个字符串列"和"一个数字"时,它的类型转换规则规定:把字符串那一边,转换成数字,再做比较。这就埋下了祸根:"把 phone 转换成数字",意味着 MySQL 要对表里每一行的 phone 值,都套上一层 CAST(phone AS 数字) 的操作——你的查询条件,实际上等价于 WHERE CAST(phone AS DECIMAL) = 13800138000。而一旦索引列被函数或表达式包裹了,索引就失效了:因为索引里存的,是 phone 的原始字符串值(按字符串排好序的),而不是它们转换成数字后的值;MySQL 没法用一个"按字符串排序的索引",去高效地查找"转换成数字后等于某值"的行。于是,MySQL 别无选择,只能退化成全表扫描(type: ALL)——把 500 万行每一行都捞出来,逐行执行 CAST 转换、再和 13800138000 比较。这,就是我那条 SQL 慢得像没有索引的根本原因:它确实没用上索引,而这一切,仅仅是因为我少写了一对引号、把字符串当成了数字。而正确的写法,只需要给手机号加上引号 '13800138000'——让比较的两边类型一致(都是字符串),不再触发隐式转换,MySQL 就能直接用上 phone 的索引(type: ref,rows: 1),瞬间从扫 500 万行,变成精准定位 1 行。
第一件事:搞懂"索引列被函数/转换包裹"就会失效
定位到根源,我必须把"为什么隐式类型转换会让索引失效"这个机制,以及它背后的通用规律,彻底搞清楚:
-- 核心机制: 索引列一旦被"函数/表达式/类型转换"包裹, 索引就用不上了
-- 为什么? 索引是基于"列的原始值"建立的有序结构(B+树)。
-- 它能高效查找的, 是"列的原始值 = / > / < 某值"这种条件。
-- 但如果你对列做了运算(函数、转换), 索引里没有"运算后的值",
-- MySQL 无法用原索引定位 → 只能全表扫描, 逐行运算再比较。
-- 几种"把索引列包进函数/表达式"导致失效的典型:
-- 1. 隐式类型转换(本文): 字符串列 = 数字 → 等价 CAST(列) = 数字
EXPLAIN SELECT * FROM users WHERE phone = 13800138000; -- ✗ 失效
EXPLAIN SELECT * FROM users WHERE phone = '13800138000'; -- ✓ 走索引
-- 2. 在索引列上用函数
EXPLAIN SELECT * FROM orders WHERE DATE(created_at) = '2024-01-15'; -- ✗ 失效
EXPLAIN SELECT * FROM orders
WHERE created_at >= '2024-01-15' AND created_at < '2024-01-16'; -- ✓ 改成范围, 走索引
-- 3. 在索引列上做运算
EXPLAIN SELECT * FROM products WHERE price + 10 > 100; -- ✗ 失效
EXPLAIN SELECT * FROM products WHERE price > 90; -- ✓ 把运算移到另一边
-- 4. 字符串用前导通配的 LIKE
EXPLAIN SELECT * FROM users WHERE name LIKE '%三'; -- ✗ 失效(前面是%)
EXPLAIN SELECT * FROM users WHERE name LIKE '张%'; -- ✓ 走索引(前缀匹配)
-- 规律一句话: 让"索引列"在 WHERE 里保持"光秃秃的原始样子",
-- 别给它套函数、别让它参与运算、别让它被隐式转换。
-- 把运算/转换, 都挪到"常量那一边"去。
原理终于清晰了。索引失效的一个最核心、最通用的规律是:索引列一旦被"函数、表达式、或类型转换"包裹,索引就用不上了。这背后的道理很朴素:索引(B+ 树),是基于列的原始值,建立起来的一个有序结构;它能高效查找的,是"列的原始值 = / > / < 某个值"这种条件。可一旦你对索引列做了运算(套函数、做转换、参与计算),索引里就没有"运算后的那个值",MySQL 自然没法用这个原始索引去定位,只能退化成全表扫描,把每行都捞出来、运算一遍、再比较。而能"把索引列包进函数/表达式"、导致失效的情形,有好几类典型:第一,就是本文的隐式类型转换(字符串列 = 数字,等价于 CAST(列));第二,在索引列上直接用函数(如 WHERE DATE(created_at) = ...,应改写成范围查询);第三,在索引列上做运算(如 WHERE price + 10 > 100,应把运算移到常量那边 price > 90);第四,字符串用前导通配的 LIKE(LIKE '%三' 失效,LIKE '张%' 才能走索引)。这些情形,都可以用同一句话来概括和预防:让"索引列"在 WHERE 条件里,永远保持它"光秃秃的原始样子"——别给它套函数、别让它参与运算、别让它被隐式转换;凡是需要的运算或转换,都挪到"常量那一边"去做。守住这一条,就守住了索引能被用上的大前提。
第二件事:正解——让比较两边的类型严格一致
搞懂了根因——"字符串列和数字比,触发隐式转换、索引失效"——正解就一目了然了:让 WHERE 条件里,比较两边的类型严格一致:字段是字符串,传值就传字符串(加引号);字段是数字,传值就传数字。从根上,杜绝隐式类型转换的发生。
-- 正解1: 字段是字符串(varchar), 传值就加引号(传字符串)
SELECT * FROM users WHERE phone = '13800138000'; -- ✓ 两边都是字符串, 走索引
-- 正解2: 字段是数字, 传值就传数字(别给数字字段加引号也最好别)
-- (注意: 数字列 = '字符串' 时, MySQL 是把字符串转数字, 通常还能走索引,
-- 但仍建议类型一致, 别依赖这种"碰巧能用")
SELECT * FROM orders WHERE order_no = 123456; -- order_no 若是数字, 这样写
-- 正解3: 从源头保证——应用层(ORM/代码)绑定参数时, 用对类型
-- Java(MyBatis): 确保传入的是 String, 而不是 long
-- AND phone = #{phone} -- phone 应为 String
-- → 用预编译参数(PreparedStatement)时, 参数类型要和列类型匹配
-- 正解4: 确认列的字符集/排序规则一致(JOIN 时尤其重要)
-- 两张表 JOIN 的关联列, 如果字符集不同(utf8 vs utf8mb4),
-- 也会触发隐式转换、导致索引失效! → 统一字符集。
-- 排查手段: 永远用 EXPLAIN 验证!
EXPLAIN SELECT * FROM users WHERE phone = '13800138000';
-- 看 type(别是 ALL)、key(应是你的索引名, 别是 NULL)、rows(应该很小)
-- 核心: "类型一致"是走索引的前提之一。
-- 字符串配字符串、数字配数字, 别让数据库"偷偷"帮你转类型。
这个正解的核心,是一个朴素却极其重要的原则:让 WHERE 条件里参与比较的两边,类型严格一致,从源头上杜绝隐式类型转换。具体来说:字段是字符串(varchar),传值时就加上引号(传字符串字面量),比如 phone = '13800138000';字段是数字,传值时就传数字。更进一步,真正的根治,要落到应用层:很多时候,这个 bug 不是手写 SQL 写出来的,而是代码里、ORM 里,把一个本该是字符串的手机号,用成了 long/int 类型的参数,绑定到 SQL 里时,就成了数字。所以,要确保在代码里(比如 MyBatis 的参数、JPA 的查询条件),传给"字符串列"的参数,本身就是 String 类型。这里还有一个更隐蔽、也很容易踩的坑:两张表 JOIN 时,如果关联的那两个列,字符集/排序规则不一致(比如一个是 utf8、一个是 utf8mb4),同样会触发隐式转换、导致 JOIN 走不了索引、性能暴跌——所以要保证关联列的字符集统一。而贯穿所有正解的、最重要的一个习惯是:永远用 EXPLAIN 来验证!写完一条你以为会走索引的 SQL,别想当然,跑一下 EXPLAIN,看看 type 是不是 ALL(全表扫描)、key 是不是 NULL(没用索引)、rows 是不是大得吓人——只有 EXPLAIN 告诉你它确实走了你期望的索引,你才能放心。我那次的错误,就是只顾着"建了索引",却从没用 EXPLAIN 确认过"索引到底有没有被用上"。
下面这张图,对比了"类型不一致"和"类型一致"两条路径:
这张图的对比很清楚:左边红色那条,给字符串列传了个数字(漏引号),触发隐式转换、索引列被 CAST 包裹、索引失效、退化成全表扫描;右边绿色那条,传字符串(加引号),两边类型一致、无需转换、索引列保持原始值、正常走索引。两条路的差距,仅仅是一对引号——却是 500 万行全表扫描和 1 行精准定位的天壤之别。
第三件事:还有哪些常见写法会让索引失效
填平了隐式转换这个坑,我系统排查了一遍:除了类型转换,还有哪些常见的 SQL 写法,会让明明存在的索引失效。我整理成了一份"索引失效黑名单":
-- 索引失效黑名单(明明有索引, 却用不上):
-- 1. 索引列上用函数/运算/隐式转换(本文重点)
WHERE DATE(created_at) = '2024-01-15' -- ✗ → 改范围查询
WHERE phone = 13800138000 -- ✗ → 加引号
-- 2. 前导通配 LIKE
WHERE name LIKE '%关键词%' -- ✗ 前面有% → 失效(考虑全文索引/ES)
WHERE name LIKE '关键词%' -- ✓ 前缀匹配, 走索引
-- 3. OR 连接的条件, 有一个列没索引
WHERE indexed_col = 1 OR no_index_col = 2 -- ✗ 可能整体失效 → 拆成 UNION 或都建索引
-- 4. 复合索引不满足"最左前缀"
-- 索引 (a, b, c):
WHERE b = 2 AND c = 3 -- ✗ 没用最左的 a, 失效
WHERE a = 1 AND b = 2 -- ✓ 满足最左前缀
WHERE a = 1 AND c = 3 -- △ 只能用到 a 部分
-- 5. != / <> / NOT IN / NOT EXISTS(不等查询)
WHERE status != 1 -- ✗ 不等条件常常用不上索引
-- 6. 索引列上有 IS NULL(看情况, 有时能走有时不能)
-- 7. 数据量太小, 或区分度太低(如性别), 优化器主动放弃索引
-- (这时全表扫描可能反而更快, 是优化器的合理选择)
-- 8. 隐式字符集转换(JOIN 关联列字符集不同)
-- 关键: 别假设"建了索引就一定会用", 写完用 EXPLAIN 确认它真的走了索引。
这一排查,让我对"索引失效"有了全面的警觉。除了隐式类型转换,还有一大堆"明明建了索引、却用不上"的常见写法,构成了一份必须记牢的"黑名单":前导通配的 LIKE(LIKE '%词%' 失效,前缀匹配 LIKE '词%' 才行,模糊全文检索该上全文索引或 ES);OR 连接了一个没索引的列(可能导致整体失效,可拆成 UNION 或给相关列都建索引);复合索引不满足"最左前缀"(索引 (a,b,c),查询必须从最左的 a 开始用起,跳过 a 直接用 b/c 会失效);不等查询(!=/NOT IN 常常用不上索引);以及优化器的主动放弃(数据量太小、或列的区分度太低如"性别",优化器会判断全表扫描反而更快,主动不走索引——这其实是它的合理选择)。这些情形共同指向一个核心教训:千万别假设"建了索引,它就一定会被用上"。索引能不能被用上,取决于你的 SQL 写法、取决于复合索引的顺序、甚至取决于优化器对数据分布的判断。所以,唯一可靠的办法,就是写完每一条你期望走索引的 SQL,都用 EXPLAIN 亲眼确认一遍——它,到底走没走你建的那个索引。
第四件事:学会用 EXPLAIN 这把"照妖镜"
这次踩坑,让我真正学会了用 EXPLAIN 这把排查慢查询的"照妖镜"。我把它几个关键字段的含义,和该怎么看,系统地总结了一遍:
-- EXPLAIN: 看 SQL 到底怎么执行的(走没走索引、扫了多少行)
EXPLAIN SELECT * FROM users WHERE phone = '13800138000';
-- 重点看这几列:
-- 1. type(访问类型)—— 最重要! 性能从好到坏:
-- system > const > eq_ref > ref > range > index > ALL
-- - const/eq_ref/ref: 走索引精确查找(好)
-- - range: 走索引范围扫描(还行)
-- - index: 扫了整个索引树(一般)
-- - ALL: 全表扫描(最坏! 见到要警惕)
-- 2. key(实际用的索引)—— 是不是你期望的那个索引?
-- - NULL: 没用任何索引(危险!)
-- - idx_xxx: 用了这个索引(好)
-- 3. rows(预计扫描行数)—— 越小越好
-- - 几百万: 基本是全表扫了, 慢
-- - 个位数/几十: 很好, 精准定位
-- 4. Extra(额外信息)—— 有些值要警惕:
-- - Using index: 覆盖索引, 不用回表(很好!)
-- - Using where: 在存储引擎层之上又过滤了
-- - Using filesort: 用了文件排序(没走索引排序, 可能要优化)
-- - Using temporary: 用了临时表(常见于 GROUP BY, 可能要优化)
-- 排查慢查询的标准动作:
-- 1. EXPLAIN 一下, 看 type 是不是 ALL、key 是不是 NULL
-- 2. 是 → 分析为什么没走索引(类型转换? 函数? 最左前缀? LIKE %?)
-- 3. 改写 SQL / 调整索引, 再 EXPLAIN 确认走上了索引
-- 4. 看 rows 大幅下降、type 变成 ref/range, 即优化成功
这一总结,让 EXPLAIN 从一个我"听说过但没用过"的命令,变成了我排查慢查询的第一件武器。EXPLAIN 能告诉你一条 SQL 到底是怎么执行的——有没有走索引、走的哪个索引、预计要扫多少行。看 EXPLAIN 的结果,重点盯这几列:type(访问类型,最重要):性能从好到坏是 const > eq_ref > ref > range > index > ALL——看到 ALL(全表扫描)就要高度警惕;key(实际用的索引):是不是你期望的那个?NULL 就说明一个索引都没用上;rows(预计扫描行数):越小越好,几百万基本就是全表扫了;Extra(额外信息):Using index(覆盖索引,很好)、Using filesort(文件排序,可能要优化)、Using temporary(临时表,可能要优化)。而排查慢查询,我总结出了一套标准动作:先 EXPLAIN 看 type/key;如果是全表扫描,就分析为什么没走索引(类型转换?函数?最左前缀?前导 LIKE?);然后改写 SQL 或调整索引,再 EXPLAIN 确认走上了索引;最后看 rows 大幅下降、type 变成 ref/range,就说明优化成功了。把 EXPLAIN 几个关键字段,整理成一张速查表:
| 字段 | 好的值 | 要警惕的值 | 含义 |
|---|---|---|---|
| type | const/ref/range | ALL | 访问方式,ALL 是全表扫 |
| key | 你的索引名 | NULL | 实际用到的索引 |
| rows | 个位/几十 | 几百万 | 预计扫描行数 |
| Extra | Using index | Using filesort/temporary | 额外操作 |
第五件事:加了索引,不等于用上了索引
这次踩坑,在认知层面给了我最大的纠偏——它打破了我"加了索引就万事大吉"的天真。我把这层反思,沉淀了下来:
认知纠偏: "加了索引"和"用上了索引"是两码事
# 我的误解(错误的):
# "我在 phone 上建了索引, 查 phone 肯定就快了。"
# → 我只做到了"建索引", 却从没验证过"查询有没有真的用上它"。
# 真相: 索引能不能被用上, 取决于你的 SQL 怎么写
# - 同样有索引, 写得对(类型一致)就走索引、飞快;
# 写得错(隐式转换/函数/LIKE%)就不走索引、跟全表扫一样慢。
# - "建索引"只是有了"用索引的可能", 能不能用上, 看 SQL 和优化器。
# 这是一类普遍的"以为做了就有效"的错觉:
# - 加了缓存 → 但缓存命中率极低, 等于没加
# - 加了索引 → 但 SQL 写法让它失效, 等于没加
# - 加了线程池 → 但核心数配错, 没发挥并发
# → "做了一个动作" ≠ "达到了那个效果"。中间隔着"正确地用"和"验证"。
# 正确的习惯:
# 1. 做了优化(加索引/缓存等), 一定要"验证"它真的生效了。
# 加索引 → EXPLAIN 确认走了索引; 加缓存 → 看命中率。
# 2. 别凭"我做了"就安心, 要凭"我验证了它有效"才安心。
# 3. 性能问题, 用数据(EXPLAIN、监控、压测)说话, 别靠猜。
核心: 加了索引不等于用上了索引。任何优化, "做了"之后,
都要用工具/数据去"验证"它真的生效了——否则可能是自我安慰。
这层反思,是这次踩坑给我最高维度的收获。复盘我的误解,根源是一个天真的等式——"我在 phone 上建了索引,那查 phone 肯定就快了"。我只做到了"建索引"这个动作,却从没去验证过"我的查询,到底有没有真的用上这个索引"。可真相是:索引能不能被用上,取决于你的 SQL 怎么写——同样一个索引,SQL 写对了(类型一致),就走索引、飞快;写错了(隐式转换/套函数/前导 LIKE),就不走索引、慢得跟全表扫描一样。"建索引",只是给了你"有可能用上索引"的前提,而能不能真用上,要看你的 SQL 和优化器的判断。而这,其实是一类极其普遍的"以为做了就有效"的错觉:加了缓存,但命中率极低,等于没加;加了索引,但 SQL 写法让它失效,等于没加;加了线程池,但参数配错,没发挥出并发——"做了一个动作",从来不等于"达到了那个效果",这中间,隔着"正确地使用"和"验证"这两道关。由此,我给自己立下了几条对治"自我安慰"的习惯:第一,每做一个优化(加索引、加缓存等),都一定要验证它真的生效了——加索引,就用 EXPLAIN 确认走了索引;加缓存,就去看命中率。第二,别凭"我做了"就安心,要凭"我验证了它有效"才安心。第三,性能问题,永远用数据(EXPLAIN、监控、压测)说话,别靠猜、靠想当然。归根结底:加了索引,不等于用上了索引;任何优化,在"做了"之后,都要用工具和数据去"验证"它确实生效了——否则,你所谓的优化,很可能只是一场自我安慰。把"以为做了就有效"和"验证才安心"两种心态对比成一张表:
| 维度 | 以为做了就有效(踩坑) | 验证才安心(稳) |
|---|---|---|
| 加索引后 | 觉得肯定快了 | EXPLAIN 确认走了索引 |
| 判断依据 | "我做了这个动作" | "我验证了它有效" |
| 性能问题 | 靠猜、靠想当然 | 用 EXPLAIN/监控/压测 |
| 典型错觉 | 加缓存命中率却极低 | 盯着命中率优化 |
| 结果 | 优化是自我安慰 | 优化真正落地 |
一套"慢查询该怎么排查"的决策流程
把这次踩坑的全部教训,我浓缩成了一张"发现一条 SQL 慢、该怎么排查"的决策图,贴在了团队的数据库规范里:
这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:发现一条 SQL 慢,第一步永远是 EXPLAIN;看 type 是不是 ALL(全表扫描)——若是,看这列有没有索引:没有就加,有却没走就要查为什么失效(类型转换?函数?最左前缀?前导 LIKE?),然后改写 SQL 让索引列保持"原始值",再 EXPLAIN 验证;若已走索引但 rows 还很大,就进一步优化(覆盖索引、分页方式等)。每一次改动,都回到 EXPLAIN 去确认效果。这条以 EXPLAIN 为核心、不断闭环验证的排查链,现在是我们团队排查每一个慢查询的标准流程。
我立下的几条 SQL 与索引规矩
这次"一对引号废掉整个索引"的踩坑,让我把 SQL 与索引的注意事项,认真地立成了几条规矩:
- WHERE 条件两边类型必须一致。字符串列传字符串(加引号),数字列传数字,杜绝隐式类型转换。
- 别在索引列上套函数/做运算。让索引列保持"光秃秃的原始样子",运算挪到常量那一边(
DATE(col)=x改范围、col+10>100改col>90)。 - 记牢索引失效黑名单。前导 LIKE
'%x'、OR 接无索引列、不满足最左前缀、!=/NOT IN——都可能让索引失效。 - 应用层绑定参数用对类型。很多隐式转换的根源在代码/ORM 里把字符串列用成了数字参数。
- JOIN 关联列字符集要统一。字符集不一致(utf8 vs utf8mb4)也会触发隐式转换、JOIN 走不了索引。
- 写完期望走索引的 SQL,必用 EXPLAIN 验证。看
type别是 ALL、key别是 NULL、rows要小。 - "加了索引"不等于"用上了索引"。任何优化做了之后,都要用数据验证它真的生效,别自我安慰。
写在最后
这次"我的查询明明建了索引、却慢得像全表扫描,最后发现是一对引号引发的隐式类型转换"的经历,是我在数据库路上,一次很打脸、却也很受用的成长。它教给我的,远不止"字符串列要传字符串"这一条具体的技术经验,更是两个更根本的道理:其一,魔鬼藏在细节里——一对小小的引号、一次不起眼的类型转换,就能让你精心建立的索引彻底失效,让性能差出几个数量级;其二,"做了"不等于"有效"——你建了索引,不代表查询就用上了它;任何优化,都必须用工具和数据,去亲眼验证它确实生效了。
所以,当你写下一条期望走索引的 SQL、或做完任何一项性能优化时,请别凭着"我建了索引""我做了优化"就心安理得——而要养成一个习惯:用 EXPLAIN、用监控、用数据,去验证它到底有没有按你期望的那样工作。就像那条慢查询,只要我在上线前,随手 EXPLAIN 一下,就会立刻发现那个刺眼的 type: ALL 和 key: NULL,根本不会让它带着一个失效的索引,慢吞吞地跑在生产上。对细节多一分较真,对"是否真的生效"多一分验证,是从一个"会建索引"的开发,走向一个"能写出高性能查询"的工程师,必经的修炼。愿你建的每一个索引,都货真价实地被用上;也愿你我,在每一次优化之后,都用数据,给自己一个确凿的答案。共勉。
—— 别看了 · 2026