SQL Server는 동등하게 분할 된 두 테이블에서 병렬 병합 조인을 최적화하지 않습니다.


21

매우 상세한 질문에 대해 사과드립니다. 문제를 재현하기위한 전체 데이터 세트를 생성하는 쿼리를 포함 시켰으며 32 코어 컴퓨터에서 SQL Server 2012를 실행하고 있습니다. 그러나 이것이 SQL Server 2012에만 해당되는 것은 아니며이 특정 예제에 대해 MAXDOP를 10으로 설정했습니다.

동일한 파티션 구성표를 사용하여 파티션 된 두 개의 테이블이 있습니다. 파티셔닝에 사용되는 열에서 이들을 함께 결합 할 때 SQL Server가 예상대로 병렬 병합 조인을 최적화 할 수 없으므로 대신 HASH JOIN을 사용하도록 선택했습니다. 이 특별한 경우에는 쿼리를 분할 기능을 기반으로 10 개의 분리 된 범위로 분할하고 각 쿼리를 SSMS에서 동시에 실행하여 훨씬 더 최적의 병렬 MERGE JOIN을 수동으로 시뮬레이션 할 수 있습니다. WAITFOR를 사용하여 정확하게 동시에 실행하면 모든 쿼리가 원래 병렬 HASH JOIN에서 사용한 총 시간의 ~ 40 %에 완료됩니다.

동등하게 분할 된 테이블의 경우 SQL Server가이 최적화를 자체적으로 수행 할 수있는 방법이 있습니까? SQL Server는 MERGE JOIN을 병렬로 만들기 위해 일반적으로 많은 오버 헤드가 발생할 수 있지만이 경우 최소한의 오버 헤드로 매우 자연스러운 샤딩 방법이있는 것 같습니다. 아마도 옵티마이 저가 아직 인식하기에 충분하지 않은 특수한 경우일까요?

이 문제를 재현하기 위해 단순화 된 데이터 세트를 설정하는 SQL은 다음과 같습니다.

/* Create the first test data table */
CREATE TABLE test_transaction_properties 
    ( transactionID INT NOT NULL IDENTITY(1,1)
    , prop1 INT NULL
    , prop2 FLOAT NULL
    )

/* Populate table with pseudo-random data (the specific data doesn't matter too much for this example) */
;WITH E1(N) AS (
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 
    UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)
, E2(N) AS (SELECT 1 FROM E1 a CROSS JOIN E1 b)
, E4(N) AS (SELECT 1 FROM E2 a CROSS JOIN E2 b)
, E8(N) AS (SELECT 1 FROM E4 a CROSS JOIN E4 b)
INSERT INTO test_transaction_properties WITH (TABLOCK) (prop1, prop2)
SELECT TOP 10000000 (ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) % 5) + 1 AS prop1
                , ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) * rand() AS prop2
FROM E8

/* Create the second test data table */
CREATE TABLE test_transaction_item_detail
    ( transactionID INT NOT NULL
    , productID INT NOT NULL
    , sales FLOAT NULL
    , units INT NULL
    )

 /* Populate the second table such that each transaction has one or more items
     (again, the specific data doesn't matter too much for this example) */
INSERT INTO test_transaction_item_detail WITH (TABLOCK) (transactionID, productID, sales, units)
SELECT t.transactionID, p.productID, 100 AS sales, 1 AS units
FROM test_transaction_properties t
JOIN (
    SELECT 1 as productRank, 1 as productId
    UNION ALL SELECT 2 as productRank, 12 as productId
    UNION ALL SELECT 3 as productRank, 123 as productId
    UNION ALL SELECT 4 as productRank, 1234 as productId
    UNION ALL SELECT 5 as productRank, 12345 as productId
) p
    ON p.productRank <= t.prop1

/* Divides the transactions evenly into 10 partitions */
CREATE PARTITION FUNCTION [pf_test_transactionId] (INT)
AS RANGE RIGHT
FOR VALUES
(1,1000001,2000001,3000001,4000001,5000001,6000001,7000001,8000001,9000001)

CREATE PARTITION SCHEME [ps_test_transactionId]
AS PARTITION [pf_test_transactionId]
ALL TO ( [PRIMARY] )

/* Apply the same partition scheme to both test data tables */
ALTER TABLE test_transaction_properties
ADD CONSTRAINT PK_test_transaction_properties
PRIMARY KEY (transactionID)
ON ps_test_transactionId (transactionID)

ALTER TABLE test_transaction_item_detail
ADD CONSTRAINT PK_test_transaction_item_detail
PRIMARY KEY (transactionID, productID)
ON ps_test_transactionId (transactionID)

이제 우리는 차선의 쿼리를 재현 할 준비가되었습니다!

/* This query produces a HASH JOIN using 20 threads without the MAXDOP hint,
    and the same behavior holds in that case.
    For simplicity here, I have limited it to 10 threads. */
