跳过正文

Oracle 标量子查询优化实战:从 573s 到 98s 的 SQL 改写案例

Greatfinish
作者
Greatfinish
记录 Oracle、PostgreSQL、达梦、Linux、存储与生产环境故障处理经验。

前言
#

其实写SQL优化的实战很难,主要是很多情况是需要结合业务,而我们很多开发人员为了满足业务的需求随心随意的写SQL,就产生了很多质量较差的SQL,这些SQL可能在开发测试数据量不大的环境中正常运行,可是一到生产环境就直接把整个数据库服务器资源打满。所以我们平时在写SQL过程中多做简化工作,尽量让SQL写的让别人能读懂。多考虑条件的过滤包括关键字段索引等等,查询的数据量少了,减少了全表扫描了,自然效率也就高了。最后,要满足业务的需求又要写出易懂高质量的SQL。这其实是一个需要权衡的过程、需要做取舍。

问题现象
#

运维人员反馈EBS系统有一个请求运行时间很长,每次运行需要10多分钟。影响数据库整体性能和业务开展。

分析SQL
#

通过跟踪请求,拿到sql_id:gcppkx5y05x3k,把SQL拿出来大致看一下。 初步梳理,这一条非常复杂的插入SQL。看到一共有插入23个字段,主体SQL关联7个表和4个子查询。如下:

INSERT INTO CUX_TWOVOTE_SALEINVOICE_NEW (SUPPLIERID, SALINVOICENO, SALINVDATE, SALSUMVALUE, SALINVTYPE, SALCSTCODE, SALCSTNAME, GOODS, GOODNAME, VAT_HEADER_ID, VAT_LINE_ID, PO_LINE_ID, SOURCE_PO_LINE_ID, PO_HEADER_ID, SOURCE_PO_HEADER_ID, PO_NUMBER, ORG_ID, INTER_ORG_ID, ITEM_ID, LOT_NUMBER, REQUEST_ID, ATTRIBUTE1, ATTRIBUTE2)
SELECT CAVL.DEPARTMENT_CODE,
       DECODE(
                (SELECT COUNT(1)
                 FROM CUX_SC_MINHANG_CLIENT
                 WHERE ACCOUNT_NUMBER = CAVH.CUSTOMER_NUMBER), 0, CAVH.VAT_NUMBER, DECODE(INSTR(CAVH.INVOICE_CATEGORY, '全电'), 0, CAVH.VAT_INVOICE_CODE || CAVH.VAT_NUMBER, CAVH.VAT_NUMBER)) VAT_NUMBER,
       TO_CHAR(CAVH.VAT_DATE, 'yyyy-mm-dd') VAT_DATE,
       CAVH.AMOUNT_INC_TAX SALSUMVALUE,
       COPL.PROPERTY7_NAME SALINVTYPE,
       CAVH.CUSTOMER_NUMBER,
       CAVH.CUSTOMER_NAME,
       CAVL.ITEM_CODE || '|' || CAVL.PACKAGE_NUM ITEM_CODE,
       CAVL.ITEM_DESC,
       CAVH.VAT_HEADER_ID,
       CAVL.VAT_LINE_ID,
       DECODE(
                (SELECT COUNT(1)
                 FROM CUX_OU_PROPERTY_LINES_T COUL, PO_LINES_ALL PL, PO_HEADERS_ALL PH
                 WHERE PH.PO_HEADER_ID = PL.PO_HEADER_ID
                   AND PH.VENDOR_ID = COUL.ITEM_ID
                   AND PL.PO_LINE_ID = MLN.N_ATTRIBUTE6
                   AND COUL.PROPERTY_TYPE = 'V'
                   AND NVL(COUL.PROPERTY5_NAME, 'N') <> 'N'), 0, MLN.N_ATTRIBUTE6, NVL(COMR.PO_LINE_ID, MLN.N_ATTRIBUTE6)) PO_LINE_ID,
       CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.N_ATTRIBUTE6
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN
                  (SELECT MLN_SOURCE.N_ATTRIBUTE6
                   FROM MTL_LOT_NUMBERS MLN_SOURCE,
                        CUX_OE_MTL_RESERVATIONS COMR_SOURCE
                   WHERE MLN_SOURCE.LOT_NUMBER = COMR_SOURCE.LOT_NUMBER
                     AND MLN_SOURCE.INVENTORY_ITEM_ID = COMR_SOURCE.INVENTORY_ITEM_ID
                     AND MLN_SOURCE.ORGANIZATION_ID = COMR_SOURCE.ORGANIZATION_ID
                     AND COMR_SOURCE.RELATE_PO_LINE_ID = MLN.N_ATTRIBUTE6)
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.N_ATTRIBUTE6, COMR.PO_LINE_ID)
       END AS SOURCE_PO_LINE_ID,
       MLN.N_ATTRIBUTE5 PO_HEADER_ID,
       CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.N_ATTRIBUTE5
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN
                  (SELECT MLN_SOURCE.N_ATTRIBUTE5
                   FROM MTL_LOT_NUMBERS MLN_SOURCE,
                        CUX_OE_MTL_RESERVATIONS COMR_SOURCE
                   WHERE MLN_SOURCE.LOT_NUMBER = COMR_SOURCE.LOT_NUMBER
                     AND MLN_SOURCE.INVENTORY_ITEM_ID = COMR_SOURCE.INVENTORY_ITEM_ID
                     AND MLN_SOURCE.ORGANIZATION_ID = COMR_SOURCE.ORGANIZATION_ID
                     AND COMR_SOURCE.RELATE_PO_LINE_ID = MLN.N_ATTRIBUTE6)
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.N_ATTRIBUTE5, COMR.PO_HEADER_ID)
       END AS SOURCE_PO_HEADER_ID,
       CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.C_ATTRIBUTE1
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN
                  (SELECT MLN_SOURCE.C_ATTRIBUTE1
                   FROM MTL_LOT_NUMBERS MLN_SOURCE,
                        CUX_OE_MTL_RESERVATIONS COMR_SOURCE
                   WHERE MLN_SOURCE.LOT_NUMBER = COMR_SOURCE.LOT_NUMBER
                     AND MLN_SOURCE.INVENTORY_ITEM_ID = COMR_SOURCE.INVENTORY_ITEM_ID
                     AND MLN_SOURCE.ORGANIZATION_ID = COMR_SOURCE.ORGANIZATION_ID
                     AND COMR_SOURCE.RELATE_PO_LINE_ID = MLN.N_ATTRIBUTE6)
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.C_ATTRIBUTE1, '')
       END AS PO_NUMBER,
       COMR.ORG_ID,
       COMR.INTERCOMPANY_ORG_ID,
       CAVL.ITEM_ID,
       CAVL.LOT_NUMBER,
       :B1,
       CAVH.VAT_INVOICE_CODE,
       CAVH.VAT_NUMBER
