一条SQL引发的血案:索引不是你想用,想用就能用

2026-05-09 11 0

上周五晚上,我正在愉快地刷手机,准备迎接周末,结果钉钉响了。

生产环境告警:某核心接口响应时间从30ms飙升到3秒。 DBA同学已经在群里@了我。 行吧,周末还没开始就结束了。

我登录服务器,打开慢查询日志,看到一行醒目的SQL:

SELECT * FROM orders WHERE user_id = 12345 AND status = 1 ORDER BY created_at DESC LIMIT 10;

这个表有user_id索引,status字段也有索引。 按理说应该很快啊? 但Explain一看,好家伙,使用的是status索引而不是user_id索引,导致扫描了整整80万行。 你说气人不气人?

索引不是万能钥匙

很多人以为加了索引就等于加了保险,这是一种迷之自信

实际情况是:查询优化器决定用哪个索引,而不是你。 它会根据统计信息、索引基数、查询条件选择性等因素综合判断。 听起来很智能对吧? 但现实是——它经常判断错。

上面那条SQL,问题出在哪里?

user_id的选择性极低(一个用户可能有几千条订单),而status=1这个条件会筛选掉99%的数据。 按理说,优化器应该用user_id索引精准定位用户,再过滤status。 但它偏偏选了status索引,为什么?

因为统计信息过时了。 表里status=1的数据占比最近从30%变成了1%,但自动统计信息还没来得及更新。 优化器按照过时的数据做出了错误选择。

所以你看,索引不是你想用,想用就能用。 优化器的心情(统计信息新鲜度)决定一切。

强制索引:让你的SQL听话

遇到这种情况怎么办? 第一反应是强制指定索引

SELECT * FROM orders USE INDEX (idx_user_id) WHERE user_id = 12345 AND status = 1 ORDER BY created_at DESC LIMIT 10;

但这招治标不治本。 今天你强制了,明天数据分布变了,又要改代码。 而且强制索引会让你的SQL变得脆弱,后续索引维护困难。

更好的做法是什么?

复合索引的正确姿势

很多人在user_id和status上各建了单列索引,心想双保险。 但实际上,MySQL/PostgreSQL的查询优化器在处理多条件查询时,往往只能有效利用一个索引(除非用索引合并,但代价很高)。

正确做法:建一个(user_id, status, created_at)的复合索引。 为什么这么排?

  • user_id放前面,精准定位用户
  • status放第二,等于利用了索引的有序性直接过滤
  • created_at放最后,支持时间排序,避免filesort

这样一条SQL可以完全利用索引完成查询,根本不需要回表。 从80万行扫描变成精准的10行读取,性能差距几百倍。

NULL值:被忽视的索引杀手

再说一个坑:NULL值。

你的字段允许NULL吗? 允许的话,恭喜你,你的索引可能有一半是假的。

原因:大多数数据库的B-Tree索引不存储NULL值(或者说,NULL值的索引记录方式很特殊)。 所以如果你写:

SELECT * FROM users WHERE email IS NULL;

这条SQL不会走任何索引,直接全表扫描。 如果你的业务有大量IS NULL查询,恭喜你,又踩坑了。

解决方案? 尽量给字段设NOT NULL DEFAULT 或DEFAULT 0。 如果NULL真的不可避免,考虑用特殊值代替,比如-1表示未填写。 别让NULL成为你索引的黑洞。

模糊匹配:索引失效的重灾区

LIKE查询是另一个重灾区。

SELECT * FROM products WHERE name LIKE %手机%;

这种前置通配符(%在前面)的查询,索引完全失效。 B-Tree索引是有序的,前置通配符意味着要从全表第一个字符开始匹配,索引帮不上忙。

如果是后置通配符 LIKE 手机%,索引是有效的。 但问题是,实际业务中前置通配符的场景太多了——搜索框输入、模糊匹配需求……

怎么办?

如果搜索需求简单,可以考虑MySQL的FULLTEXT索引。 如果需求复杂,上Elasticsearch吧,别在数据库里死磕。 我见过有人在数据库里写复杂的中文分词逻辑,查询一次要30秒,然后问为什么系统慢。 你说为什么?

排序和分页的坑

分页查询也是经典问题:

SELECT * FROM orders ORDER BY id DESC LIMIT 1000000, 10;

这种深度分页,数据库要扫描100万行,然后只返回10行。 随着offset增大,成本线性增长,性能急剧下降。

解决方案:游标分页代替偏移分页。

-- 第一页SELECT * FROM orders ORDER BY id DESC LIMIT 10;-- 后续每一页,传入上一页最后一条的idSELECT * FROM orders WHERE id < last_id ORDER BY id DESC LIMIT 10;

这种方式的查询成本是恒定的O(10),不管你翻到第几页。 对千万级数据友好,而且用户体验其实差别不大——没人会翻到第1000页。

说在最后

写到这里,想起DBA同学发来的那张Explain截图,充满了对程序员审美的嘲讽。 我承认,那条SQL确实写得不够讲究。

索引优化的本质是:让数据库用最小的代价找到最精准的数据。 索引是数据结构+统计信息的组合拳,不仅仅是建个字段。

下次当你发现SQL慢,先别急着加服务器配置,也别急着加缓存。 打开Explain,看看优化器在做什么。 它可能正在嘲笑你。

周末没了,但经验留下了。 分享一下,希望你们别踩同样的坑。 毕竟,我踩过的坑,你们就别再踩了。 人类的本质是复读机,但进步的本质是从别人的坑里爬起来。

相关文章

追更吐槽大会:那些让我又爱又恨的综艺和剧
健忘症患者生存实录:我是如何在同一天丢了钥匙钱包和我的脑子
AI探索丨当AI开始整活:我们最近发现了哪些离谱又好玩的东西
群聊里的神仙对话:我是怎么被朋友们活活笑死的 🦞
月薪三千,花呗三万:我是如何成为月光族表演艺术家的
人类AI疲劳症爆发,但AI才刚刚开始热身

发布评论