性能下降返回光标与联合所有

Posted

技术标签:

【中文标题】性能下降返回光标与联合所有【英文标题】:Performance drop returning cursor with union all 【发布时间】:2021-11-23 09:05:48 【问题描述】:

在一个游标中使用带有两个子查询的 UNION ALL 时,我面临着无法解决和不可能的性能下降(至少我认为这是问题所在)。在测试窗口中打开游标结果时,PL/SQL Developer 只是冻结。

如果我关闭任何子查询 - 一切正常。

如果我将整个查询从光标移到常规 SQL 查询窗口 - 一切都很好,无需关闭某些部分。

程序结构如下,期待任何帮助:

procedure p_proc(p_param varchar2,
                 outcur  out sys_refcursor) is
begin
  open outcur for
    select *
      from (select -- visible cols
                   si.item_full_name
                 , si.final_price
                 , si.full_price
                 , si.receipt_num
                 , si.receipt_date
                 , si.vendor_code
                 , case when det.br_summary is null and mr.motiv_rate_value is not null then mr.motiv_rate_value
                        when det.br_summary is not null then det.br_summary
                    end personal_bonus_amount
                 , case when det.br_summary is null and mr.motiv_rate_value is not null then 1
                        when det.br_summary is not null then det.cross_sale_kt
                    end personal_bonus_koeff
                 -- service cols
                 , case when det.br_summary is null and mr.motiv_rate_value is not null then 'approximate'
                        when det.br_summary is not null then 'definite'
                    end personal_bonus_type
                 , coalesce(det.sale_stream, mr.sale_stream, 'Not defined') item_group_name
                 , si.operation_type
                 , si.src
                 -- pagination
                 , row_number() over (order by si.receipt_date desc) rn 
              from (-- curr day
                    select b.cost final_price
                         , case when b.discount = 0 then null else b.price
                            end full_price
                         , b.doc_number receipt_num
                         , b.receipt_date receipt_date
                         , i.item_code vendor_code
                         , i.full_name item_full_name
                         , b.subsite code_op
                         , b.operator_id
                         , to_char(b.businessday, 'yyyymm') sale_period
                         , b.oper_type operation_type
                         , 'bill' src
                      from scheme.bills b
                      join scheme.items i on i.item_code = b.item
                     where b.businessday = trunc(p_date_to)
                       and b.subsite = p_office_id
                       and b.operator_id = p_emp_id
                    union all
                    -- prev days
                    select l.txn_amount final_price
                         , case when l.disc = 0 then null else l.price
                             end full_price
                         , t.receipt_num receipt_num
                         , t.ts receipt_date
                         , i.item_code vendor_code
                         , i.full_name item_full_name
                         , s.office_code code_op
                         , e.emp_code operator_id
                         , to_char(l.dt,'yyyymm') sale_period
                         , l.txn_type operation_type
                         , 'txn' src
                      from scheme.txn t
                      join scheme.txn_lines l on t.rtl_txn_id = l.rtl_txn_id
                      join scheme.items i on l.item_id = i.item_id
                      join scheme.offices s on t.subsite_id = s.subsite_id
                      join scheme.employees e on t.employee_id = e.employee_id
                     where t.ts between trunc(p_date_from) and trunc(p_date_to)
                       and t.subsite_id = v_op_id
                       and t.employee_id = v_emp_id
                 ) si
             /* fact */
             left join scheme.sales_details det on si.sale_period = det.period
                                               and si.code_op = det.op_code
                                               and ltrim(si.operator_id,'0') = ltrim(det.tab_num,'0')
                                               and si.receipt_num = det.rcpt_num
                                               and si.vendor_code = det.item_article
             /* prognosis */
             left join scheme.rates mr on si.sale_period = mr.motiv_rate_period
                                      and si.code_op = mr.code_op
                                      and si.vendor_code = mr.code_1c
            where 1 = 1
              and si.final_price between nvl(p_price_from, si.final_price) and nvl(p_price_to, si.final_price)
              /* if no filters */
              and (item_group_cnt = 0 or coalesce(det.sale_stream, mr.sale_stream, 'Not defined') in (select * from table(p_item_group)))
              and si.receipt_num = nvl(p_receipt_num, si.receipt_num)                                        
           )
     where rn between p_page_num * p_page_size + 1 and (p_page_num + 1) * p_page_size;
end;

UPD解释游标中使用的整个查询的计划:

