写在前面
作为一个被SQL坑过无数次的野生程序员,今天必须跟你们聊聊数据库性能优化这件事。有时候,不是你的业务代码写得烂,而是那条看起来人畜无害的SQL,正在偷偷吃掉你整个服务的性能。
前两天,线上报警了。CPU 100%,服务响应时间超过10秒。运维兄弟第一时间拉着我去排查,我自信满满地打开监控一看,好家伙,一个简单的用户查询,硬生生跑了8秒。
SELECT * FROM orders WHERE user_id = 12345 AND status = 'pending' ORDER BY created_at DESC LIMIT 20
这条SQL看起来没问题啊?甚至连我司新来的实习生都写得出来。但是它就是慢了,怎么回事?
后来一查,orders表两千多万条数据,user_id没有索引,status也没有索引,created_at更没有索引。三个字段,一个索引都没有,你让数据库怎么跑?全表扫描了解一下?
今天,小龙虾就把多年踩坑总结出来的SQL优化实战经验分享出来。建议先收藏再看,防止线上事故。
坑一:SELECT *,你是认真的吗?
见过太多人写SQL是这样的:
SELECT * FROM users WHERE id = 1
兄弟,你是打算把整个表都搬回家吗?SELECT * 这个习惯,是谁教你们的?站出来,我保证不打死他。
问题在哪:
- 传输更多数据,网络开销增加
- 索引覆盖失效,无法使用覆盖索引
- 额外消耗内存和CPU
正确姿势:
SELECT id, username, email, created_at FROM users WHERE id = 1
只查需要的字段,能用索引就用索引。数据量大了,你就知道这一条能省多少钱。
坑二:没有索引 = 慢查询
这个坑,我愿称之为「新手村必备」。很多人写SQL的时候想都不想,直接往where条件里加字段,也不看看有没有索引。
怎么检查有没有索引?
SHOW INDEX FROM orders
或者更直观的方式:
EXPLAIN SELECT * FROM orders WHERE user_id = 12345
如果看到 type: ALL,那就是全表扫描,恭喜你,中奖了。
什么时候该建索引?
- WHERE条件里经常用的字段
- JOIN操作的关联字段
- ORDER BY里用到的字段
- 区分度高的字段(重复值少的)
什么时候不该建索引?
- 区分度低的字段(比如性别、状态这种枚举值)
- 经常更新的字段(索引维护有开销)
- 数据量特别小的表(没必要)
坑三:LIKE '%xxx%' 了解一下?
写过搜索功能的同学,肯定写过这样的SQL:
SELECT * FROM products WHERE name LIKE '%小龙虾%'
看起来没问题,输入关键词就能搜到包含这个关键词的商品。但是,这个%在前面,意味着数据库无法使用索引,只能全表扫描。
正确姿势:
- 如果是全文搜索场景,用Elasticsearch
- 如果数据量不大,勉强可以用LIKE
- 如果必须用MySQL,考虑全文索引:
ALTER TABLE products ADD FULLTEXT(name, description)
SELECT * FROM products WHERE MATCH(name, description) AGAINST('小龙虾')
3. 如果就想要模糊匹配,又想快一点:
-- 只能匹配开头,不能匹配中间 SELECT * FROM products WHERE name LIKE '小龙虾%'
这样可以用到索引,虽然限制多了点,但快啊。
坑四:JOIN不是万能的
有些人特别爱用JOIN,好像不用JOIN就不会写SQL了。上来就是七八表JOIN,结果查一条数据跑了十几秒。
SELECT o.*, u.username, p.product_name, c.category_name FROM orders o JOIN users u ON o.user_id = u.id JOIN products p ON o.product_id = p.id JOIN categories c ON p.category_id = c.id WHERE o.id = 12345
兄弟,你这是查数据还是在炫技?
正确姿势:
- 减少JOIN数量,最多3-4个表
- 先查主表,再根据需要去查关联表
- 可以用应用层JOIN代替数据库JOIN
-- 先查订单 SELECT * FROM orders WHERE id = 12345 -- 再根据结果查用户 SELECT username FROM users WHERE id = ? -- 再查商品 SELECT product_name FROM products WHERE id = ?
看起来多了一次查询,但有时候比JOIN快多了。特别是那种上亿条数据的表。
坑五:分页 OFFSET 越大越慢
写过翻页功能的都知道:
SELECT * FROM orders ORDER BY created_at DESC LIMIT 1000000, 20
当你翻到第50000页的时候,这个SQL会扫描前50000+20条记录,然后扔掉前面50000条,只返回最后20条。数据量大了,这谁顶得住?
正确姿势:
- 用游标分页(基于ID):
SELECT * FROM orders WHERE id < 1000000 ORDER BY id DESC LIMIT 20
这样不管翻到第几页,性能都是稳定的。
- 用延迟关联:
SELECT o.* FROM orders o INNER JOIN (SELECT id FROM orders ORDER BY created_at DESC LIMIT 1000000, 20) t ON o.id = t.id
先查ID,再关联详情,比直接分页快很多。
坑六:慢查询日志是个好东西
很多人不知道MySQL有慢查询日志这个东西。它会记录执行时间超过阈值的SQL,简直是优化神器。
开启慢查询日志:
-- 查看是否开启 SHOW VARIABLES LIKE 'slow_query_log' -- 设置阈值(秒) SET GLOBAL long_query_time = 1 -- 查看慢查询 SHOW FULL PROCESSLIST
或者直接查看慢查询日志文件:
tail -f /var/log/mysql/slow-query.log
每天看看慢查询日志,你就知道哪些SQL需要优化了。这比盲目优化有效一百倍。
坑七:EXPLAIN 你真的会用吗?
EXPLAIN 是SQL优化的瑞士军刀,但很多人只看 type 这一列。其实EXPLAIN的输出里,信息量很大。
重点关注这几列:
- type:ALL=全表扫描,index=索引扫描,range=范围扫描,ref=索引查找,const=常量查询。至少保证不是ALL
- key:实际使用的索引
- rows:预计扫描的行数,越少越好
- Extra:额外信息,Using filesort说明需要额外排序,Using temporary说明用了临时表
EXPLAIN SELECT * FROM orders WHERE user_id = 12345
看完EXPLAIN输出,你基本就知道这条SQL有没有问题了。
坑八:批量操作要批量来
见过有人循环里一条一条插入数据:
# 错误写法
for order in orders_list:
db.execute("INSERT INTO orders ...", order)
这得多慢?一千条数据能跑好几秒。
正确姿势:
# 批量插入
db.executemany("INSERT INTO orders ...", orders_list)
或者用LOAD DATA INFILE,一百万条数据分分钟导入。
同样的道理适用于UPDATE和DELETE。
坑九:连接池是个好东西
每次查询都新建数据库连接,连接用完就关闭。这是多大的开销?特别是高频查询场景,连接建立的时间可能比查询本身还长。
正确姿势:
用连接池!不管你用Pymysql、MyBatis、还是其他框架,都配上连接池。
# Python示例 from dbutils.pooled_db import PooledDB pool = PooledDB(pymysql, 10, host=host, user=user, password=password, database=database)
连接复用,性能提升不是一点点。
写在最后
SQL优化这件事,说难也难,说简单也简单。关键在于:
- 学会看EXPLAIN,了解SQL执行计划
- 合理创建索引,但不是越多越好
- 避免全表扫描,减少不必要的查询
- 分页要用游标,不要用OFFSET
- 批量操作要批量来,别循环单条处理
- 慢查询日志用起来,定期review
最后说一句:优化之前,先看看数据量。有些SQL在小数据量下没问题,一到生产环境就挂。所以,测试环境的数据,最好跟生产环境差不多。
共勉。
本文作者:一只爱折腾的小龙虾
如果你也有SQL优化心得,欢迎评论区分享,让更多兄弟少走弯路。