Резюме по свойствам соединений

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

По материалам статьи Craig Freedman: Summary of Join Properties

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

 

Nested Loops Join

Merge Join

Hash Join

Лучше подходит…

Для относительно маленьких таблиц с индексом у внутренней таблицы по ключу соединения.

Для относительно больших таблиц с индексами, позволяющими упорядочить данные по ключам соединения эквивалентности и/или, когда требуется упорядочивание после соединения.

Для запросов к большим хранилищам и большим таблицам. Хорошее распараллеливание, которое масштабируется линейно.

Параллелизм

Поддерживает большое число параллельных подключений пользователей.

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

Лучше подходит для не большого числа параллельных подключений пользователей с высокими требованиями к производительности.

Stop and go

Нет

Нет

Да (только для стадии компоновки)

Требуется соединение по эквивалентности

Нет

Да (кроме полного внешнего соединения)

Да

Внешние и полусоединения

Только левые соединения (полные внешние соединения только через трансформацию)

Все типы соединений

Все типы соединений

Использование памяти

Нет

Нет (может потребоваться для сортировки, которая использует память)

Да

Использование tempdb

Нет

Да (только для соединений "многие ко многим")

Да (если соединение выйдет за пределы памяти и при сбросе на диск)

Требует упорядочивания

Нет

Да

Нет

Сохраняет порядок

Да (только внешнего входного потока)

Да

Нет

Примеры

В этой части статьи демонстрируется, как небольшие изменения в запросе могут привести к изменениям в выборе оптимизатором типа соединения. Давайте начнём с двух таблиц, по 1000 строк в каждой:

create table   T1 (a int, b int,   x char(200))
create table   T2 (a int, b int,   x char(200))

set nocount   on
declare @i int
set @i = 0

while @i < 1000
  begin
    insert T1 values   (@i * 2, @i * 5, @i)
    insert T2 values   (@i * 3, @i * 7, @i)
    set @i = @i + 1
  end

Так как у таблиц нет индексов, простое их соединение будет выполнено посредством хэш-соединения:

select * from   T1 join T2 on   T1.a = T2.a

 |--Hash Match(Inner Join, HASH:([T1].[a])=([T2].[a]),RESIDUAL:([T2].[a]=[T1].[a]))

       |--Table Scan(OBJECT:([T1]))

       |--Table Scan(OBJECT:([T2]))

Однако, есть одна вещь, для которой хэш-соединение работает плохо, это когда в запросе нужно быстро вернуть первые несколько строк. Предположим, что мы хотим получить только первые 10 строк:

select top   10 * from   T1 join T2 on   T1.a = T2.a

 |--Top(TOP EXPRESSION:((10)))

       |--Nested Loops(Inner Join, WHERE:([T2].[a]=[T1].[a]))

            |--Table Scan(OBJECT:([T1]))

            |--Table Spool

                 |--Table Scan(OBJECT:([T2]))

Теперь мы наблюдает использование соединения вложенных циклов. Индекса нет, так что этот план предполагает просмотр всей T2 для каждой строки из T1, пока не будут найдены 10 пар строк. Если для нахождения 10 пар потребуется выполнить много просмотров таблицы T2, этот план станет обходиться очень дорого. К счастью, если Вы выполняете этот запрос с включённым сбором статистики, Вы увидите, что просмотр T2 выполниться только 28 раз.

Примечания:

  1. Как можно было ожидать, оператор в вершине возвратит первые N строк, а затем закончит работу со своим поддеревом. В этом примере это N - 10.

  2. Организация очереди для таблицы может помочь оптимизации. В этом примере, очереди почти не используются. Однако, давайте посмотрим что будет, если вместо "select *" мы бы написали "select T2.a". В таблице T2 имеется три столбца, включая столбец с типом char (200), но нам нужен только один целочисленный столбец T2.a. Таким образом, мы можем хранить в очереди только T2.a. T2 может содержать около 40-ка строк на страницу, поскольку размер строки у неё чуть более 200 байт. Сканирование всех 1000 строк в T2 потребует обработки приблизительно 25 страниц. Очередь может вместить значения столбца T2.a для всех 1000 строк, что займёт всего одну или две страницы.

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

