Превышен уровень вложенности скалярных функций при использовании ссылки

24

Цель

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

Единственное отличие заключается в добавлении SELECTв тело функции, что приводит к разному плану выполнения для обоих.


Функция, которая работает

CREATE FUNCTION dbo.test5(@i int)
RETURNS INT
AS 
BEGIN
RETURN(
SELECT TOP 1
CASE 
WHEN @i = 1 THEN 1
WHEN @i = 2 THEN 2
WHEN @i = 3 THEN  dbo.test5(1) + dbo.test5(2)
END
)
END;

Вызов функции

SELECT dbo.test5(3);

Возвращает

(No column name)
3

Функция, которая не работает

CREATE FUNCTION dbo.test6(@i int)
RETURNS INT
AS 
BEGIN
RETURN(
SELECT TOP 1
CASE 
WHEN @i = 1 THEN 1
WHEN @i = 2 THEN 2
WHEN @i = 3 THEN (SELECT dbo.test6(1) + dbo.test6(2))
END
)END;

Вызов функции

SELECT dbo.test6(3);

или

SELECT dbo.test6(2);

Приводит к ошибке

Превышен максимальный уровень вложенности хранимой процедуры, функции, триггера или представления (предел 32).

Угадывая причину

На предполагаемом плане сбойной функции есть дополнительный вычислительный скаляр, вызывающий

<ColumnReference Column="Expr1002" />
<ScalarOperator ScalarString="CASE WHEN [@i]=(1) THEN (1) ELSE CASE WHEN [@i]=(2) THEN (2) ELSE CASE WHEN [@i]=(3) THEN [Expr1000] ELSE NULL END END END">

И expr1000 существо

<ColumnReference Column="Expr1000" />
<ScalarOperator ScalarString="[dbo].[test6]((1))+[dbo].[test6]((2))">

Что может объяснить рекурсивные ссылки, превышающие 32.

Актуальный вопрос

При добавлении SELECTфункция вызывает саму функцию снова и снова, что приводит к бесконечному циклу, но почему добавление SELECTдает этот результат?


Дополнительная информация

Предполагаемые планы выполнения

DB <> Скрипки

Build version:
14.0.3045.24

Проверено на уровнях совместимости 100 и 140

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

Ответы:

26

Это ошибка в нормализации проекта , обнаруженная с помощью подзапроса внутри выражения case с недетерминированной функцией.

Чтобы объяснить, нам нужно отметить две вещи заранее:

  1. SQL Server не может выполнять подзапросы напрямую, поэтому они всегда развертываются или преобразуются в приложение .
  2. Семантика CASEтакова, что THENвыражение должно оцениваться только в том случае, если WHENпредложение возвращает true.

Таким образом, (тривиальный) подзапрос, введенный в проблемном случае, приводит к оператору apply (объединение вложенных циклов). Чтобы соответствовать второму требованию, SQL Server изначально помещает выражениеdbo.test6(1) + dbo.test6(2) на внутренней стороне применения:

выделенный вычислить скаляр

[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))

... с CASE семантикой, учитываемой сквозным предикатом в соединении:

[@i]=(1) OR [@i]=(2) OR IsFalseOrNull [@i]=(3)

Внутренняя сторона цикла оценивается, только если условие прохода оценивается как ложное (имеется в виду@i = 3 ). Пока это все правильно. Compute Scalar следующие соединения вложенных циклов также чтит CASEсемантику правильно:

[Expr1001] = Scalar Operator(CASE WHEN [@i]=(1) THEN (1) ELSE CASE WHEN [@i]=(2) THEN (2) ELSE CASE WHEN [@i]=(3) THEN [Expr1000] ELSE NULL END END END)

Проблема заключается в том, что на этапе нормализации проекта компиляции запроса он Expr1000не коррелирует и определяет, что это будет безопасно ( рассказчик: это не так). ) вывести его за пределы цикла:

перенесенный проект

[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))

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

Вы должны сообщить об этой ошибке. Обходной путь - предотвратить перемещение выражения за пределы приложения, сделав его коррелированным (т. Е. Включив его @iв выражение), но это, конечно, хак. Есть способ отключить нормализацию проекта, но раньше меня просили не делиться им публично, поэтому я не буду.

Эта проблема не возникает в SQL Server 2019, когда скалярная функция встроена , потому что логика встраивания работает непосредственно в разобранном дереве (задолго до нормализации проекта). Простая логика в вопросе может быть упрощена с помощью встроенной логики к нерекурсивной:

[Expr1019] = (Scalar Operator((1)))
[Expr1045] = Scalar Operator(CONVERT_IMPLICIT(int,CONVERT_IMPLICIT(int,[Expr1019],0)+(2),0))

... который возвращает 3.

Другой способ проиллюстрировать основную проблему:

-- Not schema bound to make it non-det
CREATE OR ALTER FUNCTION dbo.Error() 
RETURNS integer 
-- WITH INLINE = OFF -- SQL Server 2019 only
AS
BEGIN
    RETURN 1/0;
END;
GO
DECLARE @i integer = 1;

SELECT
    CASE 
        WHEN @i = 1 THEN 1
        WHEN @i = 2 THEN 2
        WHEN @i = 3 THEN (SELECT dbo.Error()) -- 'subquery'
        ELSE NULL
    END;

Воспроизводит на последних сборках всех версий от 2008 R2 до 2019 CTP 3.0.

Еще один пример (без скалярной функции), предоставленный Мартином Смитом :

SELECT IIF(@@TRANCOUNT >= 0, 1, (SELECT CRYPT_GEN_RANDOM(4)/ 0))

Это имеет все ключевые элементы, необходимые:

  • CASE(реализовано как ScaOp_IIF)
  • Недетерминированная функция ( CRYPT_GEN_RANDOM)
  • Подзапрос на ветви, который не должен быть выполнен ( (SELECT ...))

* Строго говоря, приведенное выше преобразование все еще может быть правильным, если оценка Expr1000была отложена правильно, поскольку на него ссылается только безопасная конструкция:

[Expr1002] = Scalar Operator(CASE WHEN [@i]=(1) THEN (1) ELSE CASE WHEN [@i]=(2) THEN (2) ELSE CASE WHEN [@i]=(3) THEN [Expr1000] ELSE NULL END END END)

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

Отчет об ошибке на сайте обратной связи Azure для SQL Server.

Пол Уайт говорит, что GoFundMonica
источник