SELECT COUNT(*)
FROM test_transaction_item_detail i
JOIN test_transaction_properties t
    ON t.transactionID = i.transactionID
OPTION (MAXDOP 10)

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

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

그러나 단일 스레드를 사용하여 각 파티션을 처리하면 (아래의 첫 번째 파티션의 예) 훨씬 효율적인 계획으로 이어집니다. 정확히 같은 순간에 10 개의 파티션 각각에 대해 아래와 같은 쿼리를 실행하여이를 테스트했으며, 10 개 모두 1 초 만에 완료되었습니다.

SELECT COUNT(*)
FROM test_transaction_item_detail i
INNER MERGE JOIN test_transaction_properties t
    ON t.transactionID = i.transactionID
WHERE t.transactionID BETWEEN 1 AND 1000000
OPTION (MAXDOP 1)

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

답변:


18

SQL Server 최적화 프로그램이 병렬 MERGE조인 계획 을 생성하지 않는 것이 좋습니다 (이 대안은 비용이 많이 듭니다). Parallel은 MERGE항상 두 조인 입력 모두에서 재 파티셔닝 교환을 필요로하며, 더 중요한 것은 이러한 교환에서 행 순서를 유지해야한다는 것입니다.

병렬 처리는 각 스레드가 독립적으로 실행될 수있을 때 가장 효율적입니다. 주문 보존은 종종 동기화 대기를 자주 야기하며, 궁극적으로 교환이 유출되어 tempdb쿼리 내 교착 상태를 해결합니다.

이러한 문제는 하나의 스레드 에서 전체 쿼리 의 여러 인스턴스를 각각 실행 하여 각 스레드가 배타적 범위의 데이터를 처리 함으로써 피할 수 있습니다 . 그러나 이것은 옵티마이 저가 기본적으로 고려하는 전략이 아닙니다. 원래 병렬 처리를위한 원래 SQL Server 모델은 교환시 쿼리를 중단하고 여러 스레드에서 해당 분할에 의해 형성된 계획 세그먼트를 실행합니다.

독점적 인 데이터 세트 범위에 걸쳐 여러 스레드에서 전체 쿼리 계획을 실행하는 방법이 있지만 모든 사람이 만족하지는 않을 것입니다. 이러한 접근 방식 중 하나는 분할 된 테이블의 파티션을 반복하고 각 스레드에 소계 생성 작업을 제공하는 것입니다. 결과는 SUM각 독립 스레드가 리턴 한 행 수입니다.

메타 데이터에서 파티션 번호를 쉽게 얻을 수 있습니다.

DECLARE @P AS TABLE
(
    partition_number integer PRIMARY KEY
);

INSERT @P (partition_number)
SELECT
    p.partition_number
FROM sys.partitions AS p 
WHERE 
    p.[object_id] = OBJECT_ID(N'test_transaction_properties', N'U')
    AND p.index_id = 1;

그런 다음이 숫자를 사용하여 상관 된 조인 ( APPLY) 을 구동하고 $PARTITION각 스레드를 현재 파티션 번호로 제한 하는 기능을 사용합니다.

SELECT
    row_count = SUM(Subtotals.cnt)
FROM @P AS p
CROSS APPLY
(
    SELECT
        cnt = COUNT_BIG(*)
    FROM dbo.test_transaction_item_detail AS i
    JOIN dbo.test_transaction_properties AS t ON
        t.transactionID = i.transactionID
    WHERE 
        $PARTITION.pf_test_transactionId(t.transactionID) = p.partition_number
        AND $PARTITION.pf_test_transactionId(i.transactionID) = p.partition_number
) AS SubTotals;

쿼리 계획은 MERGE테이블의 각 행에 대해 수행 되는 조인을 보여줍니다 @P. 클러스터 된 인덱스 스캔 특성은 각 반복에서 단일 파티션 만 처리되는지 확인합니다.

연속 요금제 적용

불행히도, 이것은 파티션의 순차적 인 직렬 처리만을 초래합니다. 제공 한 데이터 세트에서 내 4 코어 (하이퍼 스레드 8) 노트북은 모든 데이터가 메모리에 있는 상태에서 7 초 내에 정확한 결과를 반환합니다 .

MERGE하위 계획을 동시에 실행 하려면 파티션 ID가 사용 가능한 스레드 ( MAXDOP)에 분산 되고 각 MERGE하위 계획이 한 파티션의 데이터를 사용하여 단일 스레드에서 실행 되는 병렬 계획이 필요 합니다. 불행하게도, 옵티마이 저는 종종 MERGE비용 기반 에서 병렬 을 결정 하고 병렬 계획을 강제 할 문서화 된 방법은 없습니다. 추적 플래그 8649를 사용하여 문서화되지 않은 (및 지원되지 않는) 방법이 있습니다 .