FROM CUX_AR_VAT_HEADERS CAVH,
     CUX_AR_VAT_LINES CAVL,
     CUX_OE_MTL_RESERVATIONS COMR,
     CUX_OE_ORDER_HEADERS COOH,
     MTL_LOT_NUMBERS MLN,
     CUX_MST_ITEM ISI,
     CUX_FND_LOOKUP_VALUES SCP,

  (SELECT ITEM_ID,
          PROPERTY7_NAME,
          OU_ID
   FROM CUX_OU_PROPERTY_LINES_T
   WHERE PROPERTY_TYPE = 'C'
     AND NVL(PROPERTY7_NAME, 'N') <> 'N') COPL,

  (SELECT T.TRANSACTION_TYPE_ID
   FROM OE_TRANSACTION_TYPES_ALL TT,
        OE_TRANSACTION_TYPES_TL T,
        FND_DOCUMENT_SEQUENCES A
   WHERE TT.TRANSACTION_TYPE_ID = T.TRANSACTION_TYPE_ID
     AND T.LANGUAGE = USERENV('LANG')
     AND TT.ATTRIBUTE1 = A.DOC_SEQUENCE_ID
     AND TT.TRANSACTION_TYPE_CODE = 'ORDER'
     AND TT.ATTRIBUTE15 <> '01') TT_TYPE,

  (SELECT HOU.ORGANIZATION_ID,
          FLV.MEANING
   FROM HR_ORGANIZATION_UNITS_V HOU,
        CUX_FND_LOOKUP_VALUES FLV
   WHERE HOU.ATTRIBUTE6 = FLV.LOOKUP_CODE
     AND FLV.LOOKUP_TYPE = 'MST-0031'
     AND HOU.ATTRIBUTE6 IS NOT NULL) COM1,

  (SELECT HOU.ORGANIZATION_ID,
          FLV.MEANING
   FROM HR_ORGANIZATION_UNITS_V HOU,
        CUX_FND_LOOKUP_VALUES FLV
   WHERE HOU.ATTRIBUTE6 = FLV.LOOKUP_CODE
     AND FLV.LOOKUP_TYPE = 'MST-0031'
     AND HOU.ATTRIBUTE6 IS NOT NULL) COM2
