Mengapa SQL Server menggunakan rencana eksekusi yang lebih baik ketika saya sebaris variabel?

32

Saya memiliki kueri SQL yang ingin saya optimalkan:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'

SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

MyTable memiliki dua indeks:

CREATE NONCLUSTERED INDEX IX_MyTable_SomeTimestamp_Includes
ON dbo.MyTable (SomeTimestamp ASC)
INCLUDE(Id, SomeInt)

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp)

Ketika saya menjalankan kueri persis seperti yang ditulis di atas, SQL Server memindai indeks pertama, menghasilkan 189.703 bacaan logis dan durasi 2-3 detik.

Ketika saya sebaris @Idvariabel dan menjalankan query lagi, SQL Server mencari indeks kedua, menghasilkan hanya 104 pembacaan logis dan durasi 0,001 detik (pada dasarnya instan).

Saya memerlukan variabel, tetapi saya ingin SQL menggunakan rencana yang baik. Sebagai solusi sementara saya memberikan petunjuk indeks pada permintaan, dan permintaan pada dasarnya instan. Namun, saya mencoba untuk menjauh dari petunjuk indeks bila memungkinkan. Saya biasanya berasumsi bahwa jika optimizer kueri tidak dapat melakukan tugasnya, maka ada sesuatu yang dapat saya lakukan (atau berhenti lakukan) untuk membantunya tanpa secara eksplisit mengatakan apa yang harus dilakukan.

Jadi, mengapa SQL Server datang dengan rencana yang lebih baik ketika saya sebaris variabel?

Rainbolt
sumber

Jawaban:

44

Dalam SQL Server, ada tiga bentuk umum dari predikat non-gabung:

Dengan nilai literal :

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = 1;

Dengan parameter :

CREATE PROCEDURE dbo.SomeProc(@Reputation INT)
AS
BEGIN
    SELECT COUNT(*) AS records
    FROM   dbo.Users AS u
    WHERE  u.Reputation = @Reputation;
END;

Dengan variabel lokal :

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Hasil

Ketika Anda menggunakan nilai literal , dan paket Anda bukan a) Sepele dan b) Parameter Sederhana atau c) Anda tidak mengaktifkan Parameterisasi Paksa , pengoptimal membuat rencana yang sangat khusus hanya untuk nilai itu.

Saat Anda menggunakan parameter , pengoptimal akan membuat paket untuk parameter itu (ini disebut sniffing parameter ), dan kemudian menggunakan kembali paket itu, tidak ada kompilasi ulang petunjuk, penggusuran cache paket, dll.

Saat Anda menggunakan variabel lokal , pengoptimal membuat rencana untuk ... Sesuatu .

Jika Anda menjalankan kueri ini:

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Rencananya akan terlihat seperti ini:

GILA

Dan taksiran jumlah baris untuk variabel lokal akan terlihat seperti ini:

GILA

Meskipun kueri mengembalikan hitungan 4.744.427.

Variabel lokal, karena tidak diketahui, tidak menggunakan bagian histogram 'baik' untuk estimasi kardinalitas. Mereka menggunakan tebakan berdasarkan vektor kerapatan.

GILA

SELECT 5.280389E-05 * 7250739 AS [poo]

Itu akan memberi Anda 382.86722457471, yang merupakan dugaan pengoptimal.

Tebakan yang tidak dikenal ini biasanya tebakan yang sangat buruk, dan seringkali dapat menyebabkan rencana yang buruk dan pilihan indeks yang buruk.

Memperbaikinya?

Pilihan Anda umumnya adalah:

  • Petunjuk indeks rapuh
  • Petunjuk kompilasi ulang yang berpotensi mahal
  • SQL dinamis parameter
  • Prosedur tersimpan
  • Perbaiki indeks saat ini

Pilihan Anda secara khusus adalah:

Meningkatkan indeks saat ini berarti memperluasnya untuk mencakup semua kolom yang dibutuhkan oleh kueri:

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp, SomeTimestamp, SomeInt)
WITH (DROP_EXISTING = ON);

Dengan asumsi bahwa Idnilainya selektif, ini akan memberi Anda rencana yang baik, dan membantu pengoptimal dengan memberikan metode akses data yang 'jelas'.

Lebih Banyak Membaca

Anda dapat membaca lebih lanjut tentang penyematan parameter di sini:

