Mengapa indeks saya tidak digunakan dalam SELECT TOP?

15

Inilah run-down: Saya melakukan kueri pemilihan. Setiap kolom dalam klausa WHEREdan ORDER BYberada dalam indeks tunggal non-cluster IX_MachineryId_DateRecorded, baik sebagai bagian dari kunci, atau sebagai INCLUDEkolom. Saya memilih semua kolom, sehingga akan menghasilkan pencarian bookmark, tapi saya hanya mengambil TOP (1), jadi pasti server dapat memberitahu pencarian hanya perlu dilakukan sekali, pada akhirnya.

Yang paling penting, ketika saya memaksakan kueri untuk menggunakan indeks IX_MachineryId_DateRecorded, itu berjalan dalam waktu kurang dari satu detik. Jika saya membiarkan server memutuskan indeks mana yang akan digunakan, itu mengambil IX_MachineryId, dan itu memakan waktu hingga satu menit. Itu benar-benar menunjukkan kepada saya bahwa saya telah membuat indeks benar, dan server hanya membuat keputusan yang buruk. Mengapa?

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]);

Tabel dipartisi ke dalam rentang bulan (meskipun saya masih tidak benar-benar mengerti apa yang terjadi di sana).

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)

Kueri yang biasanya saya jalankan:

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

Paket pertanyaan: https://www.brentozar.com/pastetheplan/?id=r1c-RpxNx

Rencana kueri dengan indeks paksa: https://www.brentozar.com/pastetheplan/?id=SywwTagVe

Rencana yang dimasukkan adalah rencana pelaksanaan aktual, tetapi pada basis data pementasan (sekitar 1/100 dari ukuran live). Saya ragu untuk mengutak-atik database hidup karena saya baru mulai di perusahaan ini sekitar sebulan yang lalu.

Saya merasa itu karena partisi, dan permintaan saya biasanya mencakup setiap partisi (misalnya ketika saya ingin mendapatkan yang pertama atau terakhir OperationalSecondsyang direkam untuk satu mesin). Namun, pertanyaan yang saya tulis sendiri semuanya berjalan dengan baik 10 - 100 kali lebih cepat daripada yang dihasilkan EntityFramework , jadi saya hanya akan membuat prosedur tersimpan.

Andrew Williamson
sumber
1
Hai @AndrewWilliamson, Ini bisa menjadi masalah statistik. Jika Anda melihat paket sebenarnya dari paket yang tidak dipaksakan, jumlah baris yang diperkirakan adalah 1.22 dan yang sebenarnya adalah 1.909. Ini pada gilirannya mengarah ke pencarian kunci yang Anda lihat nanti dalam rencana. Sudahkah Anda mencoba memperbarui statistik? Jika tidak, coba dengan pemindaian penuh pada basis data pementasan.
jesijesi

Jawaban:

21

Jika saya membiarkan server memutuskan indeks mana yang akan digunakan, itu mengambil IX_MachineryId, dan itu memakan waktu hingga satu menit.

Indeks itu tidak dipartisi, sehingga pengoptimal mengenalinya dapat digunakan untuk menyediakan pemesanan yang ditentukan dalam permintaan tanpa menyortir. Sebagai indeks nonclustered non-unik, itu juga memiliki kunci indeks berkerumun sebagai subkunci, sehingga indeks dapat digunakan untuk mencari MachineryIddan DateRecordedrentang:

Indeks mencari

Indeks tidak termasuk OperationalSeconds, jadi rencana harus melihat nilai itu per baris dalam indeks berkerumun (dipartisi) untuk menguji OperationalSeconds > 0:

Mencari

Pengoptimal memperkirakan bahwa satu baris perlu dibaca dari indeks yang tidak dikelompokkan dan dicari untuk memenuhi TOP (1). Perhitungan ini didasarkan pada tujuan baris (menemukan satu baris dengan cepat), dan mengasumsikan distribusi nilai yang seragam.

Dari rencana aktual, kita bisa melihat estimasi 1 baris tidak akurat. Faktanya, 19.039 baris harus diproses untuk menemukan bahwa tidak ada baris yang memenuhi persyaratan kueri. Ini adalah kasus terburuk untuk pengoptimalan sasaran baris (diperkirakan 1 baris, semua baris sebenarnya diperlukan):

Aktual / taksiran

Anda dapat menonaktifkan sasaran baris dengan bendera jejak 4138 . Ini kemungkinan besar akan menghasilkan SQL Server memilih paket yang berbeda, mungkin yang Anda paksakan. Bagaimanapun, indeks IX_MachineryIddapat dibuat lebih optimal dengan memasukkan OperationalSeconds.

Sangat tidak biasa untuk memiliki indeks nonclustered nonblok (indeks dipartisi dengan cara yang berbeda dari tabel dasar, termasuk tidak sama sekali).

Itu benar-benar menunjukkan kepada saya bahwa saya telah membuat indeks benar, dan server hanya membuat keputusan yang buruk. Mengapa?

