Mendapatkan pemindaian meskipun saya berharap mencari

9

Saya perlu mengoptimalkan SELECTpernyataan tetapi SQL Server selalu melakukan pemindaian indeks daripada mencari. Ini adalah permintaan yang, tentu saja, dalam prosedur tersimpan:

CREATE PROCEDURE dbo.something
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL    
AS

    SELECT [IdNumber], [Code], [Status], [Sex], 
           [FirstName], [LastName], [Profession], 
           [BirthDate], [HireDate], [ActiveDirectoryUser]
    FROM Employee
    WHERE (@Status IS NULL OR [Status] = @Status)
    AND 
    (
      @IsUserGotAnActiveDirectoryUser IS NULL 
      OR 
      (
        @IsUserGotAnActiveDirectoryUser IS NOT NULL AND       
        (
          @IsUserGotAnActiveDirectoryUser = 1 AND ActiveDirectoryUser <> ''
        )
        OR
        (
          @IsUserGotAnActiveDirectoryUser = 0 AND ActiveDirectoryUser = ''
        )
      )
    )

Dan ini adalah indeksnya:

CREATE INDEX not_relevent ON dbo.Employee
(
    [Status] DESC,
    [ActiveDirectoryUser] ASC
)
INCLUDE (...all the other columns in the table...); 

Rencana:

Rencanakan gambar

Mengapa SQL Server memilih pemindaian? Bagaimana saya bisa memperbaikinya?

Definisi kolom:

[Status] int NOT NULL
[ActiveDirectoryUser] VARCHAR(50) NOT NULL

Parameter status dapat:

NULL: all status,
1: Status= 1 (Active employees)
2: Status = 2 (Inactive employees)

IsUserGotAnActiveDirectoryUser dapat:

NULL: All employees
0: ActiveDirectoryUser is empty for that employee
1: ActiveDirectoryUser  got a valid value (not null and not empty)
Bestter
sumber
Bisakah Anda memposting rencana eksekusi yang sebenarnya di suatu tempat (bukan gambar itu, tetapi file .sqlplan dalam bentuk XML)? Dugaan saya adalah Anda mengubah prosedur tetapi tidak benar-benar mendapatkan kompilasi baru di tingkat pernyataan. Bisakah Anda mengubah beberapa teks dari kueri (seperti menambahkan awalan skema ke nama tabel ), dan lalu memberikan nilai yang valid untuk @Status?
Aaron Bertrand
1
Juga definisi indeks menimbulkan pertanyaan - mengapa kunci aktif Status DESC? Berapa banyak nilai yang ada Status, untuk apa mereka (jika jumlahnya kecil), dan apakah masing-masing nilai diwakili secara setara? Tunjukkan kami hasilSELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Aaron Bertrand

Jawaban:

11

Saya tidak berpikir pemindaian disebabkan oleh pencarian untuk string kosong (dan sementara Anda bisa menambahkan indeks yang difilter untuk kasus itu, itu hanya akan membantu variasi permintaan yang sangat spesifik). Anda lebih cenderung menjadi korban dari sniffing parameter dan satu paket tidak dioptimalkan untuk semua kombinasi berbagai parameter (dan nilai parameter) yang akan Anda berikan untuk kueri ini.

Saya menyebutnya prosedur "wastafel dapur" , karena Anda mengharapkan satu permintaan untuk menyediakan semua hal, termasuk wastafel dapur.

Saya punya video tentang solusi saya untuk ini di sini , tetapi pada dasarnya, pengalaman terbaik yang saya miliki untuk pertanyaan seperti itu adalah:

  • Bangun pernyataan secara dinamis - ini akan memungkinkan Anda untuk meninggalkan klausa yang menyebutkan kolom yang tidak ada parameter yang disediakan, dan memastikan bahwa Anda akan memiliki rencana yang dioptimalkan secara tepat untuk parameter aktual yang diberikan dengan nilai.
  • GunakanOPTION (RECOMPILE) - ini mencegah nilai parameter spesifik dari memaksa jenis paket yang salah, terutama membantu ketika Anda memiliki kemiringan data, statistik yang buruk, atau ketika eksekusi pertama dari pernyataan menggunakan nilai atipikal yang akan mengarah pada rencana yang berbeda daripada nanti dan lebih sering eksekusi.
  • Gunakan opsi serveroptimize for ad hoc workloads - ini mencegah variasi kueri yang hanya digunakan satu kali dari mencemari cache rencana Anda.

