学习是为了不落后,整理则是为了不忘记。
这是我在数据分析这行干了近10年的体会。SQL写得烂,不只是慢,是浪费公司的钱——你多跑1小时的查询,集群就多烧1小时的钱。
2026年了,AI能帮你写SQL,但AI写出来的SQL不一定快。今天船长把压箱底的8个优化技巧全拆给你看,每个都有真实案例,直接复制就能用。
一、EXPLAIN先看执行计划,别猜
90%的SQL性能问题,EXPLAIN一眼就能看出来。但很多人习惯"改了再跑",跑完再说。
正确姿势:
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2026-01-01'
GROUP BY u.id, u.name;
看三个指标:Seq Scan vs Index Scan(有没有走索引)、rows estimated vs rows actual(预估行数准不准)、execution time(总耗时)。
**实战案例:**一个用户画像查询从12秒优化到0.3秒,就是因为EXPLAIN发现走了全表扫描,加了个组合索引就解决了。
二、索引不是越多越好,组合索引有讲究
见过有人给表加了20个索引,结果INSERT比SELECT还慢。索引是双刃剑,写快了读就慢,读快了写就慢。
组合索引的黄金法则:最左前缀匹配
-- 建索引
CREATE INDEX idx_user_status_date ON orders(user_id, status, created_at);
-- ✅ 能命中索引
WHERE user_id = 123 AND status = 'paid'
WHERE user_id = 123
-- ❌ 不能命中索引(跳过了最左列)
WHERE status = 'paid' AND created_at > '2026-01-01'
**船长经验:**单表索引不超过5个,组合索引列数不超过3列。超过这个数,说明你的表设计有问题。
三、子查询改成JOIN,性能差10倍
这是新手最常犯的错。子查询在MySQL 5.6之前每执行一次就是一次全表扫描,改成JOIN可以直接走索引。
慢写法(子查询):
SELECT * FROM orders
WHERE user_id IN (
SELECT id FROM users WHERE city = '北京'
);
快写法(JOIN):
SELECT o.* FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.city = '北京';
**实测数据:**一个100万行的订单表,子查询耗时3.2秒,JOIN只用了0.15秒,差了21倍。
四、避免SELECT *,只查需要的列
SELECT * 看着方便,但后果很严重:①网络传输量暴增 ②无法使用覆盖索引 ③回表次数增加。
**覆盖索引的威力:**如果你的查询只涉及索引列,数据库直接从索引返回数据,不需要回表查数据行。这个优化在某些场景下可以快100倍。
-- 假设有索引 idx_user_name_age(user_id, name, age)
-- ✅ 覆盖索引,不需要回表
SELECT user_id, name, age FROM users WHERE user_id = 123;
-- ❌ 多查了一列,必须回表
SELECT user_id, name, age, email FROM users WHERE user_id = 123;
五、LIKE模糊查询的坑
LIKE '%关键词'和LIKE '%关键词%'都会导致索引失效,触发全表扫描。
解决方案:
① 前缀匹配可以用索引:LIKE '关键词%'
② 全文检索用全文索引:MySQL的FULLTEXT或Elasticsearch
③ 精确匹配用等号:= '关键词'
**实战案例:**一个商品搜索接口,原来用LIKE '%手机%',500万商品表查询耗时8秒。改用MySQL FULLTEXT索引后,查询降到0.05秒。
六、分页查询的深分页问题
LIMIT 100000, 20这种写法,数据库要先查出100020行,再丢弃前100000行。越往后翻越慢。
优化方案:游标分页
-- ❌ 深分页:查100万行,只要最后20行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- ✅ 游标分页:只查大于上一页最后一条ID的记录
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;
**实测数据:**1000万行订单表,OFFSET分页第50页耗时2.3秒,游标分页始终在0.01秒。
七、批量INSERT代替逐条INSERT
inserting 1万条数据,逐条INSERT要30秒,批量INSERT只要0.5秒。
-- ❌ 逐条插入(循环1万次)
INSERT INTO logs (user_id, action) VALUES (1, 'click');
-- ✅ 批量插入(一次1千条)
INSERT INTO logs (user_id, action) VALUES
(1, 'click'), (2, 'view'), (3, 'click'), ...;
**船长经验:**批量大小控制在500-1000条/次,太大可能触发MySQL的max_allowed_packet限制。
八、用窗口函数替代自连接
"查每个部门的薪资最高的人",老写法是自连接,新写法用窗口函数ROW_NUMBER()。
-- ❌ 自连接:两次扫描同一张表
SELECT e.* FROM employees e
INNER JOIN (
SELECT dept, MAX(salary) AS max_sal
FROM employees GROUP BY dept
) m ON e.dept = m.dept AND e.salary = m.max_sal;
-- ✅ 窗口函数:一次扫描搞定
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY dept ORDER BY salary DESC
) AS rn
FROM employees
)
SELECT * FROM ranked WHERE rn = 1;
**实测数据:**50万行员工表,自连接耗时1.8秒,窗口函数0.4秒,快了4.5倍,代码也更清晰。
总结:SQL优化检查清单
✅ 1. 先EXPLAIN看执行计划,别凭感觉
✅ 2. 组合索引遵循最左前缀,单表不超过5个
✅ 3. 子查询改JOIN,避免全表扫描
✅ 4. 不用SELECT *,善用覆盖索引
✅ 5. LIKE前缀通配符走索引,后缀不行
✅ 6. 深分页用游标,别用OFFSET
✅ 7. 批量INSERT,每次500-1000条
✅ 8. 窗口函数替代自连接
📌 数据来源:个人工作记录统计(2024-2026年数据分析项目实战),测试环境为MySQL 8.0,表数据量100万-1000万行。
你在工作中遇到过哪些SQL性能坑?评论区聊聊,船长帮你看看。