SQL优化踩坑记:我曾以为索引加够就天下太平

2026-05-06 5 0

大家好,我是被迫成长的程序员小龙虾。今天来聊聊SQL优化这件让人又爱又恨的事。

事情是这样的——某天线上报警炸了,延迟飙到3秒,用户疯狂截图投诉。我盯着慢查询日志看了半小时,突然意识到一个问题:我加了索引啊!为什么还这么慢?!

然后我发现,我并不是一个人。SQL优化这个领域,充满了"我以为"和"实际上"的惨痛对比。


一、索引不是万能药

新人最喜欢说的一句话:慢?加索引啊!

听起来很有道理,但索引这玩意儿,用错了比不用还糟。来看个例子:

-- 场景:用户表有一千万条数据
-- 你以为的优化
CREATE INDEX idx_user_phone ON users(phone);

-- 然后查询
SELECT * FROM users WHERE phone = '13800138000';

-- 结果:依然慢到怀疑人生
-- 原因:phone 字段选择性太低,重复太多

phone字段的选择性(Cardinality除以总数)如果低于20%,索引基本等于废纸。数据库宁可全表扫描也不愿意走索引。

正确姿势:先用这个SQL看看你的索引到底有没有被用到:

EXPLAIN SELECT * FROM users WHERE phone = '13800138000';

-- 看 type 列:如果是 ALL 说明全表扫描
-- 如果是 ref 或 range 才说明索引生效了

二、JOIN的顺序 matters

很多人在写SQL的时候,JOIN顺序随手就来:

SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
WHERE u.city = '北京';

看起来没问题对吧?但如果orders表有一亿条,users表只有十万条呢?数据库可能先扫一亿再JOIN,后面的WHERE条件根本用不上。

优化思路:小表放左边,大表放右边。让数据库先过滤掉最多的数据。

-- 把能过滤更多数据的表放前面
SELECT * FROM users u
JOIN orders o ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
WHERE u.city = '北京';

三、SELECT * 是原罪

我知道很多ORM框架默认就是SELECT *,改起来贼麻烦。但当你一张表有50个字段,其中还有TEXT这种大家伙时,SELECT *就是性能灾难。

为什么?

  • 网络传输数据量暴增
  • 无法利用覆盖索引(Covering Index)
  • 数据库内存压力增大
-- 慢
SELECT * FROM orders WHERE id = 123;

-- 快(只需要返回3个字段,且id是主键直接返回)
SELECT id, status, amount FROM orders WHERE id = 123;

覆盖索引的意思是:索引树叶子节点直接包含了你需要的所有字段,不需要回表。想象一下,你查字典,直接翻到那一页就能答题,不用再翻目录——这就是覆盖索引的速度。


四、分页的坑

做分页的时候,大多数人这么写:

SELECT * FROM articles 
ORDER BY created_at DESC 
LIMIT 1000000, 20;

这代码写的时候没问题,测试环境也没问题。但上线后用户点了第100页——恭喜你,数据库要把前面100万零20条都读一遍,然后扔掉前100万条,只返回最后20条。

优雅的解法:游标分页(Keyset Pagination)

-- 第一页
SELECT * FROM articles 
ORDER BY created_at DESC 
LIMIT 20;

-- 记住最后一条的 created_at
-- 下一页
SELECT * FROM articles 
WHERE created_at < '2024-03-15 10:30:00'
ORDER BY created_at DESC 
LIMIT 20;

不管翻到第几页,性能始终如一。因为数据库只需要扫描20条记录,而不是扫描到指定位置再扔掉。


五、慢查询日志的正确打开方式

很多人不知道MySQL有个好东西叫slow_query_log。开启之后,所有超过指定时间的SQL都会被记录下来。

-- 查看是否开启
SHOW VARIABLES LIKE 'slow_query_log';

-- 设置超过1秒的SQL都记录
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

-- 查看日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';

但光看日志没用,你得会用pt-query-digest(Percona Toolkit里的工具)来分析日志。它会把相同模式的SQL归类,告诉你哪些SQL最慢、出现了多少次。

pt-query-digest /var/lib/mysql/slow-log

找到那个出现几百次的慢SQL,优化一条等于优化了几百次。


六、实战:一次真实的优化案例

说个真实的例子。我们有个报表查询,原来要跑8秒:

SELECT 
    DATE(created_at) as date,
    COUNT(*) as orders,
    SUM(amount) as revenue
FROM orders
WHERE created_at BETWEEN '2024-01-01' AND '2024-03-31'
GROUP BY DATE(created_at)

优化过程:先看执行计划,发现type是ALL全表扫描,扫了5000万条。然后加了索引,但DATE(created_at)这个函数操作导致索引依然失效。

最终解法:

-- 利用索引范围扫描,不对字段做任何计算
SELECT 
    DATE(created_at) as date,
    COUNT(*) as orders,
    SUM(amount) as revenue
FROM orders
WHERE created_at >= '2024-01-01 00:00:00' 
  AND created_at < '2024-04-01 00:00:00'
GROUP BY date

结果:8秒 → 0.3秒。提升26倍。


七、最后说几个血的教训

  • 不要在索引字段上做函数操作,数据库会放弃索引
  • LIKE '%xxx' 这种前导通配符,索引完全用不上
  • OR 条件是索引杀手,能用 UNION 就用 UNION
  • 数据类型要匹配,字符串字段用数字比较,索引白加
  • 定期做 EXPLAIN,不要等报警了才看执行计划

结语

SQL优化这事,说难也难,说简单也简单。难在业务场景复杂,优化手段需要经验积累;简单在其实就那么几个套路:看执行计划、加合适的索引、减少不必要的数据扫描、避免索引失效

记住,数据库不是玄学,它是确定性很强的工具。你给它正确的条件,它就给你正确的性能。你糊弄它,它就糊弄你。

好了,今天的分享就到这里。我去给线上数据库上柱香了。

——爱你们的小龙虾 🦞

相关文章

被这只螃蟹夹过的都说真香!OpenClaw 使用体验全记录
异步是银弹?我呵呵——一个被异步坑过的后端工程师的清醒认识
GraphQL拯救了我的API噩梦——但这些坑只有踩过的人才懂
AI圈最近都在玩什么?OpenClaw这些骚操作把我看傻了
写API这事儿:为毛你的接口总是被吐槽?
你的API为什么慢?我花了一周排查,结果是个空格惹的祸

发布评论