Ringkasan
Masalah utama adalah:
- Pilihan rencana pengoptimal mengasumsikan distribusi nilai yang seragam.
- Kurangnya indeks yang sesuai berarti:
- Memindai tabel adalah satu-satunya opsi.
- Gabung adalah loop bersarang naif bergabung, bukan indeks bersarang loop bergabung. Dalam join yang naif, predikat join dievaluasi pada join daripada didorong ke sisi dalam join.
Detail
Kedua paket ini pada dasarnya sangat mirip, meskipun kinerjanya mungkin sangat berbeda:
Rencanakan dengan kolom tambahan
Mengambil satu dengan kolom tambahan yang tidak lengkap dalam waktu yang wajar terlebih dahulu:
Fitur yang menarik adalah:
- Atas pada simpul 0 membatasi baris yang dikembalikan ke 100. Ini juga menetapkan sasaran baris untuk pengoptimal, sehingga segala sesuatu di bawahnya dalam paket dipilih untuk mengembalikan 100 baris pertama dengan cepat.
- Pindai di simpul 4 menemukan baris dari tabel di mana
Start_Time
bukan nol, State
adalah 3 atau 4, dan Operation_Type
merupakan salah satu nilai yang terdaftar. Tabel dipindai sepenuhnya satu kali, dengan setiap baris diuji terhadap predikat yang disebutkan. Hanya baris yang lulus semua tes yang mengalir ke Sort. Pengoptimal memperkirakan 38.283 baris akan memenuhi syarat.
- Sort at at node 3 mengkonsumsi semua baris dari Scan at node 4, dan mengurutkannya sesuai urutan
Start_Time DESC
. Ini adalah urutan presentasi final yang diminta oleh kueri.
- Pengoptimal memperkirakan bahwa 93 baris (sebenarnya 93.2791) harus dibaca dari Sortir agar keseluruhan rencana mengembalikan 100 baris (memperhitungkan efek yang diharapkan dari gabungan).
- Gabung Nested Loops pada node 2 diharapkan untuk mengeksekusi input dalam (cabang bawah) 94 kali (sebenarnya 94.2791). Baris tambahan diperlukan oleh stop parallelism exchange di node 1 karena alasan teknis.
- Pindai di simpul 5 sepenuhnya memindai tabel pada setiap iterasi. Ia menemukan baris di mana
Start_Time
tidak nol dan State
3 atau 4. Ini diperkirakan menghasilkan 400.875 baris pada setiap iterasi. Lebih dari 94.2791 iterasi, jumlah baris hampir 38 juta.
- Gabung Nested Loop pada simpul 2 juga menerapkan predikat gabung. Ini memeriksa yang
Operation_Type
cocok, bahwa Start_Time
dari simpul 4 kurang dari Start_Time
dari simpul 5, bahwa Start_Time
dari simpul 5 kurang dari Finish_Time
dari simpul 4, dan bahwa dua Id
nilai tidak cocok.
- Gather Streams (stop parallelism exchange) pada node 1 menggabungkan aliran yang dipesan dari setiap utas hingga 100 baris telah dihasilkan. Sifat pengawetan pesanan dari penggabungan beberapa aliran adalah yang membutuhkan baris tambahan yang disebutkan dalam langkah 5.
Inefisiensi besar jelas pada langkah 6 dan 7 di atas. Pemindaian penuh tabel pada simpul 5 untuk setiap iterasi hanya sedikit masuk akal jika hanya terjadi 94 kali seperti yang diprediksi oleh optimizer. Perbandingan ~ 38 juta per-baris pada node 2 juga merupakan biaya yang besar.
Yang terpenting, estimasi sasaran baris 93/94 juga sangat mungkin salah, karena bergantung pada distribusi nilai. Pengoptimal mengasumsikan distribusi seragam tanpa adanya informasi yang lebih rinci. Secara sederhana, ini berarti bahwa jika 1% dari baris dalam tabel diharapkan untuk memenuhi syarat, alasan pengoptimal untuk menemukan 1 baris yang cocok, itu perlu membaca 100 baris.
Jika Anda menjalankan kueri ini hingga selesai (yang mungkin membutuhkan waktu sangat lama), kemungkinan besar Anda akan menemukan bahwa lebih dari 93/94 baris harus dibaca dari Sortir untuk akhirnya menghasilkan 100 baris. Dalam kasus terburuk, baris ke-100 akan ditemukan menggunakan baris terakhir dari Sort. Dengan asumsi estimasi optimizer pada node 4 benar, ini berarti menjalankan Scan at node 5 38.284 kali, dengan total sekitar 15 miliar baris. Bisa jadi lebih jika estimasi Pindai juga tidak aktif.
Rencana eksekusi ini juga termasuk peringatan indeks yang hilang:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 72.7096%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([Operation_Type],[State],[Start_Time])
INCLUDE ([Id],[Parameters])
Pengoptimal memberi tahu Anda bahwa menambahkan indeks ke tabel akan meningkatkan kinerja.
Rencanakan tanpa kolom tambahan
Ini pada dasarnya adalah rencana yang sama persis seperti yang sebelumnya, dengan penambahan Index Spool pada node 6 dan Filter pada node 5. Perbedaan penting adalah:
- Indeks Spool di node 6 adalah Eager Spool. Ini dengan bersemangat mengkonsumsi hasil pemindaian di bawahnya, dan membangun indeks sementara dikunci pada
Operation_Type
dan Start_Time
, dengan Id
sebagai kolom non-kunci.
- Nested Loops Join di node 2 sekarang menjadi index join. Tidak ada bergabung predikat dievaluasi di sini, bukan nilai-nilai saat ini per-iterasi
Operation_Type
, Start_Time
, Finish_Time
, dan Id
dari scan pada node 4 dilewatkan ke cabang batin-side sebagai referensi luar.
- Pemindaian di simpul 7 dilakukan hanya sekali.
- Indeks Spool pada node 6 mencari baris dari indeks sementara di mana
Operation_Type
cocok dengan nilai referensi luar saat ini, dan Start_Time
berada dalam kisaran yang ditentukan oleh referensi luar Start_Time
dan Finish_Time
.
- Filter di node 5 menguji
Id
nilai dari Spool Indeks untuk ketidaksetaraan terhadap nilai referensi luar saat ini dari Id
.
Perbaikan utama adalah:
- Pemindaian sisi dalam dilakukan hanya sekali
- Indeks sementara pada (
Operation_Type
, Start_Time
) dengan Id
sebagai kolom yang disertakan memungkinkan loop bersarang indeks bergabung. Indeks digunakan untuk mencari baris yang cocok pada setiap iterasi daripada memindai seluruh tabel setiap kali.
Seperti sebelumnya, pengoptimal menyertakan peringatan tentang indeks yang hilang:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 24.1475%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([State],[Start_Time])
INCLUDE ([Id],[Operation_Type])
GO
Kesimpulan
Paket tanpa kolom tambahan lebih cepat karena pengoptimal memilih untuk membuat indeks sementara untuk Anda.
Rencana dengan kolom tambahan akan membuat indeks sementara lebih mahal untuk dibangun. The [Parameters
Kolom] adalah nvarchar(2000)
, yang akan menambahkan hingga 4000 byte untuk setiap baris dari indeks. Biaya tambahan cukup untuk meyakinkan pengoptimal bahwa membangun indeks sementara pada setiap eksekusi tidak akan membayar sendiri.
Pengoptimal memperingatkan dalam kedua kasus bahwa indeks permanen akan menjadi solusi yang lebih baik. Komposisi indeks yang ideal tergantung pada beban kerja Anda yang lebih luas. Untuk permintaan khusus ini, indeks yang disarankan adalah titik awal yang masuk akal, tetapi Anda harus memahami manfaat dan biaya yang terlibat.
Rekomendasi
Berbagai kemungkinan indeks akan bermanfaat untuk kueri ini. Hal penting yang perlu diperhatikan adalah diperlukan semacam indeks yang tidak tercakup. Dari informasi yang diberikan, indeks yang masuk akal menurut saya adalah:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);
Saya juga akan tergoda untuk mengatur kueri sedikit lebih baik, dan menunda mencari [Parameters]
kolom lebar dalam indeks berkerumun sampai setelah 100 baris teratas telah ditemukan (menggunakan Id
sebagai kunci):
SELECT TOP (100)
BTQ1.id,
BTQ2.id,
BTQ3.[Parameters],
BTQ4.[Parameters]
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
-- Look up the [Parameters] values
JOIN dbo.Batch_Tasks_Queue AS BTQ3
ON BTQ3.Id = BTQ1.Id
JOIN dbo.Batch_Tasks_Queue AS BTQ4
ON BTQ4.Id = BTQ2.Id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
-- These predicates are not strictly needed
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
Jika [Parameters]
kolom tidak diperlukan, kueri dapat disederhanakan menjadi:
SELECT TOP (100)
BTQ1.id,
BTQ2.id
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
The FORCESEEK
petunjuk yang ada untuk membantu memastikan optimizer memilih sebuah loop bersarang diindeks berencana (ada godaan berbasis biaya untuk optimizer untuk memilih hash atau (banyak-banyak) merge bergabung sebaliknya, yang cenderung untuk tidak bekerja dengan baik dengan jenis dalam praktek. Keduanya berakhir dengan residu besar; banyak item per ember dalam kasus hash, dan banyak mundur untuk penggabungan).
Alternatif
Jika kueri (termasuk nilai spesifiknya) sangat penting untuk kinerja baca, saya akan mempertimbangkan dua indeks yang difilter sebagai gantinya:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
CREATE NONCLUSTERED INDEX i2
ON dbo.Batch_Tasks_Queue (Operation_Type, [State], Start_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
Untuk kueri yang tidak memerlukan [Parameters]
kolom, taksiran paket menggunakan indeks yang difilter adalah:
Pemindaian indeks secara otomatis mengembalikan semua baris yang memenuhi syarat tanpa mengevaluasi predikat tambahan apa pun. Untuk setiap iterasi dari loop bersarang indeks, pencarian indeks melakukan dua operasi pencarian:
- Pencocokan awalan mencari pada
Operation_Type
dan State
= 3, kemudian mencari rentang Start_Time
nilai, predikat residual pada Id
ketidaksetaraan.
- Pencocokan awalan mencari pada
Operation_Type
dan State
= 4, kemudian mencari rentang Start_Time
nilai, predikat residual pada Id
ketidaksetaraan.
Di mana [Parameters]
kolom diperlukan, rencana kueri hanya menambahkan maksimum 100 pencarian tunggal untuk setiap tabel:
Sebagai catatan akhir, Anda harus mempertimbangkan untuk menggunakan tipe integer standar bawaan alih-alih yang numeric
berlaku.