关键词:AWS RDS、MySQL、临时表溢出、The table is full、SQL 优化、JOIN 重构、窗口函数性能
适用场景:Java 后端开发、DBA、运维工程师、定时任务系统维护者
环境:AWS RDS for MySQL(5.7/8.0)、Spring Boot + MyBatis、XXL-JOB 定时任务

一个每日执行的发货邮件通知定时任务(WorkDaySendShipmentEmailJob)突然失败。查看日志,发现如下关键错误:

ERROR ... - workDaySendShipmentEmailJob error:
org.springframework.jdbc.UncategorizedSQLException:
### Error querying database.  
Cause: java.sql.SQLException: (conn=913773) The table '/rdsdbdata/tmp/#sql14b_df16d_1bd' is full
...
error code [1114]; (conn=913773) The table '/rdsdbdata/tmp/#sql14b_df16d_1bd' is full

这表示 MySQL 在执行查询时使用的临时表空间(tmpdir)已满,无法继续创建或写入临时表。在 AWS RDS for MySQL 环境中,这是非常典型的性能/资源瓶颈问题。

一、错误原因分析

错误本质:临时表(Temporary Table)溢出

在执行复杂 SQL 时,MySQL 会使用内存或磁盘上的临时表来存储中间结果。当数据量过大,超出 tmpdir 可用空间(RDS 中为 /rdsdbdata/tmp/),就会抛出:

Error 1114: The table ‘xxx’ is full

尤其在以下操作中极易触发:

  • DISTINCT
  • GROUP BY / ORDER BY 非索引字段
  • 多表 JOIN 导致结果集爆炸
  • 窗口函数(如 ROW_NUMBER()
  • 子查询物化(Materialization)

而我们的 SQL 正好“集齐”了所有高危操作!

原始SQL :

SELECT DISTINCT
  ...,
  CASE WHEN pn.creation_date ... THEN ... END AS paymentStatus,
  ...
FROM task_shipment ts
INNER JOIN task_network_delivery_record tndr ON ...
LEFT JOIN project_network pn ON ...
LEFT JOIN project p ON ...
-- ⚠️ 重复 JOIN 同一张表 5 次!
LEFT JOIN project_network_milestone pnm  ON ... milestone='3A'
LEFT JOIN project_network_milestone pnm2 ON ... milestone='3B'
LEFT JOIN project_network_milestone pnm3 ON ... milestone='3B2'
LEFT JOIN project_network_milestone pnm4 ON ... milestone='3P'
LEFT JOIN milestone_sap_retry msr ON ... milestone='3X'
-- ⚠️ 窗口函数子查询
LEFT JOIN (
  SELECT ..., ROW_NUMBER() OVER (PARTITION BY network_number, delivery_group ...) AS rn
  FROM network_delivery_info
) ndi ON ...
WHERE ts.create_time BETWEEN ? AND ?

数据规模:核心表均达 数百万行task_shipmentproject_network_milestone 等)。

即使单表不大,但多重 JOIN + 函数计算 + 无有效过滤,导致中间结果轻松突破千万行,临时表直接撑爆。

优化策略:从“治标”到“治本”

生产保命妙招

扩大 临时表大小:

temptable_max_ram:限制临时表最多可以使用多少内存.

temptable_max_mmap:当内存用尽时,临时表可使用多少磁盘空间作为缓存

【参考文档】:

【1】 https://docs.amazonaws.cn/zh_cn/AmazonRDS/latest/AuroraUserGuide/aurora-mysql-write-forwarding.html 【2】 https://docs.amazonaws.cn/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Managing.Performance.html#AuroraMySQL.Managing.TempStorage 【3】 https://repost.aws/knowledge-center/rds-aurora-mysql-table-storage-issues

1. 【核心】合并重复 JOIN:用聚合替代多次连接

问题:对 project_network_milestone 表按不同 milestone 值 JOIN 5 次,极易产生笛卡尔积。

优化后

LEFT JOIN (
    SELECT 
        network_number,
        MAX(CASE WHEN milestone = '3A'  THEN actual_date END) AS actual_date_3A,
        MAX(CASE WHEN milestone = '3B'  THEN actual_date END) AS actual_date_3B,
        MAX(CASE WHEN milestone = '3B2' THEN actual_date END) AS actual_date_3B2,
        MAX(CASE WHEN milestone = '3P'  THEN actual_date END) AS actual_date_3P,
        MAX(CASE WHEN milestone = '3X'  THEN success END)     AS success_3X
    FROM project_network_milestone
    WHERE deleted = '0' OR deleted IS NULL
    GROUP BY network_number
) pnm_agg ON pnm_agg.network_number = pn.network_number

效果:只需扫描一次里程碑表,避免 5 倍中间结果膨胀。


2. 优化窗口函数子查询

原写法:

LEFT JOIN (
  SELECT ..., ROW_NUMBER() OVER (PARTITION BY network_number, delivery_group ORDER BY id DESC) AS rn
  FROM network_delivery_info WHERE deleted = 0
) ndi ON ndi.rn = 1

建议

  • 添加复合索引加速分区取最新:

    CREATE INDEX idx_ndi_lookup 
    ON network_delivery_info (network_number, delivery_group, id DESC);
    
  • 若业务允许,加时间范围过滤(如近 6 个月)。


3. 避免字段函数转换,让索引生效

原条件:

STR_TO_DATE(pn.creation_date, '%Y%m%d') > STR_TO_DATE('20250614', '%Y%m%d')

问题:无法使用索引,每行都要计算。

解决方案

  • 长期:将 creation_date 改为 DATE 类型。

  • 短期(MySQL 8.0+):创建函数索引:

    CREATE INDEX idx_pn_creation_expr 
    ON project ((STR_TO_DATE(creation_date, '%Y%m%d')));
    

4. 谨慎使用 DISTINCT

检查是否因 JOIN 不精确导致重复。若可通过主键关联或聚合消除重复,则应移除 DISTINCT,大幅降低排序与临时表开销。


5. 分批次处理大数据查询

即使优化后,若 create_time 范围过大(如查整周),仍可能超限。

推荐做法

  • 定时任务每次只处理 1 小时或 1 天 的数据
  • 应用层循环调用小窗口查询
// 伪代码
for (LocalDateTime windowStart = start; windowStart.isBefore(end); ) {
    LocalDateTime windowEnd = windowStart.plusHours(1);
    job.execute(windowStart, windowEnd);
    windowStart = windowEnd;
}

优化前后对比

指标优化前优化后
执行状态失败(临时表满)成功
执行时间< 10 秒(原预估 > 2 分钟)
临时表大小> 10GB(估算)< 500MB
RDS CPU/IOPS高峰飙升平稳

经验总结

  1. 不要迷信“几百万行不算大” —— 复杂查询会让中间结果指数级增长。
  2. RDS 的 tmp 空间是隐形瓶颈,无法直接扩容,必须从 SQL 入手。
  3. 重复 JOIN 同一张表是性能杀手,优先考虑聚合展开。
  4. 分页 or 分批 是处理大数据定时任务的黄金法则。
  5. EXPLAIN 是你的朋友:重点关注 Using temporary; Using filesort

更多推荐