【问题标题】:Avoid full table scan with SELECT using PL/SQL table使用 PL/SQL 表使用 SELECT 避免全表扫描
【发布时间】:2021-05-20 11:28:23
【问题描述】:

测试数据

CREATE TABLE parent AS ( SELECT ROWNUM AS id, 'XXX' AS dummy FROM dual CONNECT BY ROWNUM <= 1000 );
CREATE UNIQUE INDEX idx_parent ON parent(id);

CREATE TABLE child AS ( SELECT CEIL(ROWNUM/5) AS id, 'XXX' AS dummy FROM dual CONNECT BY ROWNUM <= 5000 );
CREATE INDEX idx_child ON child(id);

EXEC dbms_stats.gather_table_stats(USER, 'parent');
EXEC dbms_stats.gather_table_stats(USER, 'child');

问题

即使考虑了 CARDINALITY 提示,以下查询也会对子级进行全表扫描(包括 12.1 和 19.0)。
当然,真正的查询需要来自子级的一些额外数据。

SELECT child.id
FROM parent
JOIN
(
    SELECT child.id
    FROM child
    GROUP BY child.id
) child ON ( child.id = parent.id )
WHERE parent.id IN ( SELECT /*+ CARDINALITY( tab 1 ) */ COLUMN_VALUE FROM TABLE (sys.odcinumberlist(1) ) tab );
-----------------------------------------------------------------------------------------------------
| Id  | Operation                              | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                       |            |     1 |    19 |    35   (3)| 00:00:01 |
|*  1 |  HASH JOIN RIGHT SEMI                  |            |     1 |    19 |    35   (3)| 00:00:01 |
|   2 |   COLLECTION ITERATOR CONSTRUCTOR FETCH|            |     1 |     2 |    29   (0)| 00:00:01 |
|   3 |   NESTED LOOPS                         |            |  1000 | 17000 |     6  (17)| 00:00:01 |
|   4 |    VIEW                                |            |  1000 | 13000 |     6  (17)| 00:00:01 |
|   5 |     HASH GROUP BY                      |            |  1000 |  4000 |     6  (17)| 00:00:01 |
|   6 |      TABLE ACCESS FULL                 | CHILD      |  5000 | 20000 |     5   (0)| 00:00:01 |
|*  7 |    INDEX UNIQUE SCAN                   | IDX_PARENT |     1 |     4 |     0   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------

如果我将 WHERE 子句替换为以下内容,则两个索引都会按预期使用:

WHERE parent.id IN ( 1 );
----------------------------------------------------------------------------------
| Id  | Operation           | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |            |     5 |    35 |     2   (0)| 00:00:01 |
|   1 |  NESTED LOOPS       |            |     5 |    35 |     2   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN | IDX_PARENT |     1 |     4 |     1   (0)| 00:00:01 |
|   3 |   VIEW              |            |     5 |    15 |     1   (0)| 00:00:01 |
|   4 |    SORT GROUP BY    |            |     5 |    20 |     1   (0)| 00:00:01 |
|*  5 |     INDEX RANGE SCAN| IDX_CHILD  |     5 |    20 |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------

当我删除 GROUP BY 时它也可以工作。


知道如何解决这个问题吗?