Aktifkan optimisasi untuk beban kerja ad hoc:

EXEC sys.sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'optimize for ad hoc workloads', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'show advanced options', 0;
GO
RECONFIGURE WITH OVERRIDE;

Ubah prosedur Anda:

ALTER PROCEDURE dbo.Whatever
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL
AS
BEGIN 
  SET NOCOUNT ON;
  DECLARE @sql NVARCHAR(MAX) = N'SELECT [IdNumber], [Code], [Status], 
     [Sex], [FirstName], [LastName], [Profession],
     [BirthDate], [HireDate], [ActiveDirectoryUser]
   FROM dbo.Employee -- please, ALWAYS schema prefix
   WHERE 1 = 1';

   IF @Status IS NOT NULL
     SET @sql += N' AND ([Status]=@Status)'

   IF @IsUserGotAnActiveDirectoryUser = 1
     SET @sql += N' AND ActiveDirectoryUser <> ''''';
   IF @IsUserGotAnActiveDirectoryUser = 0
     SET @sql += N' AND ActiveDirectoryUser = ''''';

   SET @sql += N' OPTION (RECOMPILE);';

   EXEC sys.sp_executesql @sql, N'@Status INT, @Status;
END
GO

Setelah Anda memiliki beban kerja berdasarkan kumpulan pertanyaan yang dapat Anda monitor, Anda dapat menganalisis eksekusi dan melihat mana yang paling diuntungkan dari indeks tambahan atau berbeda - Anda dapat melakukan ini dari berbagai sudut, dari yang sederhana "yang merupakan kombinasi dari parameter disediakan paling sering? " ke "kueri individual manakah yang memiliki runtime terpanjang?" Kami tidak dapat menjawab pertanyaan-pertanyaan itu hanya berdasarkan kode Anda, kami hanya dapat menyarankan bahwa indeks apa pun hanya akan membantu untuk subset dari semua kombinasi parameter yang mungkin Anda coba dukung. Misalnya, jika@Statusadalah NULL, maka tidak ada upaya untuk mencegah bahwa indeks non-cluster adalah mungkin. Jadi untuk kasus-kasus di mana pengguna tidak peduli tentang status, Anda akan mendapatkan pemindaian, kecuali jika Anda memiliki indeks yang sesuai dengan klausa lain (tetapi indeks tersebut tidak akan berguna juga, mengingat logika permintaan Anda saat ini) - baik string kosong atau tidak string kosong tidak sepenuhnya selektif).

Dalam hal ini, tergantung pada set Statusnilai yang mungkin dan seberapa terdistribusi nilai-nilai itu, OPTION (RECOMPILE)mungkin tidak diperlukan. Tetapi jika Anda memiliki beberapa nilai yang akan menghasilkan 100 baris dan beberapa nilai yang akan menghasilkan ratusan ribu, Anda mungkin menginginkannya di sana (bahkan dengan biaya CPU, yang seharusnya marjinal mengingat kompleksitas kueri ini), sehingga Anda dapat dapatkan berusaha dalam banyak kasus sebanyak mungkin. Jika rentang nilai cukup terbatas, Anda bahkan bisa melakukan sesuatu yang rumit dengan SQL dinamis, di mana Anda berkata "Saya memiliki nilai yang sangat selektif ini @Status, jadi ketika nilai tertentu dilewatkan, buat sedikit perubahan pada teks kueri sehingga ini dianggap sebagai kueri yang berbeda dan dioptimalkan untuk nilai param itu. "

Aaron Bertrand
sumber
3
Saya telah menggunakan pendekatan ini berkali-kali dan ini adalah cara yang fantastis untuk mendapatkan pengoptimal untuk melakukan sesuatu dengan cara yang menurut Anda harus tetap melakukannya. Kim Tripp berbicara tentang solusi serupa di sini: sqlskills.com/blogs/kimberly/high-performance-procedures Dan memiliki video sesi yang dia lakukan di PASS beberapa tahun yang lalu yang benar-benar menjadi rincian gila mengapa itu bekerja. Yang mengatakan, itu benar-benar tidak menambah satu ton untuk apa yang dikatakan Mr. Bertrand di sini. Ini adalah salah satu alat yang setiap orang harus simpan di sabuk pengaman mereka. Ini benar-benar dapat menyimpan beberapa rasa sakit yang besar untuk semua pertanyaan yang ada.
mskinner
3

