재귀 공통 테이블 표현식에서 EXCEPT 사용


33

다음 쿼리가 무한 행을 반환하는 이유는 무엇입니까? 나는 그 EXCEPT절이 재귀를 끝내기를 기대했을 것이다 .

with cte as (
    select *
    from (
        values(1),(2),(3),(4),(5)
    ) v (a)
)
,r as (
    select a
    from cte
    where a in (1,2,3)
    union all
    select a
    from (
        select a
        from cte
        except
        select a
        from r
    ) x
)
select a
from r

Stack Overflow에 대한 질문 에 답변하려고 하면서이 문제가 발생 했습니다.

답변:


26

재귀 CTE 의 현재 상태에 대한 내용은 Martin Smith의 답변 을 참조하십시오 EXCEPT.

보고있는 내용과 이유를 설명하려면

여기서 앵커 값과 재귀 항목을 명확하게 구분하기 위해 테이블 ​​변수를 사용하고 있습니다 (시맨틱은 변경하지 않음).

DECLARE @V TABLE (a INTEGER NOT NULL)
INSERT  @V (a) VALUES (1),(2)
;
WITH rCTE AS 
(
    -- Anchor
    SELECT
        v.a
    FROM @V AS v

    UNION ALL

    -- Recursive
    SELECT
        x.a
    FROM
    (
        SELECT
            v2.a
        FROM @V AS v2

        EXCEPT

        SELECT
            r.a
        FROM rCTE AS r
    ) AS x
)
SELECT
    r2.a
FROM rCTE AS r2
OPTION (MAXRECURSION 0)

쿼리 계획은 다음과 같습니다.

재귀 CTE 계획

실행은 계획의 루트 (SELECT)에서 시작되며 제어는 트리를 인덱스 스풀, 연결 및 최상위 테이블 스캔으로 전달합니다.

스캔의 첫 번째 행은 트리를 통과하며 (a) 스택 스풀에 저장되고 (b) 클라이언트로 반환됩니다. 어떤 행이 먼저 정의되지 않았지만 인수를 위해 값이 {1} 인 행이라고 가정하겠습니다. 따라서 첫 번째 행은 {1}입니다.

제어는 다시 테이블 스캔으로 전달됩니다 (연결 연산자는 다음 행을 열기 전에 가장 바깥 쪽 입력에서 모든 행을 소비합니다). 스캔은 두 번째 행 (값 {2})을 내보내고 스택에 저장 될 트리를 다시 전달하여 클라이언트로 출력합니다. 클라이언트는 이제 {1}, {2} 시퀀스를 수신했습니다.

LIFO 스택의 상단이 왼쪽에있는 규칙을 채택하면 스택에 {2, 1}이 포함됩니다. 제어가 다시 테이블 스캔으로 전달되면 더 이상 행을보고하지 않고 제어는 연결 연산자로 다시 전달되어 두 번째 입력 (스레드 스풀로 전달하려면 행이 필요함)을 열고 제어는 내부 결합으로 전달됩니다. 처음으로.

내부 결합은 외부 입력에서 테이블 스풀을 호출하여 스택 {2}에서 맨 위 행을 읽고 작업 테이블에서 삭제합니다. 스택은 이제 {1}을 포함합니다.

외부 입력에서 행을 수신 한 내부 결합은 내부 입력을 LASJ (Left Anti-Semi Join)로 전달합니다. 그러면 외부 입력에서 행을 요청하여 제어를 정렬로 전달합니다. 정렬은 블로킹 반복기이므로 테이블 변수에서 모든 행을 읽고 오름차순으로 정렬합니다 (발생하는대로).

따라서 Sort에서 생성 된 첫 번째 행은 값 {1}입니다. LASJ의 내부는 재귀 멤버의 현재 값 (스택에서 튀어 나온 값) 인 {2}를 리턴합니다. LASJ의 값은 {1} 및 {2}이므로 값이 일치하지 않으므로 {1}이 생성됩니다.

이 행 {1}은 쿼리 계획 트리를 인덱스 (스택) 스풀로 플로우하여 스택에 추가되며 이제 스택에 추가되며 {1, 1}이 (가) 포함되어 클라이언트로 방출됩니다. 클라이언트는 이제 {1}, {2}, {1} 시퀀스를 수신했습니다.

제어는 이제 연결로 돌아가서 내부 쪽을 다시 내려갑니다 (마지막으로 행을 반환하고 다시 시도 할 수도 있음). 내부 조인을 통해 LASJ로 내려갑니다. 내부 입력을 다시 읽고 정렬에서 {2} 값을 얻습니다.

재귀 멤버는 여전히 {2}이므로 LASJ는 {2} 및 {2}를 찾아서 행이 생성되지 않습니다. 내부 입력에서 더 이상 행을 찾지 못하면 (정렬이 이제 행을 벗어남) 제어는 내부 결합으로 다시 전달됩니다.

내부 조인이 외부 입력을 읽고 결과적으로 {1} 값이 스택 {1, 1}에서 튀어 나와 스택이 {1} 만 남습니다. 이제 LASJ 테스트를 통과하고 스택에 추가되고있는 테이블 스캔 및 정렬의 새로운 호출에서 값 {2}로 프로세스가 반복되어 {1}, {2}을 (를) 수신 한 클라이언트로 전달됩니다. {1}, {2} ... 그리고 우리는 간다.

