이것은 의도하지 않은 행동처럼 보입니다. 카디널리티 예상은 계획의 각 단계에서 일관 될 필요는 없지만 이것은 비교적 간단한 쿼리 계획이며 최종 카디널리티 추정은 쿼리가 수행하는 작업과 일치하지 않습니다. 이와 같은 카디널리티 추정치가 낮아지면 더 복잡한 계획에서 다른 테이블 다운 스트림에 대한 조인 유형 및 액세스 방법을 잘못 선택할 수 있습니다.
시행 착오를 통해 문제가 나타나지 않는 몇 가지 유사한 쿼리를 얻을 수 있습니다.
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT -1)
END AS ID2
FROM dbo.X_HEAP;
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END AS ID2
FROM dbo.X_HEAP;
또한 문제가 나타나는 더 많은 쿼리를 얻을 수 있습니다.
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE)
END AS ID2
FROM dbo.X_HEAP;
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT -1)
END AS ID2
FROM dbo.X_HEAP;
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END AS ID2
FROM dbo.X_HEAP;
패턴이있는 것으로 보입니다. CASE
실행될 것으로 예상되지 않는 표현식이 있고 결과 표현식이 테이블에 대한 서브 쿼리 인 경우 행 추정값은 해당 표현식 다음에 1로 떨어집니다.
클러스터 된 인덱스가있는 테이블에 대해 쿼리를 작성하면 규칙이 약간 변경됩니다. 동일한 데이터를 사용할 수 있습니다 :
CREATE TABLE dbo.X_CI (ID INT NOT NULL, PRIMARY KEY (ID))
INSERT INTO dbo.X_CI WITH (TABLOCK)
SELECT * FROM dbo.X_HEAP;
UPDATE STATISTICS X_CI WITH FULLSCAN;
이 쿼리에는 1000 행의 최종 예상치가 있습니다.
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
END
FROM dbo.X_CI;
그러나이 쿼리에는 1 행의 최종 예상치가 있습니다.
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END
FROM dbo.X_CI;
이것을 더 자세히 파기 위해 문서화되지 않은 추적 플래그 2363을 사용할 수 있습니다 을 하여 쿼리 최적화 프로그램이 선택성 계산을 수행 한 방법에 대한 정보를 얻을 수 있습니다. 해당 추적 플래그를 문서화되지 않은 추적 플래그 8606 과 함께 사용하면 도움이됩니다 . TF 2363은 프로젝트 정규화 후 단순화 된 트리와 트리 모두에 대해 선택성 계산을 제공하는 것으로 보입니다. 두 추적 플래그를 모두 사용하면 어떤 계산이 어떤 트리에 적용되는지 명확하게 알 수 있습니다.
질문에 게시 된 원래 쿼리에 대해 시도해보십시오.
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2)
END AS ID2
FROM X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
다음은 일부 의견과 관련이 있다고 생각되는 출력 부분의 일부입니다.
Plan for computation:
CSelCalcColumnInInterval -- this is the type of calculator used
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID -- this is the column used for the calculation
Pass-through selectivity: 0 -- all rows are expected to have a true value for the case expression
Stats collection generated:
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- the row estimate after the join will still be 1000
CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)
...
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 1 -- no rows are expected to have a true value for the case expression
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter) -- the row estimate after the join will still be 1
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- here is the row estimate after the previous join
CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: X_OTHER_TABLE_2)
이제 문제가없는 유사한 쿼리를 사용해 봅시다. 나는 이것을 사용할 것입니다 :
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT -1)
END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
맨 끝에 디버그 출력 :
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 1
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollConstTable(ID=4, CARD=1) -- this is different than before because we select a constant instead of from a table
잘못된 행 추정치가있는 다른 쿼리를 시도해 보겠습니다.
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE)
END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
마지막에는 카디널리티 예상이 통과 선택성 = 1 후에 다시 1 행으로 떨어집니다. 카디널리티 추정은 0.501 및 0.499의 선택 후에 유지됩니다.
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 0.501
...
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 0.499
...
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 1
Stats collection generated:
CStCollOuterJoin(ID=12, CARD=1 x_jtLeftOuter) -- this is associated with the ELSE expression
CStCollOuterJoin(ID=11, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=10, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
CStCollBaseTable(ID=4, CARD=1 TBL: X_OTHER_TABLE)
문제가없는 다른 유사한 쿼리로 다시 전환 해 봅시다. 나는 이것을 사용할 것입니다 :
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
디버그 출력에는 패스 스루 선택도가 1 인 단계가 없습니다. 카디널리티 예상치는 1000 행으로 유지됩니다.
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 0.499
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
End selectivity computation
클러스터 된 인덱스가있는 테이블과 관련된 쿼리는 어떻습니까? 행 추정 문제가있는 다음 쿼리를 고려하십시오.
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
디버그 출력의 끝은 이미 본 것과 비슷합니다.
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_CI].ID
Pass-through selectivity: 1
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
그러나 문제가없는 CI에 대한 쿼리의 결과는 다릅니다. 이 쿼리를 사용하여 :
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
다른 계산기가 사용됩니다. CSelCalcColumnInInterval
더 이상 나타나지 않습니다 :
Plan for computation:
CSelCalcFixedFilter (0.559)
Pass-through selectivity: 0.559
Stats collection generated:
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
...
Plan for computation:
CSelCalcUniqueKeyFilter
Pass-through selectivity: 0.001
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE)
결론적으로, 우리는 다음과 같은 조건에서 하위 쿼리 이후에 잘못된 행 추정치를 얻는 것으로 보입니다.
그만큼 CSelCalcColumnInInterval
선택성 계산이 사용된다. 나는 이것이 언제 사용되는지 정확히 알지 못하지만 기본 테이블이 힙일 때 훨씬 더 자주 나타납니다.
통과 선택도 = 1입니다. 즉, CASE
식 중 하나가 모든 행에 대해 거짓으로 평가 될 것으로 예상됩니다. 첫 번째 경우 중요하지 않습니다CASE
표현식이 모든 행에 대해 true로 평가 .
에 외부 조인이 CStCollBaseTable
있습니다. 다시 말해CASE
결과 표현식은 테이블에 대한 하위 쿼리입니다. 상수 값이 작동하지 않습니다.
이러한 조건 하에서 쿼리 최적화 프로그램은 의도하지 않게 통과 루프 선택성을 중첩 루프의 내부에서 수행 된 작업 대신 외부 테이블의 행 추정값에 적용합니다. 그러면 행 추정값이 1로 줄어 듭니다.
두 가지 해결 방법을 찾을 수있었습니다. APPLY
하위 쿼리 대신 사용할 때 문제를 재현 할 수 없었습니다 . 추적 플래그 2363의 출력은와 매우 다릅니다 APPLY
. 질문에서 원래 쿼리를 다시 작성하는 방법은 다음과 같습니다.
SELECT
h.ID
, a.ID2
FROM X_HEAP h
OUTER APPLY
(
SELECT CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2)
END
) a(ID2);
레거시 CE도이 문제를 피하는 것으로 보입니다.
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2)
END AS ID2
FROM X_HEAP
OPTION (USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'));
연결 항목 (폴 화이트는 그의 대답에서 제공하는 세부 사항의 일부)이 문제에 대한 제출되었습니다.