SQL Server에서 예측할 수없는 선택 결과 (dbms 오류?)


37

아래는 예상치 못한 이상한 결과를 반환하여 팀에서 설명 할 수없는 간단한 예입니다. 우리가 잘못하고 있거나 SQL Server 오류입니까?

조사 후 subquery에서 검색 영역을 union 절로 줄였습니다.이 쿼리 는 "men"테이블에서 하나의 레코드를 선택합니다.

SQL Server 2000에서는 예상대로 작동하지만 (12 행 반환) 2008 년과 2012 년에는 1 행만 반환합니다.

create table dual (dummy int)

insert into dual values (0)

create table men (
man_id int,
wife_id int )

-- there are 12 men, 6 married 
insert into men values (1, 1)
insert into men values (2, 2)
insert into men values (3, null)
insert into men values (4, null)
insert into men values (5, null)
insert into men values (6, 3)
insert into men values (7, 5)
insert into men values (8, 7)
insert into men values (9, null)
insert into men values (10, null)
insert into men values (11, null)
insert into men values (12, 9)

한 행만 반환합니다. 1 1 2

select 
man_id,
wife_id,
(select count( * ) from 
    (select dummy from dual
     union select men.wife_id  ) family_members
) as family_size
from men
--where wife_id = 2 -- uncomment me and try again

마지막 줄을 주석 해제하면 다음과 같이 나타납니다. 2 2 2

이상한 행동이 많이 있습니다.

  • 일련의 드랍, "맨"테이블에서 생성, 절단 및 삽입 후 때때로 작동 함 (12 행 반환)
  • "union select men.wife_id"를 "union all select men.wife_id"또는 "union select isnull (men.wife_id, null)"(!!!)로 변경하면 예상대로 12 개의 행 이 반환 됩니다.
  • 이상한 동작은 "wife_id"열의 데이터 유형과 관련이없는 것 같습니다. 우리는 훨씬 더 큰 데이터 세트를 가진 개발 시스템에서이를 관찰했습니다.
  • "where wife_id> 0"은 6 행을 반환합니다
  • 우리는 또한 이런 종류의 진술로 이상한 행동을 관찰합니다. SELECT *는 행의 하위 집합을 반환하고 SELECT TOP 1000은 모두를 반환

답변:


35

우리가 잘못하고 있거나 SQL Server 오류입니까?

일반적인 지원 채널을 통해보고해야하는 잘못된 결과 버그입니다. 지원 계약이없는 경우 Microsoft에서 해당 동작을 버그로 확인하면 유료 사건 이 정상적으로 환불 된다는 것을 알면 도움이 될 수 있습니다 .

이 버그에는 세 가지 성분이 필요합니다.

  1. 외부 참조가있는 중첩 루프 (적용)
  2. 외부 참조를 찾는 내부 측 게으른 인덱스 스풀
  3. 내부 연결 연산자

예를 들어 질문의 쿼리는 다음과 같은 계획을 생성합니다.

주석이 달린 계획

이러한 요소 중 하나를 제거하는 방법에는 여러 가지가 있으므로 버그가 더 이상 재현되지 않습니다.

예를 들어 옵티마이 저가 Lazy Index Spool을 사용하지 않도록 선택하는 인덱스 또는 통계를 작성할 수 있습니다. 또는 연결을 사용하는 대신 힌트를 사용하여 해시 또는 병합 통합을 강제 할 수 있습니다. 같은 의미론을 표현하기 위해 쿼리를 다시 작성할 수도 있지만 하나 이상의 필수 요소가 누락 된 다른 계획 형태가됩니다.

자세한 내용은

Lazy Index Spool은 외부 참조 (상관 된 매개 변수) 값으로 인덱스 된 작업 테이블에서 내부 결과 행을 느리게 캐시합니다. Lazy Index Spool에 이전에 본 외부 참조가 요청되면 작업 테이블 ( "되감기")에서 캐시 된 결과 행을 페치합니다. 스풀이 이전에 보지 못한 외부 참조 값을 요청하면 현재 외부 참조 값으로 서브 트리를 실행하고 결과 ( "리 바인드")를 캐시합니다. Lazy Index Spool의 seek 술어는 해당 작업 테이블의 키를 나타냅니다.

스풀이 새 외부 참조가 이전에 본 참조와 같은지 확인하면이 특정 계획 형태에서 문제가 발생합니다. Nested Loops Join은 외부 참조를 올바르게 업데이트하고 PrepRecompute인터페이스 메소드 를 통해 내부 입력에 대해 운영자에게 알립니다 . 이 검사를 시작할 때 내부 연산자는 CParamBounds:FNeedToReload속성을 읽어 외부 참조가 마지막 시간에서 변경되었는지 확인합니다. 스택 추적 예제는 다음과 같습니다.

CParamBounds : FNeedToReload

위에 표시된 하위 트리, 특히 연결이 사용되는 하위 트리가 존재 CParamBounds:FNeedToReload하는 경우 외부 참조가 실제로 변경되었는지 여부에 관계없이 바인딩이 잘못되어 (아마도 ByVal / ByRef / Copy 문제) 바인딩이 잘못됩니다.

