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


15

要約は次のとおりです。選択クエリを実行しています。WHEREand ORDER BY句のすべての列IX_MachineryId_DateRecordedは、キーの一部として、またはINCLUDE列として、単一の非クラスター化インデックスに含まれます。すべての列を選択しているので、ブックマークルックアップTOP (1)になりますが、を取得するだけなので、サーバーはルックアップを最後に一度だけ実行する必要があることを確認できます。

最も重要なことは、クエリでindexを使用するように強制すると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

含まれる計画は実際の実行計画ですが、ステージングデータベース(ライブのサイズの約1/100)にあります。ライブデータベースをいじるのをためらうのは、1か月ほど前にこの会社で始めたばかりだからです。

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


1
こんにちは、@ AndrewWilliamson、統計の問題かもしれません。強制されていない計画から実際の計画を見ると、推定行数は1.22であり、実際の計画は19039です。これにより、計画の後半で参照するキールックアップにつながります。統計を更新しようとしましたか?そうでない場合は、ステージングデータベースでフルスキャンを試してください。
ジェシエシ

回答:


21

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

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

インデックスシーク

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

調べる

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

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

実際/推定

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

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

それは、私がインデックスを正しく作成し、サーバーが悪い判断を下していることを本当に示唆しています。どうして?

いつものように、オプティマイザーは、考慮する最も安いプランを選択しています。

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

IX_MachineryId_DateRecordedプランの推定コストははるかに高く、0.27ユニットです。これは、主に、インデックスから5,515行を読み取り、それらを並べ替えて、最も低い(byでDateRecorded)並べ替える行を返すためです。

上位Nソート

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

パーティションシーク

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


ソースクエリを更新して、およびデータ型データ型が列()に一致するようにする必要が@Fromあり@Toます。現時点では、SQL Serverは実行時の型の不一致のためにダイナミックレンジを計算しています(マージ間隔演算子とそのサブツリーを使用):DateRecordeddatetime

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

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

パーティションIDは、パーティションインデックスの暗黙的な先行キーです。通常、オプティマイザーは、パーティションIDによる順序付け(昇順IDがの昇順、非結合の値にマップされるDateRecordedDateRecordedが、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;

ソートを求めない

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

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

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

ただし、が固定値であり、クエリの実行ごとに変化しない場合、フィルター選択されたインデックスは特定のクエリに対してさらに優れています> 0

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

OperationalSeconds3番目の列にあるインデックスとフィルター選択されたインデックスには、2つの違いがあります。

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

  • 第二に、これはクエリにとってより微妙で重要です。クエリで使用されるフィルタに一致する行のみがあることです。この3列目の値によっては、これが非常に重要になる場合があります。
    例えば、特定のためのパラメータのセットMachineryIdDateRecorded1000行を生じ得ます。これらの行のすべてまたはほぼすべてが(OperationalSeconds > 0)フィルターに一致する場合、両方のインデックスが適切に動作します。ただし、フィルターに一致する行が非常に少ない(または最後の行のみ、またはまったくない)場合、最初のインデックスは、一致するものが見つかるまで多くの行または1000行すべてを通過する必要があります。一方、フィルター選択されたインデックスは、フィルターに一致する行のみが保存されるため、一致する行を見つける(または0行を返す)ために1回のシークのみを必要とします。


1
インデックスを追加すると、クエリがより効率的になりましたか?
ypercubeᵀᴹ

ステージングデータベースではなく(適切にテストするには実際により多くのデータが必要です)、まだライブで試していないため、新しいインデックスを構築するには1時間以上かかります。また、稼働中のデータベースは既に低速で実行されているため、ライブデータベースに対して何かを行うこともかなりためらっています。ライブをステージングにクローンするためのより良いシステムが必要です。
アンドリューウィリアムソン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.