왜 내 인덱스가 SELECT TOP에서 사용되지 않습니까?


15

요약은 다음과 같습니다. 선택 쿼리를 수행하고 있습니다. WHEREand ORDER BY절의 모든 열 IX_MachineryId_DateRecorded은 키의 일부 또는 INCLUDE열 로서 하나의 비 클러스터형 인덱스 에 있습니다 . 모든 열을 선택 하므로 책갈피 조회가 발생하지만을 가져 오는 TOP (1)것이므로 서버는 조회를 한 번만 수행해야한다고 알릴 수 있습니다.

가장 중요한 것은 쿼리가 index를 사용하도록 강제하면 IX_MachineryId_DateRecorded1 초 안에 실행됩니다. 서버가 사용할 인덱스를 결정하게하면을 선택 IX_MachineryId하고 최대 1 분이 걸립니다. 그것은 내가 인덱스를 올바르게 만들었다는 것을 나에게 제안하고 서버는 잘못된 결정을 내립니다. 왜?

CREATE TABLE [dbo].[MachineryReading] (
    [Id]                 INT              IDENTITY (1, 1) NOT NULL,
    [Location]           [sys].[geometry] NULL,
    [Latitude]           FLOAT (53)       NOT NULL,
    [Longitude]          FLOAT (53)       NOT NULL,
    [Altitude]           FLOAT (53)       NULL,
    [Odometer]           INT              NULL,
    [Speed]              FLOAT (53)       NULL,
    [BatteryLevel]       INT              NULL,
    [PinFlags]           BIGINT           NOT NULL,
    [DateRecorded]       DATETIME         NOT NULL,
    [DateReceived]       DATETIME         NOT NULL,
    [Satellites]         INT              NOT NULL,
    [HDOP]               FLOAT (53)       NOT NULL,
    [MachineryId]        INT              NOT NULL,
    [TrackerId]          INT              NOT NULL,
    [ReportType]         NVARCHAR (1)     NULL,
    [FixStatus]          INT              DEFAULT ((0)) NOT NULL,
    [AlarmStatus]        INT              DEFAULT ((0)) NOT NULL,
    [OperationalSeconds] INT              DEFAULT ((0)) NOT NULL,
    CONSTRAINT [PK_dbo.MachineryReading] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Machinery_MachineryId] FOREIGN KEY ([MachineryId]) REFERENCES [dbo].[Machinery] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Tracker_TrackerId] FOREIGN KEY ([TrackerId]) REFERENCES [dbo].[Tracker] ([Id]) ON DELETE CASCADE
);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId]
    ON [dbo].[MachineryReading]([MachineryId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_TrackerId]
    ON [dbo].[MachineryReading]([TrackerId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId_DateRecorded]
    ON [dbo].[MachineryReading]([MachineryId] ASC, [DateRecorded] ASC)
    INCLUDE([OperationalSeconds], [FixStatus]);

테이블은 월 범위로 분할되어 있습니다 (그러나 나는 아직도 무슨 일이 일어나고 있는지 이해하지 못합니다).

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-01-01T00:00:00.000') 

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-02-01T00:00:00.000') 
...

CREATE UNIQUE CLUSTERED INDEX [PK_dbo.MachineryReadingPs] ON MachineryReading(DateRecorded, Id) ON PartitionSchemeMonthRange(DateRecorded)

내가 정상적으로 실행하는 쿼리 :

SELECT TOP (1) [Id], [Location], [Latitude], [Longitude], [Altitude], [Odometer], [ReportType], [FixStatus], [AlarmStatus], [Speed], [BatteryLevel], [PinFlags], [DateRecorded], [DateReceived], [Satellites], [HDOP], [OperationalSeconds], [MachineryId], [TrackerId]
    FROM [dbo].[MachineryReading]
    --WITH(INDEX(IX_MachineryId_DateRecorded)) --This makes all the difference
    WHERE ([MachineryId] = @p__linq__0) AND ([DateRecorded] >= @p__linq__1) AND ([DateRecorded] < @p__linq__2) AND ([OperationalSeconds] > 0)
    ORDER BY [DateRecorded] ASC

쿼리 계획 : https://www.brentozar.com/pastetheplan/?id=r1c-RpxNx

강제 색인이있는 쿼리 계획 : https://www.brentozar.com/pastetheplan/?id=SywwTagVe

포함 된 계획은 실제 실행 계획이지만 준비 데이터베이스 (실제 크기의 약 1/100)입니다. 약 한 달 전에이 회사에서 시작했기 때문에 라이브 데이터베이스를 다루는 것이 주저합니다.

파티션으로 인한 느낌이 들며, 쿼리는 일반적으로 모든 단일 파티션에 걸쳐 있습니다 (예 : OperationalSeconds한 머신에 대해 처음 또는 마지막 으로 기록을 원할 때 ). 그러나 필자가 직접 작성 한 쿼리는 EntityFramework 에서 생성 된 것보다 10-100 배 빠릅니다 . 따라서 저장 프로 시저를 만들 것입니다.


1
@AndrewWilliamson 님 안녕하세요, 통계 문제 일 수 있습니다. 비 강제 계획에서 실제 계획을 볼 경우 예상 행 수는 1.22이고 실제는 19039입니다. 이는 나중에 계획에서 나중에 볼 수있는 키 조회로 이어집니다. 통계를 업데이트하려고 했습니까? 그렇지 않은 경우 준비 데이터베이스에서 전체 검사를 시도하십시오.
jesijesi

답변:


21

서버가 사용할 인덱스를 결정하게하면을 선택 IX_MachineryId하고 최대 1 분이 걸립니다.

해당 인덱스는 분할되지 않았으므로 옵티마이 저는 정렬없이 쿼리에 지정된 순서를 제공하는 데 사용될 수 있음을 인식합니다. 고유하지 않은 비 클러스터형 인덱스로서 클러스터형 인덱스의 키도 하위 키로 가지므로 인덱스를 사용 MachineryId하여 DateRecorded범위 를 탐색 할 수 있습니다 .

색인 찾기

인덱스는를 포함하지 않으므로 OperationalSeconds계획은 테스트하기 위해 (파티셔닝 된) 클러스터형 인덱스에서 행당 해당 값을 찾아야합니다 OperationalSeconds > 0.

조회

옵티마이 저는 비 클러스터형 인덱스에서 하나의 행을 읽고을 만족시키기 위해 조회해야한다고 추정합니다 TOP (1). 이 계산은 행 목표 (한 행을 빠르게 찾음)를 기반으로하며 균일 한 값 분포를 가정합니다.

실제 계획에서 1 행의 예상치가 부정확하다는 것을 알 수 있습니다. 실제로 쿼리 조건을 만족하는 행이 없다는 것을 발견하기 위해 19,039 개의 행을 처리해야합니다. 이것은 행 목표 최적화에서 최악의 경우입니다 (1 행 추정, 모든 행이 실제로 필요함).

실제 / 추정

추적 플래그 4138을 사용하여 행 목표를 비활성화 할 수 있습니다 . 이로 인해 SQL Server가 다른 계획을 선택했을 가능성이 높습니다. 어쨌든을 포함하여 색인을 IX_MachineryId보다 최적으로 만들 수 있습니다 OperationalSeconds.

정렬되지 않은 비 클러스터형 인덱스 (기본 테이블과 다른 방식으로 파티션 된 인덱스는 전혀 포함되지 않음)가있는 경우는 매우 드 unusual니다.

그것은 내가 인덱스를 올바르게 만들었다는 것을 나에게 제안하고 서버는 잘못된 결정을 내립니다. 왜?

평상시처럼 옵티마이 저는 가장 저렴한 계획을 선택합니다.

IX_MachineryId계획 의 예상 비용은 한 행이 테스트되고 반환 될 것이라는 잘못된 행 목표 가정에 따라 0.01 비용 단위입니다.

IX_MachineryId_DateRecorded계획 의 예상 비용은 0.27 단위로 훨씬 높습니다. 주로 인덱스에서 5,515 행을 읽고 정렬하고 가장 낮은 정렬 기준을 반환하기 때문입니다 DateRecorded.

상위 N 정렬

이 인덱스는 분할되어 있으며 DateRecorded순서대로 행을 직접 반환 할 수 없습니다 (나중에 참조). 각 파티션 내에서 검색 MachineryId하고 DateRecorded범위를 지정할 수 있지만 정렬이 필요합니다.

분할 탐색

이 인덱스가 분할되지 않은 경우 정렬이 필요하지 않으며 추가로 포함 된 열이있는 다른 (분할되지 않은) 인덱스와 매우 유사합니다. 분할되지 않은 필터링 된 인덱스는 여전히 약간 더 효율적입니다.


있도록 소스 쿼리를 업데이트해야합니다 데이터 형식@From@To매개 변수 와 일치DateRecorded 열을 ( datetime). 현재 SQL Server는 Merge Interval 연산자와 해당 하위 트리를 사용하여 런타임시 형식 불일치로 인해 동적 범위를 계산하고 있습니다.

<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@From],NULL,(22))">
<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@To],NULL,(22))">