WHERE CAVH.VAT_HEADER_ID = CAVL.VAT_HEADER_ID
  AND CAVH.VAT_HEADER_ID = COMR.VAT_HEADER_ID
  AND COMR.HEADER_ID = COOH.HEADER_ID
  AND COMR.SOURCE_LINE_ID = CAVL.ORDER_LINE_ID
  AND COMR.INVENTORY_ITEM_ID = MLN.INVENTORY_ITEM_ID
  AND COMR.ORGANIZATION_ID = MLN.ORGANIZATION_ID
  AND COMR.LOT_NUMBER = MLN.LOT_NUMBER
  AND COMR.INVENTORY_ITEM_ID = ISI.INVENTORY_ITEM_ID
  AND COMR.ORGANIZATION_ID = ISI.ORGANIZATION_ID
  AND ISI.OPERATE_SCOPE = SCP.LOOKUP_CODE
  AND SCP.LOOKUP_TYPE = 'MST-0004'
  AND SCP.SEGMENT4 = 'Y'
  AND COOH.ORDER_TYPE_ID = TT_TYPE.TRANSACTION_TYPE_ID
  AND NVL(COOH.ORDER_SOURCE_REFERENCE, 'N') NOT IN ('QUICK_ORDER',
                                                    'QUICK_RET')
  AND CAVH.CUSTOMER_ID = COPL.ITEM_ID
  AND CAVH.ORG_ID = COPL.OU_ID
  AND COMR.ORG_ID = COM1.ORGANIZATION_ID
  AND COMR.INTERCOMPANY_ORG_ID = COM2.ORGANIZATION_ID
  AND CAVH.ENABLED_FLAG = 'Y'
  AND UPPER(CAVH.VAT_NUMBER) NOT LIKE 'X%'
  AND UPPER(CAVH.VAT_NUMBER) NOT LIKE 'D%'
  AND CAVH.VAT_NUMBER IS NOT NULL
  AND CAVH.AMOUNT_INC_TAX > 0
  AND CAVL.AMOUNT_INC_TAX > 0
  AND MLN.C_ATTRIBUTE4 NOT IN
    (SELECT MEANING
     FROM CUX_FND_LOOKUP_VALUES
     WHERE LOOKUP_TYPE = 'AP-013'
       AND TAG = 'SUP'
       AND SEGMENT1 = 'Y'
       AND ENABLED_FLAG = 'Y')
  AND TRUNC(CAVH.LAST_UPDATE_DATE) >= TRUNC(SYSDATE - 1)
  AND NOT EXISTS
    (SELECT 1
     FROM CUX_TWOVOTE_SALEINVOICE_NEW
     WHERE VAT_HEADER_ID = CAVH.VAT_HEADER_ID
       AND TRUNC(IMPORT_DATE) >= TRUNC(SYSDATE - 1))

使用SQLHC(SQL Tuning Health-Check)工具得到执行计划如下图

执行计划20251126.png

由于SQL复杂,对表和字段的定义无法从业务侧得知,如何取舍优化。只能从DBA角度来添加索引或者改写SQL逐步分析。

先对SELECT的23个字段部分开始:

上图执行计划里Id从12~26,写了多个标量子查询回填同几张表(MTL_LOT_NUMBERS,CUX_OE_MTL_RESERVATIONS):等于重复访问同一数据源。如下部分

CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.N_ATTRIBUTE6
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN
                  (SELECT MLN_SOURCE.N_ATTRIBUTE6
                   FROM MTL_LOT_NUMBERS MLN_SOURCE,
                        CUX_OE_MTL_RESERVATIONS COMR_SOURCE
                   WHERE MLN_SOURCE.LOT_NUMBER = COMR_SOURCE.LOT_NUMBER
                     AND MLN_SOURCE.INVENTORY_ITEM_ID = COMR_SOURCE.INVENTORY_ITEM_ID
                     AND MLN_SOURCE.ORGANIZATION_ID = COMR_SOURCE.ORGANIZATION_ID
                     AND COMR_SOURCE.RELATE_PO_LINE_ID = MLN.N_ATTRIBUTE6)
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.N_ATTRIBUTE6, COMR.PO_LINE_ID)
       END AS SOURCE_PO_LINE_ID,
       MLN.N_ATTRIBUTE5 PO_HEADER_ID,
       CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.N_ATTRIBUTE5
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN
                  (SELECT MLN_SOURCE.N_ATTRIBUTE5
                   FROM MTL_LOT_NUMBERS MLN_SOURCE,
                        CUX_OE_MTL_RESERVATIONS COMR_SOURCE
                   WHERE MLN_SOURCE.LOT_NUMBER = COMR_SOURCE.LOT_NUMBER
                     AND MLN_SOURCE.INVENTORY_ITEM_ID = COMR_SOURCE.INVENTORY_ITEM_ID
                     AND MLN_SOURCE.ORGANIZATION_ID = COMR_SOURCE.ORGANIZATION_ID
                     AND COMR_SOURCE.RELATE_PO_LINE_ID = MLN.N_ATTRIBUTE6)
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.N_ATTRIBUTE5, COMR.PO_HEADER_ID)
       END AS SOURCE_PO_HEADER_ID,
       CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.C_ATTRIBUTE1
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN
                  (SELECT MLN_SOURCE.C_ATTRIBUTE1
                   FROM MTL_LOT_NUMBERS MLN_SOURCE,
                        CUX_OE_MTL_RESERVATIONS COMR_SOURCE
                   WHERE MLN_SOURCE.LOT_NUMBER = COMR_SOURCE.LOT_NUMBER
                     AND MLN_SOURCE.INVENTORY_ITEM_ID = COMR_SOURCE.INVENTORY_ITEM_ID
                     AND MLN_SOURCE.ORGANIZATION_ID = COMR_SOURCE.ORGANIZATION_ID
                     AND COMR_SOURCE.RELATE_PO_LINE_ID = MLN.N_ATTRIBUTE6)
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.C_ATTRIBUTE1, '')
       END AS PO_NUMBER,

在查看标量子查询中表的数据量非常大,会造成大量的等待消耗。

SQL> select count(1) from CUX.CUX_OE_MTL_RESERVATIONS;

  COUNT(1)
----------
  45514896

SQL> select count(1) from INV.MTL_LOT_NUMBERS;

  COUNT(1)
----------
  11859933

SQL>

解释一下什么是标量子查询当一个子查询介于SELECT与FROM之间,这种子查询就叫标量子查询。

标量子查询类似一个天然的嵌套循环,而且驱动表固定为主表,嵌套循环被驱动表的连接列必须包含在索引中。同理,标量子查询中子查询的表连接列也必须包含在索引中。

建议在实际生产工作中,尽量避免使用标量子查询。原因是:假如主表返回大量数据,主表的连接列基数又很高,那么子查询中的表会被多次扫描,从而严重影响SQL性能;如果主表数据量小,或者主表的连接列基数很低,那么这个时候也可以使用标量子查询,但要给子查询中表的连接列建立索引。

当SQL里面有标量子查询,可以将标量子查询等价改写为外连接,从而使它们可以进行HASH连接。

  • 标量子查询=外层每一行都要去子表再查一次
  • 外层行数一大 + 子表一大 ⇒ 执行次数爆炸

执行计划中Id第51行部分CUX_OU_PROPERTY_LINES_T全表扫描

