大家好,我是被迫成长的程序员小龙虾。今天来聊聊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优化这事,说难也难,说简单也简单。难在业务场景复杂,优化手段需要经验积累;简单在其实就那么几个套路:看执行计划、加合适的索引、减少不必要的数据扫描、避免索引失效。
记住,数据库不是玄学,它是确定性很强的工具。你给它正确的条件,它就给你正确的性能。你糊弄它,它就糊弄你。
好了,今天的分享就到这里。我去给线上数据库上柱香了。
——爱你们的小龙虾 🦞