【问题标题】:Why are queries faster when subqueries within the WITH clause are duplicated?当 WITH 子句中的子查询重复时,为什么查询会更快?
【发布时间】:2016-10-24 12:57:51
【问题描述】:

假设一个项目使用分区来构造其数据。这个概念纯粹是特定于业务的,与数据库分区无关。

假设业务逻辑是这样的:

  • 从 output_table 中删除 partition =
  • 插入到 output_table (select * from input_table where partition = )

请记住,一切都是这样的结构,让我们将问题复杂化(以解决实际问题)。

假设我有一个潜在的杀手查询(SELECT 查询),就时间而言:

insert into output_table (
  select * 
   from input_table
   left outer join additional_table additional_table1  
     on input_table.id = additional_table1.id
   left outer join additional_table additional_table2  
    on additional_table2.id = additional_table1.parent
  where partition =  <partitionX>
)

让我们对此进行优化并探索选项。 请记住,每个表都有分区。还要注意 table2 如何连接两次,但在不同的列上。而且,还要注意附加表是如何连接到自身上的

一切都使用 WITH 子句,但有几个选项,我想知道为什么其中一个更好。

A. WITH 部分中的直接和重复查询

WITH 
CACHED_input_table AS (
  SELECT *
  FROM input_table
  WHERE PARTITION_ID = < partition  X >
),
CACHED_additional_table1 AS (
  SELECT *
  FROM additional_table 
  WHERE PARTITION_ID = < partition  X >
),
CACHED_additional_table2 AS (
  SELECT *
  FROM additional_table 
  WHERE PARTITION_ID = < partition  X >
)
SELECT *    
FROM CACHED_input_table input_table
  LEFT OUTER JOIN CACHED_additional_table1 additional_table1 
    ON input_table.ID = additional_table1.ID
  LEFT OUTER JOIN CACHED_additional_table2 additional_table2 
    ON additional_table1.PARENT_ID = additional_table2.ID

B.在 FROM 部分重复使用查询

WITH 
CACHED_input_table AS (
  SELECT *
  FROM input_table
  WHERE PARTITION_ID = < partition  X >
),
CACHED_additional_table AS (
  SELECT *
  FROM additional_table 
  WHERE PARTITION_ID = < partition  X >
)
SELECT *    
FROM CACHED_input_table input_table
  LEFT OUTER JOIN CACHED_additional_table additional_table1 
    ON input_table.ID = additional_table1.ID
  LEFT OUTER JOIN CACHED_additional_table additional_table2 
    ON additional_table1.PARENT_ID = additional_table2.ID

C.在 WITH 部分重用查询

WITH 
CACHED_input_table AS (
  SELECT *
  FROM input_table
  WHERE PARTITION_ID = < partition  X >
),
CACHED_additional_table1 AS (
  SELECT *
  FROM additional_table 
  WHERE PARTITION_ID = < partition  X >
),
CACHED_additional_table2 AS (
  SELECT *
  FROM CACHED_additional_table1 
)
SELECT *    
FROM CACHED_input_table input_table

LEFT OUTER JOIN CACHED_additional_table1 additional_table1 
 ON input_table.ID = additional_table1.ID

LEFT OUTER JOIN CACHED_additional_table2 additional_table2 
 ON additional_table1.PARENT_ID = additional_table2.ID

根据经验,选项 A 是最快的。但为什么?有人可以解释一下吗? (我玩的是Oracle v11.2)

我知道,我围绕这家公司特定的分区概念进行的优化可能与我所询问的围绕 WITH 子句的通用 sql 优化无关,但请将其作为现实生活中的示例。

解释计划

选项 A(7 秒内 9900 行)

------------------------------------------------------------------------------------------------------------------------
| Id  | Operation               | Name                         | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |                              |     1 |  1037 | 18540   (8)| 00:00:03 |       |       |
|*  1 |  HASH JOIN OUTER        |                              |     1 |  1037 | 18540   (8)| 00:00:03 |       |       |
|*  2 |   HASH JOIN OUTER       |                              |     1 |   605 |  9271   (8)| 00:00:02 |       |       |
|   3 |    PARTITION LIST SINGLE|                              |     1 |   173 |     2   (0)| 00:00:01 |   KEY |   KEY |
|   4 |     TABLE ACCESS FULL   | input_table                  |     1 |   173 |     2   (0)| 00:00:01 |    24 |    24 |
|   5 |    PARTITION LIST SINGLE|                              |  1362K|   561M|  9248   (8)| 00:00:02 |   KEY |   KEY |
|   6 |     TABLE ACCESS FULL   | additional_table             |  1362K|   561M|  9248   (8)| 00:00:02 |    24 |    24 |
|   7 |   PARTITION LIST SINGLE |                              |  1362K|   561M|  9248   (8)| 00:00:02 |   KEY |   KEY |
|   8 |    TABLE ACCESS FULL    | additional_table             |  1362K|   561M|  9248   (8)| 00:00:02 |    24 |    24 |
------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("additional_table"."PARENT"="additional_table"."ID"(+))
   2 - access("input_table"."ID"="additional_table"."ID"(+))

