Masalah optimisasi dengan fungsi yang ditentukan pengguna

26

Saya memiliki masalah memahami mengapa SQL server memutuskan untuk memanggil fungsi yang ditentukan pengguna untuk setiap nilai dalam tabel meskipun hanya satu baris yang harus diambil. SQL sebenarnya jauh lebih kompleks, tapi saya bisa mengurangi masalah ini menjadi ini:

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Untuk kueri ini, SQL Server memutuskan untuk memanggil fungsi GetGroupCode untuk setiap nilai tunggal yang ada di Tabel PRODUCT, meskipun taksiran dan jumlah sebenarnya baris yang dikembalikan dari ORDERLINE adalah 1 (itu adalah kunci utama):

Rencana Kueri

Paket yang sama di paket explorer yang menunjukkan jumlah baris:

Rencanakan penjelajah Tabel:

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

Indeks yang digunakan untuk pemindaian adalah:

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

Fungsi ini sebenarnya sedikit lebih kompleks, tetapi hal yang sama terjadi dengan fungsi multi-pernyataan dummy seperti ini:

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

Saya dapat "memperbaiki" kinerja dengan memaksa SQL server untuk mengambil 1 produk teratas, meskipun 1 adalah maks yang dapat ditemukan:

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Kemudian bentuk rencana juga berubah menjadi sesuatu yang saya harapkan semula:

Rencana Kueri dengan atas

Saya juga berpikir bahwa indeks PRODUCT_FACTORY yang lebih kecil dari indeks cluster PRODUCT_PK akan memiliki pengaruh, tetapi bahkan dengan memaksa permintaan untuk menggunakan PRODUCT_PK, rencana tersebut masih sama seperti aslinya, dengan 6655 panggilan ke fungsi.

Jika saya meninggalkan ORDERHDR sepenuhnya, maka rencana dimulai dengan loop bersarang antara ORDERLINE dan PRODUCT terlebih dahulu, dan fungsinya disebut hanya sekali.

Saya ingin memahami apa yang bisa menjadi alasan untuk ini karena semua operasi dilakukan menggunakan kunci primer dan cara memperbaikinya jika itu terjadi dalam permintaan yang lebih kompleks yang tidak dapat diselesaikan dengan mudah.

Edit: Buat pernyataan tabel:

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)
James Z
sumber

Jawaban:

30

Ada tiga alasan teknis utama Anda mendapatkan rencana yang Anda lakukan:

  1. Kerangka kerja biaya pengoptimal tidak memiliki dukungan nyata untuk fungsi non-inline. Itu tidak membuat upaya untuk melihat ke dalam definisi fungsi untuk melihat seberapa mahal mungkin, itu hanya menetapkan biaya tetap yang sangat kecil, dan memperkirakan fungsi akan menghasilkan 1 baris output setiap kali dipanggil. Kedua asumsi pemodelan ini seringkali sangat tidak aman. Situasi ini sedikit meningkat pada tahun 2014 dengan penduga kardinalitas baru diaktifkan karena tebakan 1-baris yang tetap diganti dengan tebakan 100-baris yang tetap. Namun, masih belum ada dukungan untuk menghitung biaya fungsi non-inline.
  2. SQL Server awalnya runtuh bergabung dan berlaku menjadi satu n-ary logis bergabung internal. Ini membantu alasan pengoptimal tentang bergabung dalam pesanan nanti. Memperluas single n-ary bergabung menjadi kandidat bergabung dengan pesanan datang kemudian, dan sebagian besar didasarkan pada heuristik. Misalnya, sambungan dalam datang sebelum sambungan luar, meja kecil dan sambungan selektif sebelum meja besar dan sambungan kurang selektif, dan seterusnya.
  3. Ketika SQL Server melakukan optimasi berbasis biaya, ia membagi upaya menjadi fase opsional untuk meminimalkan kemungkinan menghabiskan terlalu lama mengoptimalkan permintaan biaya rendah. Ada tiga fase utama, pencarian 0, pencarian 1, dan pencarian 2. Setiap fase memiliki kondisi entri, dan fase selanjutnya memungkinkan lebih banyak eksplorasi pengoptimal daripada yang sebelumnya. Kueri Anda memenuhi syarat untuk fase pencarian yang paling tidak mampu, fase 0. Rencana biaya yang cukup rendah ditemukan di sana bahwa tahapan selanjutnya tidak dimasukkan.

