Почему этот рекурсивный CTE с параметром не использует индекс, когда он использует литерал?

8

Я использую рекурсивный CTE в древовидной структуре, чтобы перечислить всех потомков определенного узла в дереве. Если я напишу значение литерального узла в своем WHEREпредложении, SQL Server фактически применяет CTE только к этому значению, предоставляя план запроса с низким фактическим числом строк и так далее :

план запроса с литеральным значением

Однако, если я передаю значение в качестве параметра, он, похоже, реализует (спулингирует) CTE, а затем фильтрует его по факту :

план запроса со значением параметра

Я мог неправильно читать планы. Я не заметил проблемы с производительностью, но я обеспокоен тем, что реализация CTE может вызвать проблемы с большими наборами данных, особенно в загруженной системе. Кроме того, я обычно соединяю этот обход сам с собой: я иду к предкам и обратно к потомкам (чтобы я собрал все связанные узлы). Из-за того, как мои данные, каждый набор «связанных» узлов довольно мал, поэтому реализация CTE не имеет смысла. И когда SQL Server, похоже, понимает CTE, он дает мне довольно большие цифры в своих «фактических» подсчетах.

Есть ли способ заставить параметризованную версию запроса действовать как буквальная версия? Я хочу поместить CTE в вид многократного использования.

Запрос с литералом:

CREATE PROCEDURE #c AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = 24
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c;

Запрос с параметром:

CREATE PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c 24;

Код настройки:

DECLARE @count BIGINT = 100000;
CREATE TABLE #tree (
     Id BIGINT NOT NULL PRIMARY KEY
    ,ParentId BIGINT
);
CREATE INDEX tree_23lk4j23lk4j ON #tree (ParentId);
WITH number AS (SELECT
         CAST(1 AS BIGINT) Value
    UNION ALL SELECT
         n.Value * 2 + 1
    FROM number n
    WHERE n.Value * 2 + 1 <= @count
    UNION ALL SELECT
         n.Value * 2
    FROM number n
    WHERE n.Value * 2 <= @count)
INSERT #tree (Id, ParentId)
SELECT n.Value, CASE WHEN n.Value % 3 = 0 THEN n.Value / 4 END
FROM number n;
Бинки
источник

Ответы:

12

Ответ Рэнди Вертонген правильно описывает, как вы можете получить план, который вам нужен, с помощью параметризованной версии запроса. Этот ответ дополняет это, обращаясь к названию вопроса, если вас интересуют подробности.

SQL Server переписывает хвостовые рекурсивные общие табличные выражения (CTE) как итерацию. Все, что находится в Lazy Index Spool , является реализацией итеративного перевода во время выполнения. Я написал подробный отчет о том, как работает этот раздел плана выполнения в ответ на Использование EXCEPT в рекурсивном общем табличном выражении .

Вы хотите указать предикат (фильтр) за пределами CTE и попросить оптимизатор запросов поместить этот фильтр внутрь рекурсии (переписанный как итерация) и применить его к элементу привязки. Это будет означать, что рекурсия начинается только с тех записей, которые совпадают ParentId = @Id.

Это вполне разумное ожидание, используется ли литеральное значение, переменная или параметр; однако оптимизатор может делать только то, для чего были написаны правила. Правила определяют, как логическое дерево запросов изменяется для достижения определенного преобразования. Они включают логику, чтобы гарантировать, что конечный результат безопасен - т.е. он возвращает точно такие же данные, что и исходная спецификация запроса во всех возможных случаях.

Правило, отвечающее за передачу предикатов в рекурсивный CTE, называется SelOnIterator- реляционный выбор (= предикат) в итераторе, реализующем рекурсию. Точнее, это правило может копировать выделение вниз в опорную часть рекурсивной итерации:

Sel(Iter(A,R)) -> Sel(Iter(Sel(A),R))

