12

昨日、再帰的な CTE に回答したところ、これらが SQL Server に実装される方法に関する問題が明らかになりました (おそらく他の RDBMS でも?)。基本的にROW_NUMBER、現在の再帰レベルに対して使用しようとすると、現在の再帰レベルの各行サブセットに対して実行されます。これが真の SET ロジックで機能し、現在の再帰レベル全体に対して実行されることを期待します。

この MSDN の記事から、私が見つけた問題は意図された機能であると思われます。

CTE の再帰部分の分析関数と集計関数は、CTE のセットではなく、現在の再帰レベルのセットに適用されます。ROW_NUMBER などの関数は、現在の再帰レベルによって渡されたデータのサブセットに対してのみ動作し、 CTE の再帰部分に渡されたデータのセット全体に対しては動作しません。詳しくは、J. 再帰 CTE での分析関数の使用を参照してください。

私の掘り下げでは、なぜこれがそのように動作するように選択されたのかを説明する場所を見つけることができませんでした? これは、セットベースの言語での手続き型アプローチに近いため、SQL の思考プロセスに反して動作し、非常に混乱を招くと思います。再帰CTEが再帰レベルで手続き型の方法で分析関数を扱う理由を誰かが知っている、または誰かが説明できますか?


これを視覚化するのに役立つコードは次のとおりです。

RowNumberこれらのコード出力のそれぞれの列に注意してください。

これはCTEのSQLFiddleです(再帰の第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;

これは、CTEが行うことを期待する2番目のSQLFiddleです(問題を表示するには、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;
4

1 に答える 1

1

分析関数は、解決するために既知の結果セットが必要であるという点で特別です。それらは、現在の値を計算するために、次、前、または完全な結果セットに依存します。ただし、分析関数を含むビューでは、ビューのマージは許可されません。なんで?それによって結果が変わります。

元:

    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;

それは (Same execution plan and result) のように書かれたかのように正確に動作します:

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;

したがって、最初の選択は、2番目にマージすることもできません。このクエリを実行する唯一の方法は、各レベルで返される各アイテムのネストされたループであるため、リセットされます。繰り返しますが、これは手続き上の問題ではなく、可能な実行計画の問題です。

これがあなたの質問に答えてくれることを願っています。そうでない場合は教えてください:)

y

于 2012-04-14T14:43:23.770 に答える