惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
D
DataBreaches.Net
C
Check Point Blog
雷峰网
雷峰网
小众软件
小众软件
GbyAI
GbyAI
美团技术团队
P
Proofpoint News Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
罗磊的独立博客
大猫的无限游戏
大猫的无限游戏
WordPress大学
WordPress大学
MyScale Blog
MyScale Blog
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
Apple Machine Learning Research
Apple Machine Learning Research
Y
Y Combinator Blog
Jina AI
Jina AI
爱范儿
爱范儿
Last Week in AI
Last Week in AI
MongoDB | Blog
MongoDB | Blog
I
InfoQ
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美

博客园 - microsoft_xin

用户属性数据库设计:如何平衡固定信息与灵活扩展? mysql数据库千万级数据量是分库分表好还是数据归档好 git的还原提交和撤销提交可以帮你快速回滚提交和推送 java语法使用小技巧-保留两位小数-用group对list数据分组取和 java时间常用功能工具类 Navicat或DBeaver连接MySql提示错误 Unable to load authentication plugin ‘caching_sha2_password‘ 快速测试连接SQLServer数据库的方法 nlog使用小记(日志文件分割备份循环) IDEA常用快捷键 java中两个list集合取并集、交集和差集&对list数据进行对象属性筛选 Cron表达式及格式适用于各种调度服务quartz xxljob hangfire C#客户端实现域环境内Windows认证免密码自动身份认证登录 .NET List常见操作之交集并集差集(转) mysql-查看最近执行sql脚本 SharePoint安装 OpenID Connect、SAML、WS-Federation和/或OAuth2.0 SQLServer常用SQL脚本 使用VSTO创建修改和删除outlook会议加载项(二) 使用VSTO创建修改和删除outlook会议加载项(一)
MySQL 优化实战:为何 DELETE + IN 子查询性能不佳,而 JOIN ...
microsoft_xin · 2026-02-09 · via 博客园 - microsoft_xin

问题背景:一次看似简单的数据清理

在日常功能开发过程在中,我们经常需要根据某些条件清理特定数据。某天,我需要在 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 优化器的"保守策略"

经过深入分析,我发现这个问题背后有几个关键原因:

1. 子查询物化导致的性能陷阱

当 MySQL 遇到 IN (子查询) 结构时,它可能会选择将子查询的结果物化(Materialize)到一个临时表中,然后再执行主查询。这个过程包括:

  • 执行子查询并将结果写入临时表
  • 可能对临时表进行去重(如使用 DISTINCT 时)
  • 最后执行基于临时表的查询

物化过程破坏了索引使用的连续性,优化器难以将外部查询的条件与子查询的结果高效关联。

2. DELETE 操作的特殊性

与 SELECT 查询不同,DELETE 操作有以下特点:

  • 风险更高:数据删除是不可逆操作
  • 锁定要求:需要获取行锁,可能影响并发性能
  • 日志记录:需要生成回滚日志用于事务处理

因此,MySQL 优化器在处理 DELETE 语句时会更加"保守",倾向于选择更可靠而非最高效的执行计划。

3. 统计信息的影响

如果表的统计信息不是最新的,优化器可能错误地估计使用索引与全表扫描的成本,从而做出非最优决策。

解决方案:JOIN 重写的力量

将查询重写为 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 索引。

为什么 JOIN 更有效?

  1. 明确的连接关系:优化器能够清晰识别两个数据集之间的关联条件
  2. 避免不必要的物化:减少了创建临时表的开销
  3. 更好的成本估算:优化器可以更准确地评估不同执行计划的成本
  4. 直接的索引利用:连接条件直接指向索引字段,使索引使用更加直接

深度解析:MySQL 优化器的工作机制

查询重写优化

MySQL 优化器会对查询进行重写,但不同的原始写法会导致不同的重写结果:

  • IN 子查询可能被重写为 EXISTS 或物化形式
  • JOIN 语法则提供了更直接的连接语义

成本估算差异

优化器基于成本估算选择执行计划,主要考虑:

  • IO 成本:读取数据的开销
  • CPU 成本:处理数据的开销
  • 内存使用:临时表、排序等的内存需求

对于 IN 子查询,优化器可能高估使用索引的成本或低估全表扫描的成本。

其他解决方案对比

方案一:使用 EXISTS 子查询

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、在低峰期执行大规模删除操作,因为你很难确定删除期间会发生什么

通过这次优化经历,我得到了几个重要启示:

  1. 不要盲目相信直觉:看似逻辑等价的查询,实际性能可能差异巨大
  2. EXPLAIN 是关键工具:任何时候都要使用 EXPLAIN 验证执行计划
  3. 了解优化器的工作机制:理解优化器的决策过程有助于写出更高效的 SQL
  4. JOIN 通常优于子查询:在大多数情况下,JOIN 语法能提供更好的性能

这个案例再次证明了深入了解数据库内部工作机制的重要性。作为开发者,我们不仅要写出功能正确的 SQL,更要关注其性能特征,特别是在涉及到大规模数据时。

记住:最好的查询不是看起来最优雅的,而是执行最高效的。