Mengingat perkiraan kardinalitas kecil yang diberikan pada UDF berlaku, n-ary join heuristik ekspansi sayangnya memposisikannya lebih awal di pohon daripada yang Anda inginkan.

Kueri juga memenuhi syarat untuk optimasi pencarian 0 berdasarkan memiliki setidaknya tiga bergabung (termasuk berlaku). Rencana fisik terakhir yang Anda dapatkan, dengan pemindaian yang tampak aneh, didasarkan pada urutan bergabung yang dideduksi secara heuristik. Biayanya cukup rendah sehingga pengoptimal menganggap rencana itu "cukup baik". Estimasi biaya rendah dan kardinalitas untuk UDF berkontribusi pada penyelesaian awal ini.

Pencarian 0 (juga dikenal sebagai fase Pemrosesan Transaksi) menargetkan kueri tipe OLTP dengan kardinalitas rendah, dengan rencana akhir yang biasanya menampilkan gabungan loop bersarang. Lebih penting lagi, pencarian 0 hanya menjalankan sebagian kecil dari kemampuan eksplorasi pengoptimal. Subhimpunan ini tidak termasuk menarik sebuah menerapkan pohon kueri atas gabungan (aturan PullApplyOverJoin). Inilah yang diperlukan dalam kasus uji untuk memposisikan ulang UDF yang berlaku di atas sambungan, untuk muncul terakhir dalam urutan operasi (seolah-olah).

Ada juga masalah di mana pengoptimal dapat memutuskan antara simpul bertumpuk yang naif bergabung (bergabung dengan predikat pada join itu sendiri) dan gabungan terindeks yang berkorelasi (berlaku) di mana predikat yang berkorelasi diterapkan pada sisi dalam bergabung menggunakan pencarian indeks. Yang terakhir biasanya bentuk rencana yang diinginkan, tetapi pengoptimal mampu menjelajahi keduanya. Dengan perkiraan biaya dan kardinalitas yang salah, ia dapat memilih gabungan NL yang tidak berlaku, seperti dalam rencana yang diajukan (menjelaskan pemindaian).

Jadi, ada beberapa alasan berinteraksi yang melibatkan beberapa fitur pengoptimal umum yang biasanya bekerja dengan baik untuk menemukan rencana yang baik dalam waktu singkat tanpa menggunakan sumber daya yang berlebihan. Menghindari salah satu alasan cukup untuk menghasilkan bentuk rencana 'yang diharapkan' untuk kueri sampel, bahkan dengan tabel kosong:

Paketkan pada tabel kosong dengan pencarian 0 dinonaktifkan

Tidak ada cara yang didukung untuk menghindari pencarian pencarian 0 pilihan, penghentian pengoptimal awal, atau untuk meningkatkan penetapan biaya UDF (selain dari peningkatan terbatas dalam model SQL Server 2014 CE untuk ini). Ini meninggalkan hal-hal seperti panduan rencana, penulisan ulang kueri manual (termasuk TOP (1)ide atau menggunakan tabel sementara perantara) dan menghindari 'kotak hitam' dengan biaya rendah (dari sudut pandang QO) seperti fungsi non-inline.

Menulis ulang CROSS APPLYsebagai OUTER APPLYdapat juga bekerja, karena saat ini mencegah beberapa pekerjaan awal bergabung-runtuh, tetapi Anda harus berhati-hati untuk melestarikan semantik query (misalnya menolak setiap NULLbaris -extended yang mungkin diperkenalkan, tanpa optimizer runtuh kembali ke silang berlaku). Anda perlu menyadari bahwa perilaku ini tidak dijamin tetap stabil, jadi Anda harus ingat untuk menguji ulang perilaku yang diamati tersebut setiap kali Anda menambal atau memutakhirkan SQL Server.

