Mengapa memilih semua kolom hasil kueri ini lebih cepat daripada memilih satu kolom yang saya pedulikan?

13

Saya punya pertanyaan di mana menggunakan select *tidak hanya membaca jauh lebih sedikit, tetapi juga menggunakan waktu CPU secara signifikan lebih sedikit daripada menggunakan select c.Foo.

Ini pertanyaannya:

select top 1000 c.ID
from ATable a
    join BTable b on b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
    join CTable c on c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
where (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff)
    and b.IsVoided = 0
    and c.ComplianceStatus in (3, 5)
    and c.ShipmentStatus in (1, 5, 6)
order by a.LastAnalyzedDate

Ini selesai dengan 2.473.658 pembacaan logis, sebagian besar dalam Tabel B. Ini menggunakan 26.562 CPU dan memiliki durasi 7.965.

Ini adalah paket permintaan yang dihasilkan:

Rencanakan dari Memilih nilai satu kolom Pada PasteThePlan: https://www.brentozar.com/pastetheplan/?id=BJAp2mQIQ

Ketika saya beralih c.IDke *, kueri selesai dengan 107.049 pembacaan logis, cukup merata di antara ketiga tabel. Ini menggunakan 4.266 CPU dan memiliki durasi 1.147.

Ini adalah paket permintaan yang dihasilkan:

Rencanakan dari Memilih semua nilai Pada PasteThePlan: https://www.brentozar.com/pastetheplan/?id=SyZYn7QUQ

Saya mencoba menggunakan petunjuk kueri yang disarankan oleh Joe Obbish, dengan hasil ini:
select c.IDtanpa petunjuk: https://www.brentozar.com/pastetheplan/?id=SJfBdOELm
select c.ID dengan petunjuk: https://www.brentozar.com/pastetheplan/ ? id = B1W ___ N87
select * tanpa petunjuk: https://www.brentozar.com/pastetheplan/?id=HJ6qddEIm
select * dengan petunjuk: https://www.brentozar.com/pastetheplan/?id=rJhhudNIQ

Menggunakan OPTION(LOOP JOIN)petunjuk dengan select c.IDsecara drastis mengurangi jumlah bacaan dibandingkan dengan versi tanpa petunjuk, tetapi masih melakukan sekitar 4x jumlah bacaan select *kueri tanpa petunjuk apa pun. Menambahkan OPTION(RECOMPILE, HASH JOIN)ke select *kueri membuatnya berkinerja lebih buruk daripada hal lain yang telah saya coba.

Setelah memperbarui statistik pada tabel dan indeksnya menggunakan WITH FULLSCAN, select c.IDkueri berjalan jauh lebih cepat:
select c.IDsebelum pembaruan: https://www.brentozar.com/pastetheplan/?id=SkiYoOEUm
select * sebelum memperbarui: https://www.brentozar.com/ pastetheplan /? id = ryrvodEUX
select c.ID setelah pembaruan: https://www.brentozar.com/pastetheplan/?id=B1MRoO487
select * setelah pembaruan: https://www.brentozar.com/pastetheplan/?id=Hk7si_V8m

select *masih melebihi select c.IDdari segi total durasi dan jumlah membaca ( select *memiliki sekitar setengah dibaca) tetapi tidak digunakan lebih CPU. Secara keseluruhan mereka jauh lebih dekat daripada sebelum pembaruan, namun rencananya masih berbeda.

Perilaku yang sama terlihat pada 2016 berjalan dalam mode Kompatibilitas 2014 dan pada 2014. Apa yang bisa menjelaskan perbedaan antara kedua rencana? Mungkinkah indeks "benar" belum dibuat? Bisakah statistik sedikit ketinggalan zaman menyebabkan ini?

Saya mencoba memindahkan predikat ke ONbagian gabungan, dengan berbagai cara, tetapi rencana kuerinya sama setiap kali.

Setelah Indeks Dibangun Kembali

Saya membangun kembali semua indeks pada tiga tabel yang terlibat dalam kueri. c.IDmasih melakukan paling banyak membaca (lebih dari dua kali lipat *), tetapi penggunaan CPU adalah sekitar setengah dari *versi. The c.IDVersi juga tumpah ke tempdb pada pemilahan ATable:
c.ID: https://www.brentozar.com/pastetheplan/?id=HyHIeDO87
* : https://www.brentozar.com/pastetheplan/?id=rJ4deDOIQ

Saya juga mencoba memaksanya beroperasi tanpa paralelisme, dan itu memberi saya pertanyaan dengan kinerja terbaik: https://www.brentozar.com/pastetheplan/?id=SJn9-vuLX

Saya perhatikan jumlah eksekusi operator SETELAH pencarian indeks besar yang melakukan pemesanan hanya dieksekusi 1.000 kali dalam versi single-threaded, tetapi melakukan lebih signifikan dalam versi Paralelized, antara 2.622 dan 4.315 eksekusi berbagai operator.

L. Miller
sumber

Jawaban:

4

