














在日常功能开发过程在中,我们经常需要根据某些条件清理特定数据。某天,我需要在 tbl_doa_activityspecial 表中删除与另一组条件匹配的记录。直觉上,我写下了这样的 SQL:
DELETE FROM tbl_doa_activityspecial WHERE ActSetId IN (SELECT DISTINCT ActSetId FROM ...);
这个查询逻辑清晰,但执行时却发现性能极差。使用 EXPLAIN 分析执行计划后,发现了一个令人困惑的现象:MySQL 优化器没有使用 tbl_doa_activityspecial_ActSetId_IDX 这个明显应该使用的索引,而是显示 possible_keys: null。
经过深入分析,我发现这个问题背后有几个关键原因:
当 MySQL 遇到 IN (子查询) 结构时,它可能会选择将子查询的结果物化(Materialize)到一个临时表中,然后再执行主查询。这个过程包括:
物化过程破坏了索引使用的连续性,优化器难以将外部查询的条件与子查询的结果高效关联。
与 SELECT 查询不同,DELETE 操作有以下特点:
因此,MySQL 优化器在处理 DELETE 语句时会更加"保守",倾向于选择更可靠而非最高效的执行计划。
如果表的统计信息不是最新的,优化器可能错误地估计使用索引与全表扫描的成本,从而做出非最优决策。
将查询重写为 JOIN 形式后,问题迎刃而解:
DELETE t FROM tbl_doa_activityspecial t JOIN (SELECT DISTINCT ActSetId FROM ...) s ON t.ActSetId = s.ActSetId;
使用 EXPLAIN 分析新查询,确认已经正确使用了 tbl_doa_activityspecial_ActSetId_IDX 索引。
MySQL 优化器会对查询进行重写,但不同的原始写法会导致不同的重写结果:
IN 子查询可能被重写为 EXISTS 或物化形式JOIN 语法则提供了更直接的连接语义优化器基于成本估算选择执行计划,主要考虑:
对于 IN 子查询,优化器可能高估使用索引的成本或低估全表扫描的成本。
DELETE FROM tbl_doa_activityspecial t
WHERE EXISTS (
SELECT 1 FROM ... s
WHERE s.ActSetId = t.ActSetId
);
DELETE FROM tbl_doa_activityspecial FORCE INDEX (tbl_doa_activityspecial_ActSetId_IDX) WHERE ActSetId IN (SELECT ActSetId FROM ...);
DELETE t
FROM tbl_doa_activityspecial t
INNER JOIN (
SELECT DISTINCT ActSetId FROM ...
) s USING (ActSetId);
1、始终先使用 SELECT 测试
-- 先检查会影响到多少行 SELECT COUNT(*) FROM tbl_doa_activityspecial WHERE ActSetId IN (SELECT ActSetId FROM ...);
2、大批量删除分批次进行,因为大批量的删除可能会导致锁升级
-- 每次删除1000条记录,避免长事务 DELETE FROM tbl_doa_activityspecial WHERE ActSetId IN (...) LIMIT 1000;
3、在低峰期执行大规模删除操作,因为你很难确定删除期间会发生什么
通过这次优化经历,我得到了几个重要启示:
这个案例再次证明了深入了解数据库内部工作机制的重要性。作为开发者,我们不仅要写出功能正确的 SQL,更要关注其性能特征,特别是在涉及到大规模数据时。
记住:最好的查询不是看起来最优雅的,而是执行最高效的。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。