마스터 / 디테일 테이블 간의 해시 조인으로 카디널리티 추정이 너무 낮습니다.


9

마스터 테이블을 세부 테이블에 조인 할 때 SQL Server 2014에서 더 큰 (자세한) 테이블의 카디널리티 예상을 조인 출력의 카디널리티 예상으로 사용하도록하려면 어떻게해야합니까?

예를 들어 10K 마스터 행을 100K 세부 정보 행에 조인 할 때 SQL Server가 예상 세부 정보 행 수와 같은 100K 행의 조인을 추정하려고합니다. 모든 세부 정보 행에 항상 해당 마스터 행이 있다는 사실을 SQL Server 견적자가 활용하는 데 도움이되도록 쿼리 및 / 또는 테이블 및 / 또는 인덱스를 어떻게 구성해야합니까? (그들 사이의 조인은 카디널리티 추정을 줄이지 않아야 함을 의미합니다.)

자세한 내용은 다음과 같습니다. 데이터베이스에는 마스터 / 세부 테이블 쌍이 있습니다. VisitTarget각 판매 트랜잭션마다 VisitSale하나의 행이 있고 각 트랜잭션에서 각 제품마다 하나의 행이 있습니다. 일대 다 관계입니다. 평균 10 개의 VisitSale 행에 대해 하나의 VisitTarget 행입니다.

테이블은 다음과 같습니다. (이 질문의 관련 열로 단순화하고 있습니다)

-- "master" table
CREATE TABLE VisitTarget
(
  VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  SaleDate date NOT NULL,
  StoreId int NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate 
    ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);

