04-慢SQL排查实战:count、order-by、索引失效与JOIN优化全解析 04-慢SQL排查实战count、order-by、索引失效与JOIN优化全解析参考丁奇《MySQL实战45讲》第14讲count、第16讲order by、第34讲JOIN 算法、第35讲JOIN 优化、第18讲隐式转换/索引失效。本文所有命令与回显均取自真实云服务器实验的完整留档日志未做任何编造宁缺毋假。文末附「8.0 排序机制演进」增量小节超越传统 45 讲内容。一、引言半夜被一条 SQL 叫醒运维群里一句“数据库 CPU 打满”点开慢查询日志八成是下面这几类老面孔select count(*) from t跑了老半天——有人问“count(1) 是不是更快”列表页order by create_time desc越翻越慢数据一多就卡死。where tradeid 110717明明建了索引却全表扫。两张表join之后慢得离谱执行计划里出现Using join buffer。这一讲我们把这些“名场面”在实验机上逐个复现用profiling、EXPLAIN、OPTIMIZER_TRACE、SHOW WARNINGS把真实数字抓出来。环境和上一篇同源Ubuntu 24.048C/14GMySQL 8.0.46。二、实验环境说明$ uname -a Linux ecs-fb52-0002 6.8.0-106-generic #106-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 6 07:58:08 UTC 2026 x86_64 $ free -h | head -2 total used free shared buff/cache available Mem: 14Gi 510Mi 14Gi 2.5Mi 476Mi 14Gi $ mysql -uroot -e select version(); version() 8.0.46-0ubuntu0.24.04.3本讲用于慢查询实验的表t沿用 03 篇的b2.t10 万行id主键a/b为rand()随机值index(a)。t_order新建t_order(id pk auto_increment, city varchar(16), name varchar(16), age int, addr varchar(128), key(city))灌1 万行全是city杭州的数据$ mysql -uroot b2 -e drop table if exists t_order; create table t_order(id int primary key auto_increment, city varchar(16), name varchar(16), age int, addr varchar(128), key(city)); set cte_max_recursion_depth20000; insert into t_order(city,name,age,addr) with recursive s(seq) as (select 1 union all select seq1 from s where seq10000) select 杭州, concat(name, lpad(floor(rand()*100000),6,0)), floor(rand()*80)18, concat(浙江省杭州市西湖区文一西路, seq, 号) from s; select count(*) from t_order; count(*) 10000合规说明本文不出现任何密码如涉及公网 IP 一律打码为124.70.***.***本次日志中出现的登录来源 IP 均已打码。三、count 怎么写最快——profiling 实测三轮一个经典误区“count(*)慢count(1)快”。我们用profiling把四种常见写法计时每种跑三轮消除波动均返回 100000$ mysql -uroot b2 -e set profiling1; select count(*) from t; select count(1) from t; select count(id) from t; select count(a) from t; show profiles; count(*) 100000 count(1) 100000 count(id) 100000 count(a) 100000 第一轮 Query_ID Duration Query 1 0.00819050 select count(*) from t 2 0.00747175 select count(1) from t 3 0.00913975 select count(id) from t 4 0.00947625 select count(a) from t 第二轮 Query_ID Duration Query 1 0.00819275 select count(*) from t 2 0.00758725 select count(1) from t 3 0.00923525 select count(id) from t 4 0.00949250 select count(a) from t 第三轮 Query_ID Duration Query 1 0.00805825 select count(*) from t 2 0.00739350 select count(1) from t 3 0.00900725 select count(id) from t 4 0.00935900 select count(a) from t取三轮均值写法平均耗时 (s)实测结论count(1)0.007484最快count(*)0.008147与count(1)几乎相同count(id)0.009127略慢需确认主键非空count(a)0.009443最慢需逐行排除 NULL原理为什么是这个排序InnoDB没有像 MyISAM 那样缓存行数所以count必须真实扫一遍优化器会挑最小的索引来扫以省 IO。count(*)和count(1)在优化器层面被等价处理不关心具体值因此最快。本实验count(1)略快于count(*)但差距仅在微秒级~0.0007s可视为等价。count(主键)需要确认主键非空实际仍走最小索引 判断稍多一点点工作。count(普通列)必须逐行判断该列是否为 NULL、并排除 NULL因此比count(*)多一步判断。本实验a是可空二级索引列count(a)最慢平均 0.009443s比count(*)慢约 0.0013s正印证了“判 NULL”的额外开销。结论很朴素能写count(*)就写count(*)语义清晰、性能不输count(1)别再无脑把它改成count(1)更别用count(列)去统计“有多少行”它语义是“非 NULL 的行数”与行数不等价。分页总条数的替代方案InnoDB 的SELECT COUNT(*)要真扫一遍数据一大就慢。实战里常见替代用EXPLAIN的rows估算“约 N 条”或维护一张计数表在写入时同步增减或用「覆盖索引 游标分页WHERE id ? LIMIT ?」避免算总数。四、order by 为什么会慢——排序模式与 OPTIMIZER_TRACE4.1 先跑一次真实的排序t_order上只有key(city)。where city杭州 order by name的排序键name不在任何索引里必然 filesort$ mysql -uroot b2 -e explain select city,name,age from t_order where city杭州 order by name limit 1000; explain select city,name,age from t_order where city杭州 order by id limit 1000; id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t_order ref city city 67 const 5000 100.00 Using filesort -- order by name 无法利用 city 索引顺序 → filesort id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t_order ref city city 67 const 5000 100.00 NULL -- order by idcity 二级索引叶子按 (city,id) 存id 有序 → 免 filesort注意第二个查询order by id居然ExtraNULL无 filesort因为city这个二级索引的叶子节点是按(city, id)排序的——在city杭州过滤后行天然按id有序所以order by id直接吃索引顺序无需额外排序。这正是“排序键要尽量落在索引顺序里”的实机证据。4.2 外部排序实锤Sort_merge_passes6把sort_buffer_size压到32K让内存根本排不下 1 万行触发磁盘外部归并排序$ mysql -uroot b2 -e set sort_buffer_size32768; flush status; select city,name,age,addr from t_order where city杭州 order by name; show session status like Sort_merge_passes; show session status like Sort_rows; city name age addr 杭州 name000025 24 浙江省杭州市西湖区文一西路9302号 ...按 name 升序返回 1 万行... Variable_name Value Sort_merge_passes 6 Variable_name Value Sort_rows 10000Sort_merge_passes6且Sort_rows10000——确认发生了磁盘外部排序且归并了 6 趟。这正是order by突然变慢的典型信号内存排序缓冲不够MySQL 把中间结果写到磁盘、再分趟归并。4.3 用 OPTIMIZER_TRACE 看真正的排序参数打开OPTIMIZER_TRACE直接看filesort_summary默认sort_buffer_size即 256K 档$ mysql -uroot b2 -e set optimizer_traceenabledon; set optimizer_trace_max_mem_size1000000; select city,name,age from t_order where city杭州 order by name limit 1000; select trace from information_schema.OPTIMIZER_TRACE\G \ | grep -E memory_available|row_size|max_rows_per_buffer|num_rows_found|num_initial_chunks|peak_memory|sort_mode filesort_summary: { memory_available: 262144, row_size: 404, max_rows_per_buffer: 648, num_rows_estimate: 5060, num_rows_found: 10000, num_initial_chunks_spilled_to_disk: 3, peak_memory_used: 262144, sort_mode: varlen_sort_key, packed_additional_fields关键字段解读字段值含义memory_available262144排序可用内存即sort_buffer_size256Krow_size404单行排序记录长度字节max_rows_per_buffer648一个 sort buffer 能装约 648 行num_rows_found10000实际参与排序的行数num_initial_chunks_spilled_to_disk3初始阶段就向磁盘溢出了 3 个 chunkpeak_memory_used262144峰值内存用满sort_modevarlen_sort_key, packed_additional_fields排序模式注意sort_mode已经是varlen_sort_key, packed_additional_fields——这是 MySQL 8.0 的统一排序模式。接下来第八节的“8.0 排序机制演进”会重点拆解它以及为什么传统 45 讲里“全字段排序 vs rowid 排序”的二分法在 8.0 已经过时。五、8.0 排序机制演进增量价值点超越传统 45 讲传统《MySQL实战45讲》基于 5.6/5.7讲 filesort 时会强调两套sort_modesort_key, rowid单行只存“排序键 行指针(rowid)”排完按 rowid 回表取数据多一次回表sort_key, 附加字段单行含“排序键 查询需要的其它字段”免回表但单行更宽。并说由max_length_for_sort_data决定单行总长超过它就退回sort_key, rowid。但在MySQL 8.0这套说法已经过时。我们用今天07-29的实验日志逐条验证三个关键变化。5.1max_length_for_sort_data已被废弃真实 Warning 1287直接设置这个参数MySQL 8.0 明确给出废弃警告$ mysql -uroot b2 -e set max_length_for_sort_data16; show warnings; Level Code Message Warning 1287 max_length_for_sort_data is deprecated and will be removed in a future release.Warning 1287说得很清楚这个变量已被废弃未来版本会移除。所以任何“调大max_length_for_sort_data来避免 rowid 排序”的 5.7 时代经验在 8.0 上既无效参数还在但无意义又会触发废弃警告。5.2 8.0 的 sort_mode 已统一为packed_additional_fields我们分别用“默认参数”和“强制设max_length_for_sort_data16”跑同一个排序看 trace 的sort_mode-- 默认不设 max_length_for_sort_data sort_mode: varlen_sort_key, packed_additional_fields -- 强制 set max_length_for_sort_data16 之后 sort_mode: varlen_sort_key, packed_additional_fields两者完全一致仍然是varlen_sort_key, packed_additional_fields。说明 8.0 已经不再用max_length_for_sort_data去切换“全字段/rowid”两种模式——它统一成了“变长排序键 紧凑附加字段”的实现无论你怎么设那个废弃参数都不变。5.3 加 TEXT 列也还是packed_additional_fields再进一步给t_order加一个note TEXT列每行塞 200 个字符让单行变得很大再看 trace$ mysql -uroot b2 -e alter table t_order add column note text; update t_order set noterepeat(char(97floor(rand()*26)), 200) where city杭州 or 11; $ mysql -uroot b2 -e set optimizer_traceenabledon; set optimizer_trace_max_mem_size1000000; select city,name,age,note from t_order where city杭州 order by name limit 1000; select trace from information_schema.OPTIMIZER_TRACE\G \ | grep -E row_size|sort_mode row_size: 65941, sort_mode: varlen_sort_key, packed_additional_fieldsrow_size从 404 暴涨到65941因为 TEXT 大字段被纳入排序记录但sort_mode依然是varlen_sort_key, packed_additional_fields——8.0 用紧凑的变长编码 溢出页机制处理大字段不再退化成传统的 rowid 排序。这正是 8.0 排序子系统的关键改进排序模式不再因“行长超限”而二选一而是统一走紧凑打包。5.4 内存不够时磁盘溢出 79 个 chunk真实 trace把sort_buffer_size压到32K32768且查询带 TEXT 大字段row_size65941内存彻底装不下看 trace 的溢出情况$ mysql -uroot b2 -e set optimizer_traceenabledon; set optimizer_trace_max_mem_size1000000; set sort_buffer_size32768; select city,name,age,note from t_order where city杭州 order by name limit 1000; select trace from information_schema.OPTIMIZER_TRACE\G \ | grep -E memory_available|row_size|num_rows_found|num_initial_chunks|peak_memory|sort_mode memory_available: 32768, row_size: 65941, num_rows_estimate: 4930, num_rows_found: 10000, num_initial_chunks_spilled_to_disk: 79, peak_memory_used: 33792, sort_mode: varlen_sort_key, packed_additional_fields字段32K TEXT 场景对比256K 场景 §4.3memory_available32768262144row_size65941404num_initial_chunks_spilled_to_disk793peak_memory_used33792262144sort_modevarlen_sort_key, packed_additional_fields同左num_initial_chunks_spilled_to_disk79——初始阶段就向磁盘溢出了79 个 chunk这正是 §4.2 里Sort_merge_passes6的底层成因内存只 32K、单行却 64KB一个 buffer 连一行都快装不下只能疯狂落盘、再分趟归并。这把“内存不足 → 磁盘外部排序 → 慢”的链路用实机 trace 钉死了。8.0 filesort 流程图与 5.7 的本质区别 5.7 时代二分法已过时 row_size 小 ── max_length_for_sort_data 够大 ──▶ sort_key, 附加字段 免回表 row_size 大 ── 超过阈值 ────────────────────▶ sort_key, rowid 多一次回表 8.0 时代统一实测 无论参数怎么设、无论是否带 TEXT 大字段 ── 永远 ──▶ varlen_sort_key, packed_additional_fields 内存不够时 → num_initial_chunks_spilled_to_disk 飙升 → 磁盘外部排序 max_length_for_sort_data 已被废弃Warning 1287给排查者的结论在 8.0 上别再调max_length_for_sort_data已废弃、无效。想要order by快优先级是① 让排序键走索引根本免排序见 §4.1 的order by id免 filesort ② 适当调大sort_buffer_size减少落盘看num_initial_chunks_spilled_to_disk和Sort_merge_passes。出现Using filesort且Sort_merge_passes在涨就是磁盘排序的明确信号。六、索引失效排查三类典型6.1 对列做函数运算 —— 索引直接失效实机回显如下$ mysql -uroot b2 -e explain select * from t where id11000; explain select * from t where id999; id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t ALL NULL NULL NULL NULL 100256 100.00 Using where -- id11000全表扫描索引没用上 id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t const PRIMARY PRIMARY 4 const 1 100.00 NULL -- id999走主键 const仅扫 1 行where id11000在列id上套了函数优化器无法用 B树的有序性定位只能全表扫 10 万行等价写成id999立刻走主键、只扫 1 行。任何对索引列的函数/运算DATE(create_time)、id1、a*2等都会让索引失效应把运算移到常量侧。6.2 字符串列传数字 —— 隐式转换让索引“假命中”建一张tsv是varchar但被当成数字查$ mysql -uroot b2 -e create table ts(id int primary key auto_increment, v varchar(20), d datetime, key(v), key(d)); insert into ts(v,d) values(123,now()),(456,2026-01-15 10:00:00),(789,2026-03-20 11:00:00);1把数字传给 varchar 列错误写法$ mysql -uroot b2 -e explain select * from ts where v123; show warnings; id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE ts ALL v NULL NULL NULL 3 33.33 Using where -- keyNULL全表扫描 Level Code Message Warning 1739 Cannot use ref access on index v due to type or collation conversion on field v Warning 1739 Cannot use range access on index v due to type or collation conversion on field v Note 1003 /* select#1 */ select b2.ts.id AS id,b2.ts.v AS v,b2.ts.d AS d from b2.ts where (b2.ts.v 123)keyNULL、typeALL——索引彻底没用上。SHOW WARNINGS里的Warning 1739直接点明因为类型/排序规则转换v索引无法用于 ref/range 访问。优化器重写的 SQL 仍是v 123数字MySQL 在“字符串列 vs 数字常量”比较时会按类型转换规则把列侧转成数字上下文于是v上的索引无法用于等值定位退化成全表扫描。2把值用引号包成字符串正确写法$ mysql -uroot b2 -e explain select * from ts where v123; id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE ts ref v v 83 const 1 100.00 NULL -- typeref、rows1走索引等值查找3对日期列用函数month()vs 日期范围$ mysql -uroot b2 -e explain select * from ts where month(d)1; explain select * from ts where d2026-01-01 and d2026-02-01; id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE ts ALL NULL NULL NULL NULL 3 100.00 Using where -- month(d)1函数套在列上索引 d 失效全表扫 id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE ts range d d 6 NULL 1 100.00 Using index condition -- 日期范围走索引 d 的 range 扫描rows1month(d)1在列上套函数 →typeALL全表扫改成d2026-01-01 and d2026-02-01的范围查询→typerange、keyd、rows1、Using index condition。规律一致函数作用在索引列上 → 索引失效把函数挪到常量侧、用范围等价改写 → 索引恢复。隐式转换导致索引失效流程图 where v 123 (v 是 varchar, 123 是 int) MySQL 规则: 字符串列 与 数字比较 - 把列转成数字 相当于 where CAST(v AS signed)123 列上套了函数 - 索引无法定位 - 全表扫描 (typeALL, Warning 1739) 正确: where v 123 (两边都是字符串) 直接等值匹配 - ref 查找 (typeref, rows1)同类坑用数字查char/varchar主键、用字符串查int列、联表时两表关联字段类型/字符集不一致一张utf8mb4一张utf8都会触发隐式转换、索引失效。建表时让关联字段类型严格一致能从根上避免。七、JOIN 优化INLJ 还是 Hash Join7.1 实验准备t1取t的前 1000 行都有index(a)t本身 10 万行$ mysql -uroot b2 -e create table t1(id int primary key, a int, b int, index(a)); insert into t1 select id,a,b from t limit 1000; select count(*) from t1; count(*) 10007.2 INLJ被驱动表能用上索引关联字段t1.a t.at在a上有索引走Index Nested-Loop JoinINLJ$ mysql -uroot b2 -e explain select * from t1 straight_join t on t1.at.a; set profiling1; select count(*) from (select t.id from t1 straight_join t on t1.at.a) x; show profiles; id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t1 ALL a NULL NULL NULL 1000 100.00 Using where 1 SIMPLE t ref a,ab a 5 b2.t1.a 1 100.00 NULL -- t 通过 t1.a 去自己的索引 a 做 ref 查找再回表 count(*) 1945 Query_ID Duration Query 1 0.00133175 select count(*) from (select t.id from t1 straight_join t on t1.at.a) x7.3 Hash Join被驱动表无可用索引8.0 默认把关联条件改成t1.b t.bt在b上没有索引。MySQL 8.0 不会退化成老式 BNL 的“驱动表每行 × 被驱动表全表扫”而是用内存Hash Join$ mysql -uroot b2 -e explain select * from t1 straight_join t on t1.bt.b; set profiling1; select count(*) from (select t.id from t1 straight_join t on t1.bt.b) x; show profiles; show variables like join_buffer_size; id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t1 ALL NULL NULL NULL NULL 1000 100.00 NULL 1 SIMPLE t ALL NULL NULL NULL NULL 100256 10.00 Using where; Using join buffer (hash join) -- t 是 typeALL 且 Using join buffer (hash join) count(*) 2058 Query_ID Duration Query 1 0.01265925 select count(*) from (select t.id from t1 straight_join t on t1.bt.b) x Variable_name Value join_buffer_size 262144JOIN 方式执行计划特征关联行数耗时 (s)INLJ被驱动表t.a有索引t走ref索引a19450.00133175Hash Join被驱动表t.b无索引Using join buffer (hash join)20580.01265925实测差距约 9.5 倍0.0127 / 0.0013 ≈ 9.5。INLJ 下t1每行去t的索引a里做对数级查找命中才回表总成本极低而 Hash Join 必须把t的 10 万行扫一遍、装进 262144 字节的join_buffer建哈希表再探查。被驱动表越大、越该补索引转 INLJ。INLJ (Index Nested-Loop Join): for r in t1: -- 驱动表小 用 r.a 去 t 的 index(a) 查 -- 走索引, 对数级, 快 回表取 t.* -- 命中行才回表 Hash Join (8.0, 被驱动表无索引时): build: 把 t1 装进 join buffer, 建哈希索引 probe: 流式扫描 t(10万行), 每行算 hash 找 t1 匹配 -- 内存中完成, 但被驱动表全扫描 内存压力大表 JOIN 的真实代价假设驱动表 1 万行、被驱动表 1000 万行。INLJ 下每行去被驱动表索引查一次B树 3~4 层总几万次逻辑读毫秒级若被驱动表无索引走 Hash Join则必须把 1000 万行扫一遍装进 join buffer——既撑爆内存又要海量 CPU轻则慢查询、重则 OOM。所以「给被驱动表关联字段建索引」是 JOIN 优化里投入产出比最高的一件事。此外驱动表应当选小表必要时用straight_join手动指定让外层循环次数最少。7.4 关于 join_buffer_size实机参数本实验 Hash Join 用的是默认join_buffer_size262144256K。这个值决定了单趟能装多少驱动表数据进内存建哈希表若驱动表很小本例t1仅 1000 行256K 绰绰有余Hash Join 一次建表完成若驱动表很大buffer 装不下MySQL 会分批block处理每批把一部分驱动表装进 buffer 建哈希、再扫描一遍被驱动表探查匹配——这会把“扫描被驱动表”的次数放大成“批数”倍性能急剧下降。因此 Hash Join 的真实成本 被驱动表扫描次数 × 批数。当被驱动表本身就有亿级行数时哪怕只分几批也是灾难。这也从另一角度印证了结论能用 INLJ被驱动表有索引就别依赖 Hash JoinHash Join 只是“被驱动表暂时没索引”时的兜底且依赖足够大的join_buffer_size绝不是大表 JOIN 的终极方案。八、慢 SQL 排查 SOP实战流程把前面几类问题收敛成一套可复用的排查流程遇到慢 SQL 按图索骥即可先EXPLAIN看执行计划重点盯typeALL/Index/Range/Ref/Const、key实际用没用索引、rows估计扫描行数、ExtraUsing filesort/Using temporary/Using where/Using index/Using join buffer是最该警惕的信号。判断是不是全表扫typeALL且数据量大先怀疑没走索引或索引列被函数/隐式转换处理掉了第六.1 / 第六.2 节。看排序与临时表Extra出现Using filesort用Sort_merge_passes/OPTIMIZER_TRACE的num_initial_chunks_spilled_to_disk确认是否落盘本讲 §4.2 / §5.4 实机数据。看 JOIN 算法被驱动表typeALL且Using join buffer说明缺索引按第七.3 节补索引转 INLJ。怀疑隐式转换数值/字符串混用、联表字符集/类型不一致时立刻SHOW WARNINGS看优化器重写的 SQL第六.2 节的Warning 1739。上profiling/OPTIMIZER_TRACE需要量化各阶段耗时、看优化器决策细节时用这两把扳手本讲第三、四、五节均用到。必要时force index验证确认是优化器误判后用force index对比耗时但记住它只是「验证手段」长期方案还是修索引设计或避免长事务快照见 03 篇第八节。九、踩坑记录真实报错/反直觉点max_length_for_sort_data已废弃在 8.0 上设置它只换来一条Warning 1287且sort_mode永远是varlen_sort_key, packed_additional_fields调它纯属无用功§5.1 / §5.2。order by真落盘长这样sort_buffer_size32K 1 万行 →Sort_merge_passes6带 TEXT 大字段时 trace 里num_initial_chunks_spilled_to_disk79§4.2 / §5.4。内存不够磁盘排序没跑。隐式转换“假命中”索引v123数字的EXPLAIN里keyNULL、typeALL直接全表扫SHOW WARNINGS暴露Warning 1739类型转换导致无法用索引。正确写法必须加引号v123。count(*)不慢count(a)最慢三轮实测均值count(a)0.00944scount(id)0.00913scount(*)0.00815s≈count(1)0.00748s。count(列)因要判 NULL 而最慢。小表 JOINHash Join 比 INLJ 慢约 9.5 倍别一看到Using join buffer就恐慌但要认清——被驱动表无索引时大表场景它远不如 INLJ§7.3。十、面试高频问答Q1count(*)、count(1)、count(列)性能差异InnoDB 不缓存行数都要真扫。count(*)与count(1)被优化器等价处理、最快本实验均值分别 0.00815s、0.00748scount(主键)、count(普通列)需确认非空/排除 NULL略慢本实验count(a)均值 0.00944s最慢。Q2order by慢一般怎么排查先看EXPLAIN有没有Using filesort有则看Sort_merge_passes与OPTIMIZER_TRACE的num_initial_chunks_spilled_to_disk是否涨内存不够落盘。最优解是在order by列上建索引走有序扫描——本讲 §4.1 的order by id因city二级索引叶子按(city,id)有序而免 filesortExtraNULL。Q38.0 里 filesort 的 sort_mode 是什么max_length_for_sort_data还有用吗8.0 统一为varlen_sort_key, packed_additional_fields无论是否带 TEXT 大字段都不变实测row_size从 404 到 65941 都同此模式。max_length_for_sort_data已被废弃设置即Warning 1287不要再调它。Q4为什么where tradeid110717不走索引tradeid是 varchar传数字触发隐式转换MySQL 把列侧转成数字比较等效于对列用了函数索引无法定位全表扫描typeALLSHOW WARNINGS报Warning 1739。必须写成tradeid110717字符串才走ref等值查找。Q5对索引列做函数/运算会怎样where id11000在列上套函数优化器放弃索引走全表扫描typeALL扫 10 万行等价改成id999走主键const只扫 1 行。运算应放在常量侧。month(d)1同理失效改用日期范围d2026-01-01 and d2026-02-01即恢复range索引扫描。Q6JOIN 的 INLJ 和 Hash Join 怎么选被驱动表关联字段有索引 → INLJ每行走索引查找大表友好无索引 → 8.0 走 Hash Join小表进 join buffer。本实验INLJ 0.00133s vs Hash Join 0.01266s差约 9.5 倍。大表无索引时务必补索引转 INLJ。Q7磁盘外部排序是怎么被实机证实的sort_buffer_size32K下 1 万行order by→Sort_merge_passes6带 TEXT 字段单行 64KB时OPTIMIZER_TRACE显示num_initial_chunks_spilled_to_disk79、peak_memory_used33792——内存只 32K、单行却 64KB只能疯狂落盘再归并。十一、总结慢 SQL 排查不是玄学每一步都能落到实机数字count写count(*)最快count(列)因判 NULL 最慢实测均值差约 0.0013s。三轮 profiling 消除了单次波动结论稳定。order by先确认是否Using filesort有索引可走有序扫描时根本不排序本讲order by id免 filesort。filesort 落盘看Sort_merge_passes实测 32K 下 6和 trace 的num_initial_chunks_spilled_to_disk带 TEXT 时 79。8.0 排序机制演进增量价值点传统“全字段 vs rowid”二分法已过时8.0 统一为varlen_sort_key, packed_additional_fieldsmax_length_for_sort_data被废弃Warning 1287加 TEXT 大字段也不退化。调优优先级索引免排序 调大sort_buffer_size减少落盘。索引失效对列做函数id11000必失效字符串列传数字触发隐式转换全表扫Warning 1739SHOW WARNINGS一查便知month(d)1失效而日期范围恢复range。JOIN被驱动表有索引走 INLJ0.00133s无索引走 Hash Join0.01266s差约 9.5 倍大表务必补索引。慢 SQL 的本质几乎都可以归结到三件事扫了多少行、排没排序、回没回表。只要顺着EXPLAIN把这三个数字看穿再配合本讲给出的实测数据count 写法差异、order by 的 filesort 判定与 8.0 排序演进、隐式转换的Warning 1739、JOIN 的 INLJ 与 Hash Join 取舍绝大部分性能问题都能在十分钟内定位到根因而不是凭感觉加索引、盲目调参数。把EXPLAIN、SHOW WARNINGS、profiling、OPTIMIZER_TRACE这几把扳手用熟绝大多数慢 SQL 都能外科手术式定位。最后再强调一句索引与 SQL 优化没有「背下来的标准答案」只有「跑出来的真实数据」——本讲所有结论都来自实验机的实机回显建议你在自己的库上把同样的命令再跑一遍印象会比看十篇文章都深。本文实验均在真实云服务器完成输出为实机回显。