再看WHERE条件部分Id在100行部分,查看CUX_TWOVOTE_SALEINVOICE_NEW的数据量也非常大。走全表扫描也非常消耗IO

SQL> select count(1) from CUX.CUX_TWOVOTE_SALEINVOICE_NEW;

  COUNT(1)
----------
  15734505

SQL>

优化SQL
#

在我们通过执行计划加上表数据量和索引等信息分析后,做如下处理。

把SELECT后重复关联标量子查询3个字段SOURCE_PO_LINE_ID、SOURCE_PO_HEADER_ID、PO_NUMBER,通过left join方式,避免重复的扫描,改写后重复部分的子查询作为主表,如下:

SELECT 
  ...
  CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.N_ATTRIBUTE6
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN MLN_SOURCE.N_ATTRIBUTE6
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.N_ATTRIBUTE6, COMR.PO_LINE_ID)
       END AS SOURCE_PO_LINE_ID,
       MLN.N_ATTRIBUTE5 PO_HEADER_ID,
       CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.N_ATTRIBUTE5
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN MLN_SOURCE.N_ATTRIBUTE5
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.N_ATTRIBUTE5, COMR.PO_HEADER_ID)
       END AS SOURCE_PO_HEADER_ID,
       CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.C_ATTRIBUTE1
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN MLN_SOURCE.C_ATTRIBUTE1
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.C_ATTRIBUTE1, '')
       END AS PO_NUMBER,
  ...
FROM 
  ...
(SELECT COMR_SOURCE.RELATE_PO_LINE_ID,
          MLN_SOURCE.N_ATTRIBUTE6,
          MLN_SOURCE.N_ATTRIBUTE5,
          MLN_SOURCE.C_ATTRIBUTE1
   FROM MTL_LOT_NUMBERS MLN_SOURCE,
        CUX_OE_MTL_RESERVATIONS COMR_SOURCE
   WHERE MLN_SOURCE.LOT_NUMBER = COMR_SOURCE.LOT_NUMBER
     AND MLN_SOURCE.INVENTORY_ITEM_ID = COMR_SOURCE.INVENTORY_ITEM_ID
     AND MLN_SOURCE.ORGANIZATION_ID = COMR_SOURCE.ORGANIZATION_ID) MLN_SOURCE
 WHERE 
   ...
   AND MLN.N_ATTRIBUTE6 = MLN_SOURCE.RELATE_PO_LINE_ID(+) 
  

CUX_OU_PROPERTY_LINES_T全表扫描

DECODE(
                (SELECT COUNT(1)
                 FROM CUX_OU_PROPERTY_LINES_T COUL, PO_LINES_ALL PL, PO_HEADERS_ALL PH
                 WHERE PH.PO_HEADER_ID = PL.PO_HEADER_ID
                   AND PH.VENDOR_ID = COUL.ITEM_ID
                   AND PL.PO_LINE_ID = MLN.N_ATTRIBUTE6
                   AND COUL.PROPERTY_TYPE = 'V'
                   AND NVL(COUL.PROPERTY5_NAME, 'N') <> 'N'), 0, MLN.N_ATTRIBUTE6, NVL(COMR.PO_LINE_ID, MLN.N_ATTRIBUTE6)) PO_LINE_ID,

可以在PROPERTY_TYPE字段上添加索引,并重新统计信息:

SQL> create index CUX.CUX_OU_PROPERTY_LINES_T_N03 on CUX.CUX_OU_PROPERTY_LINES_T (PROPERTY_TYPE)
  tablespace CUXD
  pctfree 10
  initrans 2
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );  2    3    4    5    6    7    8    9   10   11   12

Index created.

SQL> BEGIN
  DBMS_STATS.GATHER_TABLE_STATS(
    ownname        => 'CUX',
    tabname        => 'CUX_OU_PROPERTY_LINES_T',
    method_opt     => 'FOR ALL COLUMNS SIZE AUTO',
    estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
    cascade        => TRUE,
    no_invalidate  => FALSE
  );
END;
/  2    3    4    5    6    7    8    9   10   11

PL/SQL procedure successfully completed.

