Kolom yang dihitung terus-menerus menyebabkan pemindaian

9

Mengubah kolom reguler ke kolom yang dihitung terus-menerus menyebabkan kueri ini tidak dapat melakukan pencarian indeks. Mengapa?

Diuji pada beberapa versi SQL Server, termasuk 2016 SP1 CU1.

Repros

Masalahnya adalah dengan table1, col7.

Tabel dan kueri adalah versi parsial (dan disederhanakan) dari dokumen asli. Saya sadar bahwa kueri dapat ditulis ulang secara berbeda, dan untuk beberapa alasan menghindari masalah, tetapi kita perlu menghindari menyentuh kode, dan pertanyaan mengapa table1tidak dapat dicari masih ada.

Seperti yang ditunjukkan oleh Paul White (terima kasih!), Pencarian tersedia jika dipaksa, jadi pertanyaannya adalah: Mengapa pencarian tidak dipilih oleh pengoptimal, dan apakah kita dapat melakukan sesuatu yang berbeda untuk membuat pencarian terjadi sebagaimana mestinya, tanpa mengubah kode?

Untuk memperjelas bagian yang bermasalah, inilah pemindaian yang relevan dalam rencana eksekusi yang buruk:

rencana

Alex Friedman
sumber

Jawaban:

12

Mengapa pencarian tidak dipilih oleh pengoptimal


TL: DR Definisi kolom terhitung yang diperluas mengganggu kemampuan pengoptimal untuk menyusun ulang bergabung pada awalnya. Dengan titik awal yang berbeda, pengoptimalan berbasis biaya mengambil jalur yang berbeda melalui pengoptimal, dan berakhir dengan pilihan rencana akhir yang berbeda.


Detail

Untuk semua pertanyaan kecuali yang paling sederhana, pengoptimal tidak berupaya mengeksplorasi apa pun seperti seluruh ruang rencana yang mungkin. Alih-alih, ia memilih titik awal yang tampak masuk akal , lalu menghabiskan sejumlah upaya yang dianggarkan untuk mengeksplorasi variasi logis dan fisik, dalam satu atau beberapa fase pencarian, hingga menemukan rencana yang masuk akal.

Alasan utama Anda mendapatkan paket berbeda (dengan taksiran biaya akhir berbeda) untuk kedua kasus tersebut adalah karena ada titik awal yang berbeda . Mulai dari tempat yang berbeda, pengoptimalan berakhir di tempat yang berbeda (setelah jumlah iterasi eksplorasi dan implementasi yang terbatas). Saya harap ini cukup intuitif.

Titik awal yang saya sebutkan, agak didasarkan pada representasi tekstual dari kueri, tetapi perubahan dilakukan pada representasi internal tree ketika melewati tahapan parsing, binding, normalisasi, dan penyederhanaan kompilasi query.

Yang penting, titik awal yang tepat sangat bergantung pada urutan bergabung awal yang dipilih oleh pengoptimal. Pilihan ini dibuat sebelum statistik dimuat, dan sebelum estimasi kardinalitas apa pun telah diturunkan. Kardinalitas total (jumlah baris) di setiap tabel diketahui, telah diperoleh dari metadata sistem.

Pemesanan bergabung awal karena itu didasarkan pada heuristik . Misalnya, pengoptimal mencoba untuk menulis ulang pohon sedemikian rupa sehingga tabel yang lebih kecil bergabung sebelum yang lebih besar, dan sambungan dalam datang sebelum sambungan luar (dan gabungan silang).

Kehadiran kolom yang dihitung mengganggu proses ini, paling khusus dengan kemampuan pengoptimal untuk mendorong luar bergabung ke bawah pohon kueri. Ini karena kolom yang dikomputasi diperluas ke ekspresi yang mendasarinya sebelum penggabungan ulang terjadi, dan memindahkan gabungan melewati ekspresi kompleks jauh lebih sulit daripada memindahkannya melewati referensi kolom sederhana.

Pohon-pohon yang terlibat cukup besar, tetapi untuk menggambarkan, pohon permintaan awal kolom non-dihitung dimulai dengan: (perhatikan dua gabungan luar di atas)

LogOp_Select
    LogOp_Apply (x_jtLeftOuter) 
        LogOp_LeftOuterJoin
            LogOp_NAryJoin
                LogOp_LeftAntiSemiJoin
                    LogOp_NAryJoin
                        LogOp_Get TBL: dbo.table1 (alias TBL: a4)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table6 (alias TBL: a3)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a3] .col18
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table1 (alias TBL: a1)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a1] .col2
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table5 (alias TBL: a2)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a2] .col2
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a4] .col2
                            ScaOp_Identifier QCOL: [a3] .col19
                    LogOp_Select
                        LogOp_Get TBL: dbo.table7 (alias TBL: a7)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a7] .col22
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [a7] .col23
                LogOp_Select
                    LogOp_Get TBL: table1 (alias TBL: cdc)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdc] .col6
                        ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, Tidak Dimiliki, Nilai = 4)
                LogOp_Get TBL: dbo.table5 (alias TBL: a5) 
                LogOp_Get TBL: table2 (alias TBL: cdt)  
                ScaOp_Logical x_lopAnd
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a5] .col2
                        ScaOp_Identifier QCOL: [cdc] .col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [cdc] .col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdt] .col1
                        ScaOp_Identifier QCOL: [cdc] .col1
            LogOp_Get TBL: table3 (alias TBL: ahcr)
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier QCOL: [ahcr] .col9
                ScaOp_Identifier QCOL: [cdt] .col1

Fragmen yang sama dari kueri kolom yang dihitung adalah: (perhatikan join luar jauh lebih rendah ke bawah, definisi kolom terkomputasi yang diperluas, dan beberapa perbedaan halus lainnya dalam pemesanan join (dalam))