Memang benar bahwa memilih lebih banyak kolom menyiratkan bahwa SQL Server mungkin perlu bekerja lebih keras untuk mendapatkan hasil permintaan yang diminta. Jika pengoptimal kueri dapat membuat rencana kueri yang sempurna untuk kedua kueri maka masuk akal untuk mengharapkanSELECT *kueri untuk dijalankan lebih lama dari kueri yang memilih semua kolom dari semua tabel. Anda telah mengamati yang sebaliknya untuk pasangan pertanyaan Anda. Anda harus berhati-hati ketika membandingkan biaya, tetapi permintaan lambat memiliki perkiraan total biaya 1090,08 unit pengoptimal dan permintaan cepat memiliki total perkiraan biaya 6823,11 unit pengoptimal. Dalam hal ini, dapat dikatakan bahwa pengoptimal melakukan pekerjaan yang buruk dengan memperkirakan total biaya permintaan. Itu memang memilih rencana yang berbeda untuk kueri SELECT * Anda dan mengharapkan rencana itu menjadi lebih mahal, tetapi itu tidak terjadi di sini. Jenis ketidakcocokan tersebut dapat terjadi karena berbagai alasan dan salah satu penyebab paling umum adalah masalah perkiraan kardinalitas. Biaya operator sebagian besar ditentukan oleh perkiraan kardinalitas. Jika perkiraan kardinalitas pada titik kunci dalam suatu rencana tidak akurat maka total biaya dari rencana tersebut mungkin tidak mencerminkan kenyataan. Ini adalah penyederhanaan yang berlebihan tetapi saya berharap ini akan membantu untuk memahami apa yang terjadi di sini.

Mari kita mulai dengan membahas mengapa SELECT *permintaan mungkin lebih mahal daripada memilih satu kolom. The SELECT *permintaan dapat berubah beberapa indeks yang mencakup ke noncovering indeks, yang mungkin berarti bahwa optimizer perlu melakukan pekerjaan tambahan untuk mendapatkan semua kolom perlu atau mungkin perlu membaca dari indeks yang lebih besar.SELECT *juga dapat menghasilkan set hasil menengah yang lebih besar yang perlu diproses selama eksekusi permintaan. Anda dapat melihat ini dalam tindakan dengan melihat perkiraan ukuran baris di kedua kueri. Dalam kueri cepat, ukuran baris Anda berkisar dari 664 byte hingga 3019 byte. Dalam kueri lambat, ukuran baris Anda berkisar dari 19 hingga 36 byte. Memblokir operator seperti sortir atau hash build akan memiliki biaya lebih tinggi untuk data dengan ukuran baris yang lebih besar karena SQL Server tahu lebih mahal untuk mengurutkan jumlah data yang lebih besar atau mengubahnya menjadi tabel hash.

Melihat permintaan cepat, pengoptimal memperkirakan perlu melakukan 2,4 juta indeks Database1.Schema1.Object5.Index3. Dari situlah sebagian besar biaya rencana berasal. Namun rencana aktual mengungkapkan bahwa hanya pencarian 1332 indeks dilakukan pada operator itu. Jika Anda membandingkan baris aktual dengan taksiran untuk bagian luar dari loop tersebut bergabung, Anda akan melihat perbedaan besar. Pengoptimal berpikir bahwa lebih banyak indeks akan diperlukan untuk menemukan 1000 baris pertama yang diperlukan untuk hasil kueri. Itu sebabnya permintaan memiliki rencana biaya yang relatif tinggi tetapi selesai begitu cepat: operator yang diperkirakan paling mahal melakukan kurang dari 0,1% dari pekerjaan yang diharapkan.

Melihat permintaan lambat, Anda mendapatkan rencana dengan sebagian besar hash joins (saya percaya loop join ada hanya untuk berurusan dengan variabel lokal). Perkiraan kardinalitas jelas tidak sempurna, tetapi satu-satunya masalah perkiraan yang sebenarnya tepat pada akhirnya. Saya menduga sebagian besar waktu dihabiskan untuk memindai tabel dengan ratusan juta baris.

Anda mungkin merasa terbantu untuk menambahkan petunjuk kueri ke kedua versi kueri untuk memaksa rencana kueri yang dikaitkan dengan versi lain. Petunjuk kueri dapat menjadi alat yang baik untuk mencari tahu mengapa pengoptimal membuat beberapa pilihannya. Jika Anda menambahkan OPTION (RECOMPILE, HASH JOIN)ke SELECT *kueri, saya berharap Anda akan melihat rencana kueri yang mirip dengan hash bergabung dengan kueri. Saya juga berharap bahwa biaya permintaan akan jauh lebih tinggi untuk rencana hash bergabung karena ukuran baris Anda jauh lebih besar. Jadi itu bisa jadi mengapa hash join query tidak dipilih untuk SELECT *query. Jika Anda menambahkan OPTION (LOOP JOIN)ke kueri yang memilih hanya satu kolom, saya berharap Anda akan melihat rencana kueri yang mirip dengan yang untukSELECT *pertanyaan. Dalam hal ini, mengurangi ukuran baris seharusnya tidak berdampak banyak pada biaya kueri keseluruhan. Anda mungkin melewatkan pencarian kunci tapi itu persentase kecil dari perkiraan biaya.

