강제 흐름 구별


19

나는 이와 같은 테이블을 가지고있다 :

CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
    ObjectId INT NOT NULL
)

증가하는 ID로 객체에 대한 업데이트를 필수적으로 추적합니다.

이 테이블의 소비자 UpdateId는 특정 순서대로 시작하여 100 개의 고유 한 개체 ID 청크를 선택합니다 UpdateId. 기본적으로 중단 된 위치를 추적 한 다음 업데이트를 쿼리합니다.

난 단지 쿼리를 작성하여 최대로 최적의 쿼리 계획을 생성 할 수있었습니다 때문에 나는 흥미로운 최적화 문제가이 찾은 일이 나는 인덱스로 인해 원하지만하지 않는 것을 할 보장 내가 원하는 무엇을 :

SELECT DISTINCT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId

@fromUpdateId저장 프로 시저 매개 변수는 어디에 있습니까 ?

계획 :

SELECT <- TOP <- Hash match (flow distinct, 100 rows touched) <- Index seek

UpdateId사용중인 인덱스 에 대한 검색으로 인해 결과는 이미 좋으며 원하는대로 가장 낮은 업데이트 ID에서 가장 높은 업데이트 ID로 정렬됩니다. 그리고 이것은 흐름 별개의 계획을 생성합니다 . 그러나 순서는 분명히 동작이 보장되지 않으므로 사용하고 싶지 않습니다.

이 트릭은 또한 동일한 쿼리 계획을 만듭니다 (중복 TOP이 있음에도 불구하고).

WITH ids AS
(
    SELECT ObjectId
    FROM Updates
    WHERE UpdateId > @fromUpdateId
    ORDER BY UpdateId OFFSET 0 ROWS
)
SELECT DISTINCT TOP 100 ObjectId FROM ids

그럼에도 불구하고 이것이 정말로 주문을 보장하는지 확실하지 않습니다.

SQL Server가 단순화하기에 충분히 영리하기를 바랐던 하나의 쿼리는 이것이지만 매우 잘못된 쿼리 계획을 생성하게됩니다.

SELECT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId
GROUP BY ObjectId
ORDER BY MIN(UpdateId)

계획 :

SELECT <- Top N Sort <- Hash Match aggregate (50,000+ rows touched) <- Index Seek

인덱스 검색을 사용 UpdateId하고 중복을 제거 하는 고유흐름으로 최적의 계획을 생성하는 방법을 찾으려고합니다 ObjectId. 어떤 아이디어?

원하는 경우 샘플 데이터 . 객체는 하나 이상의 업데이트를 거의하지 않으며 100 행 세트 내에 하나 이상을 가져서는 안됩니다. 그래서 내가 알지 못하는 것이 없다면 흐름이 뚜렷 하지 않습니다. 그러나 ObjectId테이블에 단일 행이 100 개를 초과하지 않는다는 보장은 없습니다 . 이 테이블에는 1,000,000 개가 넘는 행이 있으며 빠르게 성장할 것으로 예상됩니다.

이것의 사용자가 적절한 다음을 찾을 수있는 다른 방법이 있다고 가정합니다 @fromUpdateId. 이 쿼리에서 반환 할 필요가 없습니다.

답변:


15

Hash Match Flow Distinct 연산자가 주문 보존이 아니기 때문에 SQL Server 최적화 프로그램 이 필요한 보증을 제공 한 후 실행 계획을 생성 할 수 없습니다 .

그럼에도 불구하고 이것이 정말로 주문을 보장하는지 확실하지 않습니다.

많은 경우에 순서 보존을 관찰 할 수 있지만 이는 구현 세부 사항입니다. 보증이 없으므로 신뢰할 수 없습니다. 항상 그렇듯이 프레젠테이션 순서는 최상위 ORDER BY조항에 의해서만 보장 될 수 있습니다 .

아래 스크립트는 해시 일치 흐름 구별이 순서를 유지하지 않음을 보여줍니다. 두 열에서 일치하는 숫자가 1-50,000 인 문제의 테이블을 설정합니다.

IF OBJECT_ID(N'dbo.Updates', N'U') IS NOT NULL
    DROP TABLE dbo.Updates;
GO
CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1),
    ObjectId INT NOT NULL,

    CONSTRAINT PK_Updates_UpdateId PRIMARY KEY (UpdateId)
);
GO
INSERT dbo.Updates (ObjectId)
SELECT TOP (50000)
    ObjectId =
        ROW_NUMBER() OVER (
            ORDER BY C1.[object_id]) 
FROM sys.columns AS C1
CROSS JOIN sys.columns AS C2
ORDER BY
    ObjectId;

테스트 쿼리는 다음과 같습니다.

DECLARE @Rows bigint = 50000;

-- Optimized for 1 row, but will be 50,000 when executed
SELECT DISTINCT TOP (@Rows)
    U.ObjectId 
FROM dbo.Updates AS U
WHERE 
    U.UpdateId > 0
OPTION (OPTIMIZE FOR (@Rows = 1));

추정 된 계획은 인덱스 탐색과 흐름이 구별되는 것을 보여줍니다.

예상 계획

출력은 확실히 다음과 같이 시작되는 것으로 보입니다.

결과의 시작

...하지만 더 낮은 값은 '누락'되기 시작합니다.

패턴 분해

... 그리고 결국 :

혼돈이 터지다

이 특정한 경우에 대한 설명은 해시 연산자가 유출된다는 것입니다.

사후 계획

