В отличие от SQL из других языков программирования, структура рекурсивного запроса выглядит довольно странно. Пройдите через это шаг за шагом, и это, кажется, разваливается.
Рассмотрим следующий простой пример:
CREATE TABLE #NUMS
(N BIGINT);
INSERT INTO #NUMS
VALUES (3), (5), (7);
WITH R AS
(
SELECT N FROM #NUMS
UNION ALL
SELECT N*N AS N FROM R WHERE N*N < 10000000
)
SELECT N FROM R ORDER BY N;
Давайте пройдемся по нему.
Сначала выполняется элемент привязки, и набор результатов помещается в R. Таким образом, R инициализируется как {3, 5, 7}.
Затем выполнение падает ниже UNION ALL, и рекурсивный член выполняется впервые. Он выполняется на R (то есть на R, который у нас есть на данный момент: {3, 5, 7}). Это приводит к {9, 25, 49}.
Что это делает с этим новым результатом? Добавляет ли он {9, 25, 49} к существующему {3, 5, 7}, маркирует результирующее объединение R, а затем продолжает рекурсию оттуда? Или он переопределяет R как только этот новый результат {9, 25, 49} и делает все объединение позже?
Ни один из вариантов не имеет смысла.
Если R теперь {3, 5, 7, 9, 25, 49} и мы выполним следующую итерацию рекурсии, то получим {9, 25, 49, 81, 625, 2401} и мы потерял {3, 5, 7}.
Если R теперь только {9, 25, 49}, то у нас есть проблема с неправильной маркировкой. Под R понимается объединение результирующего набора якорных членов и всех последующих рекурсивных результирующих наборов членов. Принимая во внимание, что {9, 25, 49} является только компонентом R. Это не полный R, который мы накопили до сих пор. Поэтому записывать рекурсивный член как выбор из R не имеет смысла.
Я, безусловно, ценю то, что @Max Vernon и @Michael S. подробно описали ниже. А именно, что (1) все компоненты создаются до предела рекурсии или нулевого набора, а затем (2) все компоненты объединяются вместе. Вот как я понимаю рекурсию SQL на самом деле работать.
Если бы мы проектировали SQL, возможно, мы бы применили более четкий и явный синтаксис, примерно так:
WITH R AS
(
SELECT N
INTO R[0]
FROM #NUMS
UNION ALL
SELECT N*N AS N
INTO R[K+1]
FROM R[K]
WHERE N*N < 10000000
)
SELECT N FROM R ORDER BY N;
Вроде как индуктивное доказательство в математике.
Проблема с рекурсией SQL в ее нынешнем виде заключается в том, что она написана в замешательстве. То, как написано, говорит, что каждый компонент формируется путем выбора из R, но это не означает полный R, который был (или, кажется, был) построен до сих пор. Это просто означает предыдущий компонент.
источник
Ответы:
BOL-описание рекурсивных CTE описывает семантику рекурсивного выполнения следующим образом:
Таким образом, каждый уровень имеет на входе только уровень выше, а не весь набор результатов, накопленный до сих пор.
Выше, как это работает логически . Физически рекурсивные CTE в настоящее время всегда реализуются с помощью вложенных циклов и буфера стека в SQL Server. Это описано здесь и здесь и означает, что на практике каждый рекурсивный элемент просто работает с родительской строкой с предыдущего уровня, а не со всем уровнем. Но различные ограничения допустимого синтаксиса в рекурсивных CTE означают, что этот подход работает.
Если вы удалите
ORDER BY
из запроса, результаты упорядочены следующим образомЭто потому, что план выполнения работает очень похоже на следующее
C#
NB1: Как указано выше, к тому времени, когда первый дочерний элемент привязанного элемента
3
обрабатывает всю информацию о своих братьях5
и сестрах, и7
, и их потомках, уже удален из буфера и больше недоступен.NB2: C # выше имеет ту же общую семантику, что и план выполнения, но поток в плане выполнения не идентичен, так как там операторы работают в конвейерном порядке. Это упрощенный пример, демонстрирующий суть подхода. Смотрите более ранние ссылки для более подробной информации о самом плане.
NB3: сама катушка стека, по-видимому, реализована как неуникальный кластеризованный индекс с ключевым столбцом уровня рекурсии и добавлением уникализаторов по мере необходимости ( источник )
источник
IterateToDepthFirst
-Iterate(seed,rcsv)->PhysIterate(seed,rcsv)
. Просто к вашему сведению. Отличный ответ.Это всего лишь (полу) образованное предположение, и, вероятно, оно совершенно неверно. Интересный вопрос, кстати.
T-SQL - это декларативный язык; возможно, рекурсивный CTE преобразуется в операцию в виде курсора, в которой результаты с левой стороны UNION ALL добавляются во временную таблицу, а затем правая часть UNION ALL применяется к значениям с левой стороны.
Итак, сначала мы вставляем вывод левой части UNION ALL в набор результатов, затем вставляем результаты правой части UNION ALL, примененной к левой стороне, и вставляем его в набор результатов. Затем левая сторона заменяется выводом с правой стороны, а правая сторона снова применяется к «новой» левой стороне. Что-то вроде этого:
Вы можете увидеть это поведение в плане выполнения для рекурсивного CTE:
Это шаг 1 выше, где левая сторона UNION ALL добавляется к выводу:
Это правая часть UNION ALL, где вывод объединяется с набором результатов:
источник
Документация по SQL Server , в которой упоминаются T i и T i + 1 , не очень понятна и не является точным описанием фактической реализации.
Основная идея заключается в том, что рекурсивная часть запроса просматривает все предыдущие результаты, но только один раз .
Может быть полезно посмотреть, как другие базы данных реализуют это (чтобы получить тот же результат). Документация Postgres гласит:
Документация по SQLite намекает на несколько иную реализацию, и этот алгоритм по одной строке за раз может быть самым простым для понимания:
источник
Мои знания конкретно в DB2, но взгляд на диаграммы объяснения, похоже, совпадает с SQL Server.
План идет отсюда:
Посмотреть это на Вставить план
Оптимизатор не запускает объединение буквально для каждого рекурсивного запроса. Он берет структуру запроса и назначает первую часть объединения all «якорному члену», затем он проходит через вторую половину объединения all (называемую «рекурсивным членом» рекурсивно, пока не достигнет определенных ограничений. После рекурсия завершена, оптимизатор объединяет все записи вместе.
Оптимизатор воспринимает это как предопределенную операцию.
источник