-- "detail" table
CREATE TABLE VisitSale
(
  VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  VisitTargetId int NOT NULL,
  SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
  StoreId int NOT NULL, -- denormalized; copied from VisitTarget
  ItemId int NOT NULL,
  SaleQty int NOT NULL,
  SalePrice decimal(9,2) NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate 
  ON VisitSale (SaleDate)
  INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale 
  WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId 
  FOREIGN KEY (VisitTargetId)
  REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
  CHECK CONSTRAINT FK_VisitSale_VisitTargetId;

성능상의 이유로 가장 일반적인 필터링 열 (예 :)을 SaleDate마스터 테이블에서 각 세부 사항 테이블 행 으로 복사하여 부분적으로 비정규 화 한 다음 두 테이블에 모두 포함 인덱스를 추가하여 날짜 필터링 쿼리를 더 잘 지원했습니다. 날짜 필터링 된 쿼리를 실행할 때 I / O를 줄이는 데 효과적이지만 마스터 및 세부 정보 테이블을 결합 할 때이 방법으로 카디널리티 추정 문제가 발생한다고 생각합니다.

이 두 테이블을 조인하면 쿼리는 다음과 같습니다.

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt 
    JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
    vs.SaleDate BETWEEN '20170101' and '20171231'
    and vt.SaleDate BETWEEN '20170101' and '20171231'
    -- more filtering goes here, e.g. by store, by product, etc. 

세부 사항 테이블 ( VisitSale) 의 날짜 필터 는 중복됩니다. 날짜 범위별로 필터링 된 쿼리에 대해 세부 사항 테이블에서 순차 I / O (일명 인덱스 검색 연산자)를 사용할 수 있습니다.

이러한 종류의 쿼리 계획은 다음과 같습니다.

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

동일한 문제가있는 쿼리의 실제 계획은 여기 에서 찾을 수 있습니다 .

보시다시피, 조인에 대한 카디널리티 추정 (그림의 왼쪽 아래에있는 툴팁)이 4 배가 넘습니다. 실제 2.1M과 0.5M이 비교되었습니다. 이로 인해 특히 쿼리가 더 복잡한 쿼리에 사용되는 하위 쿼리 인 경우 성능 문제 (예 : tempdb로 유출)가 발생합니다.

그러나 조인의 각 분기에 대한 행 수 추정치는 실제 행 수에 가깝습니다. 조인의 상위 절반은 실제 100K 대 164K입니다. 조인의 하반부는 실제 2.1M 행 대 3.7M 예상입니다. 해시 버킷 분포도 좋아 보입니다. 이러한 관찰 결과는 각 테이블에 대한 통계가 정상이며 문제는 조인 카디널리티의 추정이라는 것입니다.

처음에는 각 테이블의 SaleDate 열이 독립적이지만 실제로는 동일하기를 기대하는 문제는 SQL Server라고 생각했습니다. 그래서 판매 날짜에 대한 동등 비교를 조인 조건 또는 WHERE 절에 추가하려고했습니다.

ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate

또는

WHERE vt.SaleDate = vs.SaleDate

이 작동하지 않았다. 심지어 카디널리티 추정치가 악화되었습니다! 따라서 SQL Server가 동등한 힌트를 사용하지 않거나 다른 것이 문제의 근본 원인입니다.

이 카디널리티 추정 문제를 해결하고 희망적으로 해결하는 방법에 대한 아이디어가 있습니까? 나의 목표는 마스터 / 디테일 조인의 카디널리티가 조인의 더 큰 ( "디테일 테이블") 입력에 대한 추정과 동일하게 추정되는 것입니다.

중요한 경우 Windows Server에서 SQL Server 2014 Enterprise SP2 CU8 빌드 12.0.5557.0을 실행하고 있습니다. 사용 가능한 추적 플래그가 없습니다. 데이터베이스 호환성 수준은 SQL Server 2014입니다. 서로 다른 여러 SQL Server에서 동일한 동작을 볼 수 있으므로 서버 관련 문제는 아닐 것입니다.

SQL Server 2014 Cardinality Estimator 에는 내가 찾는 동작과 정확히 일치 하는 최적화 기능이 있습니다 .

그러나 새 CE는 큰 테이블과 작은 테이블간에 일대 다 조인 연관이 있다고 가정하는 더 간단한 알고리즘을 사용합니다. 이는 큰 테이블의 각 행이 작은 테이블의 한 행과 정확히 일치한다고 가정합니다. 이 알고리즘은 더 큰 입력의 예상 크기를 결합 카디널리티로 반환합니다.

이상적으로는 "작은"테이블이 여전히 100K 개 이상의 행을 반환하더라도 조인의 카디널리티 예상 값이 큰 테이블의 예상 값과 동일 할 경우이 동작을 얻을 수 있습니다.

답변:


6

통계를 수행하거나 레거시 CE를 사용하여 개선을 얻을 수 없다고 가정하면 문제를 해결하는 가장 간단한 방법은 다음과 같이 변경 INNER JOIN하는 것입니다 LEFT OUTER JOIN.

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitSale vs
    LEFT OUTER JOIN VisitTarget vt on vt.VisitTargetId = vs.VisitTargetId
            AND vt.SaleDate BETWEEN '20170101' and '20171231'
WHERE vs.SaleDate BETWEEN '20170101' and '20171231'

테이블간에 외래 키가있는 경우 항상 SaleDate두 테이블 모두 에 대해 동일한 범위를 필터링하고 테이블 SaleDate간에 항상 일치하므로 쿼리 결과가 변경되지 않아야합니다. 이와 같이 외부 조인을 사용하는 것은 이상하게 보일 수 있지만, VisitTarget테이블에 대한 조인 은 쿼리에 의해 리턴되는 행 수를 줄이지 않는다는 것을 쿼리 최적화 프로그램에 알리는 것으로 생각 하십시오. 불행히도 외래 키는 카디널리티 추정을 변경하지 않으므로 때로는 이와 같은 트릭을 사용해야합니다. (Microsoft Connect 제안 : 메타 데이터를 사용하여 옵티 마이저 추정을보다 정확하게 만듭니다 .)

이 형식으로 쿼리를 작성하면 조인 후 쿼리에서 발생하는 다른 작업에 따라 제대로 작동하지 않을 수 있습니다. 임시 테이블을 사용하여 가장 중요한 카디널리티 추정값으로 결과 세트의 중간 결과를 보유 할 수 있습니다.

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