跨表OR导致SQL查询中断&慢查询优化(UNION ALL改造方案)

生产故障分析:跨表OR导致SQL查询中断&慢查询优化(UNION ALL改造方案)

一、故障现象&报错日志

1.1 核心报错信息

复制代码
desc: 2025-12-15 10:50:06.458 ERROR [TID:549c2d25daa648d9b0cfef0a7130757e.208.17657670003256301] [GlobalExceptionHandler.java:283] [defaultExceptionHandler]
org.springframework.jdbc.UncategorizedSQLException:
### Error querying database.  Cause: com.mysql.cj.jdbc.exceptions.MySQLQueryInterruptedException: Query execution was interrupted
### The error may exist in URL [jar:file:/home/dubbo/llwallet-mng-server/lib/llwallet-server-1.8.16-SNAPSHOT.jar!/BOOT-INF/lib/llwallet-module-pay-biz-1.8.16-SNAPSHOT.jar!/mapper/TradeSerialMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: select serial.order_no as orderNo, o.biz_order_id as bizOrderId, o.user_id, serial.order_type, serial.mch_id, o.sp_no, o.settle_status, o.enterprise_id, serial.serial_amount as orderAmount, serial.serial_currency as orderCurrency, serial.serial_status, serial.actually_amount,
project: llwallet-mng-server
title: 企业钱包-ERROR-告警告警触发

1.2 故障结论

SQL执行时间过长,触发查询中断异常(Query execution was interrupted) ,根源是SQL中存在跨表OR条件,导致慢查询+执行超时。

二、原SQL语句(问题版本)

