我的查询明明在手机号字段上建了索引,却慢得像在全表扫描,EXPLAIN 一看果然没走索引,折腾半天发现罪魁祸首竟是一个隐式类型转换的深度复盘

一张几百万行的用户表,我早早在 phone 字段建了索引,可"按手机号查用户"的 SQL 慢得动辄上千毫秒,跟全表扫描没区别。我一度想重建索引、加配置,最后老老实实 EXPLAIN 一看:type=ALL、key=NULL,根本没走索引!细看才发现:phone 是 varchar,我却写成 WHERE phone = 13800138000 把它当数字传了。MySQL 比较字符串列和数字时会把字符串转成数字,等价于对每行 CAST(phone),索引列被函数包裹就失效了,只能全表扫。这篇从索引列被函数/转换包裹就失效的机制,讲到类型严格一致的正解、索引失效黑名单、EXPLAIN 各字段怎么看,以及那句最戳心的——加了索引不等于用上了索引,做了优化要用数据验证。

我的查询明明在手机号字段上建了索引,却慢得像在全表扫描,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(临时表,可能要优化)。而排查慢查询,我总结出了一套标准动作:先 EXPLAINtype/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 与索引的注意事项,认真地立成了几条规矩:

  1. WHERE 条件两边类型必须一致。字符串列传字符串(加引号),数字列传数字,杜绝隐式类型转换。
  2. 别在索引列上套函数/做运算。让索引列保持"光秃秃的原始样子",运算挪到常量那一边(DATE(col)=x 改范围、col+10>100col>90)。
  3. 记牢索引失效黑名单。前导 LIKE '%x'、OR 接无索引列、不满足最左前缀、!=/NOT IN——都可能让索引失效。
  4. 应用层绑定参数用对类型。很多隐式转换的根源在代码/ORM 里把字符串列用成了数字参数。
  5. JOIN 关联列字符集要统一。字符集不一致(utf8 vs utf8mb4)也会触发隐式转换、JOIN 走不了索引。
  6. 写完期望走索引的 SQL,必用 EXPLAIN 验证。type 别是 ALL、key 别是 NULL、rows 要小。
  7. "加了索引"不等于"用上了索引"。任何优化做了之后,都要用数据验证它真的生效,别自我安慰。

写在最后

这次"我的查询明明建了索引、却慢得像全表扫描,最后发现是一对引号引发的隐式类型转换"的经历,是我在数据库路上,一次很打脸、却也很受用的成长。它教给我的,远不止"字符串列要传字符串"这一条具体的技术经验,更是两个更根本的道理:其一,魔鬼藏在细节里——一对小小的引号、一次不起眼的类型转换,就能让你精心建立的索引彻底失效,让性能差出几个数量级;其二,"做了"不等于"有效"——你建了索引,不代表查询就用上了它;任何优化,都必须用工具和数据,去亲眼验证它确实生效了。

所以,当你写下一条期望走索引的 SQL、或做完任何一项性能优化时,请别凭着"我建了索引""我做了优化"就心安理得——而要养成一个习惯:用 EXPLAIN、用监控、用数据,去验证它到底有没有按你期望的那样工作。就像那条慢查询,只要我在上线前,随手 EXPLAIN 一下,就会立刻发现那个刺眼的 type: ALLkey: NULL,根本不会让它带着一个失效的索引,慢吞吞地跑在生产上。对细节多一分较真,对"是否真的生效"多一分验证,是从一个"会建索引"的开发,走向一个"能写出高性能查询"的工程师,必经的修炼。愿你建的每一个索引,都货真价实地被用上;也愿你我,在每一次优化之后,都用数据,给自己一个确凿的答案。共勉。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我把 SimpleDateFormat 设成了静态共享变量,本地测试一切正常,一上线高并发就开始解析出乱七八糟的日期、甚至直接抛异常,我对着随机错误查了好几天的深度复盘

2026-6-1 21:48:18

技术教程

我想当然地以为 TCP 发一次就能收一次,结果客户端发来的消息要么粘成一坨、要么被截成两半,我对着错乱的数据排查了好几天才搞懂粘包半包的深度复盘

2026-6-1 21:59:01

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