Mengapa kueri ini tidak menggunakan indeks nonclustered saya, dan bagaimana saya bisa membuatnya?

12

Sebagai tindak lanjut dari pertanyaan ini tentang peningkatan kinerja kueri, saya ingin tahu apakah ada cara untuk membuat indeks saya digunakan secara default.

Kueri ini berjalan sekitar 2,5 detik:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31';

Yang ini berjalan sekitar 33 ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];

Ada indeks berkerumun di bidang [ID] (pk) dan ada indeks yang tidak berkerumun di [DateEntered], [DeviceID]. Permintaan pertama menggunakan indeks berkerumun, permintaan kedua menggunakan indeks non-berkerumun saya. Pertanyaan saya adalah dua bagian:

  • Mengapa, karena kedua kueri memiliki klausa WHERE pada bidang [DateEntered], apakah server menggunakan indeks berkerumun pada yang pertama, tetapi bukan yang kedua?
  • Bagaimana saya dapat membuat indeks non-cluster digunakan secara default pada permintaan ini bahkan tanpa orderby? (Atau mengapa saya tidak menginginkan perilaku itu?)
Nate
sumber
DateEntered adalah DateTime, dalam hal ini saya menggunakan bagian tanggal, tetapi saya terkadang menanyakan tanggal dan waktu bersamaan.
Nate

Jawaban:

9

kueri pertama melakukan pemindaian tabel berdasarkan ambang yang saya jelaskan sebelumnya: Apakah mungkin untuk meningkatkan kinerja kueri pada tabel sempit dengan jutaan baris?

(kemungkinan besar kueri Anda tanpa TOP 1000klausa akan mengembalikan lebih dari 46k baris. atau di mana antara 35k dan 46k. (area abu-abu ;-))

permintaan kedua, harus dipesan. Karena Anda indeks NC dipesan dalam urutan yang Anda inginkan, lebih murah bagi pengoptimal untuk menggunakan indeks itu, dan kemudian ke pencarian bookmark ke indeks berkerumun untuk mendapatkan kolom yang hilang sebagai dibandingkan untuk melakukan pemindaian indeks berkerumun dan kemudian membutuhkan untuk memesan itu.

membalik urutan kolom dalam ORDER BYklausa dan Anda kembali ke pemindaian indeks berkerumun karena NC INDEX kemudian tidak berguna.

sunting lupa jawaban pertanyaan kedua Anda, mengapa Anda TIDAK menginginkan ini

Menggunakan indeks non-tertutup non-clustered berarti bahwa rowID dilihat dalam indeks NC dan kemudian kolom-kolom yang hilang harus dicari dalam indeks clustered (indeks clustered berisi semua kolom dari sebuah tabel). IO untuk mencari kolom yang hilang dalam indeks berkerumun adalah IO acak.

Kuncinya di sini adalah ACAK. karena untuk setiap baris yang ditemukan dalam indeks NC, metode akses harus mencari halaman baru dalam indeks berkerumun. Ini acak, dan karenanya sangat mahal.

Sekarang, di sisi lain pengoptimal juga dapat pergi untuk memindai indeks berkerumun. Ini dapat menggunakan peta alokasi untuk mencari rentang pemindaian dan mulai membaca indeks Clustered dalam potongan besar. Ini berurutan dan jauh lebih murah. (selama meja Anda tidak terfragmentasi :-)) Kelemahannya adalah, indeks cluster WHOLE perlu dibaca. Ini buruk untuk buffer Anda dan berpotensi banyak IO. tapi tetap saja, IO berurutan.

Dalam kasus Anda, pengoptimal menentukan baris antara 35k dan 46k, lebih murah untuk memindai indeks berkerumun penuh. Ya itu salah. Dan dalam banyak kasus dengan indeks non-clustered sempit dengan WHEREklausa tidak selektif atau tabel besar dalam hal ini ini salah. (Meja Anda lebih buruk, karena juga meja yang sangat sempit.)

Sekarang, menambahkan ORDER BYmembuatnya lebih mahal untuk memindai indeks cluster penuh dan kemudian memesan hasilnya. Sebaliknya, pengoptimal menganggap lebih murah untuk menggunakan indeks NC yang sudah dipesan dan kemudian membayar penalti IO acak untuk pencarian bookmark.

Jadi pesanan Anda adalah solusi "petunjuk permintaan" yang sempurna. TAPI, pada titik tertentu, begitu hasil kueri Anda begitu besar, hukuman untuk IO acak penunjuk bookmark akan sangat besar sehingga menjadi lebih lambat. Saya menganggap pengoptimal akan mengubah rencana kembali ke pemindaian indeks berkerumun sebelum titik itu tetapi Anda tidak pernah tahu pasti.

Dalam kasus Anda, selama sisipan Anda dipesan dengan tanggal masuk, seperti yang dibahas dalam obrolan dan pertanyaan sebelumnya (lihat tautan), Anda lebih baik membuat indeks berkerumun di kolom EnterDate.

Edward Dortland
sumber
20

