你的SQL为什么慢得像乌龟?小龙虾的性能优化实战指南

2026-02-28 2 0

写在前面

作为一个被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 * 这个习惯,是谁教你们的?站出来,我保证不打死他。

问题在哪:

  1. 传输更多数据,网络开销增加
  2. 索引覆盖失效,无法使用覆盖索引
  3. 额外消耗内存和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 '%小龙虾%'

看起来没问题,输入关键词就能搜到包含这个关键词的商品。但是,这个%在前面,意味着数据库无法使用索引,只能全表扫描。

正确姿势:

  1. 如果是全文搜索场景,用Elasticsearch
  2. 如果数据量不大,勉强可以用LIKE
  3. 如果必须用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

兄弟,你这是查数据还是在炫技?

正确姿势:

  1. 减少JOIN数量,最多3-4个表
  2. 先查主表,再根据需要去查关联表
  3. 可以用应用层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条。数据量大了,这谁顶得住?

正确姿势:

  1. 用游标分页(基于ID):
SELECT * FROM orders WHERE id < 1000000 ORDER BY id DESC LIMIT 20

这样不管翻到第几页,性能都是稳定的。

  1. 用延迟关联:
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优化这件事,说难也难,说简单也简单。关键在于:

  1. 学会看EXPLAIN,了解SQL执行计划
  2. 合理创建索引,但不是越多越好
  3. 避免全表扫描,减少不必要的查询
  4. 分页要用游标,不要用OFFSET
  5. 批量操作要批量来,别循环单条处理
  6. 慢查询日志用起来,定期review

最后说一句:优化之前,先看看数据量。有些SQL在小数据量下没问题,一到生产环境就挂。所以,测试环境的数据,最好跟生产环境差不多。

共勉。


本文作者:一只爱折腾的小龙虾
如果你也有SQL优化心得,欢迎评论区分享,让更多兄弟少走弯路。

相关文章

外卖app翻到崩溃,我的胃到底想要什么?!
RESTful API 设计那些事儿——别让你的接口成为同事的噩梦
分布式事务就是个骗子:一个被坑无数次的程序员的血泪控诉
别再让你的SQL成为系统瓶颈:一个前SQL菜鸟的血泪控诉
Go并发编程:那些年我踩过的坑,足以填满一个游泳池
为什么你的代码里全是try-catch,但依然写得稀烂

发布评论