select top   100 * from   T1 join T2 on   T1.a = T2.a

 |--Top(TOP EXPRESSION:((100)))

       |--Merge Join(Inner Join, MANY-TO-MANY MERGE:([T2].[a])=([T1].[a]),RESIDUAL:([T2].[a]=[T1].[a]))

            |--Sort(ORDER BY:([T2].[a] ASC))

            |    |--Table Scan(OBJECT:([T2]))

            |--Sort(ORDER BY:([T1].[a] ASC))

                 |--Table Scan(OBJECT:([T1]))

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

select top   10 * from   T1 join T2 on   T1.a = T2.a
order by   T1.a

(План у этого запроса по существу такой же, как был ранее при TOP 100)

Теперь давайте добавим кластеризованный индекс:

create unique   clustered index   T1a on T1(a)

Возвращаясь к нашему первому соединению, мы теперь получим соединение слиянием "многие к одному":

select * from   T1 join T2 on   T1.a = T2.a

 select * from T1 join T2 on T1.a = T2.a

  |--Merge Join(Inner Join, MERGE:([T1].[a])=([T2].[a]),RESIDUAL:([T2].[a]=[T1].[a]))

       |--Clustered Index Scan(OBJECT:([T1].[T1a]), ORDERED FORWARD)

       |--Sort(ORDER BY:([T2].[a] ASC))

            |--Table Scan(OBJECT:([T2]))

Оптимизатор решил, что, так как одна таблица уже отсортирована, можно также отсортировать и другую таблицу, а потом использовать соединение слиянием вместо хэш-соединения.
Но, если к просмотру T1 добавить предикат, оптимизатор вернётся к хэш-соединению:

select * from   T1 join T2 on   T1.a = T2.a
where T1.b < 100

|--Hash Match(Inner Join, HASH:([T1].[a])=([T2].[a]),RESIDUAL:([T2].[a]=[T1].[a]))

       |--Clustered Index Scan(OBJECT:([T1].[T1a]), WHERE:([T1].[b]<(100)))

       |--Table Scan(OBJECT:([T2]))

План соединения слиянием должен сортировать все 1000 строк T1, в то время как для хэш-соединение планируется сформировать хеш-таблицу только по 20 строкам из T1, которые соответствуют предикату T1.b < 100. Таким образом, план хэш-соединения задействует меньше памяти и менее вероятен сброс на диск.
Но, если опять добавит к запросу требование по сортировке, мы снова вернёмся к соединению слиянием:

select * from   T1 join T2 on   T1.a = T2.a
where T1.b < 100
order by   T1.a

 |--Merge Join(Inner Join, MERGE:([T1].[a])=([T2].[a]),RESIDUAL:([T2].[a]=[T1].[a]))

       |--Clustered Index Scan(OBJECT:([T1].[T1a]),WHERE:([T1].[b]<(100)) ORDERED FORWARD)

       |--Sort(ORDER BY:([T2].[a] ASC))

            |--Table Scan(OBJECT:([T2]))

Как видно, соединение слиянием сохраняет порядок, и поэтому нет необходимости в заключительном упорядочивании. Хэш-соединение не сохраняет порядок, так что понадобилась бы дополнительная сортировка. Т.о. при хэш-соединении понадобились бы два потребляющих память оператора. Обратите внимание, что один потребляющий память оператор не обязательно лучше, чем два. В действительности, это зависит от того, в каком объёме памяти эти операторы нуждаются.
Далее, давайте к просмотру T2 добавим предикат:

select * from   T1 join T2 on   T1.a = T2.a
where T2.b < 100

 |--Nested Loops(Inner Join, OUTER REFERENCES:([T2].[a]))

       |--Table Scan(OBJECT:([T2]), WHERE:([T2].[b]<(100)))

       |--Clustered Index Seek(OBJECT:([T1].[T1a]),

                               SEEK:([T1].[a]=[T2].[a]) ORDERED FORWARD)

После уменьшения кардинальности T2, индексированное соединение вложенных циклов становится предпочтительным.
Наконец, давайте посмотрим что будет, если используется большая таблица T3, содержащая 100000 строк:

create table   T3 (a int, b int,   x char(200))
declare @i int
set @i = 0

while @i < 100000
  begin
    insert T3 values   (@i * 5, @i * 11, @i)
    set @i = @i + 1
  end

Рассмотрим простое соединение T1 и T3:

