SQL 재귀는 실제로 어떻게 작동합니까?


19

다른 프로그래밍 언어에서 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}가됩니다.

이 새로운 결과로 무엇을합니까? 기존 {3, 5, 7}에 {9, 25, 49}를 추가하고 결과 조합 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을 의미하지는 않습니다. 단지 이전 구성 요소를 의미합니다.


"R이 {3, 5, 7, 9, 25, 49}이고 다음 재귀 반복을 실행하면 {9, 25, 49, 81, 625, 2401}로 끝나고 우리는 ve는 {3, 5, 7}을 잃었습니다. " 그렇게 작동하면 {3,5,7}을 어떻게 잃을 지 모르겠습니다.
ypercubeᵀᴹ

@ yper-crazyhat-cubeᵀᴹ — 제가 제안한 첫 번째 가설을 따르고있었습니다. 즉, 중간 R이 그 시점까지 계산 된 모든 것의 축적이라면 어떨까요? 그런 다음 재귀 멤버의 다음 반복에서 R의 모든 요소가 제곱됩니다. 따라서 {3, 5, 7}은 {9, 25, 49}가되고 R에는 {3, 5, 7}이 없습니다. 즉, {3, 5, 7}은 R에서 손실됩니다.
UnLogicGuys

답변:


26

재귀 CTE에 대한 BOL 설명은 재귀 실행의 의미를 다음과 같이 설명합니다.

  1. CTE 표현식을 앵커 및 재귀 멤버로 분할하십시오.
  2. 첫 번째 호출 또는 기본 결과 세트 (T0)를 작성하는 앵커 멤버를 실행하십시오.
  3. Ti를 입력으로하고 Ti + 1을 출력으로하여 재귀 멤버를 실행합니다.
  4. 빈 세트가 반환 될 때까지 3 단계를 반복하십시오.
  5. 결과 집합을 반환합니다. 이것은 T0에서 Tn까지의 UNION ALL입니다.

따라서 각 레벨은 지금까지 누적 된 전체 결과 세트가 아닌 레벨을 입력으로 만 가지고 있습니다.

위의 방법은 논리적으로 작동합니다 . 물리적 재귀 CTE는 현재 SQL Server에서 항상 중첩 루프와 스택 스풀로 구현됩니다. 이것은 여기여기에 설명 되어 있으며 실제로 각 재귀 요소는 전체 수준이 아니라 이전 수준 의 부모 과 함께 작동한다는 것을 의미합니다 . 그러나 재귀 CTE에서 허용되는 구문에 대한 다양한 제한은이 접근 방식이 효과적이라는 것을 의미합니다.

ORDER BY쿼리에서 를 제거하면 결과는 다음과 같이 정렬됩니다.

+---------+
|    N    |
+---------+
|       3 |
|       5 |
|       7 |
|      49 |
|    2401 |
| 5764801 |
|      25 |
|     625 |
|  390625 |
|       9 |
|      81 |
|    6561 |
+---------+

실행 계획이 다음과 매우 유사하게 작동하기 때문입니다 C#

using System;
using System.Collections.Generic;
using System.Diagnostics;

public class Program
{
    private static readonly Stack<dynamic> StackSpool = new Stack<dynamic>();

    private static void Main(string[] args)
    {
        //temp table #NUMS
        var nums = new[] { 3, 5, 7 };

        //Anchor member
        foreach (var number in nums)
            AddToStackSpoolAndEmit(number, 0);

        //Recursive part
        ProcessStackSpool();

        Console.WriteLine("Finished");
        Console.ReadLine();
    }

    private static void AddToStackSpoolAndEmit(long number, int recursionLevel)
    {
        StackSpool.Push(new { N = number, RecursionLevel = recursionLevel });
        Console.WriteLine(number);
    }

    private static void ProcessStackSpool()
    {
        //recursion base case
        if (StackSpool.Count == 0)
            return;

        var row = StackSpool.Pop();

        int thisLevel = row.RecursionLevel + 1;
        long thisN = row.N * row.N;

        Debug.Assert(thisLevel <= 100, "max recursion level exceeded");

        if (thisN < 10000000)
            AddToStackSpoolAndEmit(thisN, thisLevel);

        ProcessStackSpool();
    }
}

NB1는 : 앵커 부재의 제 아이 때에는 상기와 3그 형제에 대한 모든 정보를 처리하고 5그리고 7, 그들의 자손, 이미 스풀로부터 폐기되지 더 이상 액세스 할 수있다.

NB2 : 위의 C #은 실행 계획과 동일한 전체 의미를 갖지만 연산자가 파이프 라인 된 실행 방식으로 작업하므로 실행 계획의 흐름은 동일하지 않습니다. 이는 접근 방식의 요지를 보여주는 간단한 예입니다. 계획 자체에 대한 자세한 내용은 이전 링크를 참조하십시오.

NB3 : 스택 스풀 자체는 분명히 재귀 수준의 키 열과 필요에 따라 추가 된 고유 식별자를 갖는 고유하지 않은 클러스터형 인덱스로 구현됩니다 ( source ).


6
SQL Server의 재귀 쿼리는 구문 분석 중에 항상 재귀에서 반복 (스태킹 사용)으로 변환됩니다. 반복 구현 규칙은 IterateToDepthFirst- Iterate(seed,rcsv)->PhysIterate(seed,rcsv)입니다. 참고로 훌륭한 답변입니다.
Paul White는 GoFundMonica를 말합니다

또한 UNION ALL 대신 UNION도 허용되지만 SQL Server는 그렇지 않습니다.
여호수아

5