Singkatnya, saya berharap bahwa ukuran baris yang lebih besar diperlukan untuk memenuhi SELECT *kueri mendorong pengoptimal ke arah rencana bergabung loop daripada rencana bergabung hash. Rencana penggabungan loop dihitung biayanya lebih tinggi daripada seharusnya karena masalah perkiraan kardinalitas. Mengurangi ukuran baris dengan memilih hanya satu kolom akan sangat mengurangi biaya rencana bergabung hash tetapi mungkin tidak akan banyak berpengaruh pada biaya untuk loop bergabung rencana, sehingga Anda berakhir dengan rencana bergabung hash yang kurang efisien. Sulit untuk mengatakan lebih dari ini untuk rencana anonim.

Joe Obbish
sumber
Terima kasih banyak atas jawaban Anda yang luas dan informatif. Saya mencoba menambahkan petunjuk yang Anda sarankan. Itu memang membuat select c.IDkueri lebih cepat, tetapi masih melakukan beberapa pekerjaan tambahan yang dilakukan select *kueri, tanpa petunjuk.
L. Miller
2

Statistik basi tentu dapat menyebabkan pengoptimal untuk memilih metode yang buruk dalam menemukan data. Sudahkah Anda mencoba melakukan UPDATE STATISTICS ... WITH FULLSCANatau melakukan penuh REBUILDpada indeks? Coba itu dan lihat apakah itu membantu.

MEMPERBARUI

Menurut pembaruan dari OP:

Setelah memperbarui statistik pada tabel dan indeksnya menggunakan WITH FULLSCAN, select c.IDkueri berjalan jauh lebih cepat

Jadi, sekarang, jika satu-satunya tindakan yang diambil adalah UPDATE STATISTICS, maka coba lakukan indeks REBUILD(bukan REORGANIZE) seperti yang saya lihat yang membantu dengan perkiraan jumlah baris di mana keduanya UPDATE STATISTICSdan indeks REORGANIZEtidak.

Solomon Rutzky
sumber
Saya bisa mendapatkan semua indeks pada tiga tabel yang terlibat untuk membangun kembali selama akhir pekan, dan telah memperbarui posting saya untuk mencerminkan hasil itu.
L. Miller
-1
  1. Bisakah Anda memasukkan skrip indeks?
  2. Sudahkah Anda menghilangkan kemungkinan masalah dengan "parameter sniffing"? https://www.mssqltips.com/sqlservertip/3257/different-approaches-to-correct-sql-server-parameter-sniffing/
  3. Saya telah menemukan teknik ini untuk membantu dalam beberapa kasus:
    a) menulis ulang setiap meja sebagai subquery, berikut aturan-aturan ini:
    b) SELECT - put bergabung kolom pertama
    c) predikat - pindah ke subqueries masing-masing
    d) ORDER BY - pindah ke mereka masing-masing subqueries, urutkan pada JOIN COLUMNS FIRST
    e) Tambahkan kueri wrapper untuk pengurutan akhir Anda dan SELECT.

Idenya adalah untuk pre-sortir kolom bergabung di dalam setiap subselect, menempatkan kolom bergabung pertama di setiap daftar pilih.

Inilah yang saya maksud ....

SELECT ... wrapper query
FROM
(
    SELECT ...
    FROM
        (SELECT ClientID, ShipKey, NextAnalysisDate
         FROM ATABLE
         WHERE (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff) -- Predicates
         ORDER BY OrderKey, ClientID, LastAnalyzedDate  ---- Pre-sort the join columns
        ) as a
        JOIN 
        (SELECT OrderKey, ClientID, OrderID, IsVoided
         FROM BTABLE
         WHERE IsVoided = 0             ---- Include all predicates
         ORDER BY OrderKey, OrderID, IsVoided       ---- Pre-sort the join columns
        ) as b ON b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
        JOIN
        (SELECT OrderID, ShipKey, ComplianceStatus, ShipmentStatus, ID
         FROM CTABLE
         WHERE ComplianceStatus in (3, 5)       ---- Include all predicates
             AND ShipmentStatus in (1, 5, 6)        ---- Include all predicates
         ORDER BY OrderID, ShipKey          ---- Pre-sort the join columns
        ) as c ON c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
) as d
ORDER BY d.LastAnalyzedDate
Victor Di Leo
sumber
1
1. Saya akan mencoba untuk menambahkan skrip indeks DDL ke posting asli, yang mungkin memerlukan waktu untuk "menggosok" mereka. 2. Saya telah menguji kemungkinan ini dengan membersihkan cache paket sebelum menjalankan, dan dengan mengganti parameter bind dengan nilai aktual. 3. Saya mencoba ini, tetapi ORDER BYtidak valid dalam subquery tanpa TOP, FORXML, dll. Saya mencobanya tanpa ORDER BYklausa tetapi itu adalah rencana yang sama.
L. Miller