本文记录了一次真实的SQL性能优化案例,通过重构复杂的串行查询,将执行时间从几十分钟优化到几十秒,适用于处理百万级数据的复杂业务场景。
问题背景:一个"致命"的采购订单统计查询
最近在优化一个采购业务统计报表,原SQL查询涉及30多张表的关联,在测试环境(100万行数据)中执行需要30分钟以上,完全无法满足业务需求。
原查询的核心问题
sql
-- 问题代码结构示意
SELECT
-- 50+个字段和聚合函数
FROM
主表
LEFT JOIN 表1 ON 条件
LEFT JOIN 表2 ON 表1.字段 -- 依赖表1的结果
LEFT JOIN 表3 ON 表2.字段 -- 依赖表2的结果
LEFT JOIN 表4 ON 表3.字段 -- 依赖表3的结果
-- ... 更多串行依赖
WHERE 条件
GROUP BY 50+个字段
问题诊断:
-
串行执行链:每个后续表都在等待前面所有表的查询结果
-
数据膨胀:多表JOIN产生大量中间结果,内存压力巨大
-
索引失效:复杂的关联条件导致索引无法有效使用
优化思路:并行化 + 分阶段处理
核心优化策略
sql
-- 优化后的架构
-- 阶段1:基础数据准备(核心信息)
SELECT 关键字段 INTO #base FROM 主表 WHERE 条件;
-- 阶段2:并行业务查询(6个查询同时执行)
-- 查询1:收料数据
SELECT 关联ID, SUM(数量) INTO #temp1 FROM 业务表1 GROUP BY 关联ID;
-- 查询2:入库数据
-- 查询3:检验数据
-- ... 其他业务查询
-- 阶段3:最终合并
SELECT b.*, t1.数量, t2.数量...
FROM #base b
LEFT JOIN #temp1 t1 ON...
LEFT JOIN #temp2 t2 ON...
具体实现方案
第一步:建立基础数据临时表
sql
-- 只获取核心信息,避免后续重复计算
SELECT
tppo.FID, tppo.FBILLNO, tppo.FDATE,
tppoe.FENTRYID, tppoe.FMATERIALID,
-- 其他核心字段...
tppo.FBILLNO+' 物料:'+物料号+' 行号:'+行号 as F_PAEZ_FonlyNo
INTO #base_data
FROM t_PUR_POOrder tppo
JOIN t_PUR_POOrderEntry tppoe ON tppoe.fid = tppo.fid
-- 必要的维度表关联
WHERE 基础条件;
第二步:并行执行6个业务查询
关键技巧 :所有业务查询基于#base_data的FENTRYID独立执行
sql
-- 2.1 收料通知数据(独立执行)
SELECT FPOORDERENTRYID, SUM(数量) as FRecQty
INTO #receive_data FROM T_PUR_RECEIVEENTRY
WHERE FPOORDERENTRYID IN (SELECT FENTRYID FROM #base_data)
GROUP BY FPOORDERENTRYID;
-- 2.2 入库数据 - 来源1(独立执行)
SELECT FSID, SUM(数量) as FInStoQty1
INTO #instock_from_receive FROM T_STK_INSTOCKENTRY_LK
WHERE FSID IN (SELECT FENTRYID FROM #base_data) AND 来源条件
GROUP BY FSID;
-- 2.3 入库数据 - 来源2(独立执行)
-- 2.4 检验数据(独立执行)
-- 2.5 退料数据 - 来源1(独立执行)
-- 2.6 退料数据 - 来源2(独立执行)
第三步:一次性合并所有结果
sql
SELECT
bd.核心字段,
-- 收料数据
ISNULL(rd.FRecQty, 0) as FRecQty,
-- 入库数据(合并两个来源)
ISNULL(is1.FInStoQty1, 0) + ISNULL(is2.FInStoQty2, 0) as FInStoQty,
-- 其他业务数据...
INTO #final_result
FROM #base_data bd
LEFT JOIN #receive_data rd ON rd.FENTRYID = bd.FENTRYID
LEFT JOIN #instock_from_receive is1 ON is1.FENTRYID = bd.FENTRYID
LEFT JOIN #instock_from_po is2 ON is2.FENTRYID = bd.FENTRYID
-- 其他LEFT JOIN...
性能对比分析
| 指标 | 优化前 | 优化后 | 提升倍数 |
|---|---|---|---|
| 执行时间 | 30+分钟 | 30秒 | 60倍 |
| CPU占用 | 持续100% | 峰值80% | 更平稳 |
| 内存使用 | 8GB+ | 2GB | 4倍 |
| 临时表空间 | 占满 | 正常 | 显著改善 |
关键技术要点
1. 打破串行依赖
-
原方案:总时间 = t1 + t2 + t3 + ... + tn
-
新方案:总时间 ≈ max(t1, t2, t3, ..., tn)
2. 减少中间结果集
每个业务查询只处理自己相关的数据,避免了大表的笛卡尔积。
3. 更好的索引利用
sql
-- 为每个业务查询创建针对性索引
CREATE INDEX IX_ReceiveEntry_POEntry
ON T_PUR_RECEIVEENTRY (FPOORDERENTRYID)
INCLUDE (FACTRECEIVEQTY);
4. 内存优化
使用临时表分段处理,降低单次查询的内存压力。
适用场景
这种优化方案特别适用于:
-
复杂报表查询:涉及多个业务模块的数据汇总
-
数据仓库ETL:需要从多个源表抽取和转换数据
-
OLAP场景:多维度的数据分析查询
-
任何有串行JOIN依赖的复杂查询
经验总结
-
诊断先行:使用执行计划分析识别串行瓶颈
-
分而治之:将复杂查询拆分为独立的子任务
-
并行执行:利用数据库的并行处理能力
-
内存管理:控制中间结果集的大小
-
索引优化:为每个子查询提供合适的索引支持
通过这次优化,我们不仅解决了当前的性能问题,更为后续类似场景提供了可复用的优化模式。记住:面对复杂查询,不要试图用一个SQL解决所有问题,合理的拆分往往是最好的优化。
本文基于真实项目案例,相关表名和字段已做脱敏处理。具体实施时请根据实际业务场景调整。