当前位置: 首页 > news >正文

MySQL多表JOIN聚合磁盘溢出?分批聚合实战:某教育平台50万行数据从崩溃到稳定

作者的话


一、事故背景

某教育集团的数据中台需要从DW层聚合生成6张DM层指标表。其中最大的4张表:

表名说明最终行数聚合方式
dm_campus_subject_fail_rate校区学科不及格率约5万行三表JOIN + GROUP BY
dm_campus_subject_avg_score校区学科平均分约2.6万行三表JOIN + GROUP BY
dm_national_subject_avg_score全国学科平均分约2.1万行两表JOIN + GROUP BY
dm_national_subject_fail_rate全国学科不及格率约1500行两表JOIN + GROUP BY

聚合SQL长这样:

INSERTINTOdm_campus_subject_fail_rate(...)SELECTs.subject_type,s.campus_id,s.campus_name,t.exam_id,t.exam_name,COUNT(DISTINCTs.student_id)AStotal_students,COUNT(DISTINCTCASEWHENs.score_statusNOTIN('pass','exempt')THENs.student_idELSENULLEND)ASfail_students,ROUND(...)ASfail_rate,NOW(),NOW(),NOW()FROMdw_student sLEFTJOINdw_enrollment rONs.student_id=r.student_idLEFTJOINdw_exam_score tONs.student_id=t.student_idWHEREs.is_deleted=0ANDr.is_deleted=0ANDt.is_deleted=0GROUPBYs.subject_type,s.campus_id,s.campus_name,t.exam_id,t.exam_name;

三张DW层表JOIN后GROUP BY,数据量约50万行。

结果?MySQL直接把RDS磁盘打满(小规格实例,磁盘仅50GB),实例卡死,同步任务崩溃。


二、为什么临时表会炸?

2.1 MySQL临时表机制

MySQL在执行GROUP BY时,如果内存放不下(超过tmp_table_size),会把中间结果写到磁盘上的临时文件。三表JOIN + GROUP BY的执行过程:

Step 1: dw_student JOIN dw_enrollment → 中间结果A(约30万行) Step 2: 中间结果A JOIN dw_exam_score → 中间结果B(约50万行) Step 3: 中间结果B GROUP BY → 临时表C(磁盘上) Step 4: 临时表C INSERT INTO dm表

中间结果B有50万行,在小规格RDS上,临时表可能占满剩余磁盘空间。

2.2 RDS磁盘监控

磁盘使用率:96.2% ← 接近满载 IOPS:接近上限 CPU:100% MySQL状态:Waiting for disk space

三、优化尝试(失败篇)

3.1 尝试1:调大tmp_table_size

SETSESSIONtmp_table_size=1073741824;-- 1GBSETSESSIONmax_heap_table_size=1073741824;-- 1GB

结果:小规格RDS内存有限,1GB的内存临时表放不下50万行中间结果,还是溢出到磁盘。

3.2 尝试2:优化SQL写法

用子查询预先过滤:

INSERTINTOdm_campus_subject_fail_rate(...)SELECT...FROM(SELECTstudent_id,subject_type,campus_id,campus_name,score_statusFROMdw_studentWHEREis_deleted=0)sINNERJOIN(SELECTstudent_idFROMdw_enrollmentWHEREis_deleted=0)rONs.student_id=r.student_idINNERJOIN(SELECTstudent_id,exam_id,exam_nameFROMdw_exam_scoreWHEREis_deleted=0)tONs.student_id=t.student_idGROUPBY...;

结果:MySQL优化器并不总是按子查询顺序执行,执行计划可能还是全表扫描。临时表依然巨大。


四、最终方案:分批聚合

4.1 核心思路

不要一次性聚合所有数据,而是按维度分批聚合。

原始SQL是一次性聚合所有校区+学科组合,改为:每次只聚合一个校区(或一个分类组合)的数据,循环执行直到覆盖所有维度。

4.2 实现代码

defexecute_dm_batch_aggregation(conn,config):cursor=conn.cursor()cursor.execute(""" SELECT DISTINCT subject_type, campus_id, campus_name FROM dw_student WHERE is_deleted = 0 """)campuses=cursor.fetchall()logger.info(f"总校区数:{len(campuses)}")execute_sql(conn,"TRUNCATE TABLE dm_campus_subject_fail_rate")total_rows=0batch_size=10fori,campusinenumerate(campuses):conn=reconnect_if_needed(conn,config)sql=""" INSERT INTO dm_campus_subject_fail_rate (...) SELECT %s, %s, %s, t.exam_id, t.exam_name, COUNT(DISTINCT s.student_id), COUNT(DISTINCT CASE WHEN s.score_status NOT IN ('pass', 'exempt') THEN s.student_id ELSE NULL END), ROUND(...), NOW(), NOW(), NOW() FROM dw_student s LEFT JOIN dw_enrollment r ON s.student_id = r.student_id LEFT JOIN dw_exam_score t ON s.student_id = t.student_id WHERE s.is_deleted = 0 AND r.is_deleted = 0 AND t.is_deleted = 0 AND s.subject_type = %s AND s.campus_id = %s GROUP BY t.exam_id, t.exam_name """params=(campus['subject_type'],campus['campus_id'],campus['campus_name'],campus['subject_type'],campus['campus_id'])rows=execute_sql(conn,sql,params,config=config)total_rows+=rowsif(i+1)%batch_size==0:release_temp_tables(conn)logger.info(f"进度:{i+1}/{len(campuses)}, 已插入{total_rows}行")gc.collect()cursor.close()deleted=execute_sql(conn,"DELETE FROM dm_campus_subject_fail_rate WHERE total_students < 50")release_temp_tables(conn)