파티션이 유출되면 동일한 파티션으로 해시되는 모든 행도 유출됩니다. 유출 된 파티션은 나중에 처리되어 발생하는 고유 한 값이 수신 된 순서대로 즉시 방출 될 것이라는 기대를 깨뜨립니다.


재귀 나 커서 사용과 같이 원하는 순서로 결과를 생성하기 위해 효율적인 쿼리를 작성하는 방법에는 여러 가지가 있습니다. 그러나 Hash Match Flow Distinct를 사용하여 수행 할 수 없습니다 .


11

이 답변에 만족하지 못했습니다. 유효한 연산자와 함께 정확한 결과를 얻을 수 없었기 때문입니다. 그러나 올바른 결과와 함께 우수한 성능을 얻을 수있는 대안이 있습니다. 불행히도 테이블에 비 클러스터형 인덱스를 만들어야합니다.

나는 가능한 열 조합을 생각 ORDER BY하고 적용 후에 올바른 결과를 얻으 려고 노력 하여이 문제에 접근 DISTINCT했습니다. 의 최소값 UpdateIdObjectId과 함께이 ObjectId하나 개의 이러한 조합이다. 그러나 최소값을 직접 요구 UpdateId하면 테이블에서 모든 행을 읽는 것으로 보입니다. 대신 UpdateId테이블에 다른 조인을 사용 하여 최소값을 간접적으로 요청할 수 있습니다 . 아이디어는 Updates테이블을 순서대로 스캔하고 UpdateId해당 행의 최소값이 아닌 행을 버리고 ObjectId처음 100 행을 유지하는 것입니다. 데이터 배포에 대한 설명을 기반으로 많은 행을 버릴 필요가 없습니다.

데이터 준비를 위해 각각의 고유 한 ObjectId에 대해 2 개의 행이있는 백만 개의 행을 테이블에 넣었습니다.

INSERT INTO Updates WITH (TABLOCK)
SELECT t.RN / 2
FROM 
(
    SELECT TOP 1000000 -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t;

CREATE INDEX IX On Updates (Objectid, UpdateId);

에 클러스터되지 않은 인덱스 Objectid와는 UpdateId중요하다. 최소 UpdateIdper 가없는 행을 효율적으로 버릴 수 Objectid있습니다. 위의 설명과 일치하는 쿼리를 작성하는 방법에는 여러 가지가 있습니다. 다음과 같은 방법이 있습니다 NOT EXISTS.

DECLARE @fromUpdateId INT = 9999;
SELECT ObjectId
FROM (
    SELECT DISTINCT TOP 100 u1.UpdateId, u1.ObjectId
    FROM Updates u1
    WHERE UpdateId > @fromUpdateId
    AND NOT EXISTS (
        SELECT 1
        FROM Updates u2
        WHERE u2.UpdateId > @fromUpdateId
        AND u1.ObjectId = u2.ObjectId
        AND u2.UpdateId < u1.UpdateId
    )
    ORDER BY u1.UpdateId, u1.ObjectId
) t;

다음은 쿼리 계획에 대한 그림입니다 .

쿼리 계획

최선의 경우 SQL Server는 비 클러스터형 인덱스에 대해 100 개의 인덱스 검색 만 수행합니다. 매우 운이 좋지 않은 것을 시뮬레이션하기 위해 쿼리를 처음 5000 행을 클라이언트에 반환하도록 변경했습니다. 결과적으로 9999 개의 인덱스 탐색이 발생하여 distinct 당 평균 100 개의 행을 얻는 것과 같습니다 ObjectId. 출력은 다음과 같습니다 SET STATISTICS IO, TIME ON.

표 '업데이트'. 스캔 카운트 10000, 논리적 읽기 31900, 물리적 읽기 0

SQL Server 실행 시간 : CPU 시간 = 31ms, 경과 시간 = 42ms


9

Flow Distinct는 내가 가장 좋아하는 운영자 중 하나입니다.

이제 보증 이 문제입니다. FD 연산자가 Seek 연산자에서 행을 순서대로 가져 와서 고유 한 것으로 판별 될 때마다 각 행을 생성한다고 생각하면 올바른 순서로 행이 제공됩니다. 그러나 FD가 한 번에 하나의 행을 처리하지 않는 시나리오가 있는지 알기가 어렵습니다.

이론적으로 FD는 Seek에 100 개의 행을 요청하여 필요한 순서대로 생성 할 수 있습니다.

쿼리 힌트 OPTION (FAST 1, MAXDOP 1)는 Seek 연산자에서 필요한 것보다 많은 행을 얻지 않기 때문에 도움이 될 수 있습니다. 그것은인가 보장 하지만? 좀 빠지는. 여전히 한 번에 한 행의 페이지를 가져 오기로 결정할 수 있습니다.

나는 OPTION (FAST 1, MAXDOP 1)귀하의 OFFSET버전이 주문에 대해 많은 확신을 줄 것이라고 생각 하지만 보장하지는 않습니다.


내가 이해했듯이 문제는 Flow Distinct 연산자가 디스크에 유출 될 수있는 해시 테이블을 사용한다는 것입니다. 유출이 발생하면 RAM에있는 부분을 사용하여 처리 할 수있는 행은 즉시 처리되지만 유출 된 데이터를 디스크에서 다시 읽을 때까지 다른 행은 처리되지 않습니다. 내가 알 수 있듯이 해시 테이블 (예 : 해시 조인)을 사용하는 모든 연산자는 유출 동작으로 인해 순서를 유지한다고 보장하지 않습니다.
sam.bishop

옳은. Paul White의 답변을 참조하십시오.
Rob Farley
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.