🚀 MySQL 生产环境 SQL 性能优化实战案例
🏗️ 背景介绍
最近在处理一个项目时,发现在生产环境的工作流相关接口中,某些查询的执行时间异常缓慢,尽管数据量仅为 2 万条。经过分析,发现以下 SQL 语句执行非常慢:
SELECT *
FROM ACT_HI_TASKINST t
LEFT JOIN ACT_HI_PROCINST p ON p.PROC_INST_ID_ = t.PROC_INST_ID_
LEFT JOIN ACT_HI_COMMENT c ON c.TASK_ID_ = t.id_;
尤其是在添加 LEFT JOIN ACT_HI_COMMENT
后,查询时间显著增加,达到了 ⏳ 1 分钟。我们需要深入分析并优化此查询。
🔍 执行计划分析
通过 EXPLAIN FORMAT=JSON
分析执行计划,得到的关键结果如下:
c
表有 23,754 行,但rows_produced_per_join
却达到了 4.19 亿 行,产生了 笛卡尔积效应 💥。data_read_per_join
高达 5TB,导致查询执行极为缓慢 🐌。JOIN
方式为 Block Nested Loop(BNL),执行效率较低。TASK_ID_
字段缺乏合适的索引,导致c
表进行全表扫描 📜。
执行计划示例:
{"query_block": {"select_id": 1,"cost_info": {"query_cost": "86412925.39"},"nested_loop": [{"table": {"table_name": "t","access_type": "ALL","rows_examined_per_scan": 17679,"rows_produced_per_join": 17679,"filtered": "100.00","cost_info": {"read_cost": "419.00","eval_cost": "3535.80","prefix_cost": "3954.80","data_read_per_join": "567M"},"used_columns": ["ID_","REV_","PROC_DEF_ID_","TASK_DEF_ID_","TASK_DEF_KEY_","PROC_INST_ID_","EXECUTION_ID_","SCOPE_ID_","SUB_SCOPE_ID_","SCOPE_TYPE_","SCOPE_DEFINITION_ID_","NAME_","PARENT_TASK_ID_","DESCRIPTION_","OWNER_","ASSIGNEE_","START_TIME_","CLAIM_TIME_","END_TIME_","DURATION_","DELETE_REASON_","PRIORITY_","DUE_DATE_","FORM_KEY_","CATEGORY_","TENANT_ID_","LAST_UPDATED_TIME_"]}},{"table": {"table_name": "p","access_type": "eq_ref","possible_keys": ["PROC_INST_ID_"],"key": "PROC_INST_ID_","used_key_parts": ["PROC_INST_ID_"],"key_length": "194","ref": ["work_order.t.PROC_INST_ID_"],"rows_examined_per_scan": 1,"rows_produced_per_join": 17679,"filtered": "100.00","cost_info": {"read_cost": "17679.00","eval_cost": "3535.80","prefix_cost": "25169.60","data_read_per_join": "319M"},"used_columns": ["ID_","REV_","PROC_INST_ID_","BUSINESS_KEY_","PROC_DEF_ID_","START_TIME_","END_TIME_","DURATION_","START_USER_ID_","START_ACT_ID_","END_ACT_ID_","SUPER_PROCESS_INSTANCE_ID_","DELETE_REASON_","TENANT_ID_","NAME_","CALLBACK_ID_","CALLBACK_TYPE_"]}},{"table": {"table_name": "c","access_type": "ALL","rows_examined_per_scan": 23754,"rows_produced_per_join": 419946966,"filtered": "100.00","using_join_buffer": "Block Nested Loop","cost_info": {"read_cost": "2398362.59","eval_cost": "83989393.20","prefix_cost": "86412925.39","data_read_per_join": "5T"},"used_columns": ["ID_","TYPE_","TIME_","USER_ID_","TASK_ID_","PROC_INST_ID_","ACTION_","MESSAGE_","FULL_MSG_"],"attached_condition": "<if>(is_not_null_compl(c), (work_order.c.TASK_ID_ = work_order.t.ID_), true)"}}]}
}
⚡ 优化方案
✅ 1. 增加索引
为 c.TASK_ID_
添加索引,以减少全表扫描带来的影响:
ALTER TABLE ACT_HI_COMMENT ADD INDEX idx_comment_task (TASK_ID_);
🔄 2. 重新分析执行计划
索引添加后,c
表的 rows_produced_per_join
从 4.19 亿 降至 24,421,查询方式变为 ref(通过索引查找),扫描行数大幅减少 📉。
优化后的执行计划示例:
{"query_block": {"select_id": 1,"cost_info": {"query_cost": "54475.04"},"nested_loop": [{"table": {"table_name": "t","access_type": "ALL","rows_examined_per_scan": 17679,"rows_produced_per_join": 17679,"filtered": "100.00","cost_info": {"read_cost": "419.00","eval_cost": "3535.80","prefix_cost": "3954.80","data_read_per_join": "567M"},"used_columns": ["ID_","REV_","PROC_DEF_ID_","TASK_DEF_ID_","TASK_DEF_KEY_","PROC_INST_ID_","EXECUTION_ID_","SCOPE_ID_","SUB_SCOPE_ID_","SCOPE_TYPE_","SCOPE_DEFINITION_ID_","NAME_","PARENT_TASK_ID_","DESCRIPTION_","OWNER_","ASSIGNEE_","START_TIME_","CLAIM_TIME_","END_TIME_","DURATION_","DELETE_REASON_","PRIORITY_","DUE_DATE_","FORM_KEY_","CATEGORY_","TENANT_ID_","LAST_UPDATED_TIME_"]}},{"table": {"table_name": "p","access_type": "eq_ref","possible_keys": ["PROC_INST_ID_"],"key": "PROC_INST_ID_","used_key_parts": ["PROC_INST_ID_"],"key_length": "194","ref": ["work_order.t.PROC_INST_ID_"],"rows_examined_per_scan": 1,"rows_produced_per_join": 17679,"filtered": "100.00","cost_info": {"read_cost": "17679.00","eval_cost": "3535.80","prefix_cost": "25169.60","data_read_per_join": "319M"},"used_columns": ["ID_","REV_","PROC_INST_ID_","BUSINESS_KEY_","PROC_DEF_ID_","START_TIME_","END_TIME_","DURATION_","START_USER_ID_","START_ACT_ID_","END_ACT_ID_","SUPER_PROCESS_INSTANCE_ID_","DELETE_REASON_","TENANT_ID_","NAME_","CALLBACK_ID_","CALLBACK_TYPE_"]}},{"table": {"table_name": "c","access_type": "ref","possible_keys": ["idx_comment_task"],"key": "idx_comment_task","used_key_parts": ["TASK_ID_"],"key_length": "195","ref": ["work_order.t.ID_"],"rows_examined_per_scan": 1,"rows_produced_per_join": 24421,"filtered": "100.00","cost_info": {"read_cost": "24421.20","eval_cost": "4884.24","prefix_cost": "54475.04","data_read_per_join": "347M"},"used_columns": ["ID_","TYPE_","TIME_","USER_ID_","TASK_ID_","PROC_INST_ID_","ACTION_","MESSAGE_","FULL_MSG_"]}}]}
}
优化后,查询时间从 ⏳ 1 分钟 降至毫秒级 🚀,性能得到了显著提升。
🔬 MySQL 8 本地测试情况
在 MySQL 8 本地环境进行测试时,原 SQL 语句的执行时间没有出现明显的性能问题,可能原因包括:
- 优化器改进:MySQL 8 对
JOIN
方式进行了优化,减少了 BNL 的使用。 - 更智能的默认索引策略:MySQL 8 在索引选择上更为智能,避免了不必要的全表扫描。
- 测试环境数据量较小:由于本地环境数据较少,无法重现生产环境中的慢查询问题。
尽管在本地 MySQL 8 上运行正常,我们仍建议在生产环境中进行 EXPLAIN
分析,以确保优化方案的有效性。
MySQL 8.0 中引入了 Hash Join 自动选择,取代了传统的 Nested Loop Join(嵌套循环连接)。执行计划中的 "using_join_buffer": "hash join"
证实了这一点。Hash Join、并行查询和 Buffer Pool 的优化是导致问题未能在本地复现的主要原因。因此,最终我们通过与生产环境完全一致的数据库版本和配置复现了问题。
执行计划:
{"query_block": {"select_id": 1,"cost_info": {"query_cost": "36941768.34"},"nested_loop": [{"table": {"table_name": "t","access_type": "ALL","rows_examined_per_scan": 16978,"rows_produced_per_join": 16978,"filtered": "100.00","cost_info": {"read_cost": "104.75","eval_cost": "1697.80","prefix_cost": "1802.55","data_read_per_join": "544M"},"used_columns": ["ID_","REV_","PROC_DEF_ID_","TASK_DEF_ID_","TASK_DEF_KEY_","PROC_INST_ID_","EXECUTION_ID_","SCOPE_ID_","SUB_SCOPE_ID_","SCOPE_TYPE_","SCOPE_DEFINITION_ID_","NAME_","PARENT_TASK_ID_","DESCRIPTION_","OWNER_","ASSIGNEE_","START_TIME_","CLAIM_TIME_","END_TIME_","DURATION_","DELETE_REASON_","PRIORITY_","DUE_DATE_","FORM_KEY_","CATEGORY_","TENANT_ID_","LAST_UPDATED_TIME_"]}},{"table": {"table_name": "p","access_type": "eq_ref","possible_keys": ["PROC_INST_ID_"],"key": "PROC_INST_ID_","used_key_parts": ["PROC_INST_ID_"],"key_length": "194","ref": ["work_order.t.PROC_INST_ID_"],"rows_examined_per_scan": 1,"rows_produced_per_join": 16978,"filtered": "100.00","cost_info": {"read_cost": "4244.50","eval_cost": "1697.80","prefix_cost": "7744.85","data_read_per_join": "306M"},"used_columns": ["ID_","REV_","PROC_INST_ID_","BUSINESS_KEY_","PROC_DEF_ID_","START_TIME_","END_TIME_","DURATION_","START_USER_ID_","START_ACT_ID_","END_ACT_ID_","SUPER_PROCESS_INSTANCE_ID_","DELETE_REASON_","TENANT_ID_","NAME_","CALLBACK_ID_","CALLBACK_TYPE_"]}},{"table": {"table_name": "c","access_type": "ALL","rows_examined_per_scan": 21447,"rows_produced_per_join": 364127166,"filtered": "100.00","using_join_buffer": "hash join","cost_info": {"read_cost": "521306.89","eval_cost": "36412716.60","prefix_cost": "36941768.34","data_read_per_join": "4T"},"used_columns": ["ID_","TYPE_","TIME_","USER_ID_","TASK_ID_","PROC_INST_ID_","ACTION_","MESSAGE_","FULL_MSG_"],"attached_condition": "<if>(is_not_null_compl(c), (`work_order`.`c`.`TASK_ID_` = `work_order`.`t`.`ID_`), true)"}}]}
}
🎯 结论
- 问题根因:缺少合适的索引,导致 MySQL 使用 BNL 方式进行
JOIN
,引发巨量扫描。 - 优化措施:为
TASK_ID_
添加索引,使得c
表的访问方式从ALL
变为ref
,减少了扫描行数。 - 最终效果:查询时间从 ⏳ 1 分钟 降至毫秒级 🎉。
📌 建议
- 📊 定期检查慢查询日志,及时优化 SQL 语句。
- 🛠️ 合理设计索引,避免全表扫描。
- 🧐 使用
EXPLAIN
分析执行计划,确保查询能够利用索引路径。
希望本文中的优化过程能对你在 MySQL 生产环境中的性能调优有所帮助!🎯💡