2015 年,一位甲骨文公司的朋友请求协助优化。有个 SQL 单次执行需要 26.57 秒,一共要执行 226 次,如图 9-12 所示。

    烂用外连接导致无法谓词推入 - 图1

    图 9-12

    SQL 代码如下。

    1. SELECT view_xj_ct.ybjshj FROM view_xj_ct
    2. WHERE view_xj_ct.ct_code = :1 AND view_xj_ct.pk_corp = :2

    view_xj_ct 是一个视图,视图定义如下。

    1. CREATE OR REPLACE FORCE VIEW "JXNC"."VIEW_XJ_CT" ("CT_CODE", "PK_CT_MANAGE", "YBJSHJ", "FKHJ", "KPJE", "JE", "PK_CORP") AS
    2. select a."CT_CODE",
    3. a."PK_CT_MANAGE",
    4. a."YBJSHJ",
    5. a."FKHJ",
    6. b.kpje,
    7. (case
    8. when b.kpje >= a.ybjshj then
    9. b.kpje
    10. else
    11. a.ybjshj
    12. end) je,
    13. pk_corp
    14. from (select cth.ct_code,
    15. cth.pk_ct_manage,
    16. sum(ctb.oritaxsummny) ybjshj,
    17. sum(ctv.ljfk) fkhj,
    18. ctb.pk_corp
    19. from ct_manage_b ctb
    20. left join ct_manage cth
    21. on ctb.pk_ct_manage = cth.pk_ct_manage
    22. left join view_xj_ct_fukuan ctv
    23. on ctv.pk_ct_manage_b = ctb.pk_ct_manage_b
    24. and ctv.pk_ct_manage = cth.pk_ct_manage
    25. where activeflag = 0
    26. and cth.dr = 0
    27. and ctb.dr = 0
    28. group by cth.ct_code, cth.pk_ct_manage, ctb.pk_corp) a
    29. left join (select cth.pk_ct_manage, sum(fp.noriginalsummny) kpje
    30. from po_invoice_b fp
    31. left join po_order_b dd
    32. on fp.csourcebillrowid = dd.corder_bid
    33. left join ct_manage_b ct
    34. on ct.pk_ct_manage_b = dd.csourcerowid
    35. left join ct_manage cth
    36. on ct.pk_ct_manage = cth.pk_ct_manage
    37. where fp.dr = 0
    38. and dd.cupsourcebilltype = 'Z2'
    39. group by cth.pk_ct_manage) b
    40. on b.pk_ct_manage = a.pk_ct_manage;

    代码中:表 ct_manage_b 有数据 266 274(26 万条记录),表 ct_manage 有数据 88 563(8.8 万条记录),表 po_invoice_b 有数据 294 467(29 万条记录),表 po_order_b 有数据 143122(14 万条记录)。

    上面视图 view_xj_ct 中又内嵌一个视图 view_xj_ct_fukuan,视图代码如下。

    1. CREATE OR REPLACE FORCE VIEW "JXNC"."VIEW_XJ_CT_FUKUAN" ("DDHH", "PK_CORP", "PK_CT_MANAGE_B", "PK_CT_MANAGE", "LJFK", "CT_CODE") AS
    2. select ddhh,
    3. a.pk_corp,
    4. a.pk_ct_manage_b,
    5. ctb.pk_ct_manage,
    6. sum(a.ljfk) ljfk,
    7. cth.ct_code
    8. from (select a.ddhh,
    9. a.dwbm pk_corp,
    10. a.zyx5 pk_ct_manage_b,
    11. a.jfybje ljfk
    12. from arap_djfb a
    13. left join arap_djzb b on a.vouchid = b.vouchid
    14. where a.dr = 0
    15. and b.dr = 0
    16. and a.djlxbm = 'D3'
    17. and a.jsfsbm in ('Z2', 'Z5','D1')
    18. and b.djzt not in ('-99', '1')) a
    19. left join ct_manage_b ctb on ctb.pk_ct_manage_b = a.pk_ct_manage_b
    20. left join ct_manage cth on cth.pk_ct_manage = ctb.pk_ct_manage
    21. group by ddhh, a.pk_corp, a.pk_ct_manage_b, ctb.pk_ct_manage, cth.ct_code
    22. order by a.pk_ct_manage_b;

    其中:表 arap_djfb 有数据 1 175 707(117 万条记录),表 arap_djzb 有数据 149 157(15 万条记录),表 ct_manage_b 有数据 266 274(26 万条记录),表 ct_manage 有数据 88 563(8.8 万条记录)。

    SQL 语句的执行计划如下。

    1. explain plan for SELECT view_xj_ct.ybjshj FROM view_xj_ct
    2. 2 WHERE view_xj_ct.ct_code = :1 AND view_xj_ct.pk_corp = :2;
    1. Explained.
    1. select * from table(dbms_xplan.display);
    1. PLAN_TABLE_OUTPUT
    2. -------------------------------------------------------------------------------------
    3. Plan hash value: 3563589558
    4. -------------------------------------------------------------------------------------
    5. | Id|Operation |Name |Rows |Bytes|TempSpc|Cost(%CPU)|Time |
    6. -------------------------------------------------------------------------------------
    7. | 0|SELECT STATEMENT | | 1| 57| |49994 (1)|00:10:00|
    8. |* 1| HASH JOIN OUTER | | 1| 57| |49994 (1)|00:10:00|
    9. | 2| VIEW | | 1| 35| |32190 (1)|00:06:27|
    10. | 3| HASH GROUP BY | | 1| 74| |32190 (1)|00:06:27|
    11. |* 4| HASH JOIN OUTER | | 1| 74| |32189 (1)|00:06:27|
    12. | 5| VIEW | | 1| 35| | 2 (0)|00:00:01|
    13. | 6| NESTED LOOPS | | | | | | |
    14. | 7| NESTED LOOPS | | 1| 95| | 2 (0)|00:00:01|
    15. |* 8| TABLE ACCESS BY INDEX ROWID |CT_MANAGE | 1| 40| | 1 (0)|00:00:01|
    16. |* 9| INDEX RANGE SCAN |I_CT_M_1 | 2| | | 1 (0)|00:00:01|
    17. |*10| INDEX RANGE SCAN |I_CT_M_B_1 | 3| | | 1 (0)|00:00:01|
    18. |*11| TABLE ACCESS BY INDEX ROWID |CT_MANAGE_B | 1| 55| | 1 (0)|00:00:01|
    19. | 12| VIEW |VIEW_XJ_CT_FUKUAN |39191|1492K| |32186 (1)|00:06:27|
    20. | 13| HASH GROUP BY | |39191|6468K| 6976K|32186 (1)|00:06:27|
    21. |*14| HASH JOIN RIGHT OUTER | |39191|6468K| 3976K|30726 (1)|00:06:09|
    22. | 15| TABLE ACCESS FULL |CT_MANAGE |88505|2938K| | 1621 (2)|00:00:20|
    23. |*16| HASH JOIN OUTER | |39191|5166K| 4024K|28636 (1)|00:05:44|
    24. |*17| HASH JOIN | |39191|3559K| 2952K|23574 (1)|00:04:43|
    25. | 18| INLIST ITERATOR | | | | | | |
    26. |*19| TABLE ACCESS BY INDEX ROWID|ARAP_DJFB |39191|2487K| |20692 (1)|00:04:09|
    27. |*20| INDEX RANGE SCAN |I_ARAP_DJFB_JSZC02| 337K| | | 251 (2)|00:00:04|
    28. |*21| TABLE ACCESS FULL |ARAP_DJZB | 127K|3476K| | 2494 (2)|00:00:30|
    29. | 22| TABLE ACCESS FULL |CT_MANAGE_B | 266K| 10M| | 4179 (2)|00:00:51|
    30. | 23| VIEW | |88480|1900K| |17802 (1)|00:03:34|
    31. | 24| HASH GROUP BY | |88480| 10M| 16M|17802 (1)|00:03:34|
    32. |*25| HASH JOIN | | 120K| 14M| 5024K|14906 (1)|00:02:59|
    33. |*26| TABLE ACCESS FULL |PO_INVOICE_B | 138K|3389K| | 5263 (1)|00:01:04|
    34. |*27| HASH JOIN RIGHT OUTER | |98165| 9M| 2856K| 8850 (2)|00:01:47|
    35. | 28| INDEX FAST FULL SCAN |PK_CT_MANAGE |88505|1815K| | 107 (2)|00:00:02|
    36. |*29| HASH JOIN OUTER | |98165|8052K| 5184K| 8154 (2)|00:01:38|
    37. |*30| TABLE ACCESS FULL |PO_ORDER_B |98165|4026K| | 3035 (1)|00:00:37|
    38. | 31| TABLE ACCESS FULL |CT_MANAGE_B | 266K| 10M| | 4179 (2)|00:00:51|
    39. -------------------------------------------------------------------------------------
    40. Predicate Information (identified by operation id):
    41. ---------------------------------------------------
    42. 1 - access("B"."PK_CT_MANAGE"(+)="A"."PK_CT_MANAGE")
    43. 4 - access("CTV"."PK_CT_MANAGE"(+)="CTH"."PK_CT_MANAGE" AND
    44. "CTV"."PK_CT_MANAGE_B"(+)="CTB"."PK_CT_MANAGE_B")
    45. 8 - filter("CTH"."DR"=0 AND "CTH"."ACTIVEFLAG"=0)
    46. 9 - access("CTH"."CT_CODE"=:1)
    47. 10 - access("CTB"."PK_CT_MANAGE"="CTH"."PK_CT_MANAGE")
    48. 11 - filter("CTB"."PK_CORP"=:2 AND "CTB"."DR"=0)
    49. 14 - access("CTH"."PK_CT_MANAGE"(+)="CTB"."PK_CT_MANAGE")
    50. 16 - access("CTB"."PK_CT_MANAGE_B"(+)="A"."ZYX5")
    51. 17 - access("A"."VOUCHID"="B"."VOUCHID")
    52. 19 - filter("A"."DJLXBM"='D3' AND "A"."DR"=0)
    53. 20 - access("A"."JSFSBM"='D1' OR "A"."JSFSBM"='Z2' OR "A"."JSFSBM"='Z5')
    54. 21 - filter("B"."DR"=0 AND "B"."DJZT"<>1 AND "B"."DJZT"<>(-99))
    55. 25 - access("FP"."CSOURCEBILLROWID"="DD"."CORDER_BID")
    56. 26 - filter("FP"."CSOURCEBILLROWID" IS NOT NULL AND "FP"."DR"=0)
    57. 27 - access("CT"."PK_CT_MANAGE"="CTH"."PK_CT_MANAGE"(+))
    58. 29 - access("CT"."PK_CT_MANAGE_B"(+)="DD"."CSOURCEROWID")
    59. 30 - filter("DD"."CUPSOURCEBILLTYPE"='Z2')
    60. 60 rows selected.

    对于上述的执行计划,甲骨文公司的朋友创建了一个 index。

    1. create index idx_jszc1026 on ARAP_djfb(jsfsbm,djlxbm,dr);

    之前大约 26 秒出结果,创建新 index 后速度是 2.6 秒出结果,新建索引后的执行计划如下。

    1. PLAN_TABLE_OUTPUT
    2. -------------------------------------------------------------------------------------
    3. Plan hash value: 2820245905
    4. -------------------------------------------------------------------------------------
    5. | Id|Operation |Name |Rows |Bytes|TempSpc|Cost(%CPU)|Time |
    6. -------------------------------------------------------------------------------------
    7. | 0|SELECT STATEMENT | | 1| 57| |32043 (1)|00:06:25|
    8. |* 1| HASH JOIN OUTER | | 1| 57| |32043 (1)|00:06:25|
    9. | 2| VIEW | | 1| 35| |14239 (2)|00:02:51|
    10. | 3| HASH GROUP BY | | 1| 74| |14239 (2)|00:02:51|
    11. |* 4| HASH JOIN OUTER | | 1| 74| |14238 (2)|00:02:51|
    12. | 5| VIEW | | 1| 35| | 2 (0)|00:00:01|
    13. | 6| NESTED LOOPS | | | | | | |
    14. | 7| NESTED LOOPS | | 1| 95| | 2 (0)|00:00:01|
    15. |* 8| TABLE ACCESS BY INDEX ROWID |CT_MANAGE | 1| 40| | 1 (0)|00:00:01|
    16. |* 9| INDEX RANGE SCAN |I_CT_M_1 | 2| | | 1 (0)|00:00:01|
    17. |*10| INDEX RANGE SCAN |I_CT_M_B_1 | 3| | | 1 (0)|00:00:01|
    18. |*11| TABLE ACCESS BY INDEX ROWID |CT_MANAGE_B | 1| 55| | 1 (0)|00:00:01|
    19. | 12| VIEW |VIEW_XJ_CT_FUKUAN|39191|1492K| |14234 (2)|00:02:51|
    20. | 13| HASH GROUP BY | |39191|6468K| 6976K|14234 (2)|00:02:51|
    21. |*14| HASH JOIN RIGHT OUTER | |39191|6468K| 3976K|12775 (2)|00:02:34|
    22. | 15| TABLE ACCESS FULL |CT_MANAGE |88505|2938K| | 1621 (2)|00:00:20|
    23. |*16| HASH JOIN OUTER | |39191|5166K| 4024K|10685 (2)|00:02:09|
    24. |*17| HASH JOIN | |39191|3559K| 2952K| 5622 (1)|00:01:08|
    25. | 18| INLIST ITERATOR | | | | | | |
    26. | 19| TABLE ACCESS BY INDEX ROWID|ARAP_DJFB |39191|2487K| | 2740 (1)|00:00:33|
    27. |*20| INDEX RANGE SCAN |IDX_JSZC1026 |39212| | | 43 (3)|00:00:01|
    28. |*21| TABLE ACCESS FULL |ARAP_DJZB | 127K|3476K| | 2494 (2)|00:00:30|
    29. | 22| TABLE ACCESS FULL |CT_MANAGE_B | 266K| 10M| | 4179 (2)|00:00:51|
    30. | 23| VIEW | |88480|1900K| |17802 (1)|00:03:34|
    31. | 24| HASH GROUP BY | |88480| 10M| 16M|17802 (1)|00:03:34|
    32. |*25| HASH JOIN | | 120K| 14M| 5024K|14906 (1)|00:02:59|
    33. |*26| TABLE ACCESS FULL |PO_INVOICE_B | 138K|3389K| | 5263 (1)|00:01:04|
    34. |*27| HASH JOIN RIGHT OUTER | |98165| 9M| 2856K| 8850 (2)|00:01:47|
    35. | 28| INDEX FAST FULL SCAN |PK_CT_MANAGE |88505|1815K| | 107 (2)|00:00:02|
    36. |*29| HASH JOIN OUTER | |98165|8052K| 5184K| 8154 (2)|00:01:38|
    37. |*30| TABLE ACCESS FULL |PO_ORDER_B |98165|4026K| | 3035 (1)|00:00:37|
    38. | 31| TABLE ACCESS FULL |CT_MANAGE_B | 266K| 10M| | 4179 (2)|00:00:51|
    39. -------------------------------------------------------------------------------------
    40. Predicate Information (identified by operation id):
    41. ---------------------------------------------------
    42. 1 - access("B"."PK_CT_MANAGE"(+)="A"."PK_CT_MANAGE")
    43. 4 - access("CTV"."PK_CT_MANAGE"(+)="CTH"."PK_CT_MANAGE" AND
    44. "CTV"."PK_CT_MANAGE_B"(+)="CTB"."PK_CT_MANAGE_B")
    45. 8 - filter("CTH"."DR"=0 AND "CTH"."ACTIVEFLAG"=0)
    46. 9 - access("CTH"."CT_CODE"=:1)
    47. 10 - access("CTB"."PK_CT_MANAGE"="CTH"."PK_CT_MANAGE")
    48. 11 - filter("CTB"."PK_CORP"=:2 AND "CTB"."DR"=0)
    49. 14 - access("CTH"."PK_CT_MANAGE"(+)="CTB"."PK_CT_MANAGE")
    50. 16 - access("CTB"."PK_CT_MANAGE_B"(+)="A"."ZYX5")
    51. 17 - access("A"."VOUCHID"="B"."VOUCHID")
    52. 20 - access(("A"."JSFSBM"='D1' OR "A"."JSFSBM"='Z2' OR "A"."JSFSBM"='Z5') AND "A"."DJLXBM"='D3' AND
    53. "A"."DR"=0)
    54. 21 - filter("B"."DR"=0 AND "B"."DJZT"<>1 AND "B"."DJZT"<>(-99))
    55. 25 - access("FP"."CSOURCEBILLROWID"="DD"."CORDER_BID")
    56. 26 - filter("FP"."CSOURCEBILLROWID" IS NOT NULL AND "FP"."DR"=0)
    57. 27 - access("CT"."PK_CT_MANAGE"="CTH"."PK_CT_MANAGE"(+))
    58. 29 - access("CT"."PK_CT_MANAGE_B"(+)="DD"."CSOURCEROWID")
    59. 30 - filter("DD"."CUPSOURCEBILLTYPE"='Z2')
    60. 60 rows selected.

    如图 9-13 所示,做一笔单据在后台要多次调用这个语句。

    烂用外连接导致无法谓词推入 - 图2

    图 9-13

    100 个 SQL 语句每个执行 2.6 秒,全部执行就要 260 秒,将近 4 分钟。到这里,甲骨文的朋友问能否进一步优化该 SQL。

    下面是分析过程。

    在尝试优化 SQL 之前,首先询问该 SQL 返回多少行数据,甲骨文的朋友回答返回 1 行数据。在进行 SQL 优化的时候,我们必须知道一个 SQL 最终应该返回多少行数据,因为知道了 SQL 最终返回数据,就能判断表连接究竟是采用嵌套循环还是采用 HASH 连接,这至关重要。因为 SQL 最终返回一行数据,所以判断 SQL 的执行计划应该走嵌套循环。但是本 SQL 执行计划中几乎全是 HASH 连接。根据 SQL 语句过滤条件入手,一步一步分析执行计划,看哪里出了问题。

    SQL 语句的过滤条件是WHERE view_xj_ct.ct_code = :1 AND view_xj_ct.pk_corp = :2

    这两个过滤条件已经在书中用阴影部分标注,为了方便读者查看现将其摘抄下来。

    1. select cth.ct_code,
    2. cth.pk_ct_manage,
    3. sum(ctb.oritaxsummny) ybjshj,
    4. sum(ctv.ljfk) fkhj,
    5. ctb.pk_corp
    6. from ct_manage_b ctb
    7. left join ct_manage cth
    8. on ctb.pk_ct_manage = cth.pk_ct_manage
    9. left join view_xj_ct_fukuan ctv
    10. on ctv.pk_ct_manage_b = ctb.pk_ct_manage_b
    11. and ctv.pk_ct_manage = cth.pk_ct_manage
    12. where activeflag = 0
    13. and cth.dr = 0
    14. and ctb.dr = 0
    15. group by cth.ct_code, cth.pk_ct_manage, ctb.pk_corp

    过滤条件分别针对 cth 和 ctb 进行过滤,执行计划中 Id=9 走的是 cth.ct_code 的索引,这说明此处发生了常量谓词推入将过滤条件(常量过滤条件)推入到视图中进行了过滤。Id=9 属于 cth,它与 id=10(ctb)走的是嵌套循环。cth 与 ctb 关联的结果集在执行计划中是 Id=5 这步,Id=5 与 Id=12(view_xj_ct_fukuan)进行的是 HASH 连接。Id=12 是一个视图。因为该 SQL 最终只返回 1 行数据,应该全走嵌套循环才对,但是关联到视图 view_xj_ct_fukuan 的时候居然走的是 HASH 连接,所以笔者判断 Id=5 与 Id=12 关联方式出错。SQL 语句中,视图 view_xj_ct_fukuan 的别名是 ctv,ctv 分别与 cth 和 ctb 进行了关联。

    1. left join view_xj_ct_fukuan ctv
    2. on ctv.pk_ct_manage_b = ctb.pk_ct_manage_b
    3. and ctv.pk_ct_manage = cth.pk_ct_manage

    如果能让 cth 与 ctb 关联之后得到的结果集通过 ctv 的连接列传值给 ctv,通过连接列将数据将数据推入到视图中,这样就可以让视图走嵌套循环了,这种方式就是连接列谓词推入,但是执行计划并没有这样做。

    于是查看如下视图 view_xj_ct_fukuan 的源代码。

    1. CREATE OR REPLACE FORCE VIEW "JXNC"."VIEW_XJ_CT_FUKUAN" ("DDHH", "PK_CORP", "PK_CT_MANAGE_B", "PK_CT_MANAGE", "LJFK", "CT_CODE") AS
    2. select ddhh,
    3. a.pk_corp,
    4. a.pk_ct_manage_b,
    5. ctb.pk_ct_manage,
    6. sum(a.ljfk) ljfk,
    7. cth.ct_code
    8. from (select a.ddhh,
    9. a.dwbm pk_corp,
    10. a.zyx5 pk_ct_manage_b,
    11. a.jfybje ljfk
    12. from arap_djfb a
    13. left join arap_djzb b on a.vouchid = b.vouchid
    14. where a.dr = 0
    15. and b.dr = 0
    16. and a.djlxbm = 'D3'
    17. and a.jsfsbm in ('Z2', 'Z5','D1')
    18. and b.djzt not in ('-99', '1')) a
    19. left join ct_manage_b ctb on ctb.pk_ct_manage_b = a.pk_ct_manage_b
    20. left join ct_manage cth on cth.pk_ct_manage = ctb.pk_ct_manage
    21. group by ddhh, a.pk_corp, a.pk_ct_manage_b, ctb.pk_ct_manage, cth.ct_code
    22. order by a.pk_ct_manage_b;

    视图 ctv.pk_ct_manage 字段来自于 ctb,而 ctb 与 a 是外连接,而且 ctb 是从表,并不是主表。

    正是因为 ctb 是视图中外连接的从表,而且视图 ctv 也是外连接的从表,所以导致 cth 不能通过连接列 pk_ct_manage 将谓词推入到 ctv.pk_ct_manage 中,从而导致走了 HASH 连接。

    1. left join view_xj_ct_fukuan ctv
    2. on ctv.pk_ct_manage_b = ctb.pk_ct_manage_b
    3. and ctv.pk_ct_manage = cth.pk_ct_manage

    如果能将视图中的外连接改成内连接,就可以将谓词推入到 ctv 中,从而走嵌套循环。

    通过反复分析 SQL 写法,我们确认可以将视图中的外连接改写为内连接。于是新建了一个视图,专门用于本 SQL,将外连接改写为内连接,而且将后面的子查询也改成了内连接。最终 SQL 能在 0.01 秒内执行完毕,执行 100 个 SQL 也仅需耗时 1 秒,从而将原本要执行 4 分钟的单据业务优化到 1 秒。

    接下来,我们通过实验为大家模拟当时情况。

    1. create table emp_new as select * from emp;
    1. Table created.
    1. create index idx_ename on emp(ename);
    1. Index created.

    视图(e)里面表关联是外连接,而且视图(e)作为外连接从表,视图(e)连接列来自从表。

    1. select /*+ push_pred(e) */ *
    2. from emp_new a
    3. left join (select d.dname, e.ename, sum(e.sal) total_sal
    4. from dept d
    5. left join emp e on d.deptno = e.deptno
    6. group by dname, ename) e on a.ename = e.ename
    7. where empno = 7900;

    执行计划如下。

    1. select /*+ push_pred(e) */ *
    2. from emp_new a
    3. left join (select d.dname, e.ename, sum(e.sal) total_sal
    4. from dept d
    5. left join emp e on d.deptno = e.deptno
    6. group by dname, ename) e on a.ename = e.ename
    7. where empno = 7900;
    1. Execution Plan
    2. ----------------------------------------------------------
    3. Plan hash value: 3023292314
    4. -------------------------------------------------------------------------------------
    5. | Id|Operation |Name | Rows | Bytes | Cost (%CPU)| Time |
    6. -------------------------------------------------------------------------------------
    7. | 0|SELECT STATEMENT | | 1 | 116 | 10 (30)| 00:00:01 |
    8. |* 1| HASH JOIN OUTER | | 1 | 116 | 10 (30)| 00:00:01 |
    9. |* 2| TABLE ACCESS FULL |EMP_NEW| 1 | 87 | 2 (0)| 00:00:01 |
    10. | 3| VIEW | | 14 | 406 | 7 (29)| 00:00:01 |
    11. | 4| HASH GROUP BY | | 14 | 364 | 7 (29)| 00:00:01 |
    12. | 5| MERGE JOIN OUTER | | 14 | 364 | 6 (17)| 00:00:01 |
    13. | 6| TABLE ACCESS BY INDEX ROWID|DEPT | 4 | 52 | 2 (0)| 00:00:01 |
    14. | 7| INDEX FULL SCAN |PK_DEPT| 4 | | 1 (0)| 00:00:01 |
    15. |* 8| SORT JOIN | | 14 | 182 | 4 (25)| 00:00:01 |
    16. | 9| TABLE ACCESS FULL |EMP | 14 | 182 | 3 (0)| 00:00:01 |
    17. -------------------------------------------------------------------------------------
    18. Predicate Information (identified by operation id):
    19. ---------------------------------------------------
    20. 1 - access("A"."ENAME"="E"."ENAME"(+))
    21. 2 - filter("A"."EMPNO"=7900)
    22. 8 - access("D"."DEPTNO"="E"."DEPTNO"(+))
    23. filter("D"."DEPTNO"="E"."DEPTNO"(+))

    当视图里面表关联是外连接,而且视图与其他表关联作为外连接从表,视图连接列来自视图里面的从表,此时不能谓词推入。

    我们将视图里面表关联改成内连接。

    1. select /*+ push_pred(e) */ *
    2. from emp_new a
    3. left join (select d.dname, e.ename, sum(e.sal) total_sal
    4. from dept d
    5. join emp e on d.deptno = e.deptno
    6. group by dname, ename) e on a.ename = e.ename
    7. where empno = 7900;

    执行计划如下。

    1. select /*+ push_pred(e) */ *
    2. from emp_new a
    3. left join (select d.dname, e.ename, sum(e.sal) total_sal
    4. from dept d
    5. join emp e on d.deptno = e.deptno
    6. group by dname, ename) e on a.ename = e.ename
    7. where empno = 7900;
    1. Execution Plan
    2. ----------------------------------------------------------
    3. Plan hash value: 3258229530
    4. -------------------------------------------------------------------------------------
    5. | Id|Operation |Name |Rows | Bytes | Cost(%CPU)| Time |
    6. -------------------------------------------------------------------------------------
    7. | 0|SELECT STATEMENT | | 1| 111 | 6 (17)| 00:00:01 |
    8. | 1| NESTED LOOPS OUTER | | 1| 111 | 6 (17)| 00:00:01 |
    9. |* 2| TABLE ACCESS FULL |EMP_NEW | 1| 87 | 2 (0)| 00:00:01 |
    10. | 3| VIEW PUSHED PREDICATE | | 1| 24 | 4 (25)| 00:00:01 |
    11. | 4| SORT GROUP BY | | 1| 26 | 4 (25)| 00:00:01 |
    12. | 5| NESTED LOOPS | | | | | |
    13. | 6| NESTED LOOPS | | 1| 26 | 3 (0)| 00:00:01 |
    14. | 7| TABLE ACCESS BY INDEX ROWID|EMP | 1| 13 | 2 (0)| 00:00:01 |
    15. |* 8| INDEX RANGE SCAN |IDX_ENAME| 1| | 1 (0)| 00:00:01 |
    16. |* 9| INDEX UNIQUE SCAN |PK_DEPT | 1| | 0 (0)| 00:00:01 |
    17. | 10| TABLE ACCESS BY INDEX ROWID|DEPT | 1| 13 | 1 (0)| 00:00:01 |
    18. -------------------------------------------------------------------------------------
    19. Predicate Information (identified by operation id):
    20. ---------------------------------------------------
    21. 2 - filter("A"."EMPNO"=7900)
    22. 8 - access("E"."ENAME"="A"."ENAME")
    23. 9 - access("D"."DEPTNO"="E"."DEPTNO")

    将视图里面的外连接改成内连接之后,我们就可以将谓词推入到视图中了。

    如果不改视图中的外连接,将 SQL 语句中的外连接改成内连接也可以将谓词推入视图。

    1. select /*+ push_pred(e) */ *
    2. from emp_new a
    3. join (select d.dname, e.ename, sum(e.sal) total_sal
    4. from dept d
    5. left join emp e on d.deptno = e.deptno
    6. group by dname, ename) e on a.ename = e.ename
    7. where empno = 7900;

    执行计划如下。

    1. select /*+ push_pred(e) */ *
    2. from emp_new a
    3. join (select d.dname, e.ename, sum(e.sal) total_sal
    4. from dept d
    5. left join emp e on d.deptno = e.deptno
    6. group by dname, ename) e on a.ename = e.ename
    7. where empno = 7900;
    1. Execution Plan
    2. ----------------------------------------------------------
    3. Plan hash value: 3747089680
    4. -------------------------------------------------------------------------------------
    5. | Id|Operation |Name | Rows | Bytes | Cost(%CPU)| Time |
    6. -------------------------------------------------------------------------------------
    7. | 0|SELECT STATEMENT | | 1 | 125 | 5 (20)| 00:00:01 |
    8. | 1| HASH GROUP BY | | 1 | 125 | 5 (20)| 00:00:01 |
    9. | 2| NESTED LOOPS | | | | | |
    10. | 3| NESTED LOOPS | | 1 | 125 | 4 (0)| 00:00:01 |
    11. | 4| NESTED LOOPS | | 1 | 112 | 3 (0)| 00:00:01 |
    12. |* 5| TABLE ACCESS FULL |EMP_NEW | 1 | 99 | 2 (0)| 00:00:01 |
    13. | 6| TABLE ACCESS BY INDEX ROWID|EMP | 1 | 13 | 1 (0)| 00:00:01 |
    14. |* 7| INDEX RANGE SCAN |IDX_ENAME| 1 | | 0 (0)| 00:00:01 |
    15. |* 8| INDEX UNIQUE SCAN |PK_DEPT | 1 | | 0 (0)| 00:00:01 |
    16. | 9| TABLE ACCESS BY INDEX ROWID |DEPT | 1 | 13 | 1 (0)| 00:00:01 |
    17. -------------------------------------------------------------------------------------
    18. Predicate Information (identified by operation id):
    19. ---------------------------------------------------
    20. 5 - filter("A"."EMPNO"=7900)
    21. 7 - access("A"."ENAME"="E"."ENAME")
    22. 8 - access("D"."DEPTNO"="E"."DEPTNO")

    笔者当时究竟是怎么判断可以将 view_xj_ct_fukuan ctv 里面的视图改成内连接的呢?

    请大家注意观察原始 view_xj_ct 部分代码。

    1. select cth.ct_code,
    2. cth.pk_ct_manage,
    3. sum(ctb.oritaxsummny) ybjshj,
    4. sum(ctv.ljfk) fkhj,
    5. ctb.pk_corp
    6. from ct_manage_b ctb
    7. left join ct_manage cth
    8. on ctb.pk_ct_manage = cth.pk_ct_manage
    9. left join view_xj_ct_fukuan ctv
    10. on ctv.pk_ct_manage_b = ctb.pk_ct_manage_b
    11. and ctv.pk_ct_manage = cth.pk_ct_manage
    12. where activeflag = 0
    13. and cth.dr = 0
    14. and ctb.dr = 0
    15. group by cth.ct_code, cth.pk_ct_manage, ctb.pk_corp

    注意观察阴影部分连接条件,视图 ctv 中的连接列也是来自 cth 和 ctb。

    1. CREATE OR REPLACE FORCE VIEW "JXNC"."VIEW_XJ_CT_FUKUAN" ("DDHH", "PK_CORP", "PK_CT_MANAGE_B", "PK_CT_MANAGE", "LJFK", "CT_CODE") AS
    2. select ddhh,
    3. a.pk_corp,
    4. a.pk_ct_manage_b,
    5. ctb.pk_ct_manage,
    6. sum(a.ljfk) ljfk,
    7. cth.ct_code
    8. from (select a.ddhh,
    9. a.dwbm pk_corp,
    10. a.zyx5 pk_ct_manage_b,
    11. a.jfybje ljfk
    12. from arap_djfb a
    13. left join arap_djzb b on a.vouchid = b.vouchid
    14. where a.dr = 0
    15. and b.dr = 0
    16. and a.djlxbm = 'D3'
    17. and a.jsfsbm in ('Z2', 'Z5','D1')
    18. and b.djzt not in ('-99', '1')) a
    19. left join ct_manage_b ctb on ctb.pk_ct_manage_b = a.pk_ct_manage_b
    20. left join ct_manage cth on cth.pk_ct_manage = ctb.pk_ct_manage
    21. group by ddhh, a.pk_corp, a.pk_ct_manage_b, ctb.pk_ct_manage, cth.ct_code
    22. order by a.pk_ct_manage_b;

    同时视图 ctv 中有对连接列进行汇总,这其实相当于如下 SQL。

    1. select e.empno, sum(sum_sal)
    2. from emp e
    3. left join (select d.deptno, sum(sal) sum_sal
    4. from dept d
    5. left join emp e on d.deptno = e.deptno
    6. group by d.deptno) d on e.deptno = d.deptno
    7. group by empno;

    上面 SQL 可以安全地将 left join 改写为 inner join。

    1. select e.empno, sum(sum_sal)
    2. from emp e
    3. left join (select d.deptno, sum(sal) sum_sal
    4. from dept d
    5. join emp e on d.deptno = e.deptno
    6. group by d.deptno) d on e.deptno = d.deptno
    7. group by empno;

    同理,原始 SQL 中后面的子查询也能改写为 inner join。

    想要优化本案例中的 SQL,必须具备较强的 SQL 优化能力以及较强的 SQL 改写能力,这两种能力缺一不可。通过本案例,我们也要反思,为什么开发人员在 SQL 中一直写 left join?我们甚至怀疑是不是开发人员只会 left join,或者不管写什么 SQL,一直 left join,这太可怕了,由此可见,在系统上线之前,SQL 审核是多么重要!