Decorrelating Subqueries

По материалам статьи Craig Freedman: Decorrelating Subqueries

В статье про скалярные подзапросы было несколько примеров, в которых оптимизатор мог переписать запрос с коррелированным подзапросом как запрос с соединением. Например, можно было видеть, как представленный ниже простой подзапрос с «in»:

create table T1 (a int, b int) create table T2 (a int, b int, c int)  select * from T1 where T1.a in (select T2.a from T2 where T2.b = T1.b)

Мог быть переписан как левое полу-соединение:

  |--Nested Loops(Left Semi Join, WHERE:([T2].[b]=[T1].[b] AND [T1].[a]=[T2].[a]))        |--Table Scan(OBJECT:([T1]))        |--Table Scan(OBJECT:([T2]))

Обратите внимание, что между двумя просмотрами таблицы отсутствует какая-либо корреляция. Просмотр любой таблицы может происходить независимо и будет использоваться такой алгоритм соединения, какой наиболее удобен.  Эту стратегию оптимизации мы называем декорреляцией. Невозможно удалить корреляцию для всех подзапросов. Например, как мы видели в предыдущих статьях, простая замена подзапроса «in» скалярным подзапросом «=» (при отсутствии уникального индекса) вынуждает оптимизатор добавить в план оператор Assert, и это не позволит избавиться от корреляции подзапроса. Даже когда мы можем декоррелировать подзапрос, в результате необязательно получаем лучший план, но появляется возможность проанализировать больше вариантов плана и найти из них лучший.

Декорреляция подзапросов с агрегацией

Вспомним представленный ниже запрос из предыдущей статьи:

select * from T1 where T1.a > (select max(T2.a) from T2 where T2.b < T1.b)
  |--Filter(WHERE:([T1].[a]>[Expr1008]))        |--Nested Loops(Inner Join, OUTER REFERENCES:([T1].[b]))             |--Table Scan(OBJECT:([T1]))             |--Stream Aggregate(DEFINE:([Expr1008]=MAX([T2].[a])))                  |--Table Scan(OBJECT:([T2]), WHERE:([T2].[b]<[T1].[b]))

Мы не можем декоррелировать этот подзапрос. Теперь рассмотрим почти идентичный запрос, но заменим в предложении where подзапроса «<» на «=»:

select * from T1 where T1.a > (select max(T2.a) from T2 where T2.b = T1.b)

Это, казалось бы, незначительное изменение позволяет нам убрать корреляцию и написать этот запрос как обычное соединение:

select T1.* from T1, (select T2.b, max(T2.a) max_a from T2 group by T2.b) S where T1.b = S.b and T1.a > S.max_a

Оба запроса выбирают такой план:

  |--Nested Loops(Inner Join, OUTER REFERENCES:([T2].[b], [Expr1008]))        |--Stream Aggregate(GROUP BY:([T2].[b]) DEFINE:([Expr1008]=MAX([T2].[a])))        |    |--Sort(ORDER BY:([T2].[b] ASC))        |         |--Table Scan(OBJECT:([T2]))        |--Index Spool(SEEK:([T1].[b]=[T2].[b] AND [T1].[a] > [Expr1008]))             |--Table Scan(OBJECT:([T1]))

Исходный план выполняет подзапрос для каждой строки T1, он работает вполне эффективно за счет предварительного вычисления всех возможных результатов агрегации для подзапроса и объединения этого результата с T2. Чтобы вычислить все возможные результаты подзапроса, к подзапросу добавляется предложение «group by» и это учитывается в ключе соединения. Оператор буферизации индекса «Index Spool» создает индекс на лету, это позволяет повысить производительность соединения. Вместо соединения вложенных циклов «Nested Loops» с просмотром таблицы на внутренней стороне у нас есть соединение «Nested Loops» индекса с поиском по индексу на внутренней стороне.

