Постоянное сканирование спулинга

14

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

CREATE TABLE #data ([Id] int, [Status] int);

INSERT INTO #data
VALUES (100, 1), (101, 2), (102, 3), (103, 2);

И у меня есть запрос, который соединяет эту таблицу с набором строк, построенных из значений таблицы (из переменных и констант), как

DECLARE @id1 int = 101, @id2 int = 105;

SELECT
    COALESCE(p.[Code], 'X') AS [Code],
    COALESCE(d.[Status], 0) AS [Status]
FROM (VALUES
        (@id1, 'A'),
        (@id2, 'B')
    ) p([Id], [Code])
    FULL JOIN #data d ON d.[Id] = p.[Id];

План выполнения запроса показывает, что решение оптимизатора заключается в использовании FULL LOOP JOINстратегии, которая представляется целесообразной, поскольку оба входа имеют очень мало строк. Одна вещь, которую я заметил (и не могу согласиться), тем не менее, состоит в том, что строки TVC находятся в очереди (см. Область плана выполнения в красном поле).

Постоянное сканирование спулинга

Почему оптимизатор вводит здесь спул, для чего это нужно? Нет ничего сложного за катушкой. Похоже, это не обязательно. Как избавиться от этого в этом случае, каковы возможные пути?


Вышеуказанный план был получен на

Microsoft SQL Server 2014 (SP2-CU11) (KB4077063) - 12.0.5579.0 (X64)

я-один
источник
Соответствующее предложение на feedback.azure.com
i-one

Ответы:

19

Почему оптимизатор вводит здесь спул, для чего это нужно? Нет ничего сложного за катушкой.

За спулом не стоит ссылаться на простую таблицу, которая может быть просто дублирована, когда используется альтернатива левого соединения / антиполусоединения. генерируется полусоединение.

Может выглядеть немного как таблица (постоянное сканирование), но для оптимизатора * это одна UNION ALLиз отдельных строк вVALUES предложении.

Дополнительной сложности достаточно, чтобы оптимизатор выбрал буферизацию и воспроизведение исходных строк, а не заменял буферизацию простым «получением таблицы» в дальнейшем. Например, начальное преобразование из полного объединения выглядит так:

ранний план

Обратите внимание на дополнительные катушки, введенные общим преобразованием. Катушки над простой таблицей get вычищаются позже по правилуSpoolGetToGet .

Если бы у оптимизатора было соответствующее SpoolConstGetToConstGetправило, оно могло бы работать в принципе так, как вы хотите.

Как избавиться от этого в этом случае, каковы возможные пути?

Используйте реальную таблицу (временную или переменную) или запишите преобразование из полного объединения вручную, например:

WITH 
    p([Id], [Code]) AS
    (
        SELECT @id1, 'A'
        UNION ALL
        SELECT @id2, 'B'
    ),
    FullJoin AS
    (
        SELECT
            p.Code,
            d.[Status]
        FROM p
        LEFT JOIN #data d 
            ON d.[Id] = p.[Id]
        UNION ALL
        SELECT
            NULL,
            D.[Status]
        FROM #data AS D
        WHERE NOT EXISTS
        (
            SELECT *
            FROM p
            WHERE p.Id = D.Id
        )
    )
SELECT
    COALESCE(FullJoin.Code, 'X') AS Code,
    COALESCE(FullJoin.Status, 0) AS [Status]
FROM FullJoin;

План ручного переписывания:

План переписывания вручную

Это оценивается в 0,0067201 единиц по сравнению с 0,0203412 единиц для оригинала.


* Это можно наблюдать как LogOp_UnionAllв Преобразованном Дереве (TF 8605). В дереве ввода (TF 8606) это LogOp_ConstTableGet. В преобразованном Дереве показывает дерево экспрессии Оптимизатора элементов после синтаксического анализа, нормализации, algebrization, связывания и некоторых других подготовительных работ. Input Дерево показывает элементы после преобразования к отрицанию нормальной форме (NNF новообращенного), постоянная времени выполнения разрушающейся, и несколько других битов и качается. NNF-конвертирование включает в себя логику для свертывания логических объединений и общих таблиц, среди прочего.

Пол Уайт 9
источник
3

Табличная таблица - это просто создание таблицы из двух наборов кортежей, представленных в VALUES предложении.

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

DROP TABLE IF EXISTS #data;
CREATE TABLE #data ([Id] int, [Status] int);

INSERT INTO #data
VALUES (100, 1), (101, 2), (102, 3), (103, 2);

DROP TABLE IF EXISTS #p;
CREATE TABLE #p
(
    Id int NOT NULL
    , Code char(1) NOT NULL
);

DECLARE @id1 int = 101, @id2 int = 105;

INSERT INTO #p (Id, Code)
VALUES
        (@id1, 'A'),
        (@id2, 'B');


SELECT
    COALESCE(p.[Code], 'X') AS [Code],
    COALESCE(d.[Status], 0) AS [Status]
FROM #p p
    FULL JOIN #data d ON d.[Id] = p.[Id];

Рассматривая план выполнения вашего запроса, мы видим, что список вывода содержит два столбца, которые используют Unionпрефикс; это подсказка, что спул создает таблицу из источника union'd:

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

FULL OUTER JOINТребует SQL Server для доступа к значениям вp два раза, один раз для каждой «стороны» соединения. Создание буфера позволяет объединяться полученным внутренним циклам для доступа к буферным данным.

Интересно, что если вы заменяете на FULL OUTER JOINa LEFT JOINи a RIGHT JOINи UNIONрезультаты вместе, SQL Server не использует спул.

SELECT
    COALESCE(p.[Code], 'X') AS [Code],
    COALESCE(d.[Status], 0) AS [Status]
FROM (VALUES
        (101, 'A'),
        (105, 'B')
    ) p([Id], [Code])
    LEFT JOIN #data d ON d.[Id] = p.[Id]
UNION
SELECT
    COALESCE(p.[Code], 'X') AS [Code],
    COALESCE(d.[Status], 0) AS [Status]
FROM (VALUES
        (101, 'A'),
        (105, 'B')
    ) p([Id], [Code])
    RIGHT JOIN #data d ON d.[Id] = p.[Id];

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

Обратите внимание, я не предлагаю использовать UNIONзапрос выше; для больших наборов входных данных он может быть не более эффективным, чем тот, который у FULL OUTER JOINвас уже есть.

Макс Вернон
источник
В вашей реальной рабочей нагрузке катушка действительно так дорога?
Макс Вернон