选项 B(10 秒内 9900 行)

---------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                  | Name                         | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |                              |     1 |  2813 | 18186  (11)| 00:00:03 |       |       |
|   1 |  TEMP TABLE TRANSFORMATION |                              |       |       |            |          |       |       |
|   2 |   LOAD AS SELECT           | SYS_TEMP_0FD9D6CA2_C26AF925  |       |       |            |          |       |       |
|   3 |    PARTITION LIST SINGLE   |                              |  1362K|   561M|  9248   (8)| 00:00:02 |   KEY |   KEY |
|   4 |     TABLE ACCESS FULL      | additional_table1            |  1362K|   561M|  9248   (8)| 00:00:02 |    24 |    24 |
|*  5 |   HASH JOIN OUTER          |                              |     1 |  2813 |  8939  (15)| 00:00:02 |       |       |
|*  6 |    HASH JOIN OUTER         |                              |     1 |  1493 |  4470  (15)| 00:00:01 |       |       |
|   7 |     PARTITION LIST SINGLE  |                              |     1 |   173 |     2   (0)| 00:00:01 |   KEY |   KEY |
|   8 |      TABLE ACCESS FULL     | input_table                  |     1 |   173 |     2   (0)| 00:00:01 |    24 |    24 |
|   9 |     VIEW                   |                              |  1362K|  1714M|  4447  (14)| 00:00:01 |       |       |
|  10 |      TABLE ACCESS FULL     | SYS_TEMP_0FD9D6CA2_C26AF925  |  1362K|   561M|  4447  (14)| 00:00:01 |       |       |
|  11 |    VIEW                    |                              |  1362K|  1714M|  4447  (14)| 00:00:01 |       |       |
|  12 |     TABLE ACCESS FULL      | SYS_TEMP_0FD9D6CA2_C26AF925  |  1362K|   561M|  4447  (14)| 00:00:01 |       |       |
---------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   5 - access("additional_table1"."PARENT"="additional_table2"."ID"(+))
   6 - access("input_table"."ID"="additional_table1"."ID"(+))

选项 C(17 秒内 9900 行)

---------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                  | Name                         | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |                              |     1 |  2813 | 18186  (11)| 00:00:03 |       |       |
|   1 |  TEMP TABLE TRANSFORMATION |                              |       |       |            |          |       |       |
|   2 |   LOAD AS SELECT           | SYS_TEMP_0FD9D6CA7_C26AF925  |       |       |            |          |       |       |
|   3 |    PARTITION LIST SINGLE   |                              |  1362K|   561M|  9248   (8)| 00:00:02 |   KEY |   KEY |
|   4 |     TABLE ACCESS FULL      | additional_table             |  1362K|   561M|  9248   (8)| 00:00:02 |    24 |    24 |
|*  5 |   HASH JOIN OUTER          |                              |     1 |  2813 |  8939  (15)| 00:00:02 |       |       |
|*  6 |    HASH JOIN OUTER         |                              |     1 |  1493 |  4470  (15)| 00:00:01 |       |       |
|   7 |     PARTITION LIST SINGLE  |                              |     1 |   173 |     2   (0)| 00:00:01 |   KEY |   KEY |
|   8 |      TABLE ACCESS FULL     | input_table                  |     1 |   173 |     2   (0)| 00:00:01 |    24 |    24 |
|   9 |     VIEW                   |                              |  1362K|  1714M|  4447  (14)| 00:00:01 |       |       |
|  10 |      TABLE ACCESS FULL     | SYS_TEMP_0FD9D6CA7_C26AF925  |  1362K|   561M|  4447  (14)| 00:00:01 |       |       |
|  11 |    VIEW                    |                              |  1362K|  1714M|  4447  (14)| 00:00:01 |       |       |
|  12 |     TABLE ACCESS FULL      | SYS_TEMP_0FD9D6CA7_C26AF925  |  1362K|   561M|  4447  (14)| 00:00:01 |       |       |
---------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   5 - access("additional_table1"."PARENT_ID"="CACHED_additional_table"."ID"(+))
   6 - access("input_table"."ID"="additional_table1"."ID"(+))

编辑

  • 添加了解释计划
  • 已编辑的基本查询:有一个 input_table 和一个附加表连接了两次,一次在 input_table 上,一次在自身上
  • 选项 A 的已编辑查询:有一个 input_table,additional_table 连接了两次,一次在 input_table 上,一次在其自身的副本 (additional_table) 上
  • 选项 B 的已编辑查询:有一个 input_table,additional_table 连接了两次,一次在 input_table 上,一次在自身上,使用相同的别名(additional_table)
  • 选项 C 的已编辑查询:有一个 input_table,additional_table 连接了两次,一次在 input_table 上,一次在 WITH 部分中从自身创建的另一个表上

