SQL Server memiliki pengoptimal berbasis biaya yang menggunakan pengetahuan tentang berbagai tabel yang terlibat dalam kueri untuk menghasilkan apa yang diputuskan sebagai rencana paling optimal dalam waktu yang tersedia selama kompilasi. Pengetahuan ini mencakup indeks apa pun yang ada dan ukurannya dan statistik kolom apa pun yang ada. Bagian dari pencarian rencana kueri yang optimal adalah mencoba meminimalkan jumlah pembacaan fisik yang diperlukan selama eksekusi rencana.
Satu hal yang saya tanyakan beberapa kali adalah mengapa pengoptimal tidak mempertimbangkan apa yang ada di kumpulan buffer SQL Server saat menyusun rencana kueri, karena pasti itu bisa membuat kueri dieksekusi lebih cepat. Dalam postingan ini, saya akan menjelaskan alasannya.
Mengetahui Isi Buffer Pool
Alasan pertama mengapa pengoptimal mengabaikan kumpulan buffer adalah karena bukan masalah sepele untuk mencari tahu apa yang ada di kumpulan buffer karena cara kumpulan buffer diatur. Halaman file data dikendalikan dalam kumpulan buffer oleh struktur data kecil yang disebut buffer, yang melacak hal-hal seperti (daftar tidak lengkap):
- ID halaman (nomor file:nomor halaman-dalam-file)
- Terakhir kali halaman direferensikan (digunakan oleh penulis yang malas untuk membantu mengimplementasikan algoritme yang paling jarang digunakan yang menciptakan ruang kosong saat dibutuhkan)
- Lokasi memori halaman 8KB di buffer pool
- Apakah halaman kotor atau tidak (halaman kotor memiliki perubahan di dalamnya yang belum ditulis kembali ke penyimpanan yang tahan lama)
- Unit alokasi yang dimiliki halaman (dijelaskan di sini) dan ID unit alokasi dapat digunakan untuk mencari tahu tabel dan indeks apa yang menjadi bagian halaman
Untuk setiap database yang memiliki halaman di buffer pool, ada daftar halaman hash, dalam urutan ID halaman, yang dapat dicari dengan cepat untuk menentukan apakah halaman sudah ada di memori atau apakah pembacaan fisik harus dilakukan. Namun, tidak ada yang dengan mudah mengizinkan SQL Server untuk menentukan berapa persentase tingkat daun untuk setiap indeks tabel yang sudah ada di memori. Kode harus memindai seluruh daftar buffer untuk database, mencari buffer yang memetakan halaman untuk unit alokasi yang dimaksud. Dan semakin banyak halaman dalam memori untuk database, semakin lama waktu pemindaian. Akan sangat mahal untuk dilakukan sebagai bagian dari kompilasi kueri.
Jika Anda tertarik, saya menulis posting beberapa waktu lalu dengan beberapa kode T-SQL yang memindai kumpulan buffer dan memberikan beberapa metrik, menggunakan sys.dm_os_buffer_descriptors DMV .
Mengapa Menggunakan Isi Buffer Pool Akan Berbahaya
Anggaplah ada *ada* mekanisme yang sangat efisien untuk menentukan konten kumpulan buffer yang dapat digunakan pengoptimal untuk membantunya memilih indeks mana yang akan digunakan dalam rencana kueri. Hipotesis yang akan saya jelajahi adalah jika pengoptimal cukup mengetahui indeks yang kurang efisien (lebih besar) sudah ada di memori, dibandingkan dengan indeks yang paling efisien (lebih kecil) untuk digunakan, ia harus memilih indeks dalam memori karena akan kurangi jumlah pembacaan fisik yang diperlukan dan kueri akan berjalan lebih cepat.
Skenario yang akan saya gunakan adalah sebagai berikut:tabel BigTable memiliki dua indeks nonclustered, Index_A dan Index_B, keduanya sepenuhnya mencakup kueri tertentu. Kueri memerlukan pemindaian lengkap tingkat daun indeks untuk mengambil hasil kueri. Tabel memiliki 1 juta baris. Index_A memiliki 200.000 halaman di tingkat daunnya, dan Index_B memiliki 1 juta halaman di tingkat daunnya, jadi pemindaian penuh Index_B membutuhkan pemrosesan lima kali lebih banyak halaman.
Saya membuat contoh yang dibuat-buat ini pada laptop yang menjalankan SQL Server 2019 dengan 8 inti prosesor, memori 32GB, dan disk solid-state. Kodenya adalah sebagai berikut:
BUAT TABEL BigTable ( c1 BIGINT IDENTITY, c2 AS (c1 * 2), c3 CHAR (1500) DEFAULT 'a', c4 CHAR (5000) DEFAULT 'b');GO INSERT INTO BigTable DEFAULT NILAI;GO 1000000 CREATE INDEKS NONCLUSTERED Index_A PADA BigTable (c2) TERMASUK (c3);-- 5 catatan per halaman =200.000 halamanGO BUAT INDEKS NONCLUSTERED Index_B PADA BigTable (c2) TERMASUK (c4);-- 1 catatan per halaman =1 juta halamanGO CHECKPOINT;GODan kemudian saya menghitung waktu pertanyaan yang dibuat-buat:
DBCC DROPCLEANBUFFERS;GO -- Index_A tidak ada di memoriSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));GO-- CPU time =796 ms, elapsed time =764 ms -- Index_A in memorySELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));GO-- CPU time =312 ms, elapsed time =52ms DBCC DROPCLEANBUFFERS;GO -- Index_B not in memorySELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B));GO- - Waktu CPU =2952 md, waktu berlalu =2761 md -- Index_B di memoriSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B));GO-- Waktu CPU =1219 md, waktu berlalu =149 mdAnda dapat melihat ketika tidak ada indeks dalam memori, Index_A dengan mudah merupakan indeks yang paling efisien untuk digunakan, dengan waktu kueri yang telah berlalu 764 md terhadap 2,761 md menggunakan Index_B, dan hal yang sama berlaku ketika kedua indeks berada dalam memori. Namun, jika Index_B ada di memori, dan Index_A tidak, jika kueri menggunakan Index_B (149 md), kueri akan berjalan lebih cepat daripada jika menggunakan Index_A (764 md).
Sekarang mari kita izinkan pengoptimal untuk mendasarkan pilihan paket pada apa yang ada di buffer pool…
Jika Index_A sebagian besar tidak ada di memori dan Index_B sebagian besar ada di memori, akan lebih efisien untuk mengkompilasi rencana kueri untuk menggunakan Index_B, untuk kueri yang berjalan pada saat itu juga. Meskipun Index_B lebih besar dan akan membutuhkan lebih banyak siklus CPU untuk memindai, pembacaan fisik jauh lebih lambat daripada siklus CPU tambahan sehingga rencana kueri yang lebih efisien meminimalkan jumlah pembacaan fisik.
Argumen ini hanya berlaku, dan rencana kueri "gunakan Index_B" hanya lebih efisien daripada rencana kueri "gunakan Index_A", jika Index_B sebagian besar tetap ada di memori, dan Index_A sebagian besar tetap tidak ada di memori. Segera setelah sebagian besar Index_A berada di memori, rencana kueri “gunakan Index_A” akan lebih efisien, dan rencana kueri “gunakan Index_B” adalah pilihan yang salah.
Situasi ketika rencana "gunakan Index_B" yang dikompilasi kurang efisien daripada rencana "gunakan Index_A" berbasis biaya adalah (menggeneralisasi):
- Index_A dan Index_B keduanya ada di memori:paket yang dikompilasi akan memakan waktu hampir tiga kali lebih lama
- Tidak ada indeks yang menyimpan memori:paket yang dikompilasi mengambil alih 3,5 kali lebih lama
- Index_A adalah penduduk memori dan Index_B tidak:semua pembacaan fisik yang dilakukan oleh paket tidak relevan, DAN akan memakan waktu 53 kali lebih lama
Ringkasan
Meskipun dalam latihan pemikiran kami, pengoptimal dapat menggunakan pengetahuan kumpulan buffer untuk mengkompilasi kueri yang paling efisien pada satu saat, itu akan menjadi cara yang berbahaya untuk mendorong kompilasi rencana karena potensi volatilitas dari isi kumpulan buffer, membuat efisiensi masa depan dari paket yang di-cache sangat tidak dapat diandalkan.
Ingat, tugas pengoptimal adalah menemukan rencana yang baik dengan cepat, belum tentu satu rencana terbaik untuk 100% dari semua situasi. Menurut pendapat saya, pengoptimal SQL Server melakukan hal yang benar dengan mengabaikan konten sebenarnya dari kumpulan buffer SQL Server, dan alih-alih mengandalkan berbagai aturan penetapan biaya untuk menghasilkan rencana kueri yang kemungkinan paling efisien sepanjang waktu .