Ini adalah bagian kedua dari seri lima bagian yang membahas secara mendalam cara memulai rencana paralel mode baris SQL Server. Pada akhir bagian pertama, kami telah membuat konteks eksekusi nol untuk tugas orang tua. Konteks ini berisi seluruh pohon operator yang dapat dieksekusi, tetapi mereka belum siap untuk model eksekusi iteratif dari mesin pemrosesan kueri.
Eksekusi Iteratif
SQL Server mengeksekusi kueri melalui proses yang dikenal sebagai pemindaian kueri . Inisialisasi rencana dimulai dari akar oleh prosesor kueri yang memanggil Open
pada simpul akar. Open
panggilan melintasi pohon iterator secara rekursif memanggil Open
pada setiap anak sampai seluruh pohon dibuka.
Proses mengembalikan baris hasil juga bersifat rekursif, dipicu oleh prosesor kueri yang memanggil GetRow
di akar. Setiap panggilan root mengembalikan satu baris pada satu waktu. Pemroses kueri terus memanggil GetRow
pada simpul akar sampai tidak ada lagi baris yang tersedia. Eksekusi ditutup dengan Close
rekursif terakhir panggilan. Susunan ini memungkinkan pemroses kueri untuk menginisialisasi, mengeksekusi, dan menutup semua rencana arbitrer dengan memanggil metode antarmuka yang sama hanya di root.
Untuk mengubah hierarki operator yang dapat dieksekusi menjadi yang cocok untuk pemrosesan baris demi baris, SQL Server menambahkan pemindaian kueri pembungkus ke masing-masing operator. pemindaian kueri objek menyediakan Open
, GetRow
, dan Close
metode yang diperlukan untuk eksekusi berulang.
Objek pemindaian kueri juga menyimpan informasi status, dan memperlihatkan metode khusus operator lain yang diperlukan selama eksekusi. Misalnya, objek pemindaian kueri untuk operator Filter Start-Up (CQScanStartupFilterNew
) memperlihatkan metode berikut:
Open
GetRow
Close
PrepRecompute
GetScrollLock
SetMarker
GotoMarker
GotoLocation
ReverseDirection
Dormant
Metode tambahan untuk iterator ini sebagian besar digunakan dalam rencana kursor.
Menginisialisasi Pemindaian Kueri
Proses pembungkusan disebut menginisialisasi pemindaian kueri . Ini dilakukan dengan panggilan dari prosesor kueri ke CQueryScan::InitQScanRoot
. Tugas induk melakukan proses ini untuk seluruh rencana (terkandung dalam konteks eksekusi nol). Proses penerjemahan itu sendiri bersifat rekursif, dimulai dari akar dan turun ke bawah.
Selama proses ini, setiap operator bertanggung jawab untuk menginisialisasi datanya sendiri dan membuat sumber daya runtime itu membutuhkan. Ini mungkin termasuk membuat objek tambahan di luar pemroses kueri, misalnya struktur yang diperlukan untuk berkomunikasi dengan mesin penyimpanan untuk mengambil data dari penyimpanan persisten.
Pengingat rencana eksekusi, dengan nomor simpul ditambahkan (klik untuk memperbesar):
Operator di root (simpul 0) dari pohon rencana yang dapat dieksekusi adalah proyek urutan . Itu diwakili oleh kelas bernama CXteSeqProject
. Seperti biasa, di sinilah transformasi rekursif dimulai.
Pembungkus pemindaian kueri
Seperti disebutkan, CXteSeqProject
objek tidak dilengkapi untuk mengambil bagian dalam pemindaian kueri it proses — tidak memiliki Open
. yang diperlukan , GetRow
, dan Close
metode. Pemroses kueri memerlukan pembungkus di sekitar operator yang dapat dieksekusi untuk menyediakan antarmuka tersebut.
Untuk mendapatkan pembungkus pemindaian kueri itu, tugas induk memanggil CXteSeqProject::QScanGet
untuk mengembalikan objek bertipe CQScanSeqProjectNew
. Peta tertaut operator yang dibuat sebelumnya diperbarui untuk mereferensikan objek pemindaian kueri baru, dan metode iteratornya terhubung ke root rencana.
Anak dari proyek urutan adalah segmen operator (simpul 1). Memanggil CXteSegment::QScanGet
mengembalikan objek pembungkus pemindaian kueri dengan tipe CQScanSegmentNew
. Peta tertaut diperbarui lagi, dan penunjuk fungsi iterator terhubung ke pemindaian kueri proyek urutan induk.
Setengah pertukaran
Operator berikutnya adalah pertukaran berkumpul streams (simpul 2). Memanggil CXteExchange::QScanGet
mengembalikan CQScanExchangeNew
seperti yang mungkin Anda harapkan sekarang.
Ini adalah operator pertama di pohon yang perlu melakukan inisialisasi ekstra yang signifikan. Ini menciptakan sisi konsumen pertukaran melalui CXTransport::CreateConsumerPart
. Ini membuat port (CXPort
) — struktur data dalam memori bersama yang digunakan untuk sinkronisasi dan pertukaran data — dan pipa (CXPipe
) untuk transportasi paket. Perhatikan bahwa produser sisi pertukaran tidak dibuat pada saat ini. Kami hanya memiliki setengah pertukaran!
Lebih banyak pembungkus
Proses penyiapan pemindaian prosesor kueri kemudian dilanjutkan dengan gabung gabung (simpul 3). Saya tidak akan selalu mengulang QScanGet
dan CQScan*
panggilan dari titik ini, tetapi mereka mengikuti pola yang ditetapkan.
Penggabungan gabungan memiliki dua anak. Penyiapan pemindaian kueri berlanjut seperti sebelumnya dengan input luar (atas) — agregat aliran (simpul 4), lalu partisi ulang mengalirkan pertukaran (simpul 5). Aliran partisi ulang hanya menciptakan pertukaran sisi konsumen, tetapi kali ini ada dua pipa yang dibuat karena DOP adalah dua. Sisi konsumen dari jenis pertukaran ini memiliki koneksi DOP ke operator induknya (satu per utas).
Selanjutnya kita memiliki agregat aliran another lainnya (simpul 6) dan sort (simpul 7). Sortir memiliki turunan yang tidak terlihat dalam rencana eksekusi — set baris mesin penyimpanan yang digunakan untuk mengimplementasikan spilling ke tempdb . CQScanSortNew
yang diharapkan oleh karena itu disertai oleh CQScanRowsetNew
child anak di pohon internal. Itu tidak terlihat di output showplan.
Pembuatan profil I/O dan operasi yang ditangguhkan
mengurutkan operator juga yang pertama kami inisialisasi sejauh ini yang mungkin bertanggung jawab atas I/O . Dengan asumsi eksekusi telah meminta data profil I/O (misalnya dengan meminta rencana 'aktual'), pengurutan membuat objek untuk merekam data profil runtime ini melalui CProfileInfo::AllocProfileIO
.
Operator berikutnya adalah skalar komputasi (simpul 8), disebut proyek secara internal. Panggilan penyiapan pemindaian kueri ke CXteProject::QScanGet
apakah tidak mengembalikan objek pemindaian kueri, karena penghitungan yang dilakukan oleh skalar komputasi ini ditangguhkan ke operator induk pertama yang membutuhkan hasilnya. Dalam rencana ini, operator itu adalah semacamnya. Pengurutan akan melakukan semua pekerjaan yang ditugaskan ke skalar komputasi, sehingga proyek pada node 8 tidak menjadi bagian dari pohon pemindaian kueri. Skalar komputasi benar-benar tidak dieksekusi saat runtime. Untuk detail selengkapnya tentang skalar komputasi yang ditangguhkan, lihat Skalar Komputasi, Ekspresi, dan Performa Rencana Eksekusi.
Pemindaian paralel
Operator terakhir setelah skalar komputasi pada cabang rencana ini adalah pencarian indeks (CXteRange
) pada node 9. Ini menghasilkan operator pemindaian kueri yang diharapkan (CQScanRangeNew
), tetapi juga memerlukan urutan inisialisasi yang rumit untuk terhubung ke mesin penyimpanan dan memfasilitasi pemindaian indeks secara paralel.
Hanya menutupi sorotan, menginisialisasi pencarian indeks:
- Membuat objek profil untuk I/O (
CProfileInfo::AllocProfileIO
). - Membuat baris baris paralel pemindaian kueri (
CQScanRowsetNew::ParallelGetRowset
). - Menyiapkan sinkronisasi objek untuk mengoordinasikan pemindaian rentang paralel runtime (
CQScanRangeNew::GetSyncInfo
). - Membuat mesin penyimpanan kursor tabel dan deskriptor transaksi . hanya-baca .
- Membuka rowset induk untuk dibaca (mengakses HoBt dan mengambil kait yang diperlukan).
- Menyetel batas waktu penguncian.
- Menyiapkan pengambilan awal (termasuk buffer memori terkait).
Menambahkan operator profil mode baris
Kami sekarang telah mencapai tingkat daun dari cabang rencana ini (pencarian indeks tidak memiliki anak). Setelah membuat objek pemindaian kueri untuk pencarian indeks, langkah selanjutnya adalah membungkus pemindaian kueri dengan kelas profil (dengan asumsi kami meminta rencana yang sebenarnya). Ini dilakukan dengan panggilan ke sqlmin!PqsWrapQScan
. Perhatikan bahwa profiler ditambahkan setelah pemindaian kueri dibuat, saat kita mulai menaiki pohon iterator.
PqsWrapQScan
membuat operator pembuatan profil baru sebagai induk pencarian indeks, melalui panggilan ke CProfileInfo::GetOrCreateProfileInfo
. operator pembuatan profil (CQScanProfileNew
) memiliki metode antarmuka pemindaian kueri yang biasa. Selain mengumpulkan data yang diperlukan untuk rencana aktual, data pembuatan profil juga diekspos melalui sys.dm_exec_query_profiles
DMV .
Menanyakan DMV pada saat yang tepat ini untuk sesi saat ini menunjukkan bahwa hanya ada satu operator paket (node 9) yang ada (artinya hanya satu yang dibungkus oleh profiler):
Tangkapan layar ini menunjukkan set hasil lengkap dari DMV saat ini (belum diedit).
Selanjutnya, CQScanProfileNew
memanggil API penghitung kinerja kueri (KERNEL32!QueryPerformanceCounterStub
) yang disediakan oleh sistem operasi untuk merekam waktu aktif pertama dan terakhir dari operator yang diprofilkan:
Waktu aktif terakhir akan diperbarui menggunakan API penghitung kinerja kueri setiap kali kode untuk iterator itu berjalan.
Kemudian, profiler menyetel perkiraan jumlah baris pada titik ini dalam rencana (CProfileInfo::SetCardExpectedRows
), memperhitungkan setiap sasaran baris (CXte::CardGetRowGoal
). Karena ini adalah paket paralel, hasilnya akan dibagi dengan jumlah utas (CXte::FGetRowGoalDefinedForOneThread
) dan menyimpan hasilnya dalam konteks eksekusi.
Perkiraan jumlah baris tidak terlihat melalui DMV pada saat ini, karena tugas induk tidak akan mengeksekusi operator ini. Sebagai gantinya, perkiraan per-utas akan diekspos nanti dalam konteks eksekusi paralel (yang belum dibuat). Namun demikian, nomor per-utas disimpan di profiler tugas induk — hanya saja tidak terlihat melalui DMV.
Nama ramah dari operator paket (“Pencarian Indeks”) kemudian disetel melalui panggilan ke CXteRange::GetPhysicalOp
:
Sebelum itu, Anda mungkin memperhatikan bahwa kueri DMV menunjukkan nama sebagai "???". Ini adalah nama permanen yang ditampilkan untuk operator yang tidak terlihat (mis. nested loop prefetch, batch sort) yang tidak memiliki nama ramah yang ditentukan.
Terakhir, indeks metadata dan statistik I/O current saat ini untuk pencarian indeks yang dibungkus ditambahkan melalui panggilan ke CQScanRowsetNew::GetIoCounters
:
Penghitung saat ini nol, tetapi akan diperbarui saat pencarian indeks melakukan I/O selama eksekusi rencana selesai.
Lebih banyak pemrosesan pemindaian kueri
Dengan operator pembuatan profil yang dibuat untuk pencarian indeks, pemrosesan pemindaian kueri bergerak kembali ke atas pohon ke sort induk (simpul 7).
Sortir melakukan tugas inisialisasi berikut:
- Mendaftarkan penggunaan memorinya dengan kueri pengelola memori (
CQryMemManager::RegisterMemUsage
) - Menghitung memori yang diperlukan untuk input pengurutan (
CQScanIndexSortNew::CbufInputMemory
) dan keluaran (CQScanSortNew::CbufOutputMemory
). - Tabel pengurutan dibuat, bersama dengan rowset mesin penyimpanan yang terkait (
sqlmin!RowsetSorted
). - Sebuah transaksi sistem yang berdiri sendiri (tidak dibatasi oleh transaksi pengguna) dibuat untuk alokasi disk tumpahan sortir, bersama dengan tabel kerja palsu (
sqlmin!CreateFakeWorkTable
). - Layanan ekspresi diinisialisasi (
sqlTsEs!CEsRuntime::Startup
) agar operator pengurutan melakukan penghitungan ditangguhkan dari skalar komputasi. - Ambil lebih dulu untuk jenis apa pun yang berjalan tumpah ke tempdb kemudian dibuat melalui (
CPrefetchMgr::SetupPrefetch
).
Terakhir, pemindaian kueri pengurutan dibungkus oleh operator pembuatan profil (termasuk I/O) seperti yang kita lihat untuk pencarian indeks:
Perhatikan bahwa skalar komputasi (simpul 8) hilang dari DMV. Itu karena pekerjaannya ditangguhkan ke pengurutan, bukan bagian dari pohon pemindaian kueri, dan karena itu tidak memiliki objek profiler pembungkus.
Pindah ke induk semacam itu, agregasi aliran operator pemindaian kueri (simpul 6) menginisialisasi ekspresi dan penghitung waktu prosesnya (mis. jumlah baris grup saat ini). Agregat aliran dibungkus dengan operator pembuatan profil, mencatat waktu awalnya:
Partisi ulang induk mengalirkan pertukaran (simpul 5) dibungkus oleh profiler (ingat hanya sisi konsumen dari pertukaran ini yang ada pada saat ini):
Hal yang sama dilakukan untuk agregat aliran induknya (simpul 4), yang juga diinisialisasi seperti yang dijelaskan sebelumnya:
Pemrosesan pemindaian kueri kembali ke induk gabungan gabungan (simpul 3) tetapi belum menginisialisasi. Sebagai gantinya, kami bergerak ke bawah sisi dalam (bawah) gabungan gabungan, melakukan tugas terperinci yang sama untuk operator tersebut (simpul 10 hingga 15) seperti yang dilakukan untuk cabang atas (luar):
Setelah operator tersebut diproses, gabungan bergabung pemindaian kueri dibuat, diinisialisasi, dan dibungkus dengan objek profil. Ini termasuk penghitung I/O karena penggabungan banyak-banyak menggunakan tabel kerja (walaupun gabungan gabungan saat ini adalah satu-banyak):
Proses yang sama diikuti untuk aliran pengumpulan induk pertukaran (simpul 2) hanya sisi konsumen, segmen (simpul 1), dan proyek urutan (simpul 0) operator. Saya tidak akan menjelaskannya secara detail.
Profil kueri DMV sekarang melaporkan set lengkap node pemindaian kueri yang dibungkus profiler:
Perhatikan urutan proyek, segmen, dan aliran pengumpulan, konsumen memiliki perkiraan jumlah baris karena operator ini akan dijalankan oleh tugas induk , bukan dengan tugas paralel tambahan (lihat CXte::FGetRowGoalDefinedForOneThread
lebih awal). Tugas induk tidak memiliki pekerjaan yang harus dilakukan di cabang paralel, jadi konsep perkiraan jumlah baris hanya masuk akal untuk tugas tambahan.
Nilai waktu aktif yang ditunjukkan di atas agak terdistorsi karena saya harus menghentikan eksekusi dan mengambil tangkapan layar DMV di setiap langkah. Eksekusi terpisah (tanpa penundaan buatan yang diperkenalkan dengan menggunakan debugger) menghasilkan pengaturan waktu berikut:
Pohon dibangun dalam urutan yang sama seperti yang dijelaskan sebelumnya, tetapi prosesnya sangat cepat hanya 1 mikrodetik perbedaan antara waktu aktif operator yang dibungkus pertama (pencarian indeks pada node 9) dan yang terakhir (proyek urutan pada node 0).
Akhir Bagian 2
Kedengarannya seperti kami telah melakukan banyak pekerjaan, tetapi ingat kami hanya membuat pohon pemindaian kueri untuk tugas induk , dan bursa hanya memiliki sisi konsumen (belum ada produsen). Paket paralel kami juga hanya memiliki satu utas (seperti yang ditunjukkan pada tangkapan layar terakhir). Bagian 3 akan melihat pembuatan tugas paralel tambahan pertama kami.