Penafian : Beberapa hal dalam jawaban ini dapat membuat DBA tersentak. Saya mendekatinya dari sudut pandang kinerja murni - cara mendapatkan Indeks Mencari ketika Anda selalu mendapatkan Indeks Scan.

Dengan hal itu, ini dia.

Permintaan Anda adalah apa yang dikenal sebagai "permintaan wastafel dapur" - satu permintaan yang dimaksudkan untuk memenuhi berbagai kondisi pencarian yang memungkinkan. Jika pengguna menetapkan @statuske nilai, Anda ingin memfilter pada status itu. Jika @statusadalah NULL, mengembalikan semua status, dan sebagainya.

Ini menimbulkan masalah dengan pengindeksan, tetapi mereka tidak terkait dengan sargability, karena semua kondisi pencarian Anda adalah kriteria "sama dengan".

Ini masuk akal:

WHERE [status]=@status

Ini tidak masuk akal karena SQL Server perlu mengevaluasi ISNULL([status], 0)untuk setiap baris alih-alih mencari nilai tunggal dalam indeks:

WHERE ISNULL([status], 0)=@status

Saya telah menciptakan kembali masalah wastafel dapur dalam bentuk yang lebih sederhana:

CREATE TABLE #work (
    A    int NOT NULL,
    B    int NOT NULL
);

CREATE UNIQUE INDEX #work_ix1 ON #work (A, B);

INSERT INTO #work (A, B)
VALUES (1,  1), (2,  1),
       (3,  1), (4,  1),
       (5,  2), (6,  2),
       (7,  2), (8,  3),
       (9,  3), (10, 3);

Jika Anda mencoba yang berikut ini, Anda akan mendapatkan Pemindaian Indeks, meskipun A adalah kolom pertama dari indeks:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE (@a IS NULL OR @a=A) AND
      (@b IS NULL OR @b=B);

Ini, bagaimanapun, menghasilkan Indeks Mencari:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL;

Selama Anda menggunakan jumlah parameter yang dapat dikelola (dua dalam kasus Anda), Anda mungkin bisa hanya UNIONsekelompok permintaan pencarian - pada dasarnya semua permutasi kriteria pencarian. Jika Anda memiliki tiga kriteria, ini akan terlihat berantakan, dengan empat kriteria itu akan sepenuhnya tidak dapat dikelola. Anda sudah diperingatkan.

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL
UNION ALL
SELECT *
FROM #work
WHERE @a=A AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b IS NULL;

Untuk yang ketiga dari keempat untuk menggunakan Indeks Mencari, Anda akan memerlukan indeks kedua (B, A). Begini cara kueri Anda terlihat dengan perubahan-perubahan ini (termasuk refactoring saya atas kueri agar lebih mudah dibaca).

DECLARE @Status int = NULL,
        @IsUserGotAnActiveDirectoryUser bit = NULL;

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='')

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='');

... plus Anda akan memerlukan indeks tambahan Employeedengan dua kolom indeks dibalik.

Untuk kelengkapan, saya harus menyebutkan bahwa x=@xsecara implisit berarti itu xtidak mungkin NULLkarena NULLtidak pernah sama dengan NULL. Itu sedikit menyederhanakan kueri.

Dan, ya, jawaban SQL dinamis Aaron Bertrand adalah pilihan yang lebih baik dalam kebanyakan kasus (yaitu kapan pun Anda dapat hidup dengan kompilasi).

Daniel Hutmacher
sumber
3

Pertanyaan dasar Anda tampaknya adalah "Mengapa" dan saya pikir Anda mungkin menemukan jawabannya sekitar menit 55 atau lebih dari presentasi Hebat ini oleh Adam Machanic di TechEd beberapa tahun yang lalu.