Secara keseluruhan, solusi yang tepat untuk Anda tergantung pada berbagai faktor yang tidak dapat kami nilai untuk Anda. Namun, saya akan mendorong Anda untuk mempertimbangkan solusi yang dijamin untuk selalu bekerja di masa depan, dan yang bekerja dengan (bukannya menentang) optimizer sedapat mungkin.

Paul White mengatakan GoFundMonica
sumber
24

Sepertinya ini adalah keputusan berbasis biaya oleh pengoptimal tetapi yang agak buruk.

Jika Anda menambahkan 50000 baris ke PRODUCT, pengoptimal menganggap pemindaian terlalu banyak dan memberi Anda rencana dengan tiga pencarian dan satu panggilan ke UDF.

Paket yang saya dapatkan untuk 6655 baris dalam PRODUCT

masukkan deskripsi gambar di sini

Dengan 50.000 baris dalam PRODUCT, saya mendapatkan paket ini sebagai gantinya.

masukkan deskripsi gambar di sini

Saya kira biaya untuk menelepon UDF terlalu diremehkan.

Salah satu solusi yang berfungsi dengan baik dalam hal ini adalah mengubah kueri untuk menggunakan terapan luar terhadap UDF. Saya mendapatkan rencana yang bagus tidak peduli berapa banyak baris yang ada dalam tabel PRODUK.

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

masukkan deskripsi gambar di sini

Solusi terbaik dalam kasus Anda mungkin adalah untuk mendapatkan nilai yang Anda butuhkan ke tabel temp dan kemudian permintaan tabel temp dengan tanda silang berlaku untuk UDF. Dengan begitu Anda yakin bahwa UDF tidak akan dieksekusi lebih dari yang diperlukan.

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

Alih-alih bertahan untuk tabel temporer Anda dapat menggunakan top()dalam tabel turunan untuk memaksa SQL Server untuk mengevaluasi hasil dari bergabung sebelum UDF dipanggil. Cukup gunakan angka yang sangat tinggi di bagian atas membuat SQL Server harus menghitung baris Anda untuk bagian dari pertanyaan sebelum dapat melanjutkan dan menggunakan UDF.

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

masukkan deskripsi gambar di sini

Saya ingin memahami apa yang bisa menjadi alasan untuk ini karena semua operasi dilakukan menggunakan kunci primer dan cara memperbaikinya jika itu terjadi dalam permintaan yang lebih kompleks yang tidak dapat diselesaikan dengan mudah.

Saya benar-benar tidak bisa menjawabnya tetapi saya pikir saya harus membagikan apa yang saya tahu. Saya tidak tahu mengapa pemindaian tabel PRODUCT dipertimbangkan sama sekali. Mungkin ada kasus di mana itu adalah hal terbaik untuk dilakukan dan ada hal-hal mengenai bagaimana pengoptimal memperlakukan UDF yang saya tidak tahu.

Satu pengamatan tambahan adalah bahwa permintaan Anda mendapatkan rencana yang baik di SQL Server 2014 dengan penduga kardinalitas baru. Itu karena perkiraan jumlah baris untuk setiap panggilan ke UDF adalah 100 bukannya 1 seperti di SQL Server 2012 dan sebelumnya. Tetapi masih akan membuat keputusan berdasarkan biaya yang sama antara versi pemindaian dan versi pencarian paket. Dengan kurang dari 500 (497 dalam kasus saya) baris dalam PRODUCT Anda mendapatkan versi pemindaian paket bahkan di SQL Server 2014.

Mikael Eriksson
sumber
2
Entah bagaimana mengingatkan saya pada sesi Adam Machanic di SQL Bits: sqlbits.com/Sessions/Event14/…
James Z