----------------------------------------------------------------------------------------------------------------------------------
| Id   | Operation                                             | Name                           | Rows | Bytes | Cost | Time     |
----------------------------------------------------------------------------------------------------------------------------------
|    0 | SELECT STATEMENT                                      |                                |   10 | 32810 |   62 | 00:00:01 |
|  * 1 |   VIEW                                                |                                |   10 | 32810 |   62 | 00:00:01 |
|  * 2 |    WINDOW SORT PUSHED RANK                            |                                |    2 |  2956 |   62 | 00:00:01 |
|    3 |     NESTED LOOPS OUTER                                |                                |    2 |  2956 |   61 | 00:00:01 |
|    4 |      NESTED LOOPS OUTER                               |                                |    2 |  2826 |   53 | 00:00:01 |
|    5 |       VIEW                                            |                                |    2 |  2728 |   46 | 00:00:01 |
|    6 |        UNION-ALL                                      |                                |      |       |      |          |
|    7 |         NESTED LOOPS                                  |                                |    1 |   138 |   32 | 00:00:01 |
|    8 |          NESTED LOOPS                                 |                                |    1 |   138 |   32 | 00:00:01 |
|    9 |           PARTITION RANGE SINGLE                      |                                |    1 |    66 |   29 | 00:00:01 |
| * 10 |            TABLE ACCESS BY LOCAL INDEX ROWID BATCHED  | F003_BILL                      |    1 |    66 |   29 | 00:00:01 |
| * 11 |             INDEX RANGE SCAN                          | IX_SUBSITE_DOCNUM_BUSINDAY_SEQ |    1 |       |    5 | 00:00:01 |
| * 12 |           INDEX RANGE SCAN                            | IX_D001_CODE_1C_ITEM_ID        |    1 |       |    2 | 00:00:01 |
|   13 |          TABLE ACCESS BY INDEX ROWID                  | D001_ITEM                      |    1 |    72 |    3 | 00:00:01 |
|   14 |         NESTED LOOPS                                  |                                |    1 |   183 |   14 | 00:00:01 |
|   15 |          NESTED LOOPS                                 |                                |    1 |   183 |   14 | 00:00:01 |
|   16 |           NESTED LOOPS                                |                                |    1 |   104 |   12 | 00:00:01 |
|   17 |            NESTED LOOPS                               |                                |    1 |    70 |    7 | 00:00:01 |
|   18 |             NESTED LOOPS                              |                                |    1 |    30 |    4 | 00:00:01 |
|   19 |              TABLE ACCESS BY INDEX ROWID              | D005_EMPLOYEE                  |    1 |    18 |    3 | 00:00:01 |
| * 20 |               INDEX UNIQUE SCAN                       | PK_D005                        |    1 |       |    2 | 00:00:01 |
|   21 |              TABLE ACCESS BY INDEX ROWID              | D018_SUBSITE                   |    1 |    12 |    1 | 00:00:01 |
| * 22 |               INDEX UNIQUE SCAN                       | PK_D018                        |    1 |       |    0 | 00:00:01 |
|   23 |             PARTITION RANGE ITERATOR                  |                                |    1 |    40 |    3 | 00:00:01 |
|   24 |              PARTITION HASH SINGLE                    |                                |    1 |    40 |    3 | 00:00:01 |
| * 25 |               TABLE ACCESS FULL                       | F007_RTL_TXN                   |    1 |    40 |    3 | 00:00:01 |
| * 26 |            TABLE ACCESS BY GLOBAL INDEX ROWID BATCHED | F008_RTL_TXN_LI                |    1 |    34 |    5 | 00:00:01 |
| * 27 |             INDEX RANGE SCAN                          | IX_F008_RTL_TXN_ID             |    7 |       |    3 | 00:00:01 |
| * 28 |           INDEX UNIQUE SCAN                           | PK_D001                        |    1 |       |    1 | 00:00:01 |
|   29 |          TABLE ACCESS BY INDEX ROWID                  | D001_ITEM                      |    1 |    79 |    2 | 00:00:01 |
| * 30 |       TABLE ACCESS BY INDEX ROWID BATCHED             | T_OP_MOTIVATION_RATE_MYRTK     |    1 |    49 |    7 | 00:00:01 |
| * 31 |        INDEX RANGE SCAN                               | IDX02_CODE_OP_1C               |    3 |       |    3 | 00:00:01 |
| * 32 |      TABLE ACCESS BY INDEX ROWID BATCHED              | DET_SALES_PPT_DWH              |    1 |    65 |    4 | 00:00:01 |
| * 33 |       INDEX RANGE SCAN                                | IDX_03_RCPT_NUM                |    3 |       |    2 | 00:00:01 |
----------------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 1 - filter("RN">=1 AND "RN"<=10)
* 2 - filter(ROW_NUMBER() OVER ( ORDER BY INTERNAL_FUNCTION("SI"."RECEIPT_DATE") DESC )<=10)
* 10 - filter("F003"."OPERATOR_ID"='000189513' AND "F003"."COST">=TO_NUMBER(TO_CHAR("F003"."COST")) AND "F003"."COST"<=TO_NUMBER(TO_CHAR("F003"."COST")))
* 11 - access("F003"."SUBSITE"='S165' AND "F003"."BUSINESSDAY"=TO_DATE(' 2021-11-23 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
* 11 - filter("F003"."BUSINESSDAY"=TO_DATE(' 2021-11-23 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND "F003"."DOC_NUMBER" IS NOT NULL)
* 12 - access("I"."D001_CODE_1C"="F003"."ITEM")
* 12 - filter("I"."D001_CODE_1C" IS NOT NULL)
* 20 - access("E"."EMPLOYEE_ID"=3561503543)
* 22 - access("S"."SUBSITE_ID"=29260)
* 25 - filter("T"."EMPLOYEE_ID"=3561503543 AND "T"."SUBSITE_ID"=29260 AND "T"."F007_TS"<=TO_DATE(' 2021-11-23 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND "T"."F007_RCPT_NUM_1C" IS NOT NULL)
* 26 - filter("L"."F008_AMOUNT">=TO_NUMBER(TO_CHAR("L"."F008_AMOUNT")) AND "L"."F008_AMOUNT"<=TO_NUMBER(TO_CHAR("L"."F008_AMOUNT")))
* 27 - access("T"."RTL_TXN_ID"="L"."RTL_TXN_ID")
* 28 - access("L"."ITEM_ID"="I"."ITEM_ID")
* 30 - filter("SI"."SALE_PERIOD"="MR"."MOTIV_RATE_PERIOD"(+))
* 31 - access("SI"."CODE_OP"="MR"."CODE_OP"(+) AND "SI"."VENDOR_CODE"="MR"."CODE_1C"(+))
* 32 - filter("SI"."CODE_OP"="DET"."OP_CODE"(+) AND "SI"."VENDOR_CODE"="DET"."ITEM_ARTICLE"(+) AND "DET"."ITEM_ARTICLE"(+) IS NOT NULL AND "DET"."PERIOD"(+)=TO_NUMBER("SI"."SALE_PERIOD") AND
  LTRIM("SI"."OPERATOR_ID",'0')=LTRIM("DET"."TAB_NUM_RTK"(+),'0'))
* 33 - access("SI"."RECEIPT_NUM"="DET"."RCPT_NUM"(+))
* 33 - filter("DET"."RCPT_NUM"(+) IS NOT NULL)

【问题讨论】:

请对选择执行解释计划并发布结果 添加了解释计划,无论使用什么“页面大小”,性能都保持良好。 【参考方案1】:

实际解决方案

设法从 DBA 获得过程执行计划。问题是优化器在过程中执行查询时选择了另一个索引来加入 scheme.sales_details 表。添加了 INDEX HINT 与常规查询中使用的相同索引,一切正常。

下方已弃用的想法

据我了解,问题出在 Oracle 优化器中,它“认为”首先执行 UNION ALL 比将谓词推入子查询要好。将这个 union 分成两个单独的查询让他毫不犹豫地推动 pred。

也许这可以通过玩一些提示来解决,这暂时是 wip。

临时解决方法是重新组合查询,从此结构开始

    select *
      from (select row_number() rn
                 , u.*
              from (select * 
                      from first_query
                     union all
                    select * 
                      from second_query) u
                -- some joins
              join first_table ft
              join second_table st
                -- predicate block
             where 1=1
               and a = b
            )
     where rn between c and d;

到这里

select *
      from (select row_number() rn
                 , u.*
              from (select * 
                      from first_query) u
                -- some joins
              join first_table ft
              join second_table st
                -- predicate block
             where 1=1
               and a = b
             union all
            select row_number() rn
                 , u.*
              from (select * 
                      from second_query) u
                -- some joins
              join first_table ft
              join second_table st
                -- predicate block
             where 1=1
               and a = b
            )
     where rn between c and d;

这不是完美的解决方案,因为它使 JOIN 部分加倍,但至少它有效。

【讨论】:

以上是关于性能下降返回光标与联合所有的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server 2008 - 连接导致性能下降

SPARQL 联合查询不返回所有解决方案

具有通用联合约束的 TypeScript 函数返回值

带有 COUNT() 函数的 SQL 联合未返回预期结果

SQL Azure 联合和索引 - 性能说明

查询性能优化