【问题讨论】:

  • 更改表名和别名不会改变选项 A 减少要连接的表的大小这一事实,因此选项 A 不等于您的基线,可能是性能提升的原因。不是由于使用 with,而是由于使用了进一步的连接条件。请参考我的建议答案了解更多
  • @Used_By_Already 我正在优化基线,所以我不希望它们相同(我认为它们最终会做同样的事情,我希望如此)。但是,我想要在这里比较选项
  • 认为您正试图理解为什么“with”显然更快。要真正弄清楚这一点,您需要研究等效查询。
  • 您对此有结论了吗?
  • @Used_By_Already 不,不是关于观察结果为何如此。选项 A 和您的优化都有很好的结果,并且肯定会显示具有较少中间对象的计划。我也没有尝试内联提示,因为我不知道它是什么意思以及如何去做(这需要我更多的研究和理解)

标签: sql oracle query-optimization


【解决方案1】:

Oracle 有能力实现 with 子句中定义的子查询,如果它认为这样做是有益的。通常(但不一定总是!),如果您在主查询中多次引用同一个子查询,它会这样做。

当 Oracle 实现子查询时,它会运行 sql,然后在后台将结果存储在全局临时表中。然后对于后续调用,它会查询临时表。

在您的情况下,我可以看到选项 A 重复与子查询相同的查询 - 您必须检查执行计划以了解 Oracle 在幕后执行的操作。

【讨论】:

  • 根据您的回答,我预计选项 B 会在运行时方面获胜。但是,一旦我对同一个子查询进行第二次连接,时间就会增加很多。
  • 我也期待同样的事情!请您编辑您的问题以包括所有三个选项的解释计划吗?这应该可以为正在发生的事情提供线索。
【解决方案2】:

在不使用递归时,公共表表达式(WITH 子句)应该与带有连接/子查询的普通选择非常相似(毕竟,这是它们的目的)。也许它可以更好地优化对同一个表的两个引用。

您必须使用实际的执行计划来发现任何差异,这将取决于您的设置,因此很难回答这个问题。

我怀疑这些查询之间会有任何显着差异,但是(我假设是 Oracle)您可以使用其他东西来优化 INSERT - APPEND hint

INSERT /* + APPEND */ INTO YourTable
SELECT ...

【讨论】:

    【解决方案3】:
    insert into output_table (
      select * 
       from input_table
       left outer join additional_table additional_table1  
         on input_table.id = additional_table1.id
       left outer join additional_table additional_table2  
        on additional_table2.id = additional_table1.parent
      where partition =  <partitionX>
    )
    

    如果以上是您的基线,则选项 A 不完全等效。我认为以下会更接近。

    insert into output_table (
      select * 
       from input_table
       left outer join additional_table additional_table1  on input_table.id = additional_table1.id
                 and additional_table1.partition =  <partitionX>
       left outer join additional_table additional_table2 on additional_table2.id = additional_table1.parent
                 and additional_table2.partition =  <partitionX>
      where partition =  <partitionX>
    )
    

    选项 A 中的要点是您减小了要连接的派生表的大小。在基线中是不正确的。

    B.1。单个 CTE 作为两个连接的基础

    WITH 
    CACHED_additional_table AS (
      SELECT *
      FROM additional_table 
      WHERE PARTITION_ID = < partition  X >
    )
    SELECT *    
    FROM input_table input_table
      LEFT OUTER JOIN CACHED_additional_table additional_table1 
        ON input_table.ID = additional_table1.ID
      LEFT OUTER JOIN CACHED_additional_table  additional_table2 
        ON additional_table1.PARENT_ID = additional_table2.ID
    

    此变体与选项 B 之间的区别在于,您只缓存单个查询结果,然后在主查询中使用该单个公用表表达式 (CTE) 两次。这是 CTE 的一个很好的用例(避免重复)。

    【讨论】:

    • 让我编辑基线以更好地匹配我分析的详细查询
    • 另外,您的建议是最初的优化:在 JOIN 部分中包含一个额外的子句来包含过滤部分,但我希望 WITH 示例得到更好的优化
    • 我也再次运行它(你的优化建议),它在 7 秒内完成了 9900 行,我会说这非常好
    • 在附加表上执行一次 CTE,然后加入两次。
    • 但那是选项 B,比选项 A 和您的建议更糟糕
    【解决方案4】:

    查询 A 必须读取三个分区,一个 input_table 分区和两次 Additional_table 单个分区。

    查询 B 必须读取两个分区,一个 input_table 分区和一个附加表分区。然后,它必须将该分区写入临时表并读取该临时表两次。

    所以,假设估计没问题: 查询 A 读取 input_table 分区中的 1 行 + 2 次 1362K 行在附加表中

    查询 B 读取 input_table 分区中的 1 行 + 3 次 1362K 行在附加表 + 临时表 + 写入 1362K 行。

    如果优化器决定实现您的分解子查询,那么您的情况会更糟。顺便说一句,您可以使用 inline-hint 来防止具体化。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-16
      相关资源
      最近更新 更多