이 변환은 옵티마이 저가 오름차순 파티션 ID ( DateRecorded오름차순으로 값 범위 포함 )와 부등식 술어 사이의 관계에 대해 올바르게 추론하지 못하게합니다 DateRecorded.

파티션 ID는 파티션 된 인덱스의 암시 적 선행 키입니다. 일반적으로 옵티마이 저는 파티션 ID 별 오름차순 (오름차순 ID가 오름차순, 비 연관 값으로 맵핑 됨 DateRecorded)을 단독으로 ( 정수로 주어진) DateRecorded오더와 동일 함을 알 수 있습니다. 이 추론 체인은 유형 변환에 의해 손상됩니다.DateRecordedMachineryID

데모

간단한 파티션 된 테이블 및 인덱스 :

CREATE PARTITION FUNCTION PF (datetime)
AS RANGE LEFT FOR VALUES ('20160101', '20160201', '20160301');

CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY]);

CREATE TABLE dbo.T (c1 integer NOT NULL, c2 datetime NOT NULL) ON PS (c2);

CREATE INDEX i ON dbo.T (c1, c2) ON PS (c2);

INSERT dbo.T (c1, c2) 
VALUES (1, '20160101'), (1, '20160201'), (1, '20160301');

일치하는 유형의 쿼리

