Почему запросы быстрее, когда подзапросы в предложении WITH дублируются?

предположим, что проект использует разделы структуры данных. Эта концепция является чисто бизнес-специфичной и не имеет отношения к секционированию базы данных.

предположим, бизнес-логика делает:

  • удалить из output_table, где partition =
  • вставить в output_table (выберите * из input_table, где partition = )

имея в виду, что все структурировано так, давайте усложняйте задачу (собственно вопрос).

предположим, что у меня есть запрос (SELECT query), который является потенциальным убийцей, с точки зрения времени :

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 
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 версии 11.2)

я знаю, что, возможно, моя оптимизация вокруг этого концепция разделов компании не имеет ничего общего с общей оптимизацией sql вокруг предложения WITH, о котором я спрашиваю, но, пожалуйста, возьмите его в качестве примера в реальной жизни.

планы

Вариант A (9900 строк в 7s)

------------------------------------------------------------------------------------------------------------------------
| 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 (9900 строк в 10s)

---------------------------------------------------------------------------------------------------------------------------
| 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 (9900 строк в 17s)

---------------------------------------------------------------------------------------------------------------------------
| 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 и additional_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

4 ответов


запрос A должен прочитать три раздела, один раздел input_table и два раза один раздел additional_table.

запрос B должен прочитать два раздела, один раздел input_table и один раздел дополнительной таблицы. Затем он должен записать эту одну секцию во временную таблицу и прочитать эту временную таблицу дважды.

Итак, предполагая, что оценки ОК: Запрос a считывает 1 строку в разделе input_table + 2 раза 1362K строк additional_table

запрос B считывает 1 строку в разделе input_table + 3 раза 1362k строк в additional_table + временная таблица + пишет 1362K строк.

если оптимизатор решит материализовать ваши факторизованные подзапросы, вам будет хуже. Кстати, вы можете предотвратить материализацию, используя встроенную подсказку.


Oracle имеет возможность материализовать подзапросы, определенные в предложении with, если он считает, что это выгодно. Обычно (но не всегда!), это будет сделано, если вы ссылаетесь на один и тот же подзапрос более одного раза в основном запросе.

когда Oracle материализует подзапрос, он запускает sql, а затем сохраняет результаты в глобальной временной таблице за кулисами. Затем для последующих вызовов, он запрашивает временную таблицу.

в вашем случае, я могу см. Этот параметр a повторяет тот же запрос, что и подзапрос - вам нужно проверить планы выполнения, чтобы увидеть, что Oracle делает за кулисами.


общие табличные выражения(предложение WITH) должны быть очень похожи на обычный select с запросами joins/sub, когда рекурсивный не используется (в конце концов, это их цель). Возможно, он может лучше оптимизировать две ссылки на одну и ту же таблицу.

вам придется использовать фактический план выполнения, чтобы найти какие-либо различия, и это будет специфично для вашей установки, поэтому трудно ответить на этот вопрос.

Я сомневаюсь, что будут какие-то разногласия значительно между этими запросами, но (я предполагаю Oracle) вы можете использовать другую вещь для оптимизации INSERT -APPEND подсказка :

INSERT /* + APPEND */ INTO YourTable
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>
)

Если выше ваша базовая линия, то вариант 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>
)

точка, являющаяся опцией in A вы уменьшили размеры производных таблиц, которые будут объединены. В базовой линии это не так.

Б. 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) дважды в основном запросе. Это хороший вариант использования для CTEs (избегая повторения).