Підсумок
SQL Server використовує правильне з'єднання (внутрішнє чи зовнішнє) та додає проекції, де це необхідно, щоб виконати всю семантику вихідного запиту під час виконання внутрішніх перекладів між застосувати та об'єднати .
Різниці в планах можна пояснити різною семантикою агрегатів з групою та без неї за допомогою пункту SQL Server.
Деталі
Приєднатися проти застосувати
Нам потрібно буде вміти розрізняти заявку та приєднання :
Застосувати
Внутрішній (нижній) вхід застосунку виконується для кожного ряду зовнішнього (верхнього) входу, з одним або декількома значеннями внутрішніх бічних параметрів, що надаються поточним зовнішнім рядком. Загальний результат застосування - це поєднання (об'єднання всіх) усіх рядків, отриманих за допомогою параметризованих внутрішніх бічних виконань. Наявність застосованих параметрів засобів іноді називають співвіднесеним з'єднанням.
Застосовуються завжди реалізується в планах виконання до вкладених циклів оператора. Оператор матиме властивість Зовнішні посилання, а не приєднувати предикати. Зовнішні посилання - це параметри, що передаються від зовнішньої сторони до внутрішньої сторони на кожній ітерації циклу.
Приєднуйтесь
З'єднання оцінює свій предикат приєднання в оператора приєднання. Об'єднання, як правило, може бути реалізовано операторами Hash Match , Merge або Nested Loops в SQL Server.
Коли вибрано вкладені петлі , його можна відрізнити від застосувань за відсутністю зовнішніх посилань (і, як правило, наявністю предиката об'єднання). Внутрішній вхід з'єднання ніколи не посилається на значення із зовнішнього входу - внутрішня сторона все ще виконується один раз для кожного зовнішнього рядка, але внутрішні бічні виконання не залежать від будь-яких значень поточного зовнішнього рядка.
Детальніше див. У моєму дописі Застосувати проти вкладених петель .
... чому в плані виконання замість внутрішнього з'єднання існує зовнішнє з'єднання?
Зовнішнє з'єднання виникає , коли оптимізатор Перетворює застосувати до приєднатися (використовуючи правило під назвою ApplyHandler
) , щоб побачити , якщо він може знайти більш дешевий план на основі об'єднання. З'єднати потрібно , щоб зовнішнє з'єднання для коректності , коли застосовуються містить скалярний агрегат . Внутрішнє з'єднання не буде гарантовано виробляти ті ж результати , як в оригіналі застосовуються , як ми побачимо.
Скалярні та векторні агрегати
- Сукупність без відповідного
GROUP BY
пункту - скалярна сукупність.
- Сукупність з відповідним
GROUP BY
пунктом - це векторна сукупність.
У SQL Server скалярний агрегат завжди буде створювати рядок, навіть якщо йому не дано рядків для агрегування. Наприклад, скалярний COUNT
сукупність без рядків дорівнює нулю. Вектор COUNT
сукупність яких - або рядків порожня множина (ні однієї рядки на всіх).
Наступні запити іграшок ілюструють різницю. Ви також можете прочитати більше про скалярні та векторні агрегати в моїй статті " Розваги зі скалярними та векторними агрегатами" .
-- Produces a single zero value
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1;
-- Produces no rows
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1 GROUP BY ();
db <> скриптова демонстрація
Трансформація застосовується для приєднання
Раніше я згадував, що з'єднання повинно бути зовнішнім з'єднанням для правильності, коли оригінальне застосування містить скалярний сукупність . Щоб детально показати, чому це так, я використаю спрощений приклад запиту:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
SELECT * FROM @A AS A
CROSS APPLY (SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A) AS CA;
Правильний результат для стовпця c
дорівнює нулю , оскільки COUNT_BIG
є скалярним сукупністю. При перекладі цього застосувати запит на приєднання до форми, SQL Server генерує внутрішню альтернативу, яка буде схожа на наступну, якби вона була виражена в T-SQL:
SELECT A.*, c = COALESCE(J1.c, 0)
FROM @A AS A
LEFT JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Щоб переписати застосунок як некорельоване з'єднання, нам потрібно ввести GROUP BY
таблицю у похідну таблицю (інакше A
стовпець, до якого можна приєднатись, не може бути ). Об'єднання повинно бути зовнішнім з'єднанням, щоб кожен рядок із таблиці @A
продовжував створювати рядок у висновку. Лівий приєднання створить NULL
стовпчик для, c
коли предикат приєднання не оцінить як істинне. Це NULL
потрібно перевести в нуль, COALESCE
щоб виконати правильну трансформацію з застосувати .
Демонстрація нижче показує, як і зовнішнє з'єднання, і COALESCE
потрібно створювати однакові результати, використовуючи приєднання, як оригінальний запит застосувати :
db <> скриптова демонстрація
З GROUP BY
... чому коментування групи за допомогою пункту призводить до внутрішнього приєднання?
Продовження спрощеного прикладу, але додавання GROUP BY
:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
-- Original
SELECT * FROM @A AS A
CROSS APPLY
(SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A GROUP BY B.A) AS CA;
COUNT_BIG
Тепер вектор сукупності, так що правильний результат для порожнього вхідного набору не більше не дорівнює нулю, то немає ні в одній рядку взагалі . Іншими словами, запуск вищевикладених операторів не дає результату.
Цю семантику набагато легше визнати при перекладі з застосування на приєднання до об'єднання , оскільки, CROSS APPLY
природно, відхиляє будь-який зовнішній ряд, який не створює внутрішніх бічних рядків. Тому ми можемо безпечно використовувати внутрішнє з'єднання зараз, без додаткової проекції виразів:
-- Rewrite
SELECT A.*, J1.c
FROM @A AS A
JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Наведене нижче демонстрація показує, що внутрішнє перезаписування приєднання дає ті самі результати, що й оригінальні застосувати з векторним агрегатом:
db <> скриптова демонстрація
Оптимізатор, як правило, вибирає внутрішнє з'єднання злиття з маленькою таблицею, оскільки він швидко знаходить дешевий план приєднання (знайдений досить хороший план). Оптимізатор, що базується на витратах, може переписати з'єднання на додаток - можливо, знайти більш дешевий план застосувати, як це буде тут, якщо буде використаний натяк на з'єднання циклу або натяк на форссек, - але в цьому випадку не варто докладати зусиль.
Примітки
У спрощених прикладах використовуються різні таблиці з різним змістом, щоб чіткіше відобразити смислові відмінності.
Можна стверджувати, що оптимізатор повинен мати можливість міркувати про те, що самостійне з'єднання не здатне генерувати будь-які невідповідні (непоєднувані) рядки, однак це не містить такої логіки сьогодні. Доступ до однієї і тієї ж таблиці декілька разів у запиті не гарантує загалом результатів однакових результатів, залежно від рівня ізоляції та паралельної активності.
Оптимізатор хвилює ці семантичні та крайові випадки, тому не потрібно.
Бонус: Внутрішній план застосування
SQL Server може створити внутрішній план застосування (а не внутрішній план приєднання !) Для прикладу запиту, він просто вирішує не з міркувань витрат. Вартість зовнішнього плану приєднання, наведеного у запитанні, становить 0,02898 одиниць на екземплярі SQL Server 2017 мого ноутбука.
Ви можете змусити застосувати (корельований приєднання) план, використовуючи недокументований і непідтримуваний прапор сліду 9114 (який вимикається ApplyHandler
тощо) лише для ілюстрації:
SELECT *
FROM #MyTable AS mt
CROSS APPLY
(
SELECT COUNT_BIG(DISTINCT mt2.Col_B) AS dc
FROM #MyTable AS mt2
WHERE mt2.Col_A = mt.Col_A
--GROUP BY mt2.Col_A
) AS ca
OPTION (QUERYTRACEON 9114);
Це створює план застосування вкладених циклів із ледачою вказівною котушкою. Загальна орієнтовна вартість 0,0463983 (вище, ніж вибраний план):
Зауважте, що план виконання за допомогою застосованих вкладених циклів дає правильні результати, використовуючи семантику "внутрішнього з'єднання" незалежно від наявності GROUP BY
пункту.
У реальному світі, ми, як правило , мають індекс для підтримки шукати на внутрішній стороні застосовуються для заохочення SQL Server , щоб вибрати цей варіант , звичайно, наприклад:
CREATE INDEX i ON #MyTable (Col_A, Col_B);
db <> скриптова демонстрація