CROSS APPLY виробляє зовнішнє з'єднання


17

У відповідь на підрахунок SQL, що відрізняється від розділу, Ерік Дарлінг опублікував цей код, щоб вирішити проблеми через відсутність COUNT(DISTINCT) OVER ():

SELECT      *
FROM        #MyTable AS mt
CROSS APPLY (   SELECT COUNT(DISTINCT mt2.Col_B) AS dc
                FROM   #MyTable AS mt2
                WHERE  mt2.Col_A = mt.Col_A
                -- GROUP BY mt2.Col_A 
            ) AS ca;

Запит використовує CROSS APPLY(не OUTER APPLY), тому чому в плані виконання замість внутрішнього з'єднання існує зовнішнє з'єднання?

введіть тут опис зображення

Крім того, чому коментування групи за допомогою пункту призводить до внутрішнього приєднання?

введіть тут опис зображення

Я не думаю, що дані є важливими, але копіювання з даних, наданих кевіном на інше питання:

create table #MyTable (
Col_A varchar(5),
Col_B int
)

insert into #MyTable values ('A',1)
insert into #MyTable values ('A',1)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',3)

insert into #MyTable values ('B',4)
insert into #MyTable values ('B',4)
insert into #MyTable values ('B',5)

Відповіді:


23

Підсумок

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 (вище, ніж вибраний план):

План застосування кодексу Spool

Зауважте, що план виконання за допомогою застосованих вкладених циклів дає правильні результати, використовуючи семантику "внутрішнього з'єднання" незалежно від наявності GROUP BYпункту.

У реальному світі, ми, як правило , мають індекс для підтримки шукати на внутрішній стороні застосовуються для заохочення SQL Server , щоб вибрати цей варіант , звичайно, наприклад:

CREATE INDEX i ON #MyTable (Col_A, Col_B);

db <> скриптова демонстрація


-3

Перехресне застосування - це логічна операція над даними. Вирішуючи, як отримати ці дані, SQL Server вибирає відповідного фізичного оператора для отримання потрібних даних.

Не існує фізичного оператора застосувань, і SQL Server переводить його у відповідний і, сподіваємось, ефективний оператор приєднання.

Список фізичних операторів ви можете знайти за посиланням нижче.

https://docs.microsoft.com/en-us/sql/relational-databases/showplan-logical-and-physical-operators-reference?view=sql-server-2017

Оптимізатор запитів створює план запитів у вигляді дерева, що складається з логічних операторів. Після того як оптимізатор запитів створює план, оптимізатор запитів вибирає найбільш ефективного фізичного оператора для кожного логічного оператора. Оптимізатор запитів використовує підхід на основі витрат, щоб визначити, який фізичний оператор реалізує логічний оператор.

Зазвичай логічну операцію можуть реалізувати кілька фізичних операторів. Однак у рідкісних випадках фізичний оператор також може реалізовувати кілька логічних операцій.

редагувати / Здається, я зрозумів ваше запитання неправильно. SQL-сервер зазвичай вибирає найбільш відповідного оператора. У вашому запиті не потрібно повертати значення для всіх комбінацій обох таблиць, коли використовується перехресне з'єднання. Достатньо лише обчислити потрібне значення для кожного рядка, що тут робиться.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.