xml 复制代码
<select id="getTransactionDetails" resultMap="TransactionDataModel">
    select
    <include refid="tradeSerialColumns"/>,
    vccps.auth_amount
    FROM trade_serial serial
    LEFT JOIN pay_order o ON serial.order_no = o.order_no
    LEFT JOIN pay_order_attach attach ON o.order_no = attach.order_no
    LEFT JOIN trade_serial vccps ON vccps.serial_id = serial.ori_serial_id
        AND serial.order_type = 'SETTLEMENT'
        AND vccps.pay_type = 'VCC'
    WHERE serial.serial_status = 'SUCCESS'
      AND o.order_status = 'SUCCESS'
      AND serial.order_type IN ('PAY', 'SETTLEMENT')
      AND (
              (o.date_acct &gt;= #{startDate} AND o.date_acct &lt; #{endDate})
              OR (serial.order_type = 'SETTLEMENT' AND serial.update_time &gt;= #{startDate} AND serial.update_time &lt; #{endDate})
          )
    -- VCC 只取结算流水
    AND (serial.pay_type != 'VCC' OR serial.order_type = 'SETTLEMENT')
    ORDER by o.date_acct
</select>

三、核心性能瓶颈定位

3.1 致命问题代码块

xml 复制代码
AND (
    (o.date_acct &gt;= #{startDate} AND o.date_acct &lt; #{endDate})
    OR (serial.order_type = 'SETTLEMENT' AND serial.update_time &gt;= #{startDate} AND serial.update_time &lt; #{endDate})
)

3.2 问题本质总结

该跨表OR条件同时依赖驱动表serial被驱动表o,MySQL优化器无法进行前置过滤,被迫全量扫描、全量关联,是本次慢查询的唯一元凶。

四、执行计划对比(核心佐证)

4.1 未使用UNION ALL(原SQL)执行计划特征

  • 驱动表trade_serial全表扫描,无前置过滤;
  • JOIN阶段生成超大中间结果集 ,触发filesort+temporary临时表;
  • 整体执行耗时:2.2s左右

4.2 使用UNION ALL(优化后)执行计划特征

  • 两个子查询均走单表索引,驱动表数据被极致过滤;
  • JOIN仅在小数据集上执行,无超大中间结果集;
  • 整体执行耗时:600ms左右。

五、核心原理:跨表OR为什么会导致性能暴跌?

5.1 前置基础(关键前提)

两张表的单表索引均正常生效 ,无索引失效问题:

pay_order.date_acct 字段 单列索引 有效;

trade_serial.order_type + trade_serial.update_time 联合索引 有效;

单独执行单表过滤SQL,均可毫秒级出结果,索引本身无问题。

5.2 致命问题1:跨表OR → 优化器放弃「驱动表前置过滤」(JOIN次数暴增)

底层核心规则

MySQL要实现JOIN前置过滤、减少关联次数,有硬性要求:

过滤条件必须「单表自治」→ 条件字段只能来自驱动表,不能包含被驱动表字段。

问题推演
  1. 驱动表为trade_serial,OR条件中:条件1属于被驱动表o条件2属于驱动表serial
  2. 优化器拿到serial单条数据时,无法判断是否满足完整OR条件 (缺失o表数据);
  3. 优化器唯一选择:放弃所有过滤,将serial全表80万行数据,全部与o表执行JOIN关联
  4. 最终结果:80万次无效JOIN执行完毕,仅筛选出7600条有效数据,做了海量无用功。

💡 关键总结:索引生效 ≠ 能前置过滤驱动表,跨表OR锁死了索引的核心优化价值。

5.3 致命问题2:跨表OR → 被迫走「JOIN后过滤」,性能最差路径

索引过滤分为两个层级,性能差距天壤之别:

✔️ 最优路径:JOIN前过滤

驱动表先通过索引筛选出少量数据 → 再与被驱动表关联 → JOIN次数极少,性能最优。

❌ 最差路径:JOIN后过滤(当前场景)
  1. 驱动表全量数据与被驱动表完成JOIN → 生成80万行临时大表;
  2. 基于OR条件对临时表逐行过滤 → 过滤耗时+临时表IO耗时爆炸;
  3. 两个单表索引仅在「最终过滤阶段」生效,无法减少核心的JOIN次数,性能优化完全失效。

5.4 慢查询终极根源

✅ 真正拖慢SQL的不是LEFT JOIN本身,而是JOIN前生成了巨大的中间结果集 ,后续还要执行filesort+临时表排序;

✅ UNION ALL的核心价值:让JOIN发生在数据被极致过滤之后,而非减少JOIN本身。

六、UNION ALL优化方案:原理&效果对比

6.1 优化核心思路

跨表OR的1个大查询 ,拆分为2个独立的单表条件子查询 ,通过UNION ALL合并结果:

  1. 每个子查询的过滤条件,完全单表自治,满足MySQL前置过滤要求;
  2. 子查询执行时,索引可在JOIN前生效,驱动表数据被极致过滤;
  3. 小数据集执行JOIN,彻底杜绝无效关联,性能拉满。

6.2 「跨表OR」VS「UNION ALL拆分」执行流程对比

🚫 方案1:原SQL(跨表OR)→ 索引生效但无用,全量执行
复制代码
1. 全扫驱动表serial → 80万行数据,无前置过滤;
2. 80万行serial数据,逐行与o表执行JOIN → 80万次无效关联;
3. 生成80万行临时表 → 基于OR条件逐行过滤 → 仅7600行有效数据;
4. 对临时表执行排序 → 执行超时、查询中断。
✅ 方案2:UNION ALL拆分 → 索引前置生效,精准执行
复制代码
# 子查询1(o.date_acct单表条件)
1. o.date_acct索引生效 → 过滤o表,仅7000行符合条件;
2. 7000行o表数据,与serial表关联 → 7000次精准JOIN;
3. 直接输出结果,无额外过滤。

# 子查询2(serial单表条件)
1. serial.order_type+update_time索引生效 → 过滤serial表,仅600行符合条件;
2. 600行serial数据,与o表关联 → 600次精准JOIN;
3. 直接输出结果,无额外过滤。

# 最终合并
UNION ALL合并两个子查询结果 → 总计7600行数据,毫秒级完成。

6.3 优化效果核心结论

✅ JOIN次数从 80万次 → 7600次 ,减少99%;

✅ 执行耗时从 分钟级/超时 → 毫秒级

✅ 彻底解决「查询中断异常」,业务恢复正常。

七、延伸知识点:OR条件的两类失效场景(避坑必备)

日常开发中OR条件导致的性能问题分两类,本质完全不同,需精准区分、针对性优化:

7.1 ✅ 场景1:跨表OR(本次故障)→ 索引「生效但无用」

特征

OR的多个分支,分别属于不同的关联表

结果

单表索引均生效,但优化器无法前置过滤驱动表,JOIN次数暴增;

解决方案

拆分为UNION ALL(根治,唯一最优解)。

7.2 ❌ 场景2:单表OR → 索引「直接失效」(开发高频踩坑)

特征

OR的所有分支,属于同一张表,但字段仅建「单列索引」、未建「联合索引」;

示例

WHERE serial.name = 'xxx' OR serial.id = 123,仅name/id有单列索引;

结果

MySQL放弃索引,直接走单表全扫;

解决方案

① 为OR所有字段建立联合索引 ;② 拆分为UNION语句。

7.3 核心区分对照表

OR条件类型 索引状态 性能问题根源 最终结果 最优解决方案
跨表OR(本次故障) 单表索引均生效 无法前置过滤驱动表 JOIN次数暴增、临时表过大 拆分为UNION ALL
单表OR(无联合索引) 单列索引失效 优化器放弃索引,走单表全扫 单表查询变慢,无过滤效果 建联合索引 / 拆分为UNION
单表OR(有联合索引) 联合索引生效 无性能问题 正常前置过滤,查询性能无影响 无需优化

八、总结

8.1 结论

结论1:跨表OR的性能问题根源

跨表OR时,单表索引依然生效 ,但优化器因「条件跨表、无法判断」,放弃驱动表前置过滤,索引无法发挥「减少JOIN次数」的核心价值,最终查询变慢。

结论2:JOIN次数暴增的本质

不是索引失效,而是跨表OR锁死了「驱动表前置过滤」,驱动表全量数据参与JOIN,做了海量无用功;UNION ALL的价值是拆分OR为单表条件,让索引重新前置生效。

结论3:慢查询核心卡点

LEFT JOIN本身不慢,慢的是JOIN前未过滤、生成了超大中间结果集,后续的filesort+临时表才是性能杀手。

九、优化后最终SQL(可直接上线)

xml 复制代码
<select id="getTransactionDetails" resultMap="TransactionDataModel">
    <!-- 子查询1:o.date_acct单表条件,自治过滤 -->
    select
    <include refid="tradeSerialColumns"/>,
    vccps.auth_amount
    FROM trade_serial serial
    LEFT JOIN pay_order o ON serial.order_no = o.order_no
    LEFT JOIN pay_order_attach attach ON o.order_no = attach.order_no
    LEFT JOIN trade_serial vccps ON vccps.serial_id = serial.ori_serial_id
        AND serial.order_type = 'SETTLEMENT'
        AND vccps.pay_type = 'VCC'
    WHERE serial.serial_status = 'SUCCESS'
      AND o.order_status = 'SUCCESS'
      AND serial.order_type IN ('PAY', 'SETTLEMENT')
      AND o.date_acct &gt;= #{startDate} 
      AND o.date_acct &lt; #{endDate}
      AND (serial.pay_type != 'VCC' OR serial.order_type = 'SETTLEMENT')
    
    UNION ALL
    
    <!-- 子查询2:serial单表条件,自治过滤 -->
    select
    <include refid="tradeSerialColumns"/>,
    vccps.auth_amount
    FROM trade_serial serial
    LEFT JOIN pay_order o ON serial.order_no = o.order_no
    LEFT JOIN pay_order_attach attach ON o.order_no = attach.order_no
    LEFT JOIN trade_serial vccps ON vccps.serial_id = serial.ori_serial_id
        AND serial.order_type = 'SETTLEMENT'
        AND vccps.pay_type = 'VCC'
    WHERE serial.serial_status = 'SUCCESS'
      AND o.order_status = 'SUCCESS'
      AND serial.order_type = 'SETTLEMENT'
      AND serial.update_time &gt;= #{startDate} 
      AND serial.update_time &lt; #{endDate}
      AND (serial.pay_type != 'VCC' OR serial.order_type = 'SETTLEMENT')
      
    ORDER by date_acct
</select>
相关推荐
煎蛋学姐2 分钟前
SSM校园扶助综合服务平台的设计与实现r941j(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm 框架·校园扶助平台
ℳ₯㎕ddzོꦿ࿐4 分钟前
企业级 MySQL 8.0 物理备份实践:使用 XtraBackup 实现全量与增量自动备份
数据库·mysql
羊小猪~~8 分钟前
数据库学习笔记(十八)--事务
数据库·笔记·后端·sql·学习·mysql
optimistic_chen21 分钟前
【Redis 系列】常用数据结构---ZSET类型
数据结构·数据库·redis·xshell·zset·redis命令
cike_y23 分钟前
Spring整合Mybatis:dao层
java·开发语言·数据库·spring·mybatis
小蒜学长24 分钟前
足球联赛管理系统(代码+数据库+LW)
java·数据库·spring boot·后端
松涛和鸣26 分钟前
45、无依赖信息查询系统(C语言+SQLite3+HTML)
c语言·开发语言·数据库·单片机·sqlite·html
智航GIS28 分钟前
9.2 多进程入门
数据库·python
DemonAvenger29 分钟前
Redis与微服务:分布式系统中的缓存设计模式
数据库·redis·性能优化
柒.梧.33 分钟前
Spring JDBC实战指南:从基础操作到事务管理全解析
数据库·oracle