Saya menyebutkan 5 menit pada menit 55 tetapi seluruh presentasi sepadan dengan waktu. Jika Anda melihat rencana kueri untuk kueri Anda, saya yakin Anda akan menemukannya memiliki Predikat Sisa untuk pencarian. Pada dasarnya SQL tidak dapat "melihat" semua bagian dari indeks karena beberapa dari mereka disembunyikan oleh ketidaksetaraan dan kondisi lainnya. Hasilnya adalah pemindaian indeks untuk super set berdasarkan Predikat. Hasil itu spooled dan kemudian dipindai kembali menggunakan predikat residual.

Periksa properti Operator Pindai (F4) dan lihat apakah Anda memiliki "Cari Predikat" dan "Predikat" di daftar properti.

Seperti yang telah ditunjukkan orang lain, kueri sulit untuk diindeks apa adanya. Saya telah mengerjakan banyak yang serupa baru-baru ini dan masing-masing membutuhkan solusi yang berbeda. :(

sinar
sumber
0

Sebelum kita mempertanyakan apakah pencarian indeks lebih disukai daripada pemindaian indeks, satu aturan praktis adalah untuk memeriksa berapa banyak baris yang dikembalikan vs total baris tabel yang mendasarinya. Misalnya, jika Anda mengharapkan permintaan Anda mengembalikan 10 baris dari 1 juta baris, maka pencarian indeks mungkin sangat disukai daripada pemindaian indeks. Namun, jika beberapa ribu baris (atau lebih) harus dikembalikan dari kueri, maka pencarian indeks TIDAK mungkin lebih disukai.

Permintaan Anda tidak rumit, jadi jika Anda dapat memposting rencana eksekusi, kami mungkin memiliki ide yang lebih baik untuk membantu Anda.

jyao
sumber
Memfilter beberapa ribu baris dari tabel 1 juta, saya masih ingin mencari - ini masih merupakan peningkatan kinerja yang luas dibandingkan pemindaian seluruh tabel.
Daniel Hutmacher
-6

ini hanya yang asli yang diformat

DECLARE @Status INT = NULL,
        @IsUserGotAnActiveDirectoryUser BIT = NULL    

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName], [Profession],
       [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE (@Status IS NULL OR [Status]=@Status)  
AND (            @IsUserGotAnActiveDirectoryUser IS NULL 
      OR (       @IsUserGotAnActiveDirectoryUser IS NOT NULL 
           AND (     @IsUserGotAnActiveDirectoryUser = 1 
                 AND ActiveDirectoryUser <> '') 
           OR  (     @IsUserGotAnActiveDirectoryUser = 0 
                 AND ActiveDirectoryUser =  '')
         )
    )

ini revisi - tidak 100% yakin tentang hal itu tetapi (mungkin) mencobanya
walaupun satu ATAU mungkin akan menjadi masalah,
ini akan memecah pada ActiveDirectoryUser null

  WHERE isnull(@Status, [Status]) = [Status]
    AND (      (     isnull(@IsUserGotAnActiveDirectoryUser, 1) = 1 
                 AND ActiveDirectoryUser <> '' ) 
           OR  (     isnull(@IsUserGotAnActiveDirectoryUser, 0) = 0 
                 AND ActiveDirectoryUser =  '' )
        )
paparazzo
sumber
3
Tidak jelas bagi saya bagaimana jawaban ini memecahkan pertanyaan OP.
Erik
@Erik Bisakah kita suka mungkin membiarkan OP mencobanya? Dua ATAU pergi. Apakah Anda tahu pasti ini tidak dapat membantu kinerja permintaan?
paparazzo
@ ypercubeᵀᴹ IsUserGotAnActiveDirectoryUser BUKAN NULL dihapus. Kedua tidak perlu menghapus OR dan menghapus IsUserGotAnActiveDirectoryUser IS NULL. Apakah Anda yakin kueri ini tidak akan berjalan cepat daripada OP?
paparazzo
@ ypercubeᵀᴹ Bisa melakukan banyak hal. Saya tidak mencari yang lebih sederhana. Dua atau hilang. Atau biasanya buruk untuk paket permintaan. Saya mendapatkan ada semacam klub di sini dan saya bukan bagian dari klub. Tetapi saya melakukan ini untuk mencari nafkah dan memposting apa yang saya tahu telah berhasil. Jawaban saya tidak terpengaruh oleh down vote.
paparazzo