【问题讨论】:

    标签: sql oracle oracle12c oracle19c


    【解决方案1】:

    问题在于 ID 列可以包含 NULL 值。如果将列定义为 NOT NULL,则使用索引。

    索引不包含 NULL 值。但是 GROUP BY 必须包含这些数据。

    当您在示例中将 parent.id 限制为 1 时,数据库可以使用索引作为具体值。

    【讨论】:

    • 感谢您的意见!我试过了,但没有任何区别。 IN( NULL ) 永远不会返回行,即使我有一些行。因为我使用= 加入,所以无论如何都会忽略NULL 值。优化器也足够聪明,可以在父节点上获取索引,如果它期望 NULL 值会产生影响(例如,当使用 LEFT JOINIS NULL 而不是 IN 时)
    • 刚刚在 19c 中尝试过,我看到了不同之处:在 ID 不可为空的情况下,我对 idx_child 进行索引扫描,而不是对子项进行表扫描;仍在努力更好地理解它
    • @Aleksej:你是对的!没有注意到差异,因为它仍然是 INDEX FAST FULL SCAN 而不是预期的 INDEX RANGE SCAN
    • @PeterLang:完全一样的行为......并且仍在尝试完全理解它
    • 执行索引快速全扫描,因为所有值都可以从索引中获取。只要例如虚拟列被添加到查询中,再次执行全表扫描。
    【解决方案2】:

    您可以使用MERGE hint 获得所需的行为

    SELECT child.id
    FROM parent
    JOIN
    (
        SELECT /*+ MERGE */ child.id  ---<<<<< merge the subquery
        FROM child
        GROUP BY child.id
    ) child ON ( child.id = parent.id )
    WHERE parent.id IN ( SELECT /*+ CARDINALITY( tab 1 ) */ COLUMN_VALUE FROM TABLE (sys.odcinumberlist(1) ) tab );
    

    执行计划如下

    --------------------------------------------------------------------------------------------------------
    | Id  | Operation                                 | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
    --------------------------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT                          |            |     5 |    70 |    32   (7)| 00:00:01 |
    |   1 |  HASH GROUP BY                            |            |     5 |    70 |    32   (7)| 00:00:01 |
    |   2 |   NESTED LOOPS                            |            |     5 |    70 |    31   (4)| 00:00:01 |
    |   3 |    NESTED LOOPS                           |            |     1 |    10 |    30   (4)| 00:00:01 |
    |   4 |     SORT UNIQUE                           |            |     1 |     2 |    29   (0)| 00:00:01 |
    |   5 |      COLLECTION ITERATOR CONSTRUCTOR FETCH|            |     1 |     2 |    29   (0)| 00:00:01 |
    |*  6 |     INDEX UNIQUE SCAN                     | IDX_PARENT |     1 |     8 |     0   (0)| 00:00:01 |
    |*  7 |    INDEX RANGE SCAN                       | IDX_CHILD  |     5 |    20 |     1   (0)| 00:00:01 |
    --------------------------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
     
       6 - access("PARENT"."ID"=VALUE(KOKBF$))
       7 - access("CHILD"."ID"="PARENT"."ID")
    

    我猜你的子表太小了,CBO 不认为这个计划是最好的;但也可能有其他原因。

    补充说明

    谓词之间有相当大的区别

    parent.id IN ( subquery )   and
    
    parent.id IN ( 1 )
    

    在后一种情况下,Oracle simple 可以在group by 子查询中推送谓词 (access("CHILD"."ID"=1))。 (见提示PUSH_PRED)。

    但无论如何,如果您 1) 知道子查询只返回一行并且 2) 您对谓词有所帮助,Oracle CBO 会正确处理 没有提示

    这里根据 1) 和 2) 稍微改变了查询 - 请参阅 cmets

    SELECT child.id
    FROM parent
    JOIN
    (
        SELECT child.id
        FROM child
        GROUP BY child.id
    ) child ON ( child.id = parent.id )
    WHERE child.id  /* 2) match with *child.id* to help Oracle to unnest */
     =  /* 1) use equal predicate as there is ony one row */
    ( SELECT /*+ CARDINALITY( tab 1 ) */ COLUMN_VALUE FROM TABLE (sys.odcinumberlist(1) ) tab );
    

    计划

    --------------------------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT                          |            |     5 |    85 |     3   (0)| 00:00:01 |
    |   1 |  NESTED LOOPS                             |            |     5 |    85 |     3   (0)| 00:00:01 |
    |   2 |   VIEW                                    |            |     5 |    65 |     3   (0)| 00:00:01 |
    |   3 |    HASH GROUP BY                          |            |     5 |    25 |     3   (0)| 00:00:01 |
    |*  4 |     INDEX RANGE SCAN                      | IDX_CHILD  |     5 |    25 |     3   (0)| 00:00:01 |
    |   5 |      COLLECTION ITERATOR CONSTRUCTOR FETCH|            |     1 |     2 |    29   (0)| 00:00:01 |
    |*  6 |   INDEX UNIQUE SCAN                       | IDX_PARENT |     1 |     4 |     0   (0)| 00:00:01 |
    --------------------------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
     
       4 - access("CHILD"."ID"= (SELECT /*+ OPT_ESTIMATE (TABLE "TAB"@"SEL$4" ROWS=1.000000 ) */ 
                  VALUE(KOKBF$) FROM TABLE() "KOKBF$0"))
       6 - access("CHILD"."ID"="PARENT"."ID")
    

    【讨论】:

    • 虽然这对我的原始查询(更复杂)没有帮助,但它对我的问题中的查询非常有用。我的原始表包含更多数据,并且类似的查询从成本 96,000 变为成本 58 与该提示。看起来优化器没有评估该选项。
    • 正如您在对另一个答案的评论中所说的那样,我也想知道为什么在使用真实表格时没有问题。好的,谢谢!
    猜你喜欢
    • 1970-01-01
    • 2011-05-19
    • 2019-07-17
    • 2015-12-04
    • 2014-02-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多