web-dev-qa-db-ja.com

SELECT TOPでインデックスが使用されないのはなぜですか?

まとめは次のとおりです。選択クエリを実行しています。 WHEREおよび_ORDER BY_句のすべての列は、キーの一部として、単一の非クラスター化インデックス_IX_MachineryId_DateRecorded_にあります。またはINCLUDE列として。 all列を選択しているので、ブックマークルックアップが発生しますが、TOP (1)のみを取得しているので、サーバーは確実にルックアップは最後に一度だけ実行する必要があります。

最も重要なのは、クエリでインデックス_IX_MachineryId_DateRecorded_を使用するように強制すると、1秒未満で実行されることですサーバーに使用するインデックスを決定させると、_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

含まれているプラ​​ンは実際の実行プランですが、ステージングデータベースにあります(ライブのサイズの約100分の1)。約1か月前にこの会社で始めたばかりなので、ライブデータベースをいじるのをためらっています。

私はそれがパーティション分割のせいだと感じており、私のクエリは通常すべてのパーティションにまたがっています(たとえば、1つのマシンで記録された最初または最後のOperationalSecondsを取得したい場合)。ただし、手動で作成したクエリはすべて、EntityFrameworkが生成したものよりも10〜100倍高速に実行されているため、ストアドプロシージャを作成します。

15

使用するインデックスをサーバーに決定させると、_IX_MachineryId_が選択され、最大1分かかります。

そのインデックスはパーティション化されていないため、オプティマイザは、インデックスを使用して、ソートせずにクエリで指定された順序を提供できることを認識します。非一意の非クラスター化インデックスとして、クラスター化インデックスのキーもサブキーとして持つため、インデックスを使用して、MachineryIdDateRecordedの範囲を検索できます。

Index Seek

インデックスにはOperationalSecondsが含まれていないため、_OperationalSeconds > 0_をテストするには、プランで(パーティション化された)クラスター化インデックスの行ごとにその値を調べる必要があります。

Lookup

オプティマイザーは、非クラスター化インデックスから1行を読み取り、TOP (1)を満たすために検索する必要があると推定します。この計算は行の目標(1行をすばやく見つける)に基づいており、値の均一な分布を前提としています。

実際の計画から、1行の見積もりが不正確であることがわかります。実際、クエリ条件を満たす行がないことを検出するには、19,039行を処理する必要があります。これは、行の目標の最適化の最悪のケースです(1行が推定され、すべての行が実際に必要です)。

Actual/estimate

トレースフラグ4138 で行の目標を無効にできます。これにより、SQL Serverが別のプランを選択する可能性が高くなります。おそらく、あなたが強制したプランです。いずれの場合でも、インデックス_IX_MachineryId_は、OperationalSecondsを含めることでより最適化できます。

アラインされていない非クラスター化インデックス(ベーステーブルとは異なる方法でパーティション化されたインデックス、まったく含まれていないインデックス)を持つことは非常にまれです。

これは、私がインデックスを正しく作成したことを示唆しており、サーバーは間違った判断を下しているだけです。どうして?

いつものように、オプティマイザは検討する最も安い計画を選択しています。

_IX_MachineryId_プランの推定コストは、1つの行がテストされて返されるという(誤った)行目標の仮定に基づいて、0.01コスト単位です。

_IX_MachineryId_DateRecorded_プランの推定コストは、0.27ユニットとはるかに高くなります。これは、主に、インデックスから5,515行を読み取り、それらを並べ替えて返すことを想定しているためです(DateRecordedによって)最下位にソートされるもの:

Top N Sort

このインデックスはパーティション化されており、DateRecordedの順序で行を直接返すことはできません(後述)。 MachineryIdDateRecordedの範囲各パーティション内を検索できます、ただしソートが必要です:

Partitioned Seek

このインデックスがパーティション化されていない場合、並べ替えは必要なく、追加の列が含まれている他の(パーティション化されていない)インデックスと非常によく似ています。パーティション化されていないフィルター処理されたインデックスは、まだ少し効率的です。


_@From_および_@To_パラメータのデータタイプパラメータmatchDateRecorded列(datetime)になるように、ソースクエリを更新する必要があります。現在、SQL Serverは、実行時の型の不一致のために、動的範囲を計算しています(マージ間隔演算子とそのサブツリーを使用)。

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

この変換により、オプティマイザーは昇順パーティションIDs(昇順でDateRecorded値の範囲をカバーする)とDateRecordedの不等式述部の関係について正しく推論できなくなります。

パーティションIDは、パーティションインデックスの暗黙的な主要キーです。通常、オプティマイザーは、パーティションIDによる順序付け(昇順のIDがDateRecordedの昇順のばらばらの値にマップされる場合)を確認できます。DateRecordedは、DateRecordedのみによる順序付けと同じです(MachineryIDが定数である場合)。この推論の連鎖は、型変換によって破壊されます。

デモ

単純なパーティションテーブルとインデックス:

_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;
_

Seek no sort

タイプが一致しないクエリ

_-- 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;
_

Merge Interval and Sort

21
Paul White 9

インデックスはクエリに非常に適しているようで、オプティマイザーによってなぜ選択されないのかわかりません(統計、パーティション分割、Azureの制限など、本当にわかりません)。

ただし、_> 0_が固定値であり、クエリの実行ごとに変化しない場合、特定のクエリではfiltered indexの方が適しています。

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

OperationalSecondsが3番目の列である場合のインデックスとフィルター処理されたインデックスには2つの違いがあります。

  • まず、フィルタリングされたインデックスは、幅(狭い)と行数の両方で小さくなります。
    これにより、SQL Serverがメモリに保持するために必要なスペースが少なくなるため、フィルターされたインデックスの効率が向上します。

  • 次に、これはより微妙であり、クエリにとって重要なのは、クエリで使用されるフィルターに一致する行のみが含まれることです。この3番目の列の値によっては、これが非常に重要になる場合があります。
    たとえば、MachineryIdおよびDateRecordedの特定のパラメータセットは、1000行を生成する場合があります。これらの行のすべてまたはほとんどすべてが_(OperationalSeconds > 0)_フィルターに一致する場合、両方のインデックスが適切に動作します。ただし、フィルターに一致する行が非常に少ない場合(または最後の行のみ、またはまったくない場合)、最初のインデックスは、一致が見つかるまで、それらの1000行の多くまたはすべてを通過する必要があります。一方、フィルター処理されたインデックスは、フィルターに一致する行のみが格納されるため、一致する行を見つける(または0行を返す)ために1回のシークで済みます。

5
ypercubeᵀᴹ