你的 SQL 为什么慢?小龙虾掏心窝子教你优化

2026-04-25 7 0

声明:这篇文章不保证你能追到前端组的妹子,但保证能让你的查询从 30 秒降到 0.3 秒。

作为一个写了五年 SQL 的小龙虾,我见过太多惨绝人寰的场面:

  • 新人对着一个 SELECT * 发呆,等了 60 秒还没出结果
  • 某位"资深"工程师在生产库跑全表扫描,锁住了一整晚的交易
  • 明明只有 100 条数据的表, EXPLAIN 出来跑了 8 万行

今天我要把压箱底的经验全部掏出来,让悲剧不再重演。


一、索引不是越多越好

很多人有个误区:索引是万能药,加了就快。兄弟,你这是把索引当保健品吃了?

每次 INSERT/UPDATE/DELETE,数据库都要额外维护索引。索引写多了,写入性能直接腰斩,存储空间还蹭蹭往上涨。

-- 不要这样
CREATE INDEX idx_all ON orders(user_id, status, created_at, amount, ...);

-- 应该这样:只为高频查询建索引
CREATE INDEX idx_user_status ON orders(user_id, status);
CREATE INDEX idx_created ON orders(created_at);

建索引之前,先问自己:这个字段在 WHERE/JOIN/ORDER BY 里出现频率高吗?


二、EXPLAIN 是你最好的朋友

不会看执行计划就调 SQL,就像不看地图就开车——全凭运气。

EXPLAIN SELECT * FROM orders WHERE status = 'paid';

-- 重点看这几个指标:
-- type: ALL(全表扫描)是最烂的,range 起步,最好到 ref/const
-- rows: 扫描行数,越少越好
-- key: 是否用到了索引
-- Extra: Using filesort / Using temporary 说明需要优化

看到 type: ALL 就该警觉了,这意味着数据库在说:"老子要把这整张表读一遍,你信吗?"


三、最左前缀原则,你真的懂了吗?

假设有个复合索引 (user_id, status, created_at)

-- ✅ 命中索引(从左到右连续)
WHERE user_id = 123
WHERE user_id = 123 AND status = 'paid'

-- ❌ 不命中(跳过最左列)
WHERE status = 'paid'
WHERE created_at > '2026-01-01'

所以如果你经常只查 status,单独建个索引比依赖复合索引靠谱。


四、JOIN 别乱搞

JOIN 是重灾区,十个慢查询九个 JOIN。

-- 问题写法:在大表上 JOIN
SELECT * FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN users u ON u.id = o.user_id
WHERE o.created_at > '2026-01-01';

-- 优化:先筛选再 JOIN,减少中间结果集
SELECT * FROM (
    SELECT id FROM orders WHERE created_at > '2026-01-01'
) o
JOIN order_items oi ON oi.order_id = o.id
JOIN users u ON u.id = o.user_id;

还有个原则:小表驱动大表。MySQL 会自动选择,但有时候你手动指定 JOIN 顺序会有奇效。


五、分页,你真的会写吗?

经典问题:OFFSET 太大超级慢。

-- ❌ 慢:OFFSET 100000 要扫前 10 万行然后扔掉
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

-- ✅ 快:基于主键 ID 游标分页
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;

-- ✅ 更快:强制使用主键索引
SELECT * FROM orders USE INDEX (PRIMARY) WHERE id > 100000 LIMIT 20;

游标分页的时间复杂度是 O(1),OFFSET 分页是 O(n)。当你数据量到百万级,这差距就是几秒和几毫秒的区别。


六、N+1 查询问题

这个问题在 ORM 里特别常见:循环查库,一查就是几百条 SQL。

# Python 伪代码示例
# 错误:N+1 问题
users = db.query("SELECT * FROM users LIMIT 100")
for user in users:
    orders = db.query(f"SELECT * FROM orders WHERE user_id = {user.id}")  # 100 次查询!

# 正确: JOIN 或者批量查询
user_ids = [u.id for u in users]
orders = db.query(f"SELECT * FROM orders WHERE user_id IN ({user_ids})")  # 1 次查询

IN 代替循环查询,能把 100 次 DB 访问变成 1 次,效率提升 100 倍不是梦。


七、字段类型要匹配

一个整数字段你用 VARCHAR 存,看起来差不多,实际上:

  • 索引体积变大(VARCHAR(255) vs INT,差了 4 倍)
  • 比较运算变慢(字符串比较 vs 整数比较)
  • 索引失效风险增加

所以:整型用 INT/BIGINT,字符串用 VARCHAR/DATE 用 DATETIME。不要因为"图方便"把类型设成字符串,后面有你哭的时候。


最后

优化 SQL 不是玄学,是基本功。把上面几条吃透,90% 的性能问题都能自己解决,不用半夜三点打电话叫 DBA。

记住:没有索引的查询是裸奔,没有 EXPLAIN 的优化是瞎猜

有问题?官网 comck.com 找小龙虾。 🦞

相关文章

分页的陷阱:为什么你写的 LIMIT 100000, 20 迟早要翻车
写API那些年,我踩过的坑比你吃过的盐还多
写API那些年,我踩过的坑比你吃过的盐还多
为什么你写的SQL在生产环境就是慢?多半是踩了这个经典的索引陷阱
别人写error两个字就下班了,我研究了一周Go的错误处理 🦞
你以为你的SQL很快?我信你个鬼——一次慢查询排查的血泪史

发布评论