使用 Explain 进行分析
Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。
比较重要的字段有:
- select_type : 查询类型,有简单查询、联合查询、子查询等
- key : 使用的索引
- rows : 扫描的行数
优化数据访问
1. 减少请求的数据量
- 只返回必要的列: 最好不要使用 SELECT * 语句。
- 只返回必要的行: 使用 LIMIT 语句来限制返回的数据。
- 缓存重复查询的数据: 使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。
2. 减少服务器端扫描的行数
最有效的方式是使用索引来覆盖查询。
重构查询方式
1. 切分大查询(Batch Delete)
原方案
DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
问题:
- 一次性删除大量数据可能导致:
-
- 长事务:锁定大量数据,阻塞其他操作。
- 日志膨胀:事务日志(如 InnoDB 的 undo log)被占满。
- 主从延迟:大事务在复制时延迟严重。
优化方案:分批删除
rows_affected = 0
do {
rows_affected = do_query(
"DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000"
)
} while rows_affected > 0
优点:
- 减少锁持有时间:每次只锁 1 万行,避免长时间阻塞。
- 控制资源占用:避免单次事务过大。
- 可中断性:随时停止不会导致部分数据未删除。
注意事项:
- 加索引:确保
create
字段有索引,否则分批删除依然全表扫描。 - 避免间隙锁:如果使用 InnoDB,建议改用
id
范围分批(避免间隙锁冲突):
DELETE FROM messages WHERE id BETWEEN 1 AND 10000 AND create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
- 休眠间隔:每次删除后休眠一段时间(如 100ms),减少对正常业务的影响。
2. 分解大连接查询(Join Decomposition)
原方案(大连接查询)
SELECT * FROM tag
JOIN tag_post ON tag_post.tag_id = tag.id
JOIN post ON tag_post.post_id = post.id
WHERE tag.tag = 'mysql';
潜在问题:
- 缓存失效:任一表更新会导致整个查询缓存失效。
- 锁竞争:多表连接可能跨表加锁。
- 执行计划复杂:优化器可能选择低效的连接顺序。
优化方案:分解为单表查询 + 应用层关联
-- 第一步:查询 tag 表
SELECT * FROM tag WHERE tag = 'mysql';
-- 假设返回 tag.id = 1234
-- 第二步:查询 tag_post 表
SELECT * FROM tag_post WHERE tag_id = 1234;
-- 假设返回 post_id 列表 (123, 456, 567)
-- 第三步:查询 post 表
SELECT * FROM post WHERE id IN (123, 456, 567);
优点:
- 缓存友好:
-
- 单表查询结果可独立缓存(如
tag.tag='mysql'
的结果被缓存后,其他查询可复用)。 - 连接查询的缓存一旦失效,所有表关联结果均失效。
- 单表查询结果可独立缓存(如
- 减少锁竞争:
-
- 单表查询锁定的范围更小。
- 避免多表连接时潜在的锁升级(如行锁升级为表锁)。
- 灵活扩展:
-
- 易于分库分表(如
post
表和tag_post
表可部署在不同节点)。
- 易于分库分表(如
- 执行效率:
-
- MySQL 对
IN()
列表的查询会按主键顺序访问,可能比随机连接更高效(尤其对机械硬盘)。
- MySQL 对
注意事项:
- 网络开销:应用层关联需多次查询,适合低延迟网络环境。
- IN() 列表限制:
-
- MySQL 的
IN()
列表长度有限制(默认max_allowed_packet
限制)。 - 超长列表可改用临时表或分批查询:
- MySQL 的
-- 使用临时表
CREATE TEMPORARY TABLE temp_post_ids (id INT PRIMARY KEY);
INSERT INTO temp_post_ids VALUES (123), (456), (567);
SELECT * FROM post WHERE id IN (SELECT id FROM temp_post_ids);
- 事务一致性:
-
- 多次查询可能读到中间状态(需在事务中执行或使用快照读)。
何时选择分解连接查询?
场景 |
适合分解 |
适合直接连接 |
查询缓存命中率高 |
⭐️ 单表查询缓存更易复用 |
❌ 连接查询缓存易失效 |
表数据更新频繁 |
⭐️ 减少锁竞争和缓存失效 |
❌ 大连接可能阻塞其他操作 |
分库分表架构 |
⭐️ 天然支持跨节点查询 |
❌ 跨节点连接难以实现 |
简单查询(如主键查询) |
❌ 额外网络开销不划算 |
⭐️ 直接连接更高效 |
复杂聚合计算(如 SUM) |
❌ 应用层聚合性能差 |
⭐️ 数据库聚合更高效 |
其他优化补充
(1)替代 IN()
的方案
- 临时表:如前述,适合超长列表。
- JOIN 替代 IN:
SELECT post.* FROM post
JOIN (SELECT 123 AS id UNION SELECT 456 UNION SELECT 567) AS ids
ON post.id = ids.id;
(2)连接查询的优化技巧
如果必须使用连接查询:
- 确保关联字段有索引(如
tag_post.tag_id
和tag_post.post_id
)。 - 控制结果集大小:添加
LIMIT
或筛选条件。 - 使用 STRAIGHT_JOIN:手动指定连接顺序(避免优化器选错计划)。