Почему запросы быстрее, когда подзапросы в предложении 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 (избегая повторения).