Tampaknya ada tiga aturan pengoptimal yang berbeda yang dapat melakukan DISTINCT
operasi dalam kueri di atas. Kueri berikut melempar kesalahan yang menunjukkan bahwa daftar ini lengkap:
SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);
Msg 8622, Lantai 16, Negara Bagian 1, Jalur 1
Prosesor kueri tidak dapat menghasilkan rencana kueri karena petunjuk yang ditentukan dalam kueri ini. Kirim ulang kueri tanpa menentukan petunjuk apa pun dan tanpa menggunakan SET FORCEPLAN.
GbAggToSort
mengimplementasikan kelompok-oleh agregat (berbeda) sebagai jenis berbeda. Ini adalah operator pemblokiran yang akan membaca semua data dari input sebelum menghasilkan baris apa pun. GbAggToStrm
mengimplementasikan agregat grup-per sebagai agregat aliran (yang juga membutuhkan semacam input dalam hal ini). Ini juga operator pemblokiran. GbAggToHS
mengimplementasikan sebagai hash cocok, yang adalah apa yang kita lihat dalam rencana buruk dari pertanyaan, tetapi dapat diimplementasikan sebagai hash cocok (agregat) atau hash cocok (mengalir berbeda).
Operator hash match ( flow berbeda ) adalah salah satu cara untuk menyelesaikan masalah ini karena tidak memblokir. SQL Server harus dapat menghentikan pemindaian setelah menemukan nilai yang cukup berbeda.
Operator logis Flow Distinct memindai input, menghapus duplikat. Sedangkan operator Distinct mengkonsumsi semua input sebelum menghasilkan output, operator Flow Distinct mengembalikan setiap baris karena diperoleh dari input (kecuali jika baris itu adalah duplikat, dalam hal ini dibuang).
Mengapa kueri dalam pertanyaan menggunakan pencocokan hash (agregat) alih-alih pencocokan hash (berbeda alur)? Karena jumlah nilai-nilai yang berbeda berubah dalam tabel saya akan mengharapkan biaya permintaan hash (aliran berbeda) menurun karena estimasi jumlah baris yang perlu dipindai ke tabel akan berkurang. Saya berharap biaya rencana hash match (agregat) meningkat karena tabel hash yang dibutuhkan untuk membangun akan semakin besar. Salah satu cara untuk menyelidiki ini adalah dengan membuat panduan rencana . Jika saya membuat dua salinan data tetapi menerapkan panduan rencana untuk salah satunya, saya harus dapat membandingkan kecocokan hash (agregat) dengan kecocokan hash (berbeda) berdampingan dengan data yang sama. Perhatikan bahwa saya tidak dapat melakukan ini dengan menonaktifkan aturan optimizer kueri karena aturan yang sama berlaku untuk kedua paket ( GbAggToHS
).
Inilah salah satu cara untuk mendapatkan panduan rencana yang saya cari:
DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;
CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);
INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);
UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;
-- run this query
SELECT DISTINCT TOP 10 VAL FROM X_PLAN_GUIDE_TARGET OPTION (MAXDOP 1)
Dapatkan pegangan paket dan gunakan untuk membuat panduan rencana:
-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM
sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;
EXEC sp_create_plan_guide_from_handle
'EVIL_PLAN_GUIDE',
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;
Panduan paket hanya berfungsi pada teks kueri yang tepat, jadi mari kita salin kembali dari panduan paket:
SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';
Setel ulang data:
TRUNCATE TABLE X_PLAN_GUIDE_TARGET;
INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);
Dapatkan paket permintaan untuk permintaan dengan panduan paket yang diterapkan:
SELECT DISTINCT TOP 10 VAL FROM X_PLAN_GUIDE_TARGET OPTION (MAXDOP 1)
Ini memiliki operator pencocokan hash (berbeda aliran) yang kami inginkan dengan data pengujian kami. Perhatikan bahwa SQL Server mengharapkan untuk membaca semua baris dari tabel dan bahwa perkiraan biaya sama persis seperti untuk rencana dengan pencocokan hash (agregat). Pengujian yang saya lakukan menyarankan bahwa biaya untuk dua paket identik ketika tujuan baris untuk paket lebih besar atau sama dengan jumlah nilai yang berbeda yang diharapkan SQL Server dari tabel, yang dalam hal ini dapat dengan mudah diturunkan dari statistik. Sayangnya (untuk kueri kami) pengoptimal memilih kecocokan hash (agregat) di atas kecocokan hash (berbeda aliran) saat biayanya sama. Jadi kita 0,0000001 unit pengoptimal sihir jauh dari rencana yang kita inginkan.
Salah satu cara untuk mengatasi masalah ini adalah dengan mengurangi tujuan baris. Jika sasaran baris dari sudut pandang pengoptimal kurang dari jumlah baris yang berbeda, kami mungkin akan mendapatkan pencocokan hash (berbeda alur). Ini dapat dilakukan dengan OPTIMIZE FOR
petunjuk kueri:
DECLARE @j INT = 10;
SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));
Untuk kueri ini, pengoptimal membuat rencana seolah-olah kueri hanya membutuhkan baris pertama, tetapi ketika kueri dijalankan, akan mengembalikan 10 baris pertama. Di komputer saya, kueri ini memindai 892800 baris dari X_10_DISTINCT_HEAP
dan menyelesaikan dalam 299 ms dengan waktu CPU 250 ms dan 2537 pembacaan logis.
Perhatikan bahwa teknik ini tidak akan berfungsi jika statistik melaporkan hanya satu nilai berbeda, yang bisa terjadi untuk statistik sampel terhadap data yang miring. Namun, dalam kasus itu, data Anda tidak cukup padat untuk dibenarkan menggunakan teknik seperti ini. Anda mungkin tidak kehilangan banyak dengan memindai semua data dalam tabel, terutama jika itu bisa dilakukan secara paralel.
Cara lain untuk menyerang masalah ini adalah dengan menggembungkan jumlah nilai berbeda yang diperkirakan SQL Server dapatkan dari tabel dasar. Ini lebih sulit dari yang diharapkan. Menerapkan fungsi deterministik tidak mungkin dapat meningkatkan jumlah hasil yang berbeda. Jika pengoptimal kueri menyadari fakta matematika itu (beberapa pengujian menyarankan setidaknya untuk tujuan kami) maka menerapkan fungsi deterministik (yang mencakup semua fungsi string ) tidak akan meningkatkan perkiraan jumlah baris yang berbeda.
Banyak fungsi nondeterministic tidak berfungsi, termasuk pilihan yang jelas NEWID()
dan RAND()
. Namun, LAG()
lakukan trik untuk kueri ini. Pengoptimal kueri mengharapkan 10 juta nilai berbeda terhadap LAG
ekspresi yang akan mendorong rencana pencocokan hash (berbeda aliran) :
SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);
Di mesin saya, kueri ini memindai 892800 baris dari X_10_DISTINCT_HEAP
dan menyelesaikan dalam 1165 ms dengan waktu CPU 1109 ms dan 2537 membaca logis, sehingga LAG()
menambahkan sedikit overhead relatif. @ Paul White menyarankan untuk mencoba pemrosesan mode batch untuk permintaan ini. Pada SQL Server 2016 kita bisa mendapatkan pemrosesan mode batch bahkan dengan MAXDOP 1
. Salah satu cara untuk mendapatkan pemrosesan mode batch untuk tabel rowstore adalah dengan bergabung ke CCI kosong sebagai berikut:
CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);
CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;
SELECT DISTINCT TOP 10 VAL
FROM
(
SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
FROM X_10_DISTINCT_HEAP
LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);
Kode itu menghasilkan rencana kueri ini .
Paul menunjukkan bahwa saya harus mengubah kueri untuk digunakan LAG(..., 1)
karena LAG(..., 0)
tampaknya tidak memenuhi syarat untuk optimasi Window Aggregate. Perubahan ini mengurangi waktu yang telah berlalu menjadi 520 ms dan waktu CPU menjadi 454 ms.
Perhatikan bahwa LAG()
pendekatannya bukan yang paling stabil. Jika Microsoft mengubah asumsi keunikan terhadap fungsi tersebut, maka Microsoft mungkin tidak lagi berfungsi. Ini memiliki perkiraan yang berbeda dengan warisan CE. Juga jenis pengoptimalan terhadap tumpukan tidak perlu ide yang baik. Jika tabel dibangun kembali mungkin berakhir dalam skenario kasus terburuk di mana hampir semua baris perlu dibaca dari tabel.
Terhadap tabel dengan kolom unik (seperti contoh indeks berkerumun dalam pertanyaan) kami memiliki opsi yang lebih baik. Misalnya kita dapat menipu pengoptimal dengan menggunakan SUBSTRING
ekspresi yang selalu mengembalikan string kosong. SQL Server tidak berpikir bahwa SUBSTRING
akan mengubah jumlah nilai yang berbeda jadi jika kita menerapkannya pada kolom unik, seperti PK, maka jumlah baris yang berbeda diperkirakan adalah 10 juta. Kueri berikut ini mendapatkan operator pencocokan hash (berbeda aliran):
SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);
Di mesin saya, kueri ini memindai 900.000 baris dari X_10_DISTINCT_CI
dan menyelesaikan dalam 333 ms dengan waktu CPU 297 ms dan 3011 pembacaan logis.
Singkatnya, pengoptimal kueri muncul untuk menganggap bahwa semua baris akan dibaca dari tabel untuk SELECT DISTINCT TOP N
kueri ketika N
> = jumlah taksiran baris berbeda dari tabel. Operator hash cocok (agregat) mungkin memiliki biaya yang sama seperti operator hash cocok (berbeda aliran) tetapi pengoptimal selalu memilih operator agregat. Hal ini dapat menyebabkan pembacaan logis yang tidak perlu ketika nilai yang cukup berbeda terletak di dekat awal pemindaian tabel. Dua cara untuk menipu pengoptimal agar menggunakan operator hash match (flow berbeda) adalah dengan menurunkan tujuan baris menggunakan OPTIMIZE FOR
petunjuk atau untuk menambah perkiraan jumlah baris berbeda menggunakan LAG()
atau SUBSTRING
pada kolom unik.
Untuk kelengkapan, cara lain untuk mendekati masalah ini adalah dengan menggunakan OUTER BERLAKU . Kami dapat menambahkan
OUTER APPLY
operator untuk setiap nilai berbeda yang perlu kami temukan. Ini serupa dalam konsep dengan pendekatan rekursif ypercube, tetapi secara efektif rekursi ditulis dengan tangan. Satu keuntungan adalah bahwa kita dapat menggunakanTOP
tabel turunan alih-alihROW_NUMBER()
solusinya. Salah satu kelemahan besar adalah teks kueri bertambah panjang seiringN
bertambahnya.Berikut ini adalah satu implementasi untuk kueri terhadap heap:
Berikut adalah rencana permintaan aktual untuk permintaan di atas. Di mesin saya permintaan ini selesai dalam 713 ms dengan 625 ms waktu CPU dan 12605 membaca logis. Kami mendapatkan nilai berbeda baru setiap baris 100rb jadi saya berharap permintaan ini memindai sekitar 900000 * 10 * 0,5 = 4500000 baris. Secara teori, kueri ini harus melakukan lima kali pembacaan logis dari kueri ini dari jawaban lain:
Kueri itu dibaca 2537 secara logis. 2537 * 5 = 12685 yang hampir mendekati 12605.
Untuk tabel dengan indeks berkerumun kami bisa melakukan yang lebih baik. Ini karena kita bisa meneruskan nilai kunci yang dikelompokkan terakhir ke tabel turunan untuk menghindari pemindaian baris yang sama dua kali. Satu implementasi:
Berikut adalah rencana permintaan aktual untuk permintaan di atas. Di komputer saya, kueri ini selesai dalam 154 ms dengan waktu CPU 140 ms dan 3203 pembacaan logis. Ini tampaknya berjalan sedikit lebih cepat daripada
OPTIMIZE FOR
permintaan terhadap tabel indeks berkerumun. Saya tidak berharap itu jadi saya mencoba mengukur kinerja lebih hati-hati. Metodologi saya adalah menjalankan setiap kueri sepuluh kali tanpa hasil dan untuk melihat angka agregat darisys.dm_exec_sessions
dansys.dm_exec_session_wait_stats
. Sesi 56 adalahAPPLY
kueri dan sesi 63 adalahOPTIMIZE FOR
kueri.Output dari
sys.dm_exec_sessions
:Tampaknya ada keuntungan yang jelas di cpu_time dan elapsed_time untuk
APPLY
kueri.Output dari
sys.dm_exec_session_wait_stats
:The
OPTIMIZE FOR
query memiliki jenis menunggu tambahan, RESERVED_MEMORY_ALLOCATION_EXT . Saya tidak tahu persis apa artinya ini. Ini mungkin hanya pengukuran overhead dalam operator hash match (flow berbeda). Bagaimanapun, mungkin tidak ada gunanya mengkhawatirkan perbedaan 70 ms dalam waktu CPU.sumber
Saya pikir Anda memiliki jawaban tentang mengapa
ini mungkin cara untuk mengatasinya.
Saya tahu itu terlihat berantakan tetapi rencana eksekusi mengatakan berbeda 2 teratas adalah 84% dari biaya
sumber