이것은 단지 (반) 교육받은 추측이며 아마도 완전히 잘못되었을 것입니다. 그런데 재미있는 질문입니다.

T-SQL은 선언적 언어입니다. 재귀 CTE는 커서 스타일 작업으로 변환되어 UNION ALL의 왼쪽 결과가 임시 테이블에 추가되고 UNION ALL의 오른쪽이 왼쪽의 값에 적용됩니다.

따라서 먼저 UNION ALL의 왼쪽 출력을 결과 집합에 삽입 한 다음 왼쪽에 적용된 UNION ALL의 오른쪽 결과를 삽입하고 결과 집합에 삽입합니다. 그런 다음 왼쪽이 오른쪽의 출력으로 바뀌고 오른쪽이 "새"왼쪽에 다시 적용됩니다. 이 같은:

  1. {3,5,7}-> 결과 세트
  2. {3,5,7}에 적용되는 재귀 문은 {9,25,49}입니다. {9,25,49}가 결과 세트에 추가되고 UNION ALL의 왼쪽을 대체합니다.
  3. {9,25,49}에 적용되는 재귀 문은 {81,625,2401}입니다. {81,625,2401}이 결과 세트에 추가되고 UNION ALL의 왼쪽을 대체합니다.
  4. {81,625,2401}에 적용되는 재귀 명령문 ({6561,390625,5764801}) {6561,390625,5764801}이 결과 집합에 추가되었습니다.
  5. 다음 반복에서 WHERE 절이 false를 리턴하므로 커서가 완료됩니다.

재귀 CTE의 실행 계획에서이 동작을 볼 수 있습니다.

여기에 이미지 설명을 입력하십시오

이것은 위의 1 단계이며 UNION ALL의 왼쪽이 출력에 추가됩니다.

여기에 이미지 설명을 입력하십시오

출력이 결과 세트에 연결된 UNION ALL의 오른쪽입니다.

여기에 이미지 설명을 입력하십시오


4

SQL Server 설명서 언급, T를 내가 하고 T I + 1 , 둘 다 매우 이해할 수 없으며, 실제 구현에 대한 정확한 설명입니다.

기본 아이디어는 쿼리의 재귀 부분이 이전의 모든 결과를 보는 것이지만 한 번만 보는 것 입니다.

다른 데이터베이스가이를 구현하는 방법을 살펴보면 같은 결과 를 얻을 수 있습니다. 포스트 그레스 문서는 말합니다 :

재귀 쿼리 평가

  1. 비 재귀 항을 평가하십시오. 의 경우 UNION(그러나 아님 UNION ALL) 중복 행을 삭제하십시오. 재귀 쿼리 결과에 나머지 행을 모두 포함하고 임시 작업 테이블 에 배치합니다 .
  2. 작업 테이블이 비어 있지 않으면 다음 단계를 반복하십시오.
    1. 재귀 자체 참조를 위해 작업 테이블의 현재 내용을 대체하여 재귀 용어를 평가하십시오. 의 경우 UNION(이 아닌 UNION ALL) 이전 행과 중복되는 행과 행을 삭제합니다. 재귀 쿼리 결과에 나머지 행을 모두 포함하고 임시 중간 테이블 에 배치합니다 .
    2. 작업 테이블의 내용을 중간 테이블의 내용으로 바꾸고 중간 테이블을 비 웁니다.

참고
엄밀히 말하면이 프로세스는 반복이 아니라 반복입니다.RECURSIVE SQL 표준위원회에서 선택한 용어입니다.

SQLite는 문서 약간 다른 구현에서 힌트,이 한 행씩 알고리즘은 이해하기 쉬운 수 있습니다 :

재귀 테이블의 내용을 계산하는 기본 알고리즘은 다음과 같습니다.

  1. 실행 initial-select 결과를 큐에 추가하십시오.
  2. 큐가 비어 있지 않은 동안 :
    1. 큐에서 단일 행을 추출하십시오.
    2. 해당 단일 행을 재귀 테이블에 삽입하십시오.
    3. 방금 추출한 단일 행이 재귀 테이블의 유일한 행인 것으로 가정하고을 실행 recursive-select하여 모든 결과를 큐에 추가하십시오.

위의 기본 절차는 다음 추가 규칙에 의해 수정 될 수 있습니다.

  • 유니온 운영자가 접속하면 initial-select으로 recursive-select더 동일한 행이 이전에 큐에 추가하지 않은 경우는, 다음 단 큐에 행을 추가한다. 반복 된 행이 재귀 단계에 의해 큐에서 이미 추출 된 경우에도 반복 된 행은 큐에 추가되기 전에 삭제됩니다. 연산자가 UNION ALL이면 initial-selectand 및에 의해 생성 된 모든 행 recursive-select이 반복 되더라도 항상 큐에 추가됩니다.
    […]

0

내 지식은 특히 DB2에 있지만 Explain 다이어그램을 보는 것은 SQL Server와 동일한 것으로 보입니다.

계획은 여기에서 온다 :

붙여 넣기 계획에서 확인하십시오

SQL Server 설명 계획

옵티마이 저는 각 재귀 쿼리마다 문자 그대로 통합을 실행하지 않습니다. 쿼리의 구조를 취하고 Union의 첫 번째 부분을 모두 "앵커 멤버"에 할당 한 다음 Union의 두 번째 절반 ( "재귀 멤버"라고 함)이 정의 된 제한에 도달 할 때까지 재귀 적으로 실행합니다. 재귀가 완료되면 옵티마이 저는 모든 레코드를 결합합니다.

최적화 프로그램은 미리 정의 된 작업을 수행하기위한 제안으로 사용합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.