[13/JUL/2025, Yusuf Leo, Oracle SQL Performance Tuning Series]
我們經常會遇到從同一表中按不同維度取出不同區間的數據,再以相同的屬性將這些數據分別匯總到一起的需求。這類需求往往迫使我們對同一個表反復去掃描,當原始數據量太大的時候,這就可能給我們帶來程序性能上的困擾。行轉列PIVOT語法或許會是較好的優化思路之一。
PIVOT需要Oracle 11g及以上版本支持。
下面我們來看看這個實例,這是某企業EBS客制化開發的一個報表,核心邏輯是從ASCP工作臺按訂單類型區分統計各物料的總需求、在庫、在途、在購等數量。
- 優化前
1.1 主程序主游標
cursor c1 isselect aa.organization_id,aa.plan_id,aa.item_segments,aa.description,aa.uom_code,aa.minimum_order_quantity, --MOQaa.fixed_lot_multiplier, --SPQaa.full_lead_time, --Lead Timeget_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_qty8from (select distinct mov.organization_id,mov.plan_id,mov.item_segments,mov.description,mov.uom_code,msi.inventory_item_id,msi.minimum_order_quantity, --MOQmsi.fixed_lot_multiplier, --SPQmsi.full_lead_timefrom MSC_ORDERS_V mov, MTL_SYSTEM_ITEMS_B MSIwhere 1 = 1and msi.organization_id = mov.organization_idand msi.segment1 = mov.item_segmentsand (trunc(mov.new_order_date) >= to_date(p_date_f, 'YYYY-MM-DD') orp_date_f is null)and (trunc(mov.new_order_date) <= to_date(p_date_e, 'YYYY-MM-DD') orp_date_e is null)and mov.item_segments like '%' || p_item_segments || '%'and mov.plan_id = p_plan_idand mov.organization_id = p_organizatino_id) aa;
1.2 主程序次級游標
cursor c2(p_item_code VARCHAR2) isselect 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 mov1where 1 = 1and mov1.item_segments = mov.item_segmentsand mov1.new_due_date = mov.new_due_dateand mov1.new_order_date = mov.new_order_dateand 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_idand mov.item_segments = p_item_codeand mov.organization_id = p_organization_idorder by new_due_date) bb;
1.3 游標調用的子函數
-- get_order_qty 核心邏輯
select round(nvl(sum(mov.quantity_rate), 0), 2)from MSC_ORDERS_V movwhere 1 = 1and mov.category_set_id = 1001and mov.item_segments = p_item_codeand mov.order_type_text = p_order_typeand mov.plan_id = p_plan_id;-- get_plan_qty 核心邏輯
select round(nvl(sum(mov.quantity_rate), 0), 2)from MSC_ORDERS_V movwhere 1 = 1and 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 = 1001and mov.item_segments = p_item_codeand mov.order_type_text = p_order_typeand mov.plan_id = p_plan_id;-- get_need_qty 核心邏輯
select abs(round(nvl(sum(mov.quantity_rate), 0), 2))from MSC_ORDERS_V movwhere 1 = 1and mov.new_due_date <= (to_date(p_due_date, 'YYYY-MM-DD') + 6)and mov.category_set_id = 1001and mov.item_segments = p_item_codeand 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,原始掃描范圍放為最大,各列統計時再分別限制其范圍。
- 優化后
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(casewhen mov1.category_set_id = 1001 thenmov1.quantity_rateelse0end) plan_qtyfrom MSC_ORDERS_V mov1where 1 = 1and mov1.new_due_date is not nulland mov1.new_order_date is not nulland 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_idand mov1.item_segments like '%' || p_item_segments || '%'and mov1.organization_id = p_organization_idgroup 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_qtyfrom (select mov.organization_id,mov.item_segments,mov.description,mov.uom_code,casewhen order_type_text in ('非標準任務需求','工作單需求','計劃單需求','銷售訂單 MDS','預測 MDS') then'需求'elseorder_type_textend as order_type_text,mov.quantity_rate order_qty,casewhen mov.new_due_date <= pq.new_due_date + 6 thenmov.quantity_rateelse0end order_qty2,pq.plan_qty,pq.new_order_date,pq.new_due_datefrom MSC_ORDERS_V mov, plan_qtys pqwhere mov.organization_id = pq.organization_idand mov.plan_id = pq.plan_idand mov.item_segments = pq.item_segmentsand mov.category_set_id = 1001)pivot(sum(order_qty), sum(order_qty2) as q2for 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, --MOQmsi.fixed_lot_multiplier, --SPQmsi.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_datefrom mov_data mov, MTL_SYSTEM_ITEMS_B MSIwhere 1 = 1and msi.organization_id = mov.organization_idand msi.segment1 = mov.item_segmentsorder by item_segments, new_due_date
優化前請求第二次運行(有緩存)用時14h51m42s,優化后請求同參數第二次運行(有緩存)用時54s,優化比例1:991
[END]