재귀 CTE 계획에 사용 된 스택 스풀에 대해 제가 가장 좋아하는 설명 은 Craig Freedman입니다.


31

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

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

위의 내용은 논리적 설명입니다. 실제 작업 순서는 여기에 설명 된대로 약간 다를 수 있습니다.

이것을 CTE에 적용하면 다음 패턴의 무한 루프가 예상됩니다.

+-----------+---------+---+---+---+
| Invocation| Results             |
+-----------+---------+---+---+---+
|         1 |       1 | 2 | 3 |   |
|         2 |       4 | 5 |   |   |
|         3 |       1 | 2 | 3 |   |
|         4 |       4 | 5 |   |   |
|         5 |       1 | 2 | 3 |   |
+-----------+---------+---+---+---+ 

때문에

select a
from cte
where a in (1,2,3)

앵커 식입니다. 이 분명히 반환 1,2,3T0

그 후 재귀 표현식이 실행됩니다.

select a
from cte
except
select a
from r

으로 1,2,3의 출력을 보장 할뿐만 입력 4,5으로서 T1다음 반환 재귀의 다음 라운드에서 그 뒷면을 연결 1,2,3등 무기한.

그러나 이것은 실제로 일어나지 않습니다. 처음 5 번의 호출 결과입니다

+-----------+---------+---+---+---+
| Invocation| Results             |
+-----------+---------+---+---+---+
|         1 |       1 | 2 | 3 |   |
|         2 |       1 | 2 | 4 | 5 |
|         3 |       1 | 2 | 3 | 4 |
|         4 |       1 | 2 | 3 | 5 |
|         5 |       1 | 2 | 3 | 4 |
+-----------+---------+---+---+---+

사용 OPTION (MAXRECURSION 1)및 상향 조정을 1통해 각 연속 레벨이 지속적으로 출력 1,2,3,4과 사이를 전환하는 사이클에 진입 함 을 알 수 있습니다 1,2,3,5.

이 블로그 게시물 에서 @Quassnoi 가 논의한대로 . 관찰 된 결과의 패턴은 각 호출이 이전 호출의 마지막 행 이있는 곳 과 같습니다.(1),(2),(3),(4),(5) EXCEPT (X)X

편집 : SQL Kiwi의 훌륭한 대답을 읽은 후에 는 이것이 왜 발생하는지, 그리고 처리 할 수없는 스택에 여전히 많은 양의 물건이 남아 있다는 점에서 이것이 전체 이야기가 아님을 분명히 알 수 있습니다.

앵커 1,2,3는 클라이언트 스택 컨텐츠로 방출 됩니다.3,2,1

스택에서 3이 튀어 나옴, 스택 내용 2,1

LASJ가 1,2,4,5스택 내용을 반환합니다 .5,4,2,1,2,1

스택에서 5 개가 튀어 나옴, 스택 내용 4,2,1,2,1

LASJ는 1,2,3,4 스택 내용을 반환4,3,2,1,5,4,2,1,2,1

스택 4 개가 튀어 나옴, 스택 내용 3,2,1,5,4,2,1,2,1

LASJ는 1,2,3,5 스택 내용을 반환5,3,2,1,3,2,1,5,4,2,1,2,1

스택에서 5 개가 튀어 나옴, 스택 내용 3,2,1,3,2,1,5,4,2,1,2,1

LASJ는 1,2,3,4 스택 내용을 반환4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1

재귀 멤버를 논리적으로 동등한 (중복 / NULL이없는 경우) 식으로 바꾸려고하면

select a
from (
    select a
    from cte
    where a not in 
    (select a
    from r)
) x

이것은 허용되지 않으며 "하위 쿼리에서 재귀 참조는 허용되지 않습니다"라는 오류를 발생시킵니다. 따라서이 EXCEPT경우에도 허용 되는 것은 감독입니다 .

추가 : Microsoft는 이제 다음과 같이 내 연결 피드백에 응답했습니다.

Jack 의 추측은 맞습니다. 이것은 구문 오류 였을 것입니다. 재귀 참조는 실제로 EXCEPT절 에서 허용되지 않아야 합니다. 향후 서비스 릴리스에서이 버그를 해결할 계획입니다. 한편, EXCEPT 절 에서 재귀 참조를 피하는 것이 좋습니다 .

재귀를 제한 할 EXCEPT때 우리는 재귀가 도입 된 이후 (1999 년에 믿었던) ANSI SQL 표준을 따릅니다. EXCEPTSQL과 같은 선언적 언어에서 재귀를 위해 의미론이 무엇이어야하는지 ( "계층화되지 않은 부정"이라고도 함)에 대한 광범위한 동의는 없습니다 . 또한 RDBMS 시스템에서 이러한 의미를 효율적으로 (합리적인 크기의 데이터베이스에) 구현하는 것은 매우 어렵습니다 (불가능하지 않은 경우).

호환성 수준이 120 이상인 데이터베이스 대해 2014 년에 최종 구현 된 것처럼 보입니다 .

EXCEPT 절의 재귀 참조는 ANSI SQL 표준을 준수하는 오류를 생성합니다.

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