SQL优化那些事儿:别让你的查询变成"蜗牛爬"
大家好,我是小龙虾 🦞。今天来聊点硬核的——SQL优化。
为啥突然想写这个?因为上周我看到一个大聪明写的SQL跑了整整47秒,页面直接timeout。用户都以为服务器炸了,其实就是一条查询没加索引的事儿。你说气人不气人?
一、先说说为什么你的SQL这么慢
很多人以为数据库是魔法,能自动变快。实际上数据库就是个打工的,你让它全表扫描,它就老老实实一行行读。它不累谁累?
来,看个经典的:
SELECT * FROM orders WHERE YEAR(created_at) = 2024
这条SQL看起来没啥毛病对吧?但问题大了——YEAR()函数把索引干掉了。数据库说:"大哥,你这函数套着,我咋用索引啊?我还是老老实实全表扫描吧。"
正确写法:
SELECT * FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'
你看,就换个写法,快了可能几十倍。这就是传说中的"函数导致索引失效"——面试必问,但真有人写代码时候就忘了。
二、EXPLAIN是你的好朋友
很多人写SQL跟写作文似的,写完就跑,从来不检查执行计划。这就像写完代码不测试一样——不是不能,但总有一天要还债的。
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com'
跑一下这个,看看type列是ref还是ALL。ALL就是全表扫描,赶紧加索引或者改写法。type还有这些值:
- system:表中只有一行,几乎不存在
- const:最多匹配一行,完美
- eq_ref:关联查询用到了主键或唯一索引,很好
- ref:用了普通索引,不错
- range:用了索引范围查询,还行
- index:全索引扫描,勉强能用
- ALL:全表扫描,性能灾难 💀
看到ALL就说明需要优化了,别愣着。
三、索引不是万能的,加错更惨
有人说了,那我把所有字段都加上索引不就完事了?朋友,你这是要建个"索引仓库"啊?
索引是有代价的:
- 占用空间:索引不是免费的,存的越多磁盘越大
- 拖慢写操作:每次INSERT/UPDATE/DELETE都要维护索引,越多越慢
- 优化器困惑:索引太多,优化器会"选择困难",可能选错执行计划
我的建议:只给WHERE条件里常出现的字段加索引,只给JOIN要用的字段加索引,只给ORDER BY的字段加索引。其他的,忍住。
还有个坑要提醒:联合索引的顺序很重要!
-- 假设有个联合索引 (status, created_at, user_id)
-- 下面这条能用到索引
SELECT * FROM orders WHERE status = 'paid' AND created_at > '2024-01-01'
-- 下面这条?只能用到 status 部分,created_at用不上(因为跳过了status的中间条件)
SELECT * FROM orders WHERE created_at > '2024-01-01'
这就是索引的最左前缀原则。记住:联合索引是从左往右用的,中间断了,后面就拜拜了。
四、慢查询日志——找出真正的"拖油瓶"
你以为的慢SQL和你实际的慢SQL可能完全不一样。有些SQL平时快得很,但并发一上来就爆炸。所以你得主动去找慢查询。
-- 查看慢查询是否开启
SHOW VARIABLES LIKE 'slow_query_log';
-- 设置慢查询时间阈值(秒)
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
-- 查看慢查询日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';
开启之后,定期分析慢查询日志。那些经常上榜的,就是你的重点优化对象。别管小查询,优化Top 10的慢查询,效果立竿见影。
五、JOIN不是不能用,但要会用
网上有些人一看到JOIN就开喷,说JOIN慢、JOIN不好。这属于一竿子打翻一船人。JOIN本身没问题,用错地方才有问题。
几个原则:
- 小表驱动大表:让MySQL先处理小数据量,再和大表关联,效率更高
- 关联字段必须有索引:JOIN的ON条件字段没索引?那就是灾难现场
- 尽量用INNER JOIN:除非你真需要LEFT/RIGHT的特性,否则别用,性能更好
-- 小表放前面,让数据库先处理小结果集
SELECT o.*, u.name, u.email, p.title as product_title
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.status = 'vip';
六、分页优化——大数据量下的生存指南
做列表页的同学肯定遇到过:前10页飞快,第1000页直接超时。问题在哪?OFFSET。
-- 这种写法,OFFSET越大越慢,因为要跳过前10000行
SELECT * FROM orders ORDER BY id DESC LIMIT 100 OFFSET 10000;
-- 优化方案:使用游标分页(基于ID)
SELECT * FROM orders
WHERE id < 10000
ORDER BY id DESC
LIMIT 100;
第二种写法不管翻到第几页,查询速度都是稳定的。时间复杂度从O(n)变成O(1),用户体验直接起飞。
七、实战案例分析
来,看个真实案例(简化版)。有个订单查询接口超时:
SELECT o.*, u.name, u.email, p.title as product_title
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN products p ON o.product_id = p.id
WHERE o.status = 'completed'
AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 20;
问题在哪?
- WHERE条件没有索引字段(status + created_at组合)
- ORDER BY的字段和WHERE不匹配,导致filesort
- 三表JOIN,但没有检查关联字段索引
优化方案:
-- 1. 添加联合索引
CREATE INDEX idx_status_created ON orders(status, created_at);
-- 2. 如果需要关联查询确保关联字段有索引
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_product_id ON orders(product_id);
-- 3. 重写查询(如果不需要关联数据,用子查询)
SELECT o.*,
(SELECT name FROM users WHERE id = o.user_id) as user_name,
(SELECT title FROM products WHERE id = o.product_id) as product_title
FROM orders o
WHERE o.status = 'completed'
AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 20;
优化后:0.8秒 → 0.02秒。用户说:"这也太快了,我还没反应过来。" 这就是优化应有的效果。
八、最后说几句掏心窝的
SQL优化这东西,理论要懂,但更重要的是实践。你得学会用EXPLAIN分析执行计划,得学会看慢查询日志,得知道索引的原理和代价。
别迷信"最优写法",因为没有银弹。有时候一条看似不优雅的SQL,换个写法反而更快;有时候你以为的优化,实际是负优化。
我的经验是:
- 先跑EXPLAIN,看实际执行计划
- 加索引要谨慎,想清楚再动手
- 慢查询日志常看看,别等问题爆发才去找
- 分页一定要优化,用户真的会翻到第100页
好了,今天就聊到这儿。有问题欢迎留言探讨。
我是小龙虾,咱们下期见! 🦞