Почему в рекурсивных CTE аналитические функции (ROW_NUMBER) выполняются процедурно? - PullRequest
12 голосов
/ 01 апреля 2012

Вчера я ответил на рекурсивный CTE, который выявил проблему с тем, как они реализованы в SQL Server (возможно, и в других СУБД?). В основном, когда я пытаюсь использовать ROW_NUMBER против текущего рекурсивного уровня, он запускается для каждого ряда подмножества текущего рекурсивного уровня . Я ожидал бы, что это будет работать в истинной логике SET и работать против всего текущего рекурсивного уровня .

Похоже, что из этой статьи MSDN , обнаруженная проблема связана с функциональностью:

Аналитические и агрегатные функции в рекурсивной части CTE применяется к набору для текущего уровня рекурсии, а не к набору для CTE. Такие функции, как ROW_NUMBER, работают только в подмножестве данные, передаваемые им по текущему уровню рекурсии, а не по всему набор данных передается в рекурсивную часть CTE . Для большего см. J. Использование аналитических функций в рекурсивном CTE.

В своем копании я не смог найти нигде, который объясняет, почему это было выбрано так, как оно работает? Это скорее процедурный подход в языке, основанном на множествах, так что это работает против моего мыслительного процесса SQL и, на мой взгляд, довольно запутанно. Кто-нибудь знает и / или кто-нибудь может объяснить, почему рекурсивный СТЕ обрабатывает аналитические функции на уровне рекурсии процедурным способом?


Вот код, который поможет визуализировать это:

Обратите внимание, столбец RowNumber в каждом из этих кодовых выходов.

Вот SQLFiddle для CTE (показывает только 2-й уровень рекурсии)

WITH myCTE
AS
(
  SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, 1 AS RecurseLevel
  FROM tblGroups
  WHERE ParentId IS NULL

  UNION ALL

  SELECT tblGroups.*, 
      ROW_NUMBER() OVER (ORDER BY myCTE.RowNumber , tblGroups.Score desc) AS RowNumber, 
      RecurseLevel + 1 AS RecurseLevel
  FROM tblGroups
      JOIN myCTE
          ON myCTE.GroupID = tblGroups.ParentID
 )
SELECT *
FROM myCTE
WHERE RecurseLevel = 2;

Вот второй SQLFiddle для того, что я ожидал от CTE (опять же, для отображения проблемы нужен только 2-й уровень)

WITH myCTE
AS
(
  SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, 1 AS RecurseLevel
  FROM tblGroups
  WHERE ParentId IS NULL
 )
  SELECT tblGroups.*, 
      ROW_NUMBER() OVER (ORDER BY myCTE.RowNumber , tblGroups.Score desc) AS RowNumber, 
      RecurseLevel + 1 AS RecurseLevel
  FROM tblGroups
      JOIN myCTE
          ON myCTE.GroupID = tblGroups.ParentID;

Я всегда предполагал, что SQL-рекурсивный CTE будет работать больше как этот цикл while

DECLARE @RecursionLevel INT
SET @RecursionLevel = 0
SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, @RecursionLevel AS recurseLevel
INTO #RecursiveTable
FROM tblGroups
WHERE ParentId IS NULL

WHILE EXISTS( SELECT tblGroups.* FROM tblGroups JOIN #RecursiveTable ON #RecursiveTable.GroupID = tblGroups.ParentID WHERE recurseLevel = @RecursionLevel)
BEGIN

    INSERT INTO #RecursiveTable
    SELECT tblGroups.*, 
        ROW_NUMBER() OVER (ORDER BY #RecursiveTable.RowNumber , tblGroups.Score desc) AS RowNumber, 
        recurseLevel + 1 AS recurseLevel
    FROM tblGroups
        JOIN #RecursiveTable
            ON #RecursiveTable.GroupID = tblGroups.ParentID
    WHERE recurseLevel = @RecursionLevel
    SET @RecursionLevel = @RecursionLevel + 1
END

SELECT * FROM #RecursiveTable ORDER BY RecurseLevel;

1 Ответ

1 голос
/ 14 апреля 2012

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

Пример:

    Select * from (
      select row_number() over (partition by c1 order by c2) rw, c3 from t) z
    where c3=123

- это не то же самое, что

    select row_number() over (partition by c1 order by c2) rw, c3 from t 
    where c3=123

. Эти 2 будут возвращать различные значения для rw.Вот почему подзапросы, содержащие аналитические функции, всегда будут полностью разрешены ранее и никогда не будут объединены с остальными.

Обновление

Просмотр 2-го запроса:

WITH myCTE
AS
(
  SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, 1 AS RecurseLevel
  FROM tblGroups
  WHERE ParentId IS NULL
 )
  SELECT tblGroups.*, 
      ROW_NUMBER() OVER (ORDER BY myCTE.RowNumber , tblGroups.Score desc) AS RowNumber, 
      RecurseLevel + 1 AS RecurseLevel
  FROM tblGroups
      JOIN myCTE
          ON myCTE.GroupID = tblGroups.ParentID;

Он работает точно так же, как если бы он был написан следующим образом (Тот же план выполнения и результат):

SELECT tblGroups.*, 
      ROW_NUMBER() OVER (ORDER BY myCTE.RowNumber , tblGroups.Score desc) AS RowNumber, 
      RecurseLevel + 1 AS RecurseLevel
FROM tblGroups
JOIN (
    SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, 1 AS RecurseLevel
    FROM tblGroups
    WHERE ParentId IS NULL
    )myCTE ON myCTE.GroupID = tblGroups.ParentID;

Этот код необходимо разбить на части, чтобы сбросить число.

Рекурсивнозапросы не работают в цикле while, они не являются процедурными.По сути, они работают как рекурсивная функция, но в зависимости от таблиц, запросов, индексов их можно оптимизировать для выполнения в ту или иную сторону.

Если мы действительно придерживаемся концепции, что представление не можетбыть объединенным при использовании аналитических функций и просмотре запроса 1. Он может выполняться только один раз, и он находится во вложенном цикле.

WITH myCTE
AS
( /*Cannot be merged*/
  SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, 1 AS RecurseLevel,
  cast(0 as bigint) n
  FROM tblGroups
  WHERE ParentId IS NULL

  UNION ALL

/*Cannot be merged*/
  SELECT tblGroups.*, 
      ROW_NUMBER() OVER (ORDER BY myCTE.RowNumber, tblGroups.Score desc) AS RowNumber,       RecurseLevel + 1 AS RecurseLevel,
  myCTE.RowNumber
  FROM tblGroups
      JOIN myCTE
          ON myCTE.GroupID = tblGroups.ParentID
 )
SELECT *
FROM myCTE;

Так что 1-й выбор не может быть объединен 2-й, ни один.Единственный способ выполнить этот запрос - это вложенный цикл для каждого элемента, возвращаемого на каждом уровне, следовательно, сброс.Опять же, это не вопрос процедурного или нет, просто вопрос возможного плана выполнения.

Надеюсь, что это ответ на ваш вопрос, позвольте мне, если это не так:)

y

...