SQL>

WHERE条件后的CUX_TWOVOTE_SALEINVOICE_NEW全表扫描

AND NOT EXISTS
    (SELECT 1
     FROM CUX_TWOVOTE_SALEINVOICE_NEW
     WHERE VAT_HEADER_ID = CAVH.VAT_HEADER_ID
       AND TRUNC(IMPORT_DATE) >= TRUNC(SYSDATE - 1))

可以在VAT_HEADER_ID字段上添加索引,并重新统计信息:

SQL> create index CUX.CUX_TWOVOTE_SALEINVOICE_NEW_N4 on CUX.CUX_TWOVOTE_SALEINVOICE_NEW (VAT_HEADER_ID)
  tablespace CUXD
  pctfree 10
  initrans 2
  maxtrans 255
  storage
  (
    initial 128K
    next 128K
    minextents 1
    maxextents unlimited
    pctincrease 0
  );  2    3    4    5    6    7    8    9   10   11   12   13

Index created.

SQL> BEGIN
  DBMS_STATS.GATHER_TABLE_STATS(
    ownname        => 'CUX',
    tabname        => 'CUX_TWOVOTE_SALEINVOICE_NEW',
    method_opt     => 'FOR ALL COLUMNS SIZE AUTO',
    estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
    cascade        => TRUE,
    no_invalidate  => FALSE
  );
END;
/  2    3    4    5    6    7    8    9   10   11

PL/SQL procedure successfully completed.

SQL>

改写后最终sql_id:3xb8q2021ztpv。如下:

INSERT INTO CUX_TWOVOTE_SALEINVOICE_NEW (SUPPLIERID, SALINVOICENO, SALINVDATE, SALSUMVALUE, SALINVTYPE, SALCSTCODE, SALCSTNAME, GOODS, GOODNAME, VAT_HEADER_ID, VAT_LINE_ID, PO_LINE_ID, SOURCE_PO_LINE_ID, PO_HEADER_ID, SOURCE_PO_HEADER_ID, PO_NUMBER, ORG_ID, INTER_ORG_ID, ITEM_ID, LOT_NUMBER, REQUEST_ID, ATTRIBUTE1, ATTRIBUTE2)
SELECT CAVL.DEPARTMENT_CODE,
       DECODE(
                (SELECT COUNT(1)
                 FROM CUX_SC_MINHANG_CLIENT
                 WHERE ACCOUNT_NUMBER = CAVH.CUSTOMER_NUMBER), 0, CAVH.VAT_NUMBER, DECODE(INSTR(CAVH.INVOICE_CATEGORY, '全电'), 0, CAVH.VAT_INVOICE_CODE || CAVH.VAT_NUMBER, CAVH.VAT_NUMBER)) VAT_NUMBER,
       TO_CHAR(CAVH.VAT_DATE, 'yyyy-mm-dd') VAT_DATE,
       CAVH.AMOUNT_INC_TAX SALSUMVALUE,
       COPL.PROPERTY7_NAME SALINVTYPE,
       CAVH.CUSTOMER_NUMBER,
       CAVH.CUSTOMER_NAME,
       CAVL.ITEM_CODE || '|' || CAVL.PACKAGE_NUM ITEM_CODE,
       CAVL.ITEM_DESC,
       CAVH.VAT_HEADER_ID,
       CAVL.VAT_LINE_ID,
       DECODE(
                (SELECT COUNT(1)
                 FROM CUX_OU_PROPERTY_LINES_T COUL, PO_LINES_ALL PL, PO_HEADERS_ALL PH
                 WHERE PH.PO_HEADER_ID = PL.PO_HEADER_ID
                   AND PH.VENDOR_ID = COUL.ITEM_ID
                   AND PL.PO_LINE_ID = MLN.N_ATTRIBUTE6
                   AND COUL.PROPERTY_TYPE = 'V'
                   AND NVL(COUL.PROPERTY5_NAME, 'N') <> 'N'), 0, MLN.N_ATTRIBUTE6, NVL(COMR.PO_LINE_ID, MLN.N_ATTRIBUTE6)) PO_LINE_ID,
       CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.N_ATTRIBUTE6
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN MLN_SOURCE.N_ATTRIBUTE6
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.N_ATTRIBUTE6, COMR.PO_LINE_ID)
       END AS SOURCE_PO_LINE_ID,
       MLN.N_ATTRIBUTE5 PO_HEADER_ID,
       CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.N_ATTRIBUTE5
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN MLN_SOURCE.N_ATTRIBUTE5
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.N_ATTRIBUTE5, COMR.PO_HEADER_ID)
       END AS SOURCE_PO_HEADER_ID,
       CASE
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 = MLN.N_ATTRIBUTE7 THEN MLN.C_ATTRIBUTE1
           WHEN COMR.ORG_ID = COMR.INTERCOMPANY_ORG_ID
                AND MLN.N_ATTRIBUTE1 <> MLN.N_ATTRIBUTE7 THEN MLN_SOURCE.C_ATTRIBUTE1
           ELSE DECODE(COM1.MEANING, COM2.MEANING, MLN.C_ATTRIBUTE1, '')
       END AS PO_NUMBER,
       COMR.ORG_ID,
       COMR.INTERCOMPANY_ORG_ID,
       CAVL.ITEM_ID,
       CAVL.LOT_NUMBER,
       :B1,
       CAVH.VAT_INVOICE_CODE,
       CAVH.VAT_NUMBER