4.3 为什么分批聚合有效?

对比项一次性聚合分批聚合
单次SQL处理数据量50万行约5000-1万行
临时表大小约5GB约50MB
磁盘占用峰值接近100%<15%
执行时间崩溃无法完成约20-30分钟
失败恢复从头来失败批次可重试

五、配套优化措施

5.1 Session参数优化

defoptimize_session(conn):withconn.cursor()ascursor:cursor.execute("SET SESSION tmp_table_size = 1073741824")# 1GBcursor.execute("SET SESSION max_heap_table_size = 1073741824")# 1GBcursor.execute("SET SESSION sort_buffer_size = 268435456")# 256MBcursor.execute("SET SESSION join_buffer_size = 268435456")# 256MBconn.commit()

5.2 主动释放临时表

defrelease_temp_tables(conn):try:withconn.cursor()ascursor:cursor.execute("FLUSH TABLES")conn.commit()except:pass

FLUSH TABLES会强制MySQL关闭所有不再使用的临时表,立即释放磁盘空间。

5.3 Python垃圾回收

importgcif(i+1)%batch_size==0:gc.collect()

5.4 连接保活与重连

defreconnect_if_needed(conn,config):try:withconn.cursor()ascursor:cursor.execute("SELECT 1")returnconnexcept:returnpymysql.connect(**config)

六、效果对比

指标优化前优化后
同步成功率0%(必崩)99%+
RDS磁盘峰值96%+<25%
单次同步耗时无法完成约20-30分钟
失败恢复从头来可从失败批次继续
运维介入频率每次需人工处理基本无需介入

七、总结

多表聚合磁盘溢出的根因是一次性处理的数据量超过了MySQL临时表的承载能力。分批聚合的本质是把一个大问题拆成N个小问题,每个小问题都在MySQL可控范围内。

分批聚合的三个关键点

  1. 选择合适的分批维度:按分类维度(如校区、科室、学科)分批,确保每批数据量均匀
  2. 每批后释放资源FLUSH TABLES+gc.collect(),防止资源累积
  3. 连接保活:长时间执行必须处理连接断开的情况

💡一句话总结:当MySQL告诉你"磁盘满了",不是让你加磁盘,而是让你把一个大SQL拆成N个小SQL。分批聚合,大道至简。


这篇文章如果帮到了你,点赞收藏是对作者最大的支持!有多表聚合经验的同学,评论区分享你的方案~

http://www.jsqmd.com/news/1075332/

相关文章:

  • 2026情感解惑APP实测对比:塔罗星盘、婚恋咨询怎么选?5款主流平台深度测评
  • 免费开源AMD Ryzen处理器调试工具SMUDebugTool终极指南
  • minimind系统学习教程 - 基础组件02:位置编码(Position encoding)
  • 【Agent Harness】Gliding Horse 的Token经济学:用 IRI 指针替代文本,让 Token 花在刀刃上
  • 工程师视角的AI技术简报:如何将Newsletter转化为可执行知识
  • ROS节点命名机制深度解析:全名、命名空间与重映射原理
  • 从HDMI规范看HDMI接口电路设计
  • C#之.Net互操作-平台调用(P_Invoke)
  • FanControl完整指南:如何免费掌控Windows电脑风扇,告别噪音烦恼
  • 你AI的 localhost:3000,可以立刻在网上访问了!
  • 小红书、抖音、支付宝都能碰一碰分享,鸿蒙7的社交新玩法
  • 波普尔病毒:人工智能大模型的系统性认知癌症——论证伪主义在AI系统中的程序化扩散与文明危害
  • Sherlock.js:让自然语言变身日程助手,3分钟解锁智能事件解析
  • DDD-030:DDD 落地常见问题与避坑指南
  • 【C语言】c语言基础知识梳理(超全)
  • LSTM股票收益率预测实战:从数据清洗到模型部署
  • TVA在物流分拣领域的独特价值(7)
  • 用数据说话!2026年最流行AI论文软件榜单,免费款也能高效产初稿
  • Canonical Livepatch 正式拥抱 Arm64:ARM 服务器终于也能零停机打内核补丁了
  • 从AI用户到建造者:2025年可落地的AI系统工程实践指南
  • Poly Haven Assets:Blender中获取免费3D资源库的终极指南
  • Geoserver表达式注入漏洞CVE-2024-36401:从原理到复现与防御
  • 终极Windows老游戏兼容解决方案:5分钟让经典游戏在Win10/11上完美运行
  • Sketch Measure:告别设计标注痛苦,让协作变得轻松有趣
  • 可以边录边编辑的音乐平台,多款录音修音一体化工具实操分享
  • 网站建设如何适配本地流量?GEO 推广导向型建站逻辑详解
  • AI录音后期处理软件:录歌、修音、剪辑导出一体化工具梳理
  • 为什么创作者要关心 CLI
  • EXTI+定时器消抖:按键中断的正确姿势
  • 终极指南:如何用Blender插件实现Unreal PSK/PSA文件导入导出