1. 优化器在“算计”什么:先说清楚EXPLAIN里那一行ALL的含义
我先给结论:MySQL的优化器并不会因为你建了索引就感动到必须用。它只关心一件事——这套执行方案的综合成本是否够低。如果它计算出全表扫描比走索引还便宜,那它就会心安理得地选择全表扫描,哪怕你刚给这张表建了三个二级索引。
很多开发同学第一次遇到这种情况都非常崩溃。明明一条SQL从语法上完全可以用到索引,条件列也建了索引,EXPLAIN结果里却写着type: ALL,key: NULL,rows还显示几十万。我见过不少同事第一反应就是“是不是索引没建成功”,然后一顿SHOW INDEX查证,结果索引好好地在那躺着。问题根本不出在索引是否存在,而是优化器在成本计算时把“走索引”的方案直接否掉了。
要弄清这件事,得先理解MySQL优化器的运行逻辑。它不是一个见索引就冲的愣头青,而是一个精打细算的“成本会计”。在生成最终执行计划之前,优化器会枚举出它认为有可能执行的路径,然后分别为每条路径估算成本,比如“全表扫描大约要花多少I/O和CPU”“走某个二级索引回表要花多少随机I/O”,最后挑成本最低的那条当最终方案。这个估算过程依赖的并不是实时数据,而是一堆从统计信息里拿到的“数字快照”。
日常开发中,判断一条SQL是否会走索引,最直接的手段就是看EXPLAIN输出。你需要重点读四个信息:type、key、rows和Extra。type为ALL代表全表扫描,key为空代表没有使用任何索引,rows是优化器预估需要扫描的行数,Extra里如果出现Using where说明存储引擎返回数据后Server层又做了条件过滤。很多人只看key,key为空就觉得“索引失效了”,其实没那么简单,真正的决策逻辑藏在优化器的成本模型里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么明摆着有索引,优化器还是选择全表扫描
2.1 回表成本太高,优化器算完觉得亏了
大家最常忽略的一个因素就是回表(table lookup)。InnoDB里二级索引的叶子节点只存了“索引列的值 + 主键值”,它不像主键索引那样直接包含着完整行数据。你想通过二级索引查出所有列,就必须先拿主键去聚簇索引里再查一遍完整行,这个过程就是回表。
回表是随机I/O,成本很高,而且行数越多,回表次数就越多,成本会线性上升。当一条查询条件能筛选出来的数据行数占全表比例很大时,比如超过20%甚至更多,优化器会倾向于认为:与其通过二级索引筛选、然后回表翻一堆数据页,不如干脆顺序把全表数据从头到尾扫一遍,反正代价差不多,甚至顺序扫描更便宜。
举个例子,一张订单表有100万行,你执行SELECT * FROM orders WHERE status = 0,如果status字段上建了索引,但status=0的数据占了80万行,优化器绝对不会走这个索引。因为它算出来,走索引要扫描80万个主键再回表80万次,而直接全表扫描只需要顺序读一遍100万行,顺序I/O的效率远高于随机I/O,显然后者划算。这也是为什么很多人觉得“给状态字段建索引应该就能加速”,结果被现实狠狠教育了一顿。
2.2 数据量小到扫表比查索引更快
另一个反直觉的情况是:表里只有几百行数据,你建了索引,优化器照样选ALL。原因很简单,InnoDB的最小存储单位是数据页,默认16KB。几百行数据甚至可能只占一个数据页,全表扫描只要顺序读一个页就够了。而走索引的话,要先读索引页,再回到数据页,这中间多了一步操作,还可能产生额外的随机读取。磁盘I/O再快,也比在内存里扫一个页要慢。
这种场景经常出现在配置表、字典表、枚举数据表上。比如一张类目表有300条记录,建了name索引,执行SELECT * FROM category WHERE name = '电子产品',EXPLAIN出来大概率还是ALL。这很合理,MySQL不傻,为了300行去读写索引完全是浪费。唯一要注意的是,这种全表扫描不会造成性能瓶颈,不要在它上面钻牛角尖,强行用FORCE INDEX反而会拖慢性能。
还有一个类似场景:MySQL有一个名为index的访问类型,它表示“扫描二级索引树”。这种情况和ALL不同,它不扫全表而扫索引,通常发生在查询的列恰好能被某个二级索引完全覆盖时,虽然访问类型不是理想的ref或range,但比ALL通常要快。很多人会把index误认为全表扫描,其实它是另一种优化路径。
2.3 统计信息不准,优化器被“错误情报”带偏了
很多有经验的DBA都遇到过这种诡异状况:同样一条SQL,昨晚跑还是走索引,今天跑突然全表扫描了,数据量和SQL可都没变。这种变化十有八九出在统计信息上。优化器做成本计算时依赖的不是实时扫描数据,而是存储在数据字典里的统计信息,比如表的行数、索引的基数(cardinality)、字段的选择度等。
如果一张表经常有大量增删改操作,但没有及时更新统计信息,比如走了大批量DELETE后表还是显示有上百万行,或者频繁UPDATE导致索引列的值的分布完全变了,可统计信息还停留在旧状态,优化器就会用错误的数据做成本估算,自然容易得出“走索引不如全表扫”的结论。
我遇到过最典型的一个案例:一张流水表每天凌晨批量插入几十万条数据,某个查询在白天走索引只要几十毫秒,到了下午突然变成了全表扫描,耗时直接涨到几秒。排查后发现,这张表的统计信息是前一晚凌晨批量导入前采样的,抽样时的数据分布和导入后的实际分布差距太大,导致优化器对索引过滤能力的预估严重偏低。执行一次ANALYZE TABLE重新收集统计信息后,执行计划立刻恢复正常。
2.4 条件写法让优化器“看不见”索引能用
还有相当大一部分“索引失效”是SQL写法直接坑了优化器。优化器不是万能推导器,很多情况下它不会复杂地对表达式做等价变换。比如你在索引列上套了函数、做了隐式类型转换、或者参与运算,索引就可能会被“封印”。
以函数为例,WHERE DATE(create_time) = '2024-01-01'这种写法,会让create_time上的索引在优化器视角里失去了排序性和范围匹配的可能性。解决办法也不是没有,可以把它改写成范围查询WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00',这样优化器就能借助索引完成快速过滤。
隐式类型转换也要留意。我见过某个用户表里phone字段是VARCHAR(20),SQL写成WHERE phone = 13812345678,因为查询参数是数字类型,MySQL会把字段值CAST成数字再比较,索引也就没法用于等值匹配了。这类问题在代码审查里很难发现,因为它不会报错,只会表现为执行计划变成全表扫描,偶尔扫几百万行然后慢查询告警。
还有一类常见的索引失效是联合索引最左前缀不满足。假设表上有个(a, b, c)联合索引,你直接WHERE b = 1 AND c = 2,由于跳过了第一列a,优化器无法直接从联合索引B+树的最左前缀开始匹配。这种情况下即便b和c的基数很高,联合索引也帮不上忙,除非MySQL选择索引跳跃扫描(index skip scan)特性,但该特性在8.0.13版本及以后才可用,而且有较多限制。
2.5 排序、LIMIT和JOIN带来的执行计划转折
还有一个容易被忽略的维度:最终执行计划往往是SQL里所有操作综合权衡的结果,不只是WHERE条件的功劳。如果你有一个ORDER BY、LIMIT或者JOIN,优化器计算成本时会把排序代价、连接顺序、能否用索引消除排序等因素全部考虑进去,某个单列索引就算能过滤不少行,也可能因为无法消除排序而在整体方案里被降权。
比如SELECT * FROM t WHERE user_id = 1 ORDER BY create_time DESC LIMIT 10。如果单独看user_id = 1能筛出的数据不多,走user_id索引再filesort一下也很快,但优化器有可能因为极端数据分布误判行数而选择全表扫。反过来,如果表上没有(user_id, create_time)联合索引,而只有user_id单列索引,优化器就需要在“过滤后排序”和“全表扫一遍但避免大量随机回表”之间做选择。很多性能问题最终都要靠联合索引来解决,就是因为联合索引可以把过滤条件和排序顺序统一到同一棵索引树上,让优化器计算出的成本明显更低。
3. 拿到一个“全表扫描”执行计划,怎么一步步定位根因
3.1 从EXPLAIN开始,摸清关键信号的准确含义
先说一个朴素的排查套路。线上有一条慢SQL被告警了,你拉出来一看发现执行计划是全表扫描,第一步先别急着骂优化器。打开控制台,把EXPLAIN结果仔细看一遍,我一般会按这个顺序确认。
先看table和type,type只要是ALL那就确实是全表扫描,不用怀疑。接着看possible_keys,如果这个字段里是NULL,说明优化器认为当前SQL连一个能用的索引都没有,这就要回到SQL本身找原因,看看是不是在索引列上做了函数操作、类型转换或者条件不满足最左前缀。如果possible_keys有值但key是NULL,那才是真正的“手中有索引却放弃”,说明优化器评估过这些索引,但觉得用了也不划算,这时就涉及到统计信息和回表代价的问题。
rows代表优化器预估要读取的行数,这个数字虽然不是真实值,但很有参考意义。如果rows接近全表行数,说明优化器认为这个条件几乎过滤不出什么数据,选择全表扫描也就不意外了。filtered表示经过条件过滤后剩余的比例,比如filtered为10%,意思是预计100行里能留10行。把这个比例乘上rows,大约就是最终要返回的行数,这个数越小,通常说明走索引应该越赚,如果优化器还是选择了ALL,那往往就是统计信息有偏差。
3.2 用OPTIMIZER_TRACE偷看优化器的“心算草稿”
EXPLAIN只能告诉你结果,不能告诉你为什么是这个结果。想彻底搞清楚优化器的决策依据,就不得不掏出MySQL 5.6以后提供的OPTIMIZER_TRACE功能,它可以记录优化器完整的心路历程,包括枚举了哪些候选索引、每一步估算的成本、最终为什么选择了这个方案。
我通常这样操作:
sql复制SET optimizer_trace='enabled=on';
SELECT * FROM orders WHERE status = 0 ORDER BY create_time LIMIT 10;
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE\G;
SET optimizer_trace='enabled=off';
在OPTIMIZER_TRACE输出里,重点看两个部分:rows_estimation里的成本估算,以及considered_execution_plans里不同方案的对比。你会看到类似这样的内容:用idx_status访问的预估成本是“12345.67”,而全表扫描的成本是“4567.89”,两者相差近三倍,优化器当然选全表扫描。数值不会说谎,看到这里你就明白了,不是MySQL有毛病,而是它在严格执行“哪个便宜用哪个”的规则。
我会把这个方法教给组里所有参与SQL优化的同学,因为很多争论在数据面前会快速消失。有人觉得应该走索引,有人觉得扫表能接受,把cost字段亮出来,谁高谁低一目了然,后续优化方案也有了明确方向。
3.3 核对统计信息和真实数据分布,别靠猜
下一步就是核对统计信息是否准确。在InnoDB里,统计信息的收集方式可以通过innodb_stats_persistent、innodb_stats_auto_recalc等参数控制。你可以在information_schema.TABLES里看到统计信息里的行数:
sql复制SELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH FROM information_schema.TABLES WHERE TABLE_NAME = 'orders';
再用真实SQL验证一下实际数量:
sql复制SELECT COUNT(*) FROM orders;
如果两者相差超过一个数量级,统计信息的可靠性就有问题了。另外还可以直接查看索引的基数:
sql复制SHOW INDEX FROM orders;
关注Cardinality列,它表示这个索引有多少个不同的值。如果cardinality极低,比如只有1或者2,说明这个索引区分度很差,优化器有非常充分的理由拒绝使用它。
统计信息偏差太大时,解决方案通常是重新收集统计信息:
sql复制ANALYZE TABLE orders;
针对大表,如果想要更精确的采样,可以在分析前临时调高innodb_stats_persistent_sample_pages的值,比如调到20或50,采样页数越多,统计信息越接近真实数据分布,但耗时也会增加。这个操作在业务低峰期执行会更稳妥,否则可能遇到锁等待。
3.4 实际案例:一条慢SQL从“莫名其妙全表扫”到“锁定真凶”
拿一个真实的线上案例来复盘。有一张收入流水表income_log,数据量约500万行,表上有两个单列索引:idx_user_id和idx_create_time。业务侧反馈一个页面打开要将近3秒,定位后发现核心SQL是这样写的:
sql复制SELECT *
FROM income_log
WHERE user_id = 10086
AND create_time BETWEEN '2024-01-01' AND '2024-03-31'
ORDER BY id DESC
LIMIT 20;
执行EXPLAIN后,type是ALL,rows预估值是500万。possible_keys里列了idx_user_id和idx_create_time,但key为NULL。第一直觉可能会想:user_id=10086显然应该走idx_user_id,为什么全表扫?
我用OPTIMIZER_TRACE看cost明细,发现全表扫描cost约12000,走idx_user_id的方案cost约98000。差距非常大,优化器根本不用纠结。为什么走idx_user_id这么贵?因为这个用户本身数据量就非常大,user_id=10086名下约有300万条流水,走二级索引虽然能定位到该用户所有的数据,但这些数据分散在数百万个数据页里,要回表拿200行以外的数据就涉及大量随机读。
进一步查看统计数据,发现user_id=10086的流水数量占全表60%,这个索引区分度对该用户来说极差。优化器算完后得出结论:扫描500万行的顺序读,比二级索引筛选300万行+回表300万次更便宜。
这个案例说明了什么?在选择索引时,不能只关注“有没有索引”,更要看条件列上这个具体值的分布情况。优化器面对的是全表的数据分布,如果一个索引的某个值的数据量偏大,单独围绕这个值建的索引价值就会大幅缩水。现实中遇到类似问题,通常会把业务查询维度收敛到时间范围,或者构建联合索引(user_id, create_time),让回表范围大幅收缩。有时候一条SQL的性能问题不是单纯建个索引能解决的,而是要重新审视业务在数据上真正的访问模式。
4. 真遇到慢SQL了,该怎么“纠正”优化器
4.1 先更新统计信息,再清理碎片,别一上来就改SQL
很多人看到全表扫描就想改写SQL或者加索引,但我想奉劝一句:在动手之前,先把统计信息刷新一下。直接执行:
sql复制ANALYZE TABLE income_log;
对InnoDB表来说,这个操作会重新计算索引的基数、表的行数等关键统计信息。尤其对于经常批量删改的表,统计信息滞后可能让优化器一直做着错误决策,你改SQL也白改,因为真正的偏差根源在底层数据“报告”就不准确。
还有一个常被忽略的隐患:大表频繁更新会留下大量碎片页,导致即使全表扫描也比预期慢很多,这会影响优化器对全表扫描成本的感知,同时拖累真实执行效率。可以考虑利用ALTER TABLE的原地重建能力来整理表空间:
sql复制ALTER TABLE income_log ENGINE=InnoDB;
这个操作在MySQL 5.6以后的InnoDB里通常是以Online DDL方式执行,但大表重建仍可能耗时较久,也会增加磁盘占用,一定要评估好运维窗口再执行。碎片整理后,数据页更紧凑,顺序扫描的真实代价会大幅降低,虽然不至于让优化器改变选择,但至少真实的执行耗时能降下来。
4.2 用FORCE INDEX强制走索引,但别把它写死在代码里
如果统计信息正常,我们也确认走索引确实更快,但优化器就是一根筋选择全表扫描,可以临时用FORCE INDEX做干预,验证一下效果:
sql复制SELECT *
FROM income_log FORCE INDEX (idx_user_id)
WHERE user_id = 10086
AND create_time BETWEEN '2024-01-01' AND '2024-03-31'
ORDER BY id DESC
LIMIT 20;
FORCE INDEX的作用不是“让优化器更愿意用索引”,而是告诉优化器“你只能从这个索引开始访问”。它比USE INDEX更强制,后者只是建议,优化器仍可拒绝。实际使用时要特别注意,FORCE INDEX属于“物理层面”的操作提示,一旦未来索引名变了、加了新索引、或者数据分布彻底变化,这条SQL可能就绑死在不优的执行路径上。
我之前在一个项目里见过一个线上问题,某条SQL因为开发同学在早期遇到过几次优化器选择错误,直接在SQL里写死了FORCE INDEX,后来表上新增了一个过滤性更好的联合索引,业务数据量也涨了几十倍,但SQL还是强制走那个老单列索引,最终导致慢查询频繁告警。所以我的建议是:FORCE INDEX是验证手段和应急措施,不是长期方案。线上使用前一定要在压测环境里充分验证,并做好注释说明,避免后人接手时无从下手。
4.3 用覆盖索引和联合索引改写SQL,从根上降低回表成本
长期来看,解决全表扫描的最佳方式不是干预优化器,而是改变查询本身需要的代价。最经典的优化思路就是覆盖索引(covering index)。如果查询需要返回的列都能在被选中的索引里找到,那么InnoDB就不需要回表,直接从二级索引返回数据,这会极大降低随机I/O成本,优化器自然会愿意走这个索引。
比如上面的income_log案例,如果业务侧只需要id、user_id、create_time、amount这几个字段,可以建立如下联合索引:
sql复制ALTER TABLE income_log ADD INDEX idx_user_time_amount (user_id, create_time, amount);
这样SELECT user_id, create_time, amount FROM income_log WHERE user_id = 10086 AND create_time BETWEEN ...整条查询要用的列都在索引里,避免回表。优化器的成本模型里,走这个联合索引的代价会显著小于全表扫描,它根本不需要人去纠正,自己就会选。
如果必须查出全行,那就要考虑用“索引下推”和“延迟关联”的手法。先通过覆盖索引快速拿到符合条件的主键列表,再用主键关联回原表取完整数据。8.0里的hash join可能帮你简化写法,但在传统OLTP场景下这个方法仍然非常有效:
sql复制SELECT l.*
FROM income_log l
INNER JOIN (
SELECT id
FROM income_log
WHERE user_id = 10086
AND create_time BETWEEN '2024-01-01' AND '2024-03-31'
ORDER BY id DESC
LIMIT 20
) tmp ON l.id = tmp.id
ORDER BY l.id DESC;
子查询里只访问索引,返回20个主键后再回表拿完整行,把回表次数从几百万次压缩到20次,执行耗时下降几个数量级是常有的事。这个方法在很多MySQL优化书籍和DBA分享里都被称为“延迟关联”,实操价值很高。
4.4 站在优化器角度设计索引:过滤、排序、回表率一起看
还有一个经验我想专门拿出来说:设计索引时不要只盯着WHERE条件,要同时考虑排序和返回列。一个真正高效的索引设计,应该同时满足三个目标:第一,能快速过滤出数据量足够小的中间结果;第二,如果SQL里有ORDER BY,最好能利用索引的有序性避免filesort;第三,如果有必要回表,回表的次数也必须可控。
举个例子,常见的分页查询:WHERE status = 1 ORDER BY create_time DESC LIMIT 10,你给status建单列索引,过滤能力可能不够,而且无法消除排序。如果建(status, create_time)联合索引,过滤和排序一步到位,优化器只要沿着索引树倒序读10条就行,连回表都很少。这种优化对执行计划的改善不是用“强制”两个字能实现的,而是让成本数据说话,让优化器主动选择。
但也要小心,不要为了覆盖所有查询而堆大量索引。索引本身也有维护成本,写入时B+树需要更新,插入操作需要分裂页,索引越多写入越慢。我在实际项目里经常提醒团队:索引不是越多越好,而是够用就好。一个表超过五六个索引就要开始警惕,每加一个索引都要想清楚它能覆盖哪些查询模式,能避免多少次全表扫描,以及在写入侧付出多少代价。
5. 一张表搞懂优化器踩坑排查:数据库调优里的常识与反常识
5.1 EXPLAIN结果变化无常?先检查这几类配置
如果你发现同一条SQL在不同时刻EXPLAIN结果不同,可能是统计信息更新引发的,也可能是系统参数设置导致的。先说统计信息,只要InnoDB启用了innodb_stats_auto_recalc,当表变更行数超过约10%时后台会自动重算统计信息,这个重算时机不确定,执行计划因此波动很正常。
还有一个影响非常大的开关是optimizer_switch,它控制了一堆优化策略是否开启,比如index_merge、mrr、semijoin、materialization等。如果你关掉了某些特性,本来能用的执行路径可能就消失了,优化器只能退回到全表扫描或次优方案。我记得MySQL 8.0里有些新查询优化特性默认开关状态不同,升级版本后执行计划大范围的改变很常见,升级前用EXPLAIN ANALYZE在测试库完整比对是很有必要的。
相关的成本参数也存在系统表里,可以通过以下语句查看:
sql复制SELECT * FROM mysql.server_cost;
SELECT * FROM mysql.engine_cost;
比如io_block_read_cost和memory_block_read_cost定义了读取一个数据块的I/O成本,如果默认值不适合你的存储介质,比如全闪存环境或机械硬盘环境,优化器的成本估算可能失真。生产环境我一般不建议频繁调整这些参数,但DBA至少要知道它们的含义,否则遇到“为什么执行计划这么反直觉”的问题时,排查思路会窄很多。
5.2 MySQL优化器常见的“反直觉”时刻
执行计划不是每天都变,但数据库中几个经典反直觉场景常年被拿出来讨论,值得梳理一遍。
第一个是LIKE前缀模糊匹配。LIKE '%关键字'无法使用普通索引,理由很直白:B+树按前缀排序,你从中间截一段字符串去搜,根本没法利用树的二分查找。至于LIKE '关键字%',只要条件满足最左前缀,索引范围扫描是完全可能的。
第二个是NOT IN和<>。这类“反向查询”在语义上往往对应一个非常大的数据范围,优化器宁可全表扫描也不会考虑用索引。反向条件通常很难利用某个确定性较小的值完成快速定位,全表扫描一般就是合理选择。要是查询里还存在NULL值,情况会更复杂,因为索引通常不存储全NULL值,逻辑判断还涉及三值逻辑。
第三个是OR条件。WHERE a = 1 OR b = 2这种写法,早期版本常常让优化器直接放弃索引。8.0版本后优化器对OR条件有了更好的处理,比如索引合并(index merge)可以在两个索引上分别扫描,再做交集或并集。但索引合并本身也有开销,尤其是两个集合都很大时,合并操作可能比单次全表扫描更贵。这个场景里,经常用的优化方式是改写为UNION,或直接设计联合索引。
第四个是关于函数和运算。WHERE id + 1 = 100这样把索引列放到表达式里的写法,会让优化器无法直接套用索引区间匹配。很多经验帖说“不要在索引列上做运算”,原因就在这里。当然,如果是WHERE id = 100 - 1这种对参数列做运算,并不影响索引使用,因为索引列本身没有被“破坏”。
5.3 常见索引与优化器排查问题速查表
| 现象 | 可能原因 | 优先排查方向 |
|---|---|---|
| possible_keys有值,key为NULL | 成本估算显示走索引比全表扫描更贵 | 看统计信息、回表成本、条件选择度 |
| type=ALL且rows远大于实际 | 统计信息滞后导致低估了过滤效果 | ANALYZE TABLE 后重新EXPLAIN |
| possible_keys为NULL | SQL写法导致索引无法被识别 | 查函数运算、隐式类型转换、最左前缀 |
| 有ORDER BY且出现Using filesort | 索引不能消除排序 | 考虑联合索引覆盖排序字段 |
| 走索引后回表次数过多,耗时偏高 | 二级索引过滤后的数据量仍然大 | 用覆盖索引或延迟关联改写 |
| FORCE INDEX后效果显著 | 成本模型与真实数据分布存在偏差 | 考虑更新统计信息、调整成本参数 |
| 同一SQL不同时间执行计划不同 | 统计信息自动更新/参数变更 | 记录优化器开关、分析统计数据变化 |
上面这个表算是日常排查的一个“最小可用清单”,我自己排查慢SQL时基本就按这个节奏走。它不适合解决所有复杂的SQL问题,但它能帮你快速把问题范围缩到“SQL本身写法有问题”“统计信息不准”“索引设计不到位”三选一,剩下就是往深处核对具体原因了。
5.4 最后放一个我踩过印象最深的坑
写这篇文章时,我又想起团队里一个小伙子曾经不无得意地把一张500万行的订单表所有的WHERE条件字段都建上了单列索引,结果写入性能骤降,单条插入从2毫秒涨到接近10毫秒,而且不少SQL还是出现了全表扫描。原因就在于他完全不理解优化器如何评估索引的价值,以为“索引越多,查询越快”,结果索引的维护开销直接反噬了业务写入,而查询条件里的字段单独看区分度都不差,却因为没有联合索引而难以被优化器选中。
我后来让他把那些“为了建而建”的单列索引全部删掉,只保留两三个能覆盖核心查询模式的联合索引,写入性能恢复正常。这件事给我的感触很深:MySQL里的索引和优化器不是魔法,而是一个需要成本和数据分布意识的权衡游戏。你越了解它的算计方式,越能写出让它“心甘情愿”走索引的SQL和表结构。希望这篇梳理能帮你少走一些弯路,也让你下次在别人面前讲出“为什么有索引还是全表扫描”时,能有理有据地给出答案。