FROM CUX_AR_VAT_HEADERS CAVH,
     CUX_AR_VAT_LINES CAVL,
     CUX_OE_MTL_RESERVATIONS COMR,
     CUX_OE_ORDER_HEADERS COOH,
     MTL_LOT_NUMBERS MLN,
     CUX_MST_ITEM ISI,
     CUX_FND_LOOKUP_VALUES SCP,

  (SELECT ITEM_ID,
          PROPERTY7_NAME,
          OU_ID
   FROM CUX_OU_PROPERTY_LINES_T
   WHERE PROPERTY_TYPE = 'C'
     AND NVL(PROPERTY7_NAME, 'N') <> 'N') COPL,

  (SELECT T.TRANSACTION_TYPE_ID
   FROM OE_TRANSACTION_TYPES_ALL TT,
        OE_TRANSACTION_TYPES_TL T,
        FND_DOCUMENT_SEQUENCES A
   WHERE TT.TRANSACTION_TYPE_ID = T.TRANSACTION_TYPE_ID
     AND T.LANGUAGE = USERENV('LANG')
     AND TT.ATTRIBUTE1 = A.DOC_SEQUENCE_ID
     AND TT.TRANSACTION_TYPE_CODE = 'ORDER'
     AND TT.ATTRIBUTE15 <> '01') TT_TYPE,

  (SELECT HOU.ORGANIZATION_ID,
          FLV.MEANING
   FROM HR_ORGANIZATION_UNITS_V HOU,
        CUX_FND_LOOKUP_VALUES FLV
   WHERE HOU.ATTRIBUTE6 = FLV.LOOKUP_CODE
     AND FLV.LOOKUP_TYPE = 'MST-0031'
     AND HOU.ATTRIBUTE6 IS NOT NULL) COM1,

  (SELECT HOU.ORGANIZATION_ID,
          FLV.MEANING
   FROM HR_ORGANIZATION_UNITS_V HOU,
        CUX_FND_LOOKUP_VALUES FLV
   WHERE HOU.ATTRIBUTE6 = FLV.LOOKUP_CODE
     AND FLV.LOOKUP_TYPE = 'MST-0031'
     AND HOU.ATTRIBUTE6 IS NOT NULL) COM2,

  (SELECT COMR_SOURCE.RELATE_PO_LINE_ID,
          MLN_SOURCE.N_ATTRIBUTE6,
          MLN_SOURCE.N_ATTRIBUTE5,
          MLN_SOURCE.C_ATTRIBUTE1
   FROM MTL_LOT_NUMBERS MLN_SOURCE,
        CUX_OE_MTL_RESERVATIONS COMR_SOURCE
   WHERE MLN_SOURCE.LOT_NUMBER = COMR_SOURCE.LOT_NUMBER
     AND MLN_SOURCE.INVENTORY_ITEM_ID = COMR_SOURCE.INVENTORY_ITEM_ID
     AND MLN_SOURCE.ORGANIZATION_ID = COMR_SOURCE.ORGANIZATION_ID) MLN_SOURCE