Seperti biasa, pengoptimal memilih paket termurah yang dipertimbangkannya.

Perkiraan biaya IX_MachineryIdpaket adalah 0,01 unit biaya, berdasarkan asumsi sasaran baris yang salah (salah) bahwa satu baris akan diuji dan dikembalikan.

Perkiraan biaya IX_MachineryId_DateRecordedrencana jauh lebih tinggi, yaitu 0,27 unit, sebagian besar karena ia mengharapkan untuk membaca 5.515 baris dari indeks, mengurutkannya, dan mengembalikan yang paling rendah DateRecorded:

Sortir N Teratas

Indeks ini dipartisi, dan tidak dapat mengembalikan baris DateRecordedsecara langsung (lihat nanti). Itu dapat mencari MachineryIddan DateRecordedkisaran dalam setiap partisi , tetapi Sort diperlukan:

Carilah yang dipartisi

Jika indeks ini tidak dipartisi, pengurutan tidak akan diperlukan, dan itu akan sangat mirip dengan indeks lainnya (tidak dipartisi) dengan kolom tambahan yang disertakan. Indeks terfilter yang tidak dipartisi akan tetap sedikit lebih efisien.


Anda harus memperbarui permintaan sumber sehingga tipe data dari @Fromdan @Toparameter sesuai dengan DateRecordedkolom ( datetime). Saat ini, SQL Server sedang menghitung rentang dinamis karena tipe ketidakcocokan saat runtime (menggunakan operator Interge Gabung dan subtree-nya):

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

Konversi ini mencegah pengoptimal dari beralasan dengan benar tentang hubungan antara ID partisi naik (mencakup berbagai DateRecordednilai dalam urutan naik) dan ketidaksetaraan prediktif aktif DateRecorded.

ID partisi adalah kunci utama implisit untuk indeks yang dipartisi. Biasanya, pengoptimal dapat melihat bahwa pemesanan dengan ID partisi (di mana ID naik peta untuk naik, nilai-nilai terpisah DateRecorded) kemudian DateRecordedsama dengan memesan DateRecordedsendiri (diberikan yang MachineryIDkonstan). Rantai penalaran ini dipatahkan oleh konversi tipe.

Demo

Tabel dan indeks yang dipartisi sederhana:

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');

Permintaan dengan jenis yang cocok

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

Tidak mencari apapun

Permintaan dengan tipe yang tidak cocok

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

Gabungkan Interval dan Sortir

Paul White 9
sumber
5

Indeks tampaknya cukup baik untuk kueri dan saya tidak yakin mengapa itu tidak dipilih oleh optimizer (statistik? Partisi? Batasan azure?, Tidak tahu benar.)

Tetapi indeks yang difilter akan lebih baik untuk kueri tertentu, jika itu > 0adalah nilai tetap dan tidak berubah dari satu eksekusi kueri ke yang lain:

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

Ada dua perbedaan antara indeks yang Anda miliki OperationalSecondsdengan kolom ke-3 dan indeks yang difilter:

  • Pertama, indeks yang disaring lebih kecil, baik dalam lebar (lebih sempit) dan jumlah baris.
    Ini membuat indeks yang difilter lebih efisien secara umum karena SQL Server membutuhkan lebih sedikit ruang untuk menyimpannya dalam memori.

  • Kedua dan ini lebih halus dan penting untuk kueri adalah hanya memiliki baris yang cocok dengan filter yang digunakan dalam kueri. Ini mungkin sangat penting, tergantung pada nilai kolom ke-3 ini.
    Misalnya seperangkat parameter khusus untuk MachineryIddan DateRecordeddapat menghasilkan 1000 baris. Jika semua atau hampir semua baris ini cocok dengan (OperationalSeconds > 0)filter, kedua indeks akan berperilaku baik. Tetapi jika baris yang cocok dengan filter sangat sedikit (atau hanya yang terakhir atau tidak sama sekali), indeks pertama harus melalui banyak atau semua 1000 baris sampai menemukan kecocokan. Di sisi lain, indeks yang disaring hanya membutuhkan satu upaya untuk menemukan baris yang cocok (atau untuk mengembalikan 0 baris) karena hanya baris yang cocok dengan filter yang disimpan.

ypercubeᵀᴹ
sumber
1
Sudahkah menambahkan indeks membuat kueri lebih efisien?
ypercubeᵀᴹ
Tidak ke database staging (itu benar-benar membutuhkan lebih banyak data di dalamnya untuk menguji dengan benar), saya belum mencobanya secara langsung, indeks baru membutuhkan waktu lebih dari satu jam untuk membangun yang satu itu. Saya juga ragu untuk melakukan apa saja ke database langsung kami, karena sudah berjalan lambat. Kita membutuhkan sistem yang lebih baik untuk mengkloning hidup kita menjadi panggung.
Andrew Williamson