LogOp_Select
    LogOp_Apply (x_jtLeftOuter)
        LogOp_NAryJoin
            LogOp_LeftAntiSemiJoin
                LogOp_NAryJoin
                    LogOp_Get TBL: dbo.table1 (alias TBL: a4)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table6 (alias TBL: a3)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a3] .col18
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table1 (alias TBL: a1
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a1] .col2
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table5 (alias TBL: a2)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a2] .col2
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [a3] .col19
                LogOp_Select
                    LogOp_Get TBL: dbo.table7 (alias TBL: a7) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a7] .col22
                        ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4] .col2
                    ScaOp_Identifier QCOL: [a7] .col23
            LogOp_Project
                LogOp_LeftOuterJoin
                    LogOp_Join
                        LogOp_Select
                            LogOp_Get TBL: table1 (alias TBL: cdc) 
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [cdc] .col6
                                ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, Tidak Dimiliki, Nilai = 4)
                        LogOp_Get TBL: table2 (alias TBL: cdt) 
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [cdc] .col1
                            ScaOp_Identifier QCOL: [cdt] .col1
                    LogOp_Get TBL: table3 (alias TBL: ahcr) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [ahcr] .col9
                        ScaOp_Identifier QCOL: [cdt] .col1
                AncOp_PrjList 
                    AncOp_PrjEl QCOL: [cdc] .col7
                        ScaOp_Convert char collate 53256, Null, Trim, ML = 6
                            ScaOp_IIF varchar menyusun 53256, Null, Var, Trim, ML = 6
                                ScaOp_Comp x_cmpEq
                                    ScaOp_Intrinsic isnumeric
                                        ScaOp_Intrinsik benar
                                            ScaOp_Identifier QCOL: [cdc] .col4
                                            ScaOp_Const TI (int, ML = 4) XVAR (int, Tidak Dimiliki, Nilai = 4)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, Tidak Dimiliki, Nilai = 0)
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 1) XVAR (varchar, Dimiliki, Nilai = Len, Data = (0,))
                                Substring ScaOp_Intrinsik
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, Tidak Dimiliki, Nilai = 6)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, Tidak Dimiliki, Nilai = 1)
                                    ScaOp_Identifier QCOL: [cdc] .col4
            LogOp_Get TBL: dbo.table5 (alias TBL: a5)
            ScaOp_Logical x_lopAnd
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a5] .col2
                    ScaOp_Identifier QCOL: [cdc] .col2
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4] .col2
                    ScaOp_Identifier QCOL: [cdc] .col2

Statistik dimuat dan estimasi kardinalitas awal dilakukan di pohon tepat setelah pesanan bergabung awal ditetapkan. Bergabung dalam pesanan berbeda juga memengaruhi taksiran ini, dan juga memiliki efek ketukan selama pengoptimalan berbasis biaya nanti.

Akhirnya untuk bagian ini, memiliki sambungan luar yang macet di tengah-tengah pohon dapat mencegah beberapa aturan bergabung lagi yang cocok selama optimasi berbasis biaya.


Dengan menggunakan panduan rencana (atau, USE PLANpetunjuk yang setara - contoh untuk permintaan Anda ) mengubah strategi pencarian menjadi pendekatan yang lebih berorientasi pada tujuan, dipandu oleh bentuk umum dan fitur-fitur templat yang disediakan. Ini menjelaskan mengapa pengoptimal dapat menemukan table1rencana pencarian yang sama terhadap skema kolom yang dihitung dan yang tidak dikomputasi, ketika panduan atau petunjuk rencana digunakan.

Apakah kita dapat melakukan sesuatu yang berbeda untuk mewujudkannya

Ini adalah sesuatu yang Anda hanya perlu khawatir jika pengoptimal tidak menemukan rencana dengan karakteristik kinerja yang dapat diterima sendiri.

Semua alat penyetelan normal berpotensi berlaku. Anda dapat, misalnya, memecah kueri menjadi bagian-bagian yang lebih sederhana, meninjau dan meningkatkan pengindeksan yang tersedia, memperbarui atau membuat statistik baru ... dan seterusnya.

Semua hal ini dapat memengaruhi perkiraan kardinalitas, jalur kode yang diambil melalui pengoptimal, dan memengaruhi keputusan berbasis biaya dengan cara yang halus.

Anda mungkin akhirnya menggunakan petunjuk (atau panduan rencana), tetapi itu biasanya bukan solusi yang ideal.


Pertanyaan tambahan dari komentar

Saya setuju bahwa yang terbaik adalah menyederhanakan kueri dll, tetapi apakah ada cara (jejak bendera) untuk membuat pengoptimal melanjutkan dengan optimasi dan mencapai hasil yang sama?

Tidak, tidak ada tanda jejak untuk melakukan pencarian lengkap, dan Anda tidak menginginkannya. Ruang pencarian yang mungkin luas, dan waktu kompilasi yang melampaui usia alam semesta tidak akan diterima dengan baik. Juga, pengoptimal tidak tahu setiap kemungkinan transformasi logis (tidak ada yang tahu).

Juga, mengapa ekspansi kompleks diperlukan, karena kolom itu tetap ada? Mengapa pengoptimal tidak dapat menghindari mengembangkannya, memperlakukannya seperti kolom biasa, dan mencapai titik awal yang sama?

Kolom yang dikomputasi diperluas (seperti halnya tampilan) untuk mengaktifkan peluang optimisasi tambahan. Ekspansi dapat dicocokkan kembali ke misalnya kolom atau indeks yang bertahan kemudian dalam proses, tetapi ini terjadi setelah pesanan gabungan awal diperbaiki.

Paul White 9
sumber