WHERE CAVH.VAT_HEADER_ID = CAVL.VAT_HEADER_ID
  AND CAVH.VAT_HEADER_ID = COMR.VAT_HEADER_ID
  AND COMR.HEADER_ID = COOH.HEADER_ID
  AND COMR.SOURCE_LINE_ID = CAVL.ORDER_LINE_ID
  AND COMR.INVENTORY_ITEM_ID = MLN.INVENTORY_ITEM_ID
  AND COMR.ORGANIZATION_ID = MLN.ORGANIZATION_ID
  AND COMR.LOT_NUMBER = MLN.LOT_NUMBER
  AND COMR.INVENTORY_ITEM_ID = ISI.INVENTORY_ITEM_ID
  AND COMR.ORGANIZATION_ID = ISI.ORGANIZATION_ID
  AND ISI.OPERATE_SCOPE = SCP.LOOKUP_CODE
  AND SCP.LOOKUP_TYPE = 'MST-0004'
  AND SCP.SEGMENT4 = 'Y'
  AND COOH.ORDER_TYPE_ID = TT_TYPE.TRANSACTION_TYPE_ID
  AND NVL(COOH.ORDER_SOURCE_REFERENCE, 'N') NOT IN ('QUICK_ORDER',
                                                    'QUICK_RET')
  AND CAVH.CUSTOMER_ID = COPL.ITEM_ID
  AND CAVH.ORG_ID = COPL.OU_ID
  AND COMR.ORG_ID = COM1.ORGANIZATION_ID
  AND COMR.INTERCOMPANY_ORG_ID = COM2.ORGANIZATION_ID
  AND MLN.N_ATTRIBUTE6 = MLN_SOURCE.RELATE_PO_LINE_ID(+)
  AND CAVH.ENABLED_FLAG = 'Y'
  AND UPPER(CAVH.VAT_NUMBER) NOT LIKE 'X%'
  AND UPPER(CAVH.VAT_NUMBER) NOT LIKE 'D%'
  AND CAVH.VAT_NUMBER IS NOT NULL
  AND CAVH.AMOUNT_INC_TAX > 0
  AND CAVL.AMOUNT_INC_TAX > 0
  AND MLN.C_ATTRIBUTE4 NOT IN
    (SELECT MEANING
     FROM CUX_FND_LOOKUP_VALUES
     WHERE LOOKUP_TYPE = 'AP-013'
       AND TAG = 'SUP'
       AND SEGMENT1 = 'Y'
       AND ENABLED_FLAG = 'Y')
  AND TRUNC(CAVH.LAST_UPDATE_DATE) >= TRUNC(SYSDATE - 1)
  AND NOT EXISTS
    (SELECT 1
     FROM CUX_TWOVOTE_SALEINVOICE_NEW
     WHERE VAT_HEADER_ID = CAVH.VAT_HEADER_ID
       AND TRUNC(IMPORT_DATE) >= TRUNC(SYSDATE - 1))

优化后执行计划如下

执行计划20251207.png

优化前后,对比平均执行时间。573s → 98s,提升近6倍

执行计划20251126优化前.png
执行计划20251207优化后.png

总结
#

在SQL优化中经常会碰到一些复杂的、而且业务逻辑无法理解的SQL。不要急,通过执行计划一部分一部分的来优化。把全表扫描、标量子查询和驱动表的关系等搞清楚。多尝试改写的方法、尽量过滤查询数据。这样再复杂的SQL也有办法进行优化。

根因:相关标量子查询在大结果集上被重复执行,反复访问大表(4551w/1185w),放大 IO/CPU。

关键动作:把 3 处重复标量子查询抽成可复用派生表并 LEFT JOIN,减少重复扫描。

配套动作:为全表扫描热点补索引并重新收集统计信息(含 VAT_HEADER_IDPROPERTY_TYPE)。

效果:平均耗时 573s → 98s(≈6x)。