-- Types match (datetime)
DECLARE 
    @From datetime = '20010101',
    @To datetime = '20090101';

-- Seek with no sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

종류를 찾지

일치하지 않는 유형의 쿼리

-- Mismatched types (datetime2 vs datetime)
DECLARE 
    @From datetime2 = '20010101',
    @To datetime2 = '20090101';

-- Merge Interval and Sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

간격 및 정렬 병합


5

인덱스는 쿼리에 매우 적합하고 옵티마이 저가 선택하지 않은 이유를 모르겠습니다 (통계, 파티셔닝, Azure 제한?, 전혀 모르겠습니다).

그러나 고정 값이고 특정 쿼리 실행에서 다른 쿼리 실행으로 변경되지 않으면 필터링 된 인덱스 가 특정 쿼리에 대해 훨씬 더 좋습니다 > 0.

CREATE NONCLUSTERED INDEX IX_MachineryId_DateRecorded_filtered
    ON dbo.MachineryReading
        (MachineryId, DateRecorded) 
    WHERE (OperationalSeconds > 0) ;

OperationalSeconds세 번째 열이있는 인덱스와 필터링 된 인덱스 사이에는 두 가지 차이점이 있습니다 .

  • 먼저 필터링 된 인덱스는 너비 (좁아짐)와 행 수에서 더 작습니다.
    이렇게하면 SQL Server가 메모리에 유지하는 데 필요한 공간이 줄어들 기 때문에 필터링 된 인덱스가 일반적으로 더 효율적입니다.

  • 두 번째로 이것은 쿼리에 대해 더 미묘하고 중요합니다. 쿼리에 사용 된 필터와 일치하는 행만 있다는 것입니다. 이것은 세 번째 열의 값에 따라 매우 중요 할 수 있습니다.
    예를 들어, 매개 변수 세트는 특정 MachineryIdDateRecorded1000 열을 수득 할 수있다. 이 행의 전부 또는 거의 전부가 (OperationalSeconds > 0)필터 와 일치하면 두 인덱스 모두 올바르게 작동합니다. 그러나 필터와 일치하는 행이 거의 없거나 (또는 ​​마지막 또는 전혀 없음) 첫 번째 색인은 일치하는 항목을 찾을 때까지 1000 행을 많이 또는 모두 통과해야합니다. 반면에 필터링 된 인덱스는 필터와 일치하는 행만 저장되므로 일치하는 행을 찾거나 0 개의 행을 찾으려고하는 하나의 탐색 만 필요합니다.


1
인덱스를 추가하면 쿼리가 더 효율적입니까?
ypercubeᵀᴹ

준비 데이터베이스가 아니라 (실제로 테스트하기 위해 더 많은 데이터가 필요합니다.) 실제로 시도하지는 않았지만 새로운 인덱스는 그 데이터베이스를 구축하는 데 1 시간 이상이 걸립니다. 또한 라이브 데이터베이스가 이미 느리게 실행되고 있기 때문에 라이브 데이터베이스에 대해 무엇이든하는 것이 주저합니다. 라이브를 스테이징으로 복제 할 수있는 더 나은 시스템이 필요합니다.
Andrew Williamson
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.