Erik Darling
sumber
12

Saya akan berasumsi bahwa Anda memiliki data miring, bahwa Anda tidak ingin menggunakan petunjuk kueri untuk memaksa pengoptimal apa yang harus dilakukan, dan bahwa Anda perlu mendapatkan kinerja yang baik untuk semua kemungkinan nilai input @Id. Anda bisa mendapatkan paket permintaan yang dijamin hanya membutuhkan beberapa bacaan logis untuk setiap nilai input yang mungkin jika Anda bersedia membuat pasangan indeks berikut (atau yang setara):

CREATE INDEX GetMinSomeTimestamp ON dbo.MyTable (Id, SomeTimestamp) WHERE SomeBit = 1;
CREATE INDEX GetMaxSomeInt ON dbo.MyTable (Id, SomeInt) WHERE SomeBit = 1;

Di bawah ini adalah data pengujian saya. Saya menempatkan 13 baris M ke dalam tabel dan membuat setengah dari mereka memiliki nilai '3A35EA17-CE7E-4637-8319-4C517B6E48CA'untuk Idkolom.

DROP TABLE IF EXISTS dbo.MyTable;

CREATE TABLE dbo.MyTable (
    Id uniqueidentifier,
    SomeTimestamp DATETIME2,
    SomeInt INT,
    SomeBit BIT,
    FILLER VARCHAR(100)
);

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT NEWID(), CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT '3A35EA17-CE7E-4637-8319-4C517B6E48CA', CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

Pertanyaan ini mungkin terlihat agak aneh pada awalnya:

DECLARE @Id UNIQUEIDENTIFIER = '3A35EA17-CE7E-4637-8319-4C517B6E48CA'

SELECT
  @Id,
  st.SomeTimestamp,
  si.SomeInt
FROM (
    SELECT TOP (1) SomeInt, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeInt DESC
) si
CROSS JOIN (
    SELECT TOP (1) SomeTimestamp, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeTimestamp ASC
) st;

Ini dirancang untuk mengambil keuntungan dari pemesanan indeks untuk menemukan nilai min atau maks dengan beberapa bacaan logis. The CROSS JOINada untuk mendapatkan hasil yang benar ketika tidak ada baris yang cocok untuk @Idnilai. Bahkan jika saya memfilter nilai paling populer dalam tabel (cocok dengan 6,5 juta baris) saya hanya mendapatkan 8 pembacaan logis:

Tabel 'MyTable'. Pindai hitungan 2, bacaan logis 8

Inilah rencana kueri:

masukkan deskripsi gambar di sini

Kedua indeks berusaha menemukan 0 atau 1 baris. Ini sangat efisien, tetapi membuat dua indeks mungkin berlebihan untuk skenario Anda. Anda dapat mempertimbangkan indeks berikut sebagai gantinya:

CREATE INDEX CoveringIndex ON dbo.MyTable (Id) INCLUDE (SomeTimestamp, SomeInt) WHERE SomeBit = 1;

Sekarang rencana permintaan untuk permintaan awal (dengan MAXDOP 1petunjuk opsional ) terlihat sedikit berbeda:

masukkan deskripsi gambar di sini

Pencarian kunci tidak lagi diperlukan. Dengan jalur akses yang lebih baik yang akan bekerja dengan baik untuk semua input Anda tidak perlu khawatir tentang pengoptimal memilih rencana kueri yang salah karena vektor kepadatan. Namun, kueri dan indeks ini tidak akan seefisien yang lain jika Anda mencari @Idnilai populer .

Tabel 'MyTable'. Pindai hitungan 1, bacaan logis 33757

Joe Obbish
sumber
2

Saya tidak bisa menjawab mengapa di sini, tetapi cara cepat dan kotor untuk memastikan bahwa kueri berjalan seperti yang Anda inginkan adalah:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'
SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable WITH (INDEX(IX_MyTable_Id_SomeBit_Includes))
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

Ini menimbulkan risiko bahwa tabel atau indeks dapat berubah di masa depan sehingga optimasi ini menjadi tidak berfungsi, tetapi tersedia jika Anda membutuhkannya. Mudah-mudahan seseorang dapat menawarkan jawaban root kepada Anda, seperti yang Anda minta, daripada solusi ini.

Jon dari Semua Perdagangan
sumber