Это правило можно отключить недокументированной подсказкой OPTION(QUERYRULEOFF SelOnIterator). Когда это используется, оптимизатор больше не может выдвигать предикаты с литеральным значением на якорь рекурсивного CTE. Вы не хотите этого, но это иллюстрирует суть.

Первоначально это правило ограничивалось работой с предикатами только с буквальными значениями. Его также можно настроить для работы с переменными или параметрами, указав OPTION (RECOMPILE), так как этот совет включает оптимизацию встраивания параметров , посредством чего литеральное значение переменной (или параметра) во время выполнения используется при составлении плана. План не кэшируется, поэтому его недостатком является новая компиляция при каждом выполнении.

В какой-то момент SelOnIteratorправило было улучшено, чтобы также работать с переменными и параметрами. Чтобы избежать неожиданных изменений плана, это было защищено под флагом трассировки 4199, уровнем совместимости базы данных и уровнем совместимости исправлений оптимизатора запросов. Это вполне нормальная схема для улучшений оптимизатора, которые не всегда документируются. Улучшения обычно хороши для большинства людей, но всегда есть вероятность, что любое изменение приведет к регрессу для кого-то.

Я хочу поместить CTE в вид многократного использования

Вы можете использовать встроенную табличную функцию вместо представления. Укажите значение, которое вы хотите передать как параметр, и поместите предикат в рекурсивный элемент привязки.

Если вы предпочитаете, глобальное включение флага трассировки 4199 также возможно. Этот флаг содержит множество изменений в оптимизаторе, поэтому вам необходимо тщательно протестировать свою рабочую нагрузку, если она включена, и быть готовым к обработке регрессий.

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

Хотя в настоящий момент у меня нет названия действительного исправления, лучший план запросов будет использоваться при включении исправлений оптимизатора запросов в вашей версии (SQL Server 2012).

Некоторые другие методы:

  • При OPTION(RECOMPILE)этом фильтрация происходит раньше, по буквальному значению.
  • В SQL Server 2016 или более поздней версии исправления до этой версии применяются автоматически, и запрос также должен выполняться в соответствии с лучшим планом выполнения.

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

Вы можете включить эти исправления с помощью

  • Traceflag 4199 до выпуска SQL Server 2016
  • ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES=ON; начиная с SQL Server 2016. (не требуется для вашего исправления)

Включение фильтрации @idранее применялось как к рекурсивным, так и к привязным элементам в плане выполнения с включенным исправлением.

Флаг трассировки может быть добавлен на уровне запроса:

OPTION(QUERYTRACEON 4199)

При выполнении запроса в SQL Server 2012 SP4 GDR или SQL Server 2014 SP3 с Traceflag 4199 выбирается лучший план запроса:

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
    OPTION( QUERYTRACEON 4199 );

END;
GO
EXEC #c 24;

План запросов на SQL Server 2014 с пакетом обновления 3 (SP3) с помощью флага трассировки 4199

План запросов в SQL Server 2012 с пакетом обновления 4 (SP4) с помощью флага трассировки 4199

План запросов на SQL Server 2012 с пакетом обновления 4 (SP4) без флага трассировки 4199

Основной консенсус заключается в том, чтобы включить трассировку флага 4199 глобально при использовании версии до SQL Server 2016. После этого открыто обсуждение вопроса о том, включать его или нет. AQ / A на этом здесь .


Уровень совместимости 130 или 140

При тестировании параметризованного запроса в базе данных с compatibility_level= 130 или 140 фильтрация происходит раньше:

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

Из-за того, что «старые» исправления из traceflag 4199 включены в SQL Server 2016 и выше.


ВАРИАНТ (RECOMPILE)

Несмотря на то, что процедура используется, SQL Server сможет фильтровать литеральное значение при добавлении OPTION(RECOMPILE);.

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
OPTION(
RECOMPILE )

END;
GO

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

План запросов в SQL Server 2012 с пакетом обновления 4 (SP4) с опцией (RECOMPILE)

Рэнди Вертонген
источник