동일한 서브 트리가 존재하지만 Merge Union 또는 Hash Union이 사용되는 경우이 필수 특성은 각 반복마다 올바르게 설정되며 Lazy Index Spool은 매번 적절하게 되감거나 리 바인드합니다. 그런데 Distinct Sort와 Stream Aggregate는 흠이 없습니다. 병합과 해시 유니온은 이전 값의 복사본을 만드는 반면 연결은 참조를 사용합니다. 불행히도 SQL Server 소스 코드에 액세스하지 않고는이를 확인할 수 없습니다.

결과적으로 문제가있는 계획 형태의 Lazy Index Spool은 항상 현재 외부 참조를 이미보고 있다고 생각하고 작업 테이블을 탐색하여 되감기하고 일반적으로 아무것도 찾지 않으므로 해당 외부 참조에 대해 행이 리턴되지 않습니다. 디버거에서 실행을 단계별로 실행하면 스풀은 해당 RewindHelper메소드 만 실행하고 메소드는 실행 하지 않습니다 ReloadHelper(이 컨텍스트에서 reload = rebind). 스풀 아래의 연산자는 모두 '실행 횟수 = 1'이므로 실행 계획에서 분명합니다.

되감기 도우미

물론 Lazy Index Spool이 제공되는 첫 번째 외부 참조는 예외입니다. 이것은 항상 서브 트리를 실행하고 작업 테이블에 결과 행을 캐시합니다. 이후의 모든 반복은 되감기를 초래하며, 현재 반복이 외부 참조에 대해 처음으로 동일한 값을 가질 때 행 (단일 캐시 된 행) 만 생성합니다.

따라서 Nested Loops Join의 바깥쪽에 지정된 입력 세트에 대해 쿼리는 처리 된 첫 번째 행의 복제본 수 (물론 첫 번째 행 자체에 대한 행 수)만큼 많은 행을 반환합니다.

데모

테이블 및 샘플 데이터 :

CREATE TABLE #T1 
(
    pk integer IDENTITY NOT NULL,
    c1 integer NOT NULL,

    CONSTRAINT PK_T1
    PRIMARY KEY CLUSTERED (pk)
);
GO
INSERT #T1 (c1)
VALUES
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6);

다음 (사소한) 쿼리는 Merge Union을 사용하여 각 행 (총 18 개)에 대해 올바른 2 카운트를 생성합니다.

SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY 
(
    SELECT COUNT_BIG(*) AS c1
    FROM
    (
        SELECT T1.c1
        UNION
        SELECT NULL
    ) AS U
) AS C;

합병 계획

이제 연결을 강제하기 위해 쿼리 힌트를 추가하면 :

SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY 
(
    SELECT COUNT_BIG(*) AS c1
    FROM
    (
        SELECT T1.c1
        UNION
        SELECT NULL
    ) AS U
) AS C
OPTION (CONCAT UNION);

실행 계획에는 문제가 있습니다.

연결 계획

그리고 결과는 이제 세 줄로 잘못되었습니다.

3 열 결과

이 동작이 보장되지는 않지만 클러스터형 인덱스 스캔의 첫 번째 행의 c1값은 1입니다.이 값을 가진 두 개의 다른 행이 있으므로 총 3 개의 행이 생성됩니다.

이제 데이터 테이블을 자르고 더 많은 '첫 번째'행과 함께로드하십시오.

TRUNCATE TABLE #T1;

INSERT #T1 (c1)
VALUES
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6),
    (1), (1), (1), (1), (1), (1);

이제 연결 계획은 다음과 같습니다.

8 열 연결 계획

표시된 바와 같이 c1 = 1물론 8 행이 생성됩니다 .

8 행 결과

이 버그에 대해 Connect 항목을 열었 지만 실제로는 프로덕션에 영향을 미치는 문제를보고 할 수있는 곳이 아닙니다. 이 경우 Microsoft 지원 부서에 문의하십시오.


이 잘못된 결과 버그는 일부 단계에서 수정되었습니다. 2012 년부터는 더 이상 모든 버전의 SQL Server에서 재생산되지 않습니다. SQL Server 2008 R2 SP3-GDR 빌드 10.50.6560.0 (X64)에서 재현됩니다.


-3

from 문없이 하위 쿼리를 사용하는 이유는 무엇입니까? 2005 년과 2008 년 서버에서 차이가 발생할 수 있다고 생각합니다. 어쩌면 당신은 명시 적 조인으로 갈 수 있습니까?

select 
m1.man_id,
m1.wife_id,
(select count( * ) from 
    (select dummy from dual
     union
     select m2.wife_id
     from men m2
     where m2.man_id = m1.man_id) family_members
) as family_size
from men m1

3
예, 작동하지만 내 버전도 작동합니다. 위의 추상 예제는 프로덕션 쿼리의 단순화 된 버전으로 훨씬 더 의미가 있습니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.