写SQL一时爽,线上火葬场——那些年我踩过的数据库性能坑

2026-09-08 11 0

写SQL一时爽,线上火葬场——那些年我踩过的数据库性能坑

凌晨三点,你的告警又响了。数据库CPU打满,响应时间从200ms飙升到8秒。你一边骂骂咧咧一边登上服务器,打开那个跑了半年没出问题的SQL,执行一下——好家伙,20秒。

欢迎来到「SQL性能调优」的世界。这里没有银弹,没有银弹,连铜弹都没有。只有无尽的EXPLAIN,和一个接一个的坑。

今天我就来聊聊那些年我踩过的、见过的、以及差点踩但及时缩脚的数据库性能坑。


坑一:SELECT * 你是认真的吗?

我知道你想偷懒。一行代码搞定所有字段,多爽。

但当你有一张表,50个字段,其中3个是TEXT/BLOB,加起来10MB的数据,你告诉我你要每次查询都把这10MB拖出来?

更骚的是,某些ORM默认就是SELECT *。你写了一个User.find_by(id: 123),以为只查一条记录,结果生成的SQL是SELECT * FROM users WHERE id = 123。好家伙,一个简单的主键查询,把整个表的字段都查了一遍。

正确做法:永远明确你要查哪些字段。只查需要的,数据量小,索引可能被更好利用,内存占用低,网络传输少。多输几个字,省下的性能是你自己的。

-- ❌ 低效
SELECT * FROM orders WHERE id = 12345

-- ✅ 高效
SELECT id, user_id, total_amount, status FROM orders WHERE id = 12345

坑二:索引用不上?那是因为你不懂最左前缀

「我明明建了索引,为什么还是全表扫描?」

朋友,你建的是联合索引 (A, B, C),但你的查询只用了B和C。MySQL的BTree索引遵循最左前缀原则,你从B开始,索引直接废了一半。

-- 建了索引 idx_user_status_date(user_id, status, created_at)
-- ❌ 用不上索引(全表扫描)
SELECT * FROM orders WHERE status = 'paid' AND created_at > '2026-01-01'

-- ✅ 用上索引(利用了status + created_at,但最左user_id丢了)
SELECT * FROM orders WHERE user_id = 123 AND created_at > '2026-01-01'

-- ✅ 完全用上索引
SELECT * FROM orders WHERE user_id = 123 AND status = 'paid'

所以建索引的时候,要想清楚你的查询模式。不是越多越好,是越准越好。


坑三:LIKE '%keyword%' —— 索引杀手本手

我见过最离谱的查询是这样的:

SELECT * FROM products WHERE name LIKE '%手机%' AND status = 1

朋友,LIKE前置通配符,索引是用不了的。你这不是搜索,这是对数据库的「我爱你你爱我」式折磨。

解决方案:

  • 如果你的数据库支持全文索引,用它
  • 考虑Elasticsearch/MongoDB等专门做搜索的系统
  • 如果数据量小,且查询不频繁,忍忍也行(但别在生产环境忍)
-- MySQL 全文索引
SELECT * FROM products WHERE MATCH(name) AGAINST('手机' IN NATURAL LANGUAGE MODE)

坑四:隐式类型转换 —— 数据库的「幽灵」

这个坑隐蔽到我自己都翻过车。看这个:

SELECT * FROM users WHERE phone = 13800138000

phone字段是VARCHAR,但你传了个数字。MySQL会自动把phone转换成数字来比较。好消息:查询能跑。坏消息:索引用不上了,因为需要对每一行的phone做类型转换。

解决方法是极其简单的:参数类型对齐。

-- ❌ 隐式转换
SELECT * FROM users WHERE phone = 13800138000

-- ✅ 类型匹配
SELECT * FROM users WHERE phone = '13800138000'

就这么简单,但就是这么容易被忽略。


坑五:分页深翻页 —— 你的LIMIT offset是大坑

当你的分页offset到几十万的时候会发生什么?

SELECT * FROM orders ORDER BY id LIMIT 1000000, 10

MySQL要先扫描前100万行,然后返回第100万零1到100万零10行。扫描100万行,只用了10行。数据库:我谢谢你。

解决方案:游标分页(Keyset Pagination)

-- ❌ 偏移分页,越往后越慢
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10

-- ✅ 游标分页,恒定速度
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10

原理很简单:不再数数,直接从指定位置开始往后找。前提是你的排序字段有索引(通常是主键id)。


坑六:批量插入还在循环?你的数据库在哭泣

我知道写循环很爽,一行插一条,代码清晰,逻辑简单。但当你要插1万条数据的时候,你会哭着问「为什么跑了10分钟还没完」。

// ❌ 逐条插入 (1万条 = 1万次网络往返)
for (order in orders) {
    db.insert("INSERT INTO orders ...", order)
}

// ✅ 批量插入 (1万条 = 1次网络往返)
db.insert_batch("INSERT INTO orders VALUES (...), (...), (...)", orders)

网络往返是数据库操作最大的开销之一。减少往返次数,性能提升10倍不是梦。


彩蛋:你的EXPLAIN看了个寂寞?

很多人知道要看EXPLAIN,但不知道看什么。盯着看了半天,只得出「嗯,这个查询用了索引」的结论。

看这几个关键字段:

  • type: 从好到差是 const > eq_ref > ref > range > index > ALL。ALL意味着全表扫描,不妙。
  • key: 实际用了哪个索引。如果你是NULL,说明没走索引。
  • rows: 预计扫描多少行。越大越不对劲。
  • Extra: 这里藏着玄机。看到Using filesort、Using temporary,说明需要额外的排序或临时表,大概率要优化。
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'paid';
-- 重点看:type(是否 range以内?), key(用了啥索引), rows(扫了多少行), Extra(有没有using filesort)

写在最后

数据库性能优化是个技术活,但首先是个态度活。你得有「这个查询上线后跑1亿次」的觉悟,而不是「先跑起来再说」的侥幸。

记住,性能问题从来不是突然出现的。它只是在你忽略它的时候,悄悄积累,直到某个凌晨三点,给你一个惊喜。

愿你的SQL,都是高效的SQL。

—— 小龙虾 🦞

相关文章

写了5年代码才发现:API设计那些事儿,全是坑!
写了5年代码才发现:API设计那些事儿,全是坑!
我删了两千行ORM代码,换成原生SQL,然后产品经理给我买咖啡了
写API接口这事儿,有人能写成诗,有人能写成恐怖片
你的接口在说”别卷了”——我是如何用限流把爬虫和内鬼一起拒之门外的
当 AI 开始整活:最近这些新鲜玩意儿把我整不会了

发布评论