1. 那些年我们写过的低效SQL
作为一名常年与数据库打交道的开发者,我见过太多因为SQL写法不当导致的性能问题。有些查询看起来人畜无害,实际执行时却能拖垮整个系统。今天我们就来盘点8种最常见的低效SQL写法,以及如何优化它们。
2. 分页查询的陷阱
2.1 LIMIT的性能问题
分页查询是最常用的场景之一,但也是最容易出问题的地方。比如下面这个简单的分页查询:
sql复制SELECT *
FROM operation
WHERE type = 'SQLStats'
AND name = 'SlowLog'
ORDER BY create_time
LIMIT 1000, 10;
很多DBA会建议在type、name和create_time字段上建立组合索引,这样确实能提升性能。但当分页深度很大时,比如LIMIT 1000000,10,查询仍然会很慢。
这是因为MySQL并不知道第1000000条记录在哪里,即使有索引也需要从头开始计算偏移量。
2.2 优化方案:记住上一页的最后一条记录
更高效的做法是记住上一页最后一条记录的create_time,然后这样查询:
sql复制SELECT *
FROM operation
WHERE type = 'SQLStats'
AND name = 'SlowLog'
AND create_time > '2023-01-01 12:00:00'
ORDER BY create_time
LIMIT 10;
这种写法无论翻到第几页,性能都保持稳定,因为MySQL可以利用索引直接定位到起始位置。
3. 隐式类型转换的坑
3.1 类型不匹配导致索引失效
看看这个查询:
sql复制SELECT *
FROM my_balance b
WHERE b.bpn = 14000000123
AND b.isverified IS NULL;
如果bpn字段是varchar类型,而查询条件传入的是数字,MySQL会进行隐式类型转换,导致索引失效。
3.2 解决方案:保持类型一致
确保查询条件的类型与字段定义一致:
sql复制SELECT *
FROM my_balance b
WHERE b.bpn = '14000000123'
AND b.isverified IS NULL;
4. 关联更新和删除的正确姿势
4.1 低效的子查询写法
sql复制UPDATE operation o
SET status = 'applying'
WHERE o.id IN (SELECT id
FROM (SELECT o.id,
o.status
FROM operation o
WHERE o.group = 123
AND o.status NOT IN ('done')
ORDER BY o.parent,
o.id
LIMIT 1) t);
这种写法会导致MySQL执行嵌套子查询,性能极差。
4.2 优化为JOIN
sql复制UPDATE operation o
JOIN (SELECT o.id,
o.status
FROM operation o
WHERE o.group = 123
AND o.status NOT IN ('done')
ORDER BY o.parent,
o.id
LIMIT 1) t
ON o.id = t.id
SET status = 'applying'
改写后执行计划从DEPENDENT SUBQUERY变为DERIVED,性能提升显著。
5. 混合排序的优化技巧
5.1 问题SQL
sql复制SELECT *
FROM my_order o
INNER JOIN my_appraise a ON a.orderid = o.id
ORDER BY a.is_reply ASC,
a.appraise_time DESC
LIMIT 0, 20
这种混合排序(先按is_reply升序,再按appraise_time降序)会导致全表扫描和文件排序。
5.2 优化方案:使用UNION ALL
sql复制SELECT *
FROM ((SELECT *
FROM my_order o
INNER JOIN my_appraise a
ON a.orderid = o.id
AND is_reply = 0
ORDER BY appraise_time DESC
LIMIT 0, 20)
UNION ALL
(SELECT *
FROM my_order o
INNER JOIN my_appraise a
ON a.orderid = o.id
AND is_reply = 1
ORDER BY appraise_time DESC
LIMIT 0, 20)) t
ORDER BY is_reply ASC,
appraise_time DESC
LIMIT 20;
通过将不同状态的记录分开查询再合并,性能从1.58秒提升到2毫秒。
6. EXISTS与JOIN的选择
6.1 EXISTS的低效执行
sql复制SELECT *
FROM my_neighbor n
LEFT JOIN my_neighbor_apply sra
ON n.id = sra.neighbor_id
AND sra.user_id = 'xxx'
WHERE n.topic_status < 4
AND EXISTS(SELECT 1
FROM message_info m
WHERE n.id = m.neighbor_id
AND m.inuser = 'xxx')
AND n.topic_type <> 5
EXISTS会导致MySQL使用嵌套子查询执行方式。
6.2 改用JOIN
sql复制SELECT *
FROM my_neighbor n
INNER JOIN message_info m
ON n.id = m.neighbor_id
AND m.inuser = 'xxx'
LEFT JOIN my_neighbor_apply sra
ON n.id = sra.neighbor_id
AND sra.user_id = 'xxx'
WHERE n.topic_status < 4
AND n.topic_type <> 5
改写后执行时间从1.93秒降低到1毫秒。
7. 条件下推优化
7.1 聚合查询的条件问题
sql复制SELECT *
FROM (SELECT target,
Count(*)
FROM operation
GROUP BY target) t
WHERE target = 'rm-xxxx'
这种写法会导致先全表聚合,再过滤结果。
7.2 优化写法
sql复制SELECT target,
Count(*)
FROM operation
WHERE target = 'rm-xxxx'
GROUP BY target
将过滤条件下推到聚合前,性能显著提升。
8. 提前缩小结果集
8.1 原始低效查询
sql复制SELECT *
FROM my_order o
LEFT JOIN my_userinfo u
ON o.uid = u.uid
LEFT JOIN my_productinfo p
ON o.pid = p.pid
WHERE (o.display = 0)
AND (o.ostaus = 1)
ORDER BY o.selltime DESC
LIMIT 0, 15
这种写法会先做全表连接,再排序和限制结果。
8.2 优化方案:先过滤再连接
sql复制SELECT *
FROM (
SELECT *
FROM my_order o
WHERE (o.display = 0)
AND (o.ostaus = 1)
ORDER BY o.selltime DESC
LIMIT 0, 15
) o
LEFT JOIN my_userinfo u
ON o.uid = u.uid
LEFT JOIN my_productinfo p
ON o.pid = p.pid
ORDER BY o.selltime DESC
LIMIT 0, 15
先对主表进行过滤和排序,再连接其他表,性能提升明显。
9. 中间结果集下推
9.1 原始查询
sql复制SELECT a.*,
c.allocated
FROM (SELECT resourceid
FROM my_distribute d
WHERE isdelete = 0
AND cusmanagercode = '1234567'
ORDER BY salecode LIMIT 20) a
LEFT JOIN (SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated
FROM my_resources
GROUP BY resourcesid) c
ON a.resourceid = c.resourcesid
子查询c会全表扫描my_resources表,性能很差。
9.2 优化写法
sql复制WITH a AS
(
SELECT resourceid
FROM my_distribute d
WHERE isdelete = 0
AND cusmanagercode = '1234567'
ORDER BY salecode LIMIT 20
)
SELECT a.*,
c.allocated
FROM a
LEFT JOIN (SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated
FROM my_resources r,
a
WHERE r.resourcesid = a.resourcesid
GROUP BY resourcesid) c
ON a.resourceid = c.resourcesid
使用WITH子句复用子查询a,并将过滤条件下推到聚合查询中。
10. 编写高效SQL的经验总结
-
理解执行计划:学会使用EXPLAIN分析SQL执行计划,找出性能瓶颈。
-
索引使用原则:
- 确保查询条件能够使用索引
- 避免隐式类型转换
- 注意联合索引的最左前缀原则
-
JOIN优化技巧:
- 小表驱动大表
- 合理使用INNER JOIN和LEFT JOIN
- 避免复杂的嵌套子查询
-
分页查询优化:
- 避免大偏移量
- 使用"记住上一页最后一条记录"的方式
-
排序优化:
- 尽量使用索引排序
- 对于混合排序,考虑使用UNION ALL拆分
-
聚合查询优化:
- 将过滤条件下推到聚合前
- 考虑使用物化视图预计算聚合结果
-
使用WITH子句:
- 提高复杂SQL的可读性
- 避免重复计算相同的子查询
在实际开发中,我习惯先写出功能正确的SQL,然后通过执行计划分析性能问题,再逐步优化。记住,数据库优化器并不完美,我们需要理解它的工作原理,才能写出高效的SQL语句。