生产故障分析:跨表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 >= #{startDate} AND o.date_acct < #{endDate})
OR (serial.order_type = 'SETTLEMENT' AND serial.update_time >= #{startDate} AND serial.update_time < #{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 >= #{startDate} AND o.date_acct < #{endDate})
OR (serial.order_type = 'SETTLEMENT' AND serial.update_time >= #{startDate} AND serial.update_time < #{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前置过滤、减少关联次数,有硬性要求:
过滤条件必须「单表自治」→ 条件字段只能来自驱动表,不能包含被驱动表字段。
问题推演
- 驱动表为
trade_serial,OR条件中:条件1属于被驱动表o、条件2属于驱动表serial; - 优化器拿到
serial单条数据时,无法判断是否满足完整OR条件 (缺失o表数据); - 优化器唯一选择:放弃所有过滤,将
serial全表80万行数据,全部与o表执行JOIN关联; - 最终结果:80万次无效JOIN执行完毕,仅筛选出7600条有效数据,做了海量无用功。
💡 关键总结:索引生效 ≠ 能前置过滤驱动表,跨表OR锁死了索引的核心优化价值。
5.3 致命问题2:跨表OR → 被迫走「JOIN后过滤」,性能最差路径
索引过滤分为两个层级,性能差距天壤之别:
✔️ 最优路径:JOIN前过滤
驱动表先通过索引筛选出少量数据 → 再与被驱动表关联 → JOIN次数极少,性能最优。
❌ 最差路径:JOIN后过滤(当前场景)
- 驱动表全量数据与被驱动表完成JOIN → 生成80万行临时大表;
- 基于OR条件对临时表逐行过滤 → 过滤耗时+临时表IO耗时爆炸;
- 两个单表索引仅在「最终过滤阶段」生效,无法减少核心的JOIN次数,性能优化完全失效。
5.4 慢查询终极根源
✅ 真正拖慢SQL的不是LEFT JOIN本身,而是JOIN前生成了巨大的中间结果集 ,后续还要执行filesort+临时表排序;
✅ UNION ALL的核心价值:让JOIN发生在数据被极致过滤之后,而非减少JOIN本身。
六、UNION ALL优化方案:原理&效果对比
6.1 优化核心思路
将跨表OR的1个大查询 ,拆分为2个独立的单表条件子查询 ,通过UNION ALL合并结果:
- 每个子查询的过滤条件,完全单表自治,满足MySQL前置过滤要求;
- 子查询执行时,索引可在JOIN前生效,驱动表数据被极致过滤;
- 小数据集执行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 >= #{startDate}
AND o.date_acct < #{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 >= #{startDate}
AND serial.update_time < #{endDate}
AND (serial.pay_type != 'VCC' OR serial.order_type = 'SETTLEMENT')
ORDER by date_acct
</select>