하위 쿼리가 행 추정치를 1로 줄이는 이유는 무엇입니까?


26

다음과 같이 고안되었지만 간단한 쿼리를 고려하십시오.

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;

이 쿼리의 최종 행 추정치가 X_HEAP테이블 의 행 수와 같을 것으로 예상 합니다. 하위 쿼리에서 수행하는 작업은 행을 필터링 할 수 없으므로 행 추정에 중요하지 않습니다. 그러나 SQL Server 2016에서는 하위 쿼리로 인해 행 예상이 1로 감소했습니다.

나쁜 질문

왜 이런 일이 발생합니까? 그것에 대해 무엇을 할 수 있습니까?

올바른 구문으로이 문제를 재현하는 것은 매우 쉽습니다. 이를 수행 할 테이블 정의 세트는 다음과 같습니다.

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL)
CREATE TABLE dbo.X_OTHER_TABLE (ID INT NOT NULL);
CREATE TABLE dbo.X_OTHER_TABLE_2 (ID INT NOT NULL);

INSERT INTO dbo.X_HEAP WITH (TABLOCK)
SELECT TOP (1000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values;

CREATE STATISTICS X_HEAP__ID ON X_HEAP (ID) WITH FULLSCAN;

db 바이올린 링크 .

답변:


22

이 카디널리티 추정 (CE) 문제는 다음과 같은 경우에 나타납니다.

  1. 조인은 통과 술어 가있는 외부 조인입니다.
  2. 통과 술어 의 선택성정확히 1로 추정됩니다 .

참고 : 선택도를 결정하는 데 사용되는 특정 계산기는 중요하지 않습니다.


세부

CE는 외부 결합의 선택성을 다음의 으로 계산합니다 .

  • 내측 가입 동일한 술어 선택성
  • 안티 조인 같은 술어 선택성

외부 조인과 내부 조인의 유일한 차이점은 외부 조인이 조인 술어에서 일치하지 않는 행을 리턴한다는 것입니다. 앤티 조인은이 차이를 정확하게 제공합니다. 내부 및 안티 결합에 대한 카디널리티 추정은 외부 결합보다 직접적으로 쉽습니다.

결합 선택성 평가 프로세스는 매우 간단합니다.

  • 먼저 통과 술어 의 선택성 이 평가됩니다. SPT
    • 이는 상황에 적합한 계산기를 사용하여 수행됩니다.
    • 술어는 부정 IsFalseOrNull요소를 포함한 모든 것 입니다.
  • 내부 조인 선택도 : = 1 - SPT
  • 안티 조인 선택도 : = SPT

앤티 조인은 조인을 '통과'할 행을 나타냅니다. 내부 조인은 '통과하지 않는'행을 나타냅니다. '통과'는 내부를 전혀 실행하지 않고 결합을 통해 흐르는 행을 의미합니다. 강조하기 위해 : 모든 행은 조인에 의해 반환되며, 구분은 새로 등장하기 전에 조인의 내부를 실행하는 행과 그렇지 않은 행 사이에 있습니다.

분명히에 추가 하면 항상 총 선택성이 1 이 되어야합니다. 즉, 예상대로 모든 행이 조인에 의해 반환됩니다.1 - SPTSPT

실제로, 위의 계산은 1제외한 모든 값에 대해 설명한대로 정확하게 작동합니다 .SPT

경우 = 1, 모두 내부 조인 안티 조인 선택성은 하나 개의 열의 ((가) 전체 조인) 카디널리티 추정 결과 제로로 추정된다. 내가 알 수있는 한, 이것은 의도하지 않은 것이며 버그로보고해야합니다.SPT


관련 문제

이 버그는 별도의 CE 제한으로 인해 생각보다 생각하기 쉽습니다. 이것은 CASE표현식이 EXISTS절을 사용할 때 발생합니다 (공통). 예를 들어 질문에서 다음과 같이 수정 된 쿼리 에 예기치 않은 카디널리티 예상치가 발생 하지 않습니다 .

-- This is fine
SELECT 
    CASE
        WHEN XH.ID = 1
        THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT) 
    END
FROM dbo.X_HEAP AS XH;

사소한 것을 소개 EXISTS하면 문제가 드러납니다.

-- This is not fine
SELECT 
    CASE
        WHEN EXISTS (SELECT 1 WHERE XH.ID = 1)
        THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT) 
    END
FROM dbo.X_HEAP AS XH;

를 사용 EXISTS하면 실행 계획에 세미 조인 (강조 표시)이 도입됩니다.

세미 가입 계획

세미 조인의 추정치는 좋습니다. 문제는 CE가 고정 된 선택성을 1로하여 관련 프로브 컬럼을 간단한 프로젝션으로 취급한다는 것입니다.

Semijoin with probe column treated as a Project.

Selectivity of probe column = 1

이는 EXISTS조항 의 내용에 관계없이이 CE 문제를 명시하는 데 필요한 조건 중 하나를 자동으로 충족시킵니다 .


중요한 배경 정보 는 Craig Freedman의 표현식의 하위 쿼리를CASE 참조하십시오 .


22

이것은 의도하지 않은 행동처럼 보입니다. 카디널리티 예상은 계획의 각 단계에서 일관 될 필요는 없지만 이것은 비교적 간단한 쿼리 계획이며 최종 카디널리티 추정은 쿼리가 수행하는 작업과 일치하지 않습니다. 이와 같은 카디널리티 추정치가 낮아지면 더 복잡한 계획에서 다른 테이블 다운 스트림에 대한 조인 유형 및 액세스 방법을 잘못 선택할 수 있습니다.

시행 착오를 통해 문제가 나타나지 않는 몇 가지 유사한 쿼리를 얻을 수 있습니다.

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)

결론적으로, 우리는 다음과 같은 조건에서 하위 쿼리 이후에 잘못된 행 추정치를 얻는 것으로 보입니다.

  1. 그만큼 CSelCalcColumnInInterval선택성 계산이 사용된다. 나는 이것이 언제 사용되는지 정확히 알지 못하지만 기본 테이블이 힙일 때 훨씬 더 자주 나타납니다.

  2. 통과 선택도 = 1입니다. 즉, CASE식 중 하나가 모든 행에 대해 거짓으로 평가 될 것으로 예상됩니다. 첫 번째 경우 중요하지 않습니다CASE 표현식이 모든 행에 대해 true로 평가 .

  3. 에 외부 조인이 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);

좋은 쿼리 1

레거시 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'));

좋은 쿼리 2

연결 항목 (폴 화이트는 그의 대답에서 제공하는 세부 사항의 일부)이 문제에 대한 제출되었습니다.

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