Mengekspresikan kueri menggunakan sintaks yang berbeda kadang-kadang dapat membantu mengomunikasikan keinginan Anda untuk menggunakan indeks yang tidak berkerumun ke pengoptimal. Anda harus menemukan formulir di bawah ini memberi Anda rencana yang Anda inginkan:

SELECT
    [ID],
    [DeviceID],
    [IsPUp],
    [IsWebUp],
    [IsPingUp],
    [DateEntered]
FROM [dbo].[Heartbeats]
WHERE
    [ID] IN
(
    -- Keys
    SELECT TOP (1000)
        [ID]
    FROM [dbo].[Heartbeats]
    WHERE 
        [DateEntered] >= CONVERT(datetime, '2011-08-30', 121)
        AND [DateEntered]  < CONVERT(datetime, '2011-08-31', 121)
);

Rencana Kueri

Bandingkan rencana itu dengan yang dihasilkan ketika indeks non-cluster dipaksa dengan petunjuk:

SELECT TOP (1000) 
    * 
FROM [dbo].[Heartbeats] WITH (INDEX(CommonQueryIndex))
WHERE 
    [DateEntered] BETWEEN '2011-08-30' and '2011-08-31';

Rencana Petunjuk Indeks Paksa

Rencana pada dasarnya sama (Pencarian Kunci tidak lebih dari pencarian pada indeks berkerumun). Kedua bentuk rencana hanya akan melakukan satu pencarian pada indeks non-clustered dan maksimum 1000 lookup ke indeks clustered.

Perbedaan yang penting adalah pada posisi operator Top. Diposisikan di antara dua pencarian, Top mencegah optimizer menggantikan dua operasi pencarian dengan pemindaian setara secara logis dari indeks berkerumun. Pengoptimal bekerja dengan mengganti bagian dari rencana logis dengan operasi relasional yang setara. Top bukan operator relasional, sehingga penulisan ulang mencegah transformasi ke pemindaian indeks berkerumun. Jika pengoptimal dapat memposisikan ulang operator Top, itu masih akan lebih suka pemindaian atas pencarian + pencarian karena cara estimasi biaya bekerja.

Biaya pemindaian dan pencarian

Pada tingkat yang sangat tinggi, model biaya pengoptimal untuk pemindaian dan pencarian cukup sederhana: ini memperkirakan bahwa 320 pencarian acak harganya sama dengan membaca 1350 halaman dalam pemindaian. Ini mungkin memiliki sedikit kemiripan dengan kemampuan perangkat keras dari setiap sistem I / O modern tertentu, tetapi itu bekerja dengan baik sebagai model praktis.

Model ini juga membuat sejumlah asumsi penyederhanaan, yang utama adalah bahwa setiap permintaan diasumsikan dimulai tanpa data atau halaman indeks yang sudah ada dalam cache. Implikasinya adalah bahwa setiap I / O akan menghasilkan I / O fisik - meskipun ini jarang terjadi dalam praktik. Bahkan dengan cache yang dingin, pre-fetching dan read-ahead berarti bahwa halaman-halaman yang dibutuhkan sebenarnya sangat mungkin berada di memori pada saat prosesor permintaan membutuhkannya.

Pertimbangan lain adalah bahwa permintaan pertama untuk baris yang tidak ada dalam memori akan menyebabkan seluruh halaman diambil dari disk. Permintaan baris berikutnya pada halaman yang sama kemungkinan besar tidak akan menimbulkan I / O fisik. Model penetapan biaya memang mengandung logika untuk memperhitungkan beberapa efek seperti ini, tetapi tidak sempurna.

Semua hal ini (dan lebih banyak lagi) berarti pengoptimal cenderung beralih ke pemindaian lebih awal daripada yang seharusnya. Acak I / O hanya 'jauh lebih mahal' daripada 'berurutan' I / O jika operasi fisik menghasilkan - mengakses halaman dalam memori memang sangat cepat. Bahkan di mana pembacaan fisik diperlukan, pemindaian mungkin tidak menghasilkan pembacaan berurutan sama sekali karena fragmentasi, dan berusaha dapat dilokasikan sedemikian rupa sehingga polanya pada dasarnya berurutan. Tambahkan ke bahwa karakteristik kinerja yang berubah dari sistem I / O modern (terutama solid-state) dan semuanya mulai terlihat sangat goyah.

Tujuan Baris

Kehadiran operator Top dalam rencana memodifikasi pendekatan penetapan biaya. Pengoptimal cukup pintar untuk mengetahui bahwa menemukan 1000 baris menggunakan pemindaian kemungkinan tidak akan memerlukan pemindaian seluruh indeks berkerumun - itu dapat berhenti segera setelah 1000 baris telah ditemukan. Ini menetapkan 'tujuan baris' dari 1000 baris di operator Top dan menggunakan informasi statistik untuk bekerja kembali dari sana untuk memperkirakan berapa banyak baris yang diharapkan dibutuhkan dari sumber baris (pemindaian dalam kasus ini). Saya menulis tentang rincian perhitungan ini di sini .

Gambar dalam jawaban ini dibuat menggunakan SQL Sentry Plan Explorer .

Paul White 9
sumber