Kueri tidak merespons saat menambahkan dua kolom

9

Ketika saya menambahkan dua kolom ke pilih saya kueri tidak merespons. Jenis kolomnya adalah nvarchar(2000). Agak aneh.

  • Versi SQL Server adalah 2014.
  • Hanya ada satu indeks utama.
  • Seluruh catatan hanya 1000 baris.

Berikut adalah rencana eksekusi sebelumnya ( XML showplan ):

masukkan deskripsi gambar di sini

Rencana eksekusi setelah ( XML showplan ):

masukkan deskripsi gambar di sini

Ini pertanyaannya:

select top(100)
  Batch_Tasks_Queue.id,
  btq.id,
  Batch_Tasks_Queue.[Parameters], -- this field
  btq.[Parameters]  -- and this field
from
        Batch_Tasks_Queue with(nolock)
    inner join  Batch_Tasks_Queue btq with(nolock)  on  Batch_Tasks_Queue.Start_Time < btq.Start_Time
                            and btq.Start_Time < Batch_Tasks_Queue.Finish_Time
                            and Batch_Tasks_Queue.id <> btq.id                            
                            and btq.Start_Time is not null
                            and btq.State in (3, 4)                          
where
    Batch_Tasks_Queue.Start_Time is not null      
    and Batch_Tasks_Queue.State in (3, 4)
    and Batch_Tasks_Queue.Operation_Type = btq.Operation_Type
    and Batch_Tasks_Queue.Operation_Type not in (23, 24, 25, 26, 27, 28, 30)

order by
    Batch_Tasks_Queue.Start_Time desc

Seluruh hasil hitungan adalah 17 baris. Data kotor (nolock hint) tidak penting.

Berikut adalah struktur tabelnya:

CREATE TABLE [dbo].[Batch_Tasks_Queue](
    [Id] [int] NOT NULL,
    [OBJ_VERSION] [numeric](8, 0) NOT NULL,
    [Operation_Type] [numeric](2, 0) NULL,
    [Request_Time] [datetime] NOT NULL,
    [Description] [varchar](1000) NULL,
    [State] [numeric](1, 0) NOT NULL,
    [Start_Time] [datetime] NULL,
    [Finish_Time] [datetime] NULL,
    [Parameters] [nvarchar](2000) NULL,
    [Response] [nvarchar](max) NULL,
    [Billing_UserId] [int] NOT NULL,
    [Planned_Start_Time] [datetime] NULL,
    [Input_FileId] [uniqueidentifier] NULL,
    [Output_FileId] [uniqueidentifier] NULL,
    [PRIORITY] [numeric](2, 0) NULL,
    [EXECUTE_SEQ] [numeric](2, 0) NULL,
    [View_Access] [numeric](1, 0) NULL,
    [Seeing] [numeric](1, 0) NULL,
 CONSTRAINT [PKBachTskQ] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [Batch_Tasks_QueueData]
) ON [Batch_Tasks_QueueData] TEXTIMAGE_ON [Batch_Tasks_QueueData]
GO    
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue]  WITH NOCHECK ADD  CONSTRAINT [FK0_BtchTskQ_BlngUsr] FOREIGN KEY([Billing_UserId])
REFERENCES [dbo].[BILLING_USER] ([ID])
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue] CHECK CONSTRAINT [FK0_BtchTskQ_BlngUsr]
GO
Hamid Fathi
sumber
Diskusi tentang pertanyaan ini telah dipindahkan ke ruang obrolan ini .
Paul White 9

Jawaban:

15

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:

Rencana lambat

Fitur yang menarik adalah:

  1. 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.
  2. Pindai di simpul 4 menemukan baris dari tabel di mana Start_Timebukan nol, Stateadalah 3 atau 4, dan Operation_Typemerupakan 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.
  3. 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.
  4. Pengoptimal memperkirakan bahwa 93 baris (sebenarnya 93.2791) harus dibaca dari Sortir agar keseluruhan rencana mengembalikan 100 baris (memperhitungkan efek yang diharapkan dari gabungan).
  5. 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.
  6. Pindai di simpul 5 sepenuhnya memindai tabel pada setiap iterasi. Ia menemukan baris di mana Start_Timetidak nol dan State3 atau 4. Ini diperkirakan menghasilkan 400.875 baris pada setiap iterasi. Lebih dari 94.2791 iterasi, jumlah baris hampir 38 juta.
  7. Gabung Nested Loop pada simpul 2 juga menerapkan predikat gabung. Ini memeriksa yang Operation_Typecocok, bahwa Start_Timedari simpul 4 kurang dari Start_Timedari simpul 5, bahwa Start_Timedari simpul 5 kurang dari Finish_Timedari simpul 4, dan bahwa dua Idnilai tidak cocok.
  8. 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

Rencana kurang lambat

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:

  1. Indeks Spool di node 6 adalah Eager Spool. Ini dengan bersemangat mengkonsumsi hasil pemindaian di bawahnya, dan membangun indeks sementara dikunci pada Operation_Typedan Start_Time, dengan Idsebagai kolom non-kunci.
  2. 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 Iddari scan pada node 4 dilewatkan ke cabang batin-side sebagai referensi luar.
  3. Pemindaian di simpul 7 dilakukan hanya sekali.
  4. Indeks Spool pada node 6 mencari baris dari indeks sementara di mana Operation_Typecocok dengan nilai referensi luar saat ini, dan Start_Timeberada dalam kisaran yang ditentukan oleh referensi luar Start_Timedan Finish_Time.
  5. Filter di node 5 menguji Idnilai 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 Idsebagai 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 [ParametersKolom] 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 Idsebagai 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 FORCESEEKpetunjuk 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:

Paket indeks sederhana yang difilter

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:

  1. Pencocokan awalan mencari pada Operation_Typedan State= 3, kemudian mencari rentang Start_Timenilai, predikat residual pada Idketidaksetaraan.
  2. Pencocokan awalan mencari pada Operation_Typedan State= 4, kemudian mencari rentang Start_Timenilai, predikat residual pada Idketidaksetaraan.

Di mana [Parameters]kolom diperlukan, rencana kueri hanya menambahkan maksimum 100 pencarian tunggal untuk setiap tabel:

Paket indeks yang difilter dengan kolom tambahan

Sebagai catatan akhir, Anda harus mempertimbangkan untuk menggunakan tipe integer standar bawaan alih-alih yang numericberlaku.

Paul White 9
sumber
-2

Harap buat indeks berikut:

create index Batch_Tasks_Queue_ix_Start_Time on Batch_Tasks_Queue(Start_Time);
David Markודו Markovitz
sumber
Sudahkah Anda mencobanya? Saya telah menjalankan beberapa tes dan itu membuat perbedaan besar.
David Markודו Markovitz