本文记录了一次真实的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解决所有问题,合理的拆分往往是最好的优化。
本文基于真实项目案例,相关表名和字段已做脱敏处理。具体实施时请根据实际业务场景调整。