SELECT
    row_count = SUM(Subtotals.cnt)
FROM @P AS p
CROSS APPLY
(
    SELECT
        cnt = COUNT_BIG(*)
    FROM dbo.test_transaction_item_detail AS i
    JOIN dbo.test_transaction_properties AS t ON
        t.transactionID = i.transactionID
    WHERE 
        $PARTITION.pf_test_transactionId(t.transactionID) = p.partition_number
        AND $PARTITION.pf_test_transactionId(i.transactionID) = p.partition_number
) AS SubTotals
OPTION (QUERYTRACEON 8649);

이제 쿼리 계획에 파티션 번호 @P가 라운드 로빈 단위로 스레드간에 분산되지 않은 것으로 표시 됩니다. 각 스레드는 단일 파티션에 대해 중첩 루프 조인의 내부를 실행하여 분리 된 데이터를 동시에 처리한다는 목표를 달성합니다. 8 개의 하이퍼 코어 에서 동일한 결과가 3 초 내에 반환되며 , 8 개 모두 100 % 사용률로 유지됩니다.

병렬 적용

이 기술을 반드시 사용하는 것은 권장하지 않습니다. 이전 경고를 참조하십시오.

자세한 내용은 내 기사 파티션 된 테이블 조인 성능 향상 을 참조하십시오.

칼럼 스토어

SQL Server 2012를 사용하고 Enterprise라고 가정하면 columnstore 인덱스를 사용할 수도 있습니다. 이는 충분한 메모리가 사용 가능한 경우 배치 모드 해시 조인의 가능성을 보여줍니다.

CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
ON dbo.test_transaction_properties (transactionID);

CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
ON dbo.test_transaction_item_detail (transactionID);

이 인덱스를 사용하여 쿼리를 배치하십시오 ...

SELECT
    COUNT_BIG(*)
FROM dbo.test_transaction_properties AS ttp
JOIN dbo.test_transaction_item_detail AS ttid ON
    ttid.transactionID = ttp.transactionID;

... 속임수없이 최적화 프로그램의 다음 실행 계획이 나타납니다.

칼럼 스토어 계획 1

2 초 만에 결과를 정정 할 수 있지만 스칼라 집계에 대한 행 모드 처리를 제거하면 더 많은 도움이됩니다.

SELECT
    COUNT_BIG(*)
FROM dbo.test_transaction_properties AS ttp
JOIN dbo.test_transaction_item_detail AS ttid ON
    ttid.transactionID = ttp.transactionID
GROUP BY
    ttp.transactionID % 1;

최적화 된 컬럼 스토어

최적화 된 열 저장소 쿼리는 851ms 에서 실행됩니다 .

Geoff Patterson은 Partition Wise Joins 버그 보고서를 작성 했지만 수정되지 않음으로 종료되었습니다.


5
훌륭한 학습 경험. 고맙습니다. +1
Edward Dortland

1
고마워 폴! 여기에 훌륭한 정보가 있으며, 확실히 질문을 자세히 설명합니다.
Geoff Patterson

2
고마워 폴! 여기에 훌륭한 정보가 있으며, 확실히 질문을 자세히 설명합니다. 우리는 혼합 된 SQL 2008/2012 환경에 있지만 앞으로 컬럼 스토어를 더 살펴볼 것입니다. 물론, 나는 여전히 SQL Server가 병렬 병합 조인 (그리고 훨씬 적은 메모리 요구 사항)을 유스 케이스에서 효과적으로 활용할 수 있기를 바랍니다. 나에 투표 : connect.microsoft.com/SQLServer/feedback/details/759266/...
제프 패터슨

0

옵티마이 저가 생각하는 방식으로 작동하도록하는 방법은 쿼리 힌트를 사용하는 것입니다.

이 경우 OPTION (MERGE JOIN)

아니면 당신은 전체 돼지를 가서 사용할 수 있습니다 USE PLAN


나는 개인적으로 이것을하지 않을 것입니다 : 힌트는 현재 데이터 볼륨 및 배포에만 유용합니다.
gbn

흥미로운 점은 OPTION (MERGE JOIN)을 사용하면 계획이 훨씬 나빠진다는 것입니다. 옵티마이 저는 파티션 기능으로 MERGE JOIN을 샤딩 할 수 있다는 사실을 충분히 알지 못하며이 힌트를 적용하면 쿼리에 ~ 46 초가 걸립니다. 매우 실망스러운!

@gbn 이것은 아마도 옵티마이 저가 먼저 해시 조인을 시작하는 이유는 무엇입니까?

@gpatterson 얼마나 짜증나! :)

유니언 (예 : 다른 유사한 쿼리와 통합 된 짧은 쿼리)을 통해 파티션을 수동으로 강제 실행하면 어떻게됩니까?
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.