Давайте рассмотрим, когда этот новый план будет лучше изначального. Если в T2.b хранится всего несколько уникальных значений и, следовательно, будет всего несколько групп для вычисления, а также если в T1 будет много строк, новый план может оказаться очень эффективным. С другой стороны, если в T2.b много уникальных значений, а в T1 всего несколько строк, может оказаться выгоднее вычислять только те агрегаты, которые нужны:

set nocount on declare @i int set @i = 0 while @i < 10000   begin     insert T2 values(@i, @i, @i)     set @i = @i + 1   end  select * from T1 where T1.a > (select max(T2.a) from T2 where T2.b = T1.b)
   |--Filter(WHERE:([T1].[a]>[Expr1008]))        |--Stream Aggregate(GROUP BY:([Bmk1000]) DEFINE:([Expr1008]=MAX([T2].[a]), [T1].[a]=ANY([T1].[a]), [T1].[b]=ANY([T1].[b])))             |--Nested Loops(Inner Join, WHERE:([T1].[b]=[T2].[b]))                  |--Table Scan(OBJECT:([T1]))                  |--Table Scan(OBJECT:([T2]))

Этот план в основном такой же, как и исходный (с корреляцией), за исключением того, что агрегация делается после соединения.

Обратите внимание, что результаты агрегации после соединения мы группируем по [Bmk1000]. Это уникальный реляционный ключ для T1. Вы можете убедиться в том, что [Bmk1000] является ключом для T1, исследовав столбец для этих значений в результатах выполнения showplan_all. Группировка по ключу T1 необходима, так как одна строка T1 может соединяться с несколькими строками T2, но запрос предполагает вычисление MAX(T2.a) значений для каждой строки T1.

Нет необходимости в сортировке потока после агрегации, поскольку соединение «Nested Loops» выбирает все строки T2, которые подлежат соединению со строкой из T1, прежде чем перейти к следующей строке T1. Другими словами, результаты соединения уже «сгруппированы» по T1, даже если они и не отсортированы по T1.

Если у T1 есть кластерный или любой уникальный индекс, мы будем использовать этот ключ вместо столбца закладки [Bmk1000]. На самом деле, если мы создадим уникальный ключ для T1, мы можем переписать запрос иначе:

create unique clustered index T1a on T1(a)  select T1.a, min(T1.b) as T1b from T1 join T2 on T1.b = T2.b group by T1.a having T1.a > max(T2.a)

План в принципе тот же:

 |--Filter(WHERE:([T1].[a]>[Expr1007]))     |--Stream Aggregate(GROUP BY:([T1].[a]) DEFINE:([Expr1007]=MAX([T2].[a]), [Expr1008]=MIN([T1].[b])])))         |--Nested Loops(Inner Join, WHERE:([T2].[b]=[T1].[b]))            |--Clustered Index Scan(OBJECT:([T1].[T1a]))            |--Table Scan(OBJECT:([T2]))

Теперь давайте рассмотрим вариант, когда в обеих таблицах много строк, размер соединения увеличивается, и оптимизатор может выбрать еще один вариант плана:

set nocount on declare @i int set @i = 0 while @i < 10000   begin     insert T1 values(@i, @i)     set @i = @i + 1   end    select * from T1 where T1.a > (select max(T2.a) from T2 where T2.b = T1.b)
   |--Hash Match(Inner Join, HASH:([T1].[b])=([T2].[b]), RESIDUAL:([T1].[b]=[T2].[b] AND [T1].[a]>[Expr1007]))        |--Clustered Index Scan(OBJECT:([T1].[T1a]))        |--Hash Match(Aggregate, HASH:([T2].[b]), RESIDUAL:([T2].[b] = [T2].[b]) DEFINE:([Expr1007]=MAX([T2].[a])))             |--Table Scan(OBJECT:([T2]))

Этот план по сути такой же, как исходный декоррелированный план, за исключением того, что вместо потоковой агрегации и соединения «Nested Loops»  используется хэш-агрегация и хеш-соединение.

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


ссылка на оригинал статьи https://habr.com/ru/post/674414/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *