Inilah run-down: Saya melakukan kueri pemilihan. Setiap kolom dalam klausa WHERE
dan ORDER BY
berada dalam indeks tunggal non-cluster IX_MachineryId_DateRecorded
, baik sebagai bagian dari kunci, atau sebagai INCLUDE
kolom. 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 OperationalSeconds
yang 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.
sumber
Jawaban:
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
MachineryId
danDateRecorded
rentang:Indeks tidak termasuk
OperationalSeconds
, jadi rencana harus melihat nilai itu per baris dalam indeks berkerumun (dipartisi) untuk mengujiOperationalSeconds > 0
: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):
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_MachineryId
dapat dibuat lebih optimal dengan memasukkanOperationalSeconds
.Sangat tidak biasa untuk memiliki indeks nonclustered nonblok (indeks dipartisi dengan cara yang berbeda dari tabel dasar, termasuk tidak sama sekali).
Seperti biasa, pengoptimal memilih paket termurah yang dipertimbangkannya.
Perkiraan biaya
IX_MachineryId
paket adalah 0,01 unit biaya, berdasarkan asumsi sasaran baris yang salah (salah) bahwa satu baris akan diuji dan dikembalikan.Perkiraan biaya
IX_MachineryId_DateRecorded
rencana jauh lebih tinggi, yaitu 0,27 unit, sebagian besar karena ia mengharapkan untuk membaca 5.515 baris dari indeks, mengurutkannya, dan mengembalikan yang paling rendahDateRecorded
:Indeks ini dipartisi, dan tidak dapat mengembalikan baris
DateRecorded
secara langsung (lihat nanti). Itu dapat mencariMachineryId
danDateRecorded
kisaran dalam setiap partisi , tetapi Sort diperlukan: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
@From
dan@To
parameter sesuai denganDateRecorded
kolom (datetime
). Saat ini, SQL Server sedang menghitung rentang dinamis karena tipe ketidakcocokan saat runtime (menggunakan operator Interge Gabung dan subtree-nya):Konversi ini mencegah pengoptimal dari beralasan dengan benar tentang hubungan antara ID partisi naik (mencakup berbagai
DateRecorded
nilai dalam urutan naik) dan ketidaksetaraan prediktif aktifDateRecorded
.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
) kemudianDateRecorded
sama dengan memesanDateRecorded
sendiri (diberikan yangMachineryID
konstan). Rantai penalaran ini dipatahkan oleh konversi tipe.Demo
Tabel dan indeks yang dipartisi sederhana:
Permintaan dengan jenis yang cocok
Permintaan dengan tipe yang tidak cocok
sumber
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
> 0
adalah nilai tetap dan tidak berubah dari satu eksekusi kueri ke yang lain:Ada dua perbedaan antara indeks yang Anda miliki
OperationalSeconds
dengan 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
MachineryId
danDateRecorded
dapat 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.sumber