select * from   T1 join T3 on   T1.a = T3.a

 |--Hash Match(Inner Join, HASH:([T1].[a])=([T3].[a]),RESIDUAL:([T3].[a]=[T1].[a]))

       |--Clustered Index Scan(OBJECT:([T1].[T1a]))

       |--Table Scan(OBJECT:([T3]))

Даже при наличии индекса по T1, выбирается хэш-соединение. Вспомним, что при соединении T1 и T2, исходя из размера таблиц, использовалось соединение слиянием. Чтобы добиться соединения слиянием нужно, что бы обе эти таблицы были одинаково отсортированы. Для хэш-соединения вполне достаточно памяти, чтобы сформировать хеш-таблицу по T1 с 1000 строк, в то время как соединению слиянием понадобился необходимый для сортировки всей имеющей 100000 строк таблицы T3 объём памяти. Таким образом, хэш-соединению памяти нужно значительно меньше.

Подсказки

Хотя мы не рекомендуем использование подсказок оптимизатору (если в этом нет абсолютной необходимости) для исследовательских целей они вполне применимы, т.к. позволяют видеть, как разные варианты и типы соединений влияют на планы исполнения запроса и производительность, поскольку, используя подсказки можно вынудить SQL Server создавать разные планы запроса. Существующие подсказки (hints) перечислены в электронной документации. Следует помнить, что с их помощью невозможно заставить сервер выбрать совершенно любой план исполнения, и что использование неправильных подсказок может привести к тому, что SQL Server не сможет найти допустимый план запроса, что приведёт к появлению ошибки:

Msg 8622, Level 16, State 1, Line 1
Query processor could not produce a query plan because of the hints defined in this query. Resubmit the query without specifying any hints and without using SET FORCEPLAN.

Кроме того, Вы можете использовать в запросе подсказки, которые укажут SQL Server использование конкретного типа соединения или зададут порядок соединения. Подсказки в запрос можно добавить с помощью предложения OPTION, добавляемого в конец инструкций. Я уже использовал некоторые из подсказок оптимизатору в примерах, опубликованных в предыдущих статьях. Для этих целей можно использовать следующие подсказки: LOOP JOIN, MERGE JOIN, HASH JOIN и FORCE ORDER. Первые три подсказки указывают оптимизатору использовать заданный тип соединения. Если были указаны две подсказки соединения, оптимизатор будет использовать только эти два типа соединения; так можно запретить использование каких то типов соединения. FORCE ORDER указывает оптимизатору порядок соединения таблиц, он будет таким, как таблицы расположены в предложении FROM.
Вы можете указать подсказки соединения непосредственно в предложении FROM, задав тип и порядок соединения. Это очень мощные подсказки. С их помощью, Вы можете точно определить тип соединения для каждого из соединений. Используя круглые скобки, можно задать любой порядок соединения, включая Bushy - деревья. В качестве отвлечённого примера, рассмотрим подсказки, которые приводят к использованию Bushy - дерева со всеми тремя типами соединений:

select *
from (T1 inner   merge join T2 on T1.a = T2.a)
    inner hash join
       (T3 inner loop join T4 on T3.a = T4.a)
    on T1.b = T3.b

Далее…

В следующей статье я опишу еще несколько проблем соединений.

Источник: https://habr.com/ru/post/657331/


Интересные статьи

Интересные статьи

Корпоративное хранилище в Лиге Ставок было создано задолго до внедрения Amplitude. Преимущественно им пользуются аналитики и исследователи. Продакты и маркетологи для получения аналитичес...
VUE.JS - это javascript фрэймворк, с версии 18.5 его добавили в ядро битрикса, поэтому можно его использовать из коробки.
Есть статьи о недостатках Битрикса, которые написаны программистами. Недостатки, описанные в них рядовому пользователю безразличны, ведь он не собирается ничего программировать.
От скорости сайта зависит многое: количество отказов, брошенных корзин. Согласно исследованию Google, большинство посетителей не ждёт загрузки больше 3 секунд и уходит к конкурентам. Бывает, что сайт ...
У нас не очень простое собеседование. Нужно пройти 3 шага: Прислать резюме, программист его посмотрит, лайкнет если всё хорошо. Рекрутер позвонит, задаст несколько вопросов Встретиться или ...