Oracle SQL - 使用行转列PIVOT减少表重复扫描(实例)

13/JUL/2025, Yusuf Leo, Oracle SQL Performance Tuning Series

我们经常会遇到从同一表中按不同维度取出不同区间的数据,再以相同的属性将这些数据分别汇总到一起的需求。这类需求往往迫使我们对同一个表反复去扫描,当原始数据量太大的时候,这就可能给我们带来程序性能上的困扰。行转列PIVOT语法或许会是较好的优化思路之一。

PIVOT需要Oracle 11g及以上版本支持。

下面我们来看看这个实例,这是某企业EBS客制化开发的一个报表,核心逻辑是从ASCP工作台按订单类型区分统计各物料的总需求、在库、在途、在购等数量。

  • 优化前

1.1 主程序主游标

sql 复制代码
cursor c1 is
  select aa.organization_id,
         aa.plan_id,
         aa.item_segments,
         aa.description,
         aa.uom_code,
         aa.minimum_order_quantity, --MOQ
         aa.fixed_lot_multiplier, --SPQ
         aa.full_lead_time, --Lead Time
         get_order_qty(aa.plan_id, aa.item_segments, '现有量') pr_qty1,
         get_order_qty(aa.plan_id, aa.item_segments, '采购订单') pr_qty7,
         get_order_qty(aa.plan_id, aa.item_segments, '采购申请') pr_qty8
    from (select distinct mov.organization_id,
                          mov.plan_id,
                          mov.item_segments,
                          mov.description,
                          mov.uom_code,
                          msi.inventory_item_id,
                          msi.minimum_order_quantity, --MOQ
                          msi.fixed_lot_multiplier, --SPQ
                          msi.full_lead_time
            from MSC_ORDERS_V mov, MTL_SYSTEM_ITEMS_B MSI
           where 1 = 1
             and msi.organization_id = mov.organization_id
             and msi.segment1 = mov.item_segments
             and (trunc(mov.new_order_date) >= to_date(p_date_f, 'YYYY-MM-DD') or
                 p_date_f is null)
             and (trunc(mov.new_order_date) <= to_date(p_date_e, 'YYYY-MM-DD') or
                 p_date_e is null)
             and mov.item_segments like '%' || p_item_segments || '%'
             and mov.plan_id = p_plan_id
             and mov.organization_id = p_organizatino_id) aa;

1.2 主程序次级游标

sql 复制代码
cursor c2(p_item_code VARCHAR2) is
  select bb.new_order_date,
         bb.new_due_date,
         get_plan_qty(bb.plan_id,
                      bb.item_segments,
                      '计划单',
                      bb.new_order_date,
                      bb.new_due_date) po_qty, --采购数量
         get_need_qty(bb.plan_id, bb.item_segments, bb.new_due_date) need_qty --总需求数量
    from (select distinct mov.organization_id,
                          mov.plan_id,
                          mov.item_segments,
                          to_char(mov.new_order_date, 'YYYY-MM-DD') new_order_date, --建议采购日期
                          to_char(mov.new_due_date, 'YYYY-MM-DD') new_due_date --建议到期日
            from MSC_ORDERS_V mov, MSC_ORDERS_V mov1
           where 1 = 1
             and mov1.item_segments = mov.item_segments
             and mov1.new_due_date = mov.new_due_date
             and mov1.new_order_date = mov.new_order_date
             and mov1.order_type_text = '计划单' --物料有计划单的输出,没有计划单的排除
             and (trunc(mov.new_order_date) >=
                 to_date(p_date_f, 'YYYY-MM-DD') or p_date_f is null)
             and (trunc(mov.new_order_date) <=
                 to_date(p_date_e, 'YYYY-MM-DD') or p_date_e is null)
             and mov.plan_id = p_plan_id
             and mov.item_segments = p_item_code
             and mov.organization_id = p_organization_id
           order by new_due_date) bb;

1.3 游标调用的子函数

sql 复制代码
-- get_order_qty 核心逻辑
select round(nvl(sum(mov.quantity_rate), 0), 2)
  from MSC_ORDERS_V mov
 where 1 = 1
   and mov.category_set_id = 1001
   and mov.item_segments = p_item_code
   and mov.order_type_text = p_order_type
   and mov.plan_id = p_plan_id;

-- get_plan_qty 核心逻辑
select round(nvl(sum(mov.quantity_rate), 0), 2)
  from MSC_ORDERS_V mov
 where 1 = 1
   and trunc(mov.new_order_date) = to_date(p_order_date, 'YYYY-MM-DD')
   and trunc(mov.new_due_date) = to_date(p_due_date, 'YYYY-MM-DD')
   and mov.category_set_id = 1001
   and mov.item_segments = p_item_code
   and mov.order_type_text = p_order_type
   and mov.plan_id = p_plan_id;

-- get_need_qty 核心逻辑
select abs(round(nvl(sum(mov.quantity_rate), 0), 2))
  from MSC_ORDERS_V mov
 where 1 = 1
   and mov.new_due_date <= (to_date(p_due_date, 'YYYY-MM-DD') + 6)
   and mov.category_set_id = 1001
   and mov.item_segments = p_item_code
   and mov.order_type_text in ('非标准任务需求',
                               '工作单需求',
                               '计划单需求',
                               '销售订单 MDS',
                               '预测 MDS')
   and mov.plan_id = p_plan_id;
  • 问题分析

该程序的主要逻辑是:主程序首先遍历主游标,从Msc_Oraders_V中取出符合参数条件的物料,再代入次级游标中进一步取出符合要求的明细数据以打印输出,并且这其中的很多数量数据是通过调用子函数计算。

我们在两个游标中都看到了很不友好的DISTINCT去重,进一步分析作者使用粗暴去重的原意发现,两层游标的设计也并非必要:次级游标中的"物料有计划单的输出,没有计划单的排除"这个筛选条件其实可以通过EXISTS手段并入主游标,而在主游标中先去重再调用子函数求值的方式则应考虑通过分组聚合的方式尝试简化写法。

除了程序结构设计的问题,该程序的性能问题还存在于对视图Msc_Oraders_V的反复扫描,这是一个带有UNION ALL拼接的大型视图,而程序中所有的数据其实都是来自这个视图,困扰作者的可能是并不能通过简单的分组聚合直接满足功能设计的需求,因为各汇总数据不仅是order_type_text不同,而是同时在其它字段上又有不同范围的限制(即三个子函数的区别)。

  • 优化思路

大方向是1、两级游标整合成一级,2、拆解子函数入主游标

原次级游标能够决定代入来的主游标物料是否打印,则应把这个限制条件直接作为物料的筛选条件;

虽然子函数都是在读取Msc_Orders_V,但又略有不同,不能通过GROUP BY直接改写,考虑尝试PIVOT,原始扫描范围放为最大,各列统计时再分别限制其范围。

  • 优化后
sql 复制代码
with plan_qtys as
 (select mov1.organization_id,
         mov1.plan_id,
         mov1.item_segments,
         trunc(mov1.new_order_date) new_order_date, --建议采购日期
         trunc(mov1.new_due_date) new_due_date, --建议到期日
         sum(case
               when mov1.category_set_id = 1001 then
                mov1.quantity_rate
               else
                0
             end) plan_qty
    from MSC_ORDERS_V mov1
   where 1 = 1
     and mov1.new_due_date is not null
     and mov1.new_order_date is not null
     and mov1.order_type_text = '计划单' --物料有计划单的输出,没有计划单的排除
     and mov1.new_order_date >=
         nvl(to_date(p_date_f, 'YYYY-MM-DD'), mov1.new_order_date)
     and mov1.new_order_date <=
         nvl(to_date(p_date_e, 'YYYY-MM-DD') + .99999, mov1.new_order_date)
     and mov1.plan_id = p_plan_id
     and mov1.item_segments like '%' || p_item_segments || '%'
     and mov1.organization_id = p_organization_id
   group by mov1.organization_id,
            mov1.plan_id,
            mov1.item_segments,
            trunc(mov1.new_order_date),
            trunc(mov1.new_due_date)),
mov_data as
 (select organization_id,
         item_segments,
         description,
         uom_code,
         new_order_date,
         new_due_date,
         round(nvl(pr_qty1, 0), 2) pr_qty1,
         round(nvl(pr_qty7, 0), 2) pr_qty7,
         round(nvl(pr_qty8, 0), 2) pr_qty8,
         round(nvl(plan_qty, 0), 2) plan_qty,
         abs(round(nvl((need_qty_q2), 0), 2)) need_qty
    from (select mov.organization_id,
                 mov.item_segments,
                 mov.description,
                 mov.uom_code,
                 case
                   when order_type_text in ('非标准任务需求',
                                            '工作单需求',
                                            '计划单需求',
                                            '销售订单 MDS',
                                            '预测 MDS') then
                    '需求'
                   else
                    order_type_text
                 end as order_type_text,
                 mov.quantity_rate order_qty,
                 case
                   when mov.new_due_date <= pq.new_due_date + 6 then
                    mov.quantity_rate
                   else
                    0
                 end order_qty2,
                 pq.plan_qty,
                 pq.new_order_date,
                 pq.new_due_date
            from MSC_ORDERS_V mov, plan_qtys pq
           where mov.organization_id = pq.organization_id
             and mov.plan_id = pq.plan_id
             and mov.item_segments = pq.item_segments
             and mov.category_set_id = 1001)
  pivot(sum(order_qty), sum(order_qty2) as q2
     for order_type_text in('现有量' as pr_qty1,
                           '采购订单' as pr_qty7,
                           '采购申请' as pr_qty8,
                           '需求' as need_qty)))
select mov.organization_id,
       mov.item_segments,
       mov.description,
       mov.uom_code, --单位
       msi.inventory_item_id,
       msi.minimum_order_quantity, --MOQ
       msi.fixed_lot_multiplier, --SPQ
       msi.full_lead_time,
       mov.pr_qty1,
       mov.pr_qty7,
       mov.pr_qty8,
       mov.need_qty,
       mov.plan_qty,
       mov.new_order_date,
       mov.new_due_date
  from mov_data mov, MTL_SYSTEM_ITEMS_B MSI
 where 1 = 1
   and msi.organization_id = mov.organization_id
   and msi.segment1 = mov.item_segments
 order by item_segments, new_due_date

优化前请求第二次运行(有缓存)用时14h51m42s,优化后请求同参数第二次运行(有缓存)用时54s,优化比例1:991

END

相关推荐
小陈工1 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
科技小花6 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸6 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain6 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希7 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神7 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员7 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java7 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿7 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴7 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存