Ubah kueri untuk meningkatkan taksiran operator

14

Saya punya pertanyaan yang berjalan dalam jumlah waktu yang dapat diterima tetapi saya ingin memeras kinerja sebanyak mungkin dari itu.

Operasi yang saya coba tingkatkan adalah "Index Seek" di sebelah kanan paket, dari Node 17.

masukkan deskripsi gambar di sini

Saya telah menambahkan indeks yang sesuai tetapi perkiraan yang saya dapatkan untuk operasi itu adalah setengah dari yang seharusnya.

Saya telah mencari untuk mengubah indeks saya dan menambahkan tabel sementara dan menulis ulang kueri, tetapi saya tidak bisa menyederhanakannya lebih dari ini untuk mendapatkan perkiraan yang tepat.

Adakah yang punya saran tentang apa lagi yang bisa saya coba?

Rencana lengkap dan detailnya dapat ditemukan di sini .

Paket tanpa anonim dapat ditemukan di sini.

Memperbarui:

Saya merasa versi awal dari pertanyaan ini menimbulkan banyak kebingungan, jadi saya akan menambahkan kode asli dengan beberapa penjelasan.

create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
    set nocount on;

    declare @dist_ca_id int;

    select *
    into #temp
    from @customAttrValIds
        where id is not null;

    select @dist_ca_id = count(distinct CustomAttrID) 
    from CustomAttributeValues c
        inner join #temp a on c.Id = a.id;

    select a.Id
        , a.AssortmentId 
    from Assortments a
        inner join AssortmentCustomAttributeValues acav
            on a.Id = acav.Assortment_Id
        inner join CustomAttributeValues cav 
            on cav.Id = acav.CustomAttributeValue_Id
    where a.AssortmentType = @asType
        and acav.CustomAttributeValue_Id in (select id from #temp)
    group by a.AssortmentId
        , a.Id
    having count(distinct cav.CustomAttrID) = @dist_ca_id
    option(recompile);

end

Jawaban:

  1. Mengapa penamaan awal yang aneh di tautan pasteThePlan?

    Jawaban : Karena saya menggunakan rencana anonim dari SQL Sentry Plan Explorer.

  2. Mengapa OPTION RECOMPILE?

    Jawab : Karena saya mampu melakukan kompilasi ulang untuk menghindari sniffing parameter (data / bisa miring). Saya telah menguji dan saya senang dengan rencana yang dihasilkan Pengoptimal saat menggunakan OPTION RECOMPILE.

  3. WITH SCHEMABINDING?

    Jawab : Saya benar-benar ingin menghindarinya dan hanya akan menggunakannya ketika saya memiliki tampilan yang diindeks Bagaimanapun, ini adalah fungsi sistem ( COUNT()) jadi tidak ada gunanya di SCHEMABINDINGsini.

Jawaban untuk pertanyaan yang lebih mungkin:

  1. Kenapa saya menggunakan INSERT INTO #temp FROM @customAttrributeValues?

    Jawaban : Karena saya perhatikan dan sekarang tahu bahwa ketika menggunakan variabel yang dicolokkan ke kueri, setiap perkiraan yang keluar dari bekerja dengan variabel selalu 1. Dan saya menguji menempatkan data ke tabel temp dan Estimasi kemudian sama dengan Baris Aktual .

  2. Kenapa saya menggunakan and acav.CustomAttributeValue_Id in (select id from #temp)?

    Jawab : Saya bisa menggantinya dengan BERGABUNG di #temp, tetapi pengembang sangat bingung dan menawarkan INopsi. Saya tidak benar-benar berpikir akan ada perbedaan bahkan dengan mengganti dan bagaimanapun, tidak ada masalah dengan ini.

Radu Gheorghiu
sumber
Saya akan menebak bahwa #tempkreasi dan penggunaan akan menjadi masalah untuk kinerja, bukan keuntungan. Anda menyimpan ke tabel yang tidak diindeks hanya untuk digunakan satu kali. Coba hapus sepenuhnya (dan mungkin mengubahnya in (select id from #temp)menjadi existssubquery.
ypercubeᵀᴹ
@ ypercubeᵀᴹ Benar, hanya beberapa halaman yang kurang dibaca dengan menggunakan variabel daripada tabel temp.
Radu Gheorghiu
Ngomong-ngomong, variabel tabel akan memberikan taksiran jumlah baris yang benar ketika digunakan dengan Option (Recompile) - tetapi masih tidak memiliki statistik granular, kardinalitas dll.
TH
@ TH Nah, saya memang melihat dalam rencana eksekusi aktual pada perkiraan, ketika menggunakan select id from @customAttrValIdsbukannya select id from #tempdan jumlah baris diperkirakan 1untuk variabel dan 3untuk #temp (yang cocok dengan # baris sebenarnya). Itulah mengapa saya diganti @dengan #. Dan saya DO ingat bicara (dari Brent O atau Aaron Bertrand) di mana mereka mengatakan bahwa ketika menggunakan variabel tbl perkiraan untuk itu akan selalu 1. dan sebagai perbaikan untuk mendapatkan perkiraan yang lebih baik mereka akan menggunakan tabel sementara.
Radu Gheorghiu
@ RaduGheorghiu Ya tapi di dunia orang-orang itu, opsi (kompilasi ulang) jarang menjadi pilihan, dan mereka juga lebih suka tabel temp untuk alasan yang sah lainnya. Mungkin perkiraan itu selalu keliru menunjukkan 1, karena itu mengubah rencana seperti yang terlihat di sini: theboreddba.com/Categories/FunWithFlags/…
TH

Jawaban:

12

Paket ini dikompilasi pada contoh SQL Server 2008 R2 RTM (build 10.50.1600). Anda harus menginstal Paket Layanan 3 (build 10.50.6000), diikuti oleh tambalan terbaru untuk membawanya ke build terbaru 10.50.6542. Ini penting karena sejumlah alasan, termasuk keamanan, perbaikan bug, dan fitur baru.

Parameter Optimalisasi Penempatan

Relevan dengan pertanyaan ini, SQL Server 2008 R2 RTM tidak mendukung Parameter Embedding Optimization (PEO) untuk OPTION (RECOMPILE). Saat ini, Anda membayar biaya kompilasi ulang tanpa menyadari salah satu manfaat utama.

Ketika PEO tersedia, SQL Server dapat menggunakan nilai literal yang disimpan dalam variabel dan parameter lokal langsung di paket kueri. Ini dapat menyebabkan penyederhanaan dramatis dan peningkatan kinerja. Ada informasi lebih lanjut tentang itu di artikel saya, Parameter Sniffing, Embedding, dan Opsi RECOMPILE .

Hash, Sortir, dan Tukar Tumpahan

Ini hanya ditampilkan dalam rencana eksekusi ketika kueri dikompilasi pada SQL Server 2012 atau yang lebih baru. Dalam versi sebelumnya, kami harus memantau tumpahan saat kueri mengeksekusi menggunakan Profiler atau Extended Events. Tumpahan selalu mengakibatkan I / O fisik menjadi (dan dari) tempdb dukungan penyimpanan persisten , yang dapat memiliki konsekuensi kinerja yang penting, terutama jika tumpahan besar, atau jalur I / O berada di bawah tekanan.

Dalam rencana eksekusi Anda, ada dua operator Pencocokan Hash (Agregat). Memori yang dicadangkan untuk tabel hash didasarkan pada estimasi untuk baris output (dengan kata lain, ini sebanding dengan jumlah grup yang ditemukan saat runtime). Memori yang diberikan diperbaiki tepat sebelum eksekusi dimulai, dan tidak dapat tumbuh selama eksekusi, terlepas dari berapa banyak memori bebas yang dimiliki instance. Dalam paket yang disediakan, kedua operator Pencocokan Hash (Agregat) menghasilkan lebih banyak baris dari yang diharapkan oleh pengoptimal, dan karenanya mungkin mengalami tumpahan ke tempdb saat runtime.

Ada juga operator Hash Match (Inner Join) dalam paket tersebut. Memori yang dicadangkan untuk tabel hash didasarkan pada estimasi untuk baris input sisi probe . Input input memperkirakan 847.399 baris, tetapi 1.223.636 ditemui pada saat dijalankan. Kelebihan ini juga dapat menyebabkan tumpahan hash.

Agregat Redundan

Pencocokan Hash (Agregat) pada node 8 melakukan operasi pengelompokan aktif (Assortment_Id, CustomAttrID), tetapi baris input sama dengan baris output:

Pertandingan Hode Node 8 (Agregat)

Ini menunjukkan bahwa kombinasi kolom adalah kunci (sehingga pengelompokan secara semantik tidak diperlukan). Biaya melakukan agregat redundan dinaikkan oleh kebutuhan untuk melewati 1,4 juta baris dua kali di seluruh pertukaran partisi hash (operator Paralelisme di kedua sisi).

Mengingat bahwa kolom yang terlibat berasal dari tabel yang berbeda, lebih sulit daripada biasanya untuk mengomunikasikan informasi keunikan ini kepada pengoptimal, sehingga dapat menghindari operasi pengelompokan yang berlebihan dan pertukaran yang tidak perlu.

Distribusi benang tidak efisien

Seperti dicatat dalam jawaban Joe Obbish , pertukaran di simpul 14 menggunakan partisi hash untuk mendistribusikan baris di antara utas. Sayangnya, sejumlah kecil baris dan penjadwal yang tersedia berarti ketiga baris berakhir pada satu utas. Rencana yang tampaknya paralel berjalan secara serial (dengan overhead paralel) sejauh pertukaran pada simpul 9.

Anda bisa mengatasinya (untuk mendapatkan round-robin atau mempartisi partisi) dengan menghilangkan Sorting Distinct pada node 13. Cara termudah untuk melakukannya adalah dengan membuat kunci primer yang dikelompokkan di atas #tempmeja, dan melakukan operasi yang berbeda saat memuat tabel:

CREATE TABLE #Temp
(
    id integer NOT NULL PRIMARY KEY CLUSTERED
);

INSERT #Temp
(
    id
)
SELECT DISTINCT
    CAV.id
FROM @customAttrValIds AS CAV
WHERE
    CAV.id IS NOT NULL;

Caching statistik tabel sementara

Meskipun menggunakan OPTION (RECOMPILE), SQL Server masih dapat men-cache objek tabel sementara dan statistik terkait antara panggilan prosedur. Ini umumnya merupakan optimasi kinerja yang disambut baik, tetapi jika tabel sementara diisi dengan jumlah data yang sama pada panggilan prosedur yang berdekatan, rencana yang dikompilasi ulang mungkin didasarkan pada statistik yang salah (di-cache dari eksekusi sebelumnya). Ini dijelaskan dalam artikel saya, Tabel Sementara dalam Prosedur yang Disimpan dan Penjelasan Tabel Sementara Caching Dijelaskan .

Untuk menghindari ini, gunakan OPTION (RECOMPILE)bersama-sama dengan eksplisit UPDATE STATISTICS #TempTablesetelah tabel sementara diisi, dan sebelum direferensikan dalam kueri.

Penulisan ulang kueri

Bagian ini mengasumsikan bahwa perubahan pada pembuatan #Temptabel telah dilakukan.

Mengingat biaya dari kemungkinan tumpahan hash dan agregat berlebihan (dan pertukaran sekitarnya), mungkin membayar untuk mewujudkan set di node 10:

CREATE TABLE #Temp2
(
    CustomAttrID integer NOT NULL,
    Assortment_Id integer NOT NULL,
);

INSERT #Temp2
(
    Assortment_Id,
    CustomAttrID
)
SELECT
    ACAV.Assortment_Id,
    CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
    ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
    ON T.id = ACAV.CustomAttributeValue_Id;

ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);

Itu PRIMARY KEYditambahkan dalam langkah terpisah untuk memastikan membangun indeks memiliki informasi kardinalitas yang akurat, dan untuk menghindari masalah caching statistik tabel sementara.

Materialisasi ini sangat mungkin terjadi dalam memori (menghindari tempdb I / O) jika instance memiliki cukup memori. Ini bahkan lebih mungkin setelah Anda memutakhirkan ke SQL Server 2012 (SP1 CU10 / SP2 CU1 atau lebih baru), yang telah meningkatkan perilaku Eager Write .

Tindakan ini memberikan informasi kardinalitas akurat pengoptimal pada set perantara, memungkinkannya untuk membuat statistik, dan memungkinkan kami untuk mendeklarasikan (Assortment_Id, CustomAttrID)sebagai kunci.

Rencana untuk populasi #Temp2seharusnya terlihat seperti ini (perhatikan pemindaian indeks berkerumun #Temp, tidak ada Sorting Berbeda, dan pertukaran sekarang menggunakan partisi round-robin row):

# Populasi Temp2

Dengan set yang tersedia, kueri terakhir menjadi:

SELECT
    A.Id,
    A.AssortmentId
FROM
(
    SELECT
        T.Assortment_Id
    FROM #Temp2 AS T
    GROUP BY
        T.Assortment_Id
    HAVING
        COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
    ON A.Id = DT.Assortment_Id
WHERE
    A.AssortmentType = @asType
OPTION (RECOMPILE);

Kami dapat menulis ulang secara manual COUNT_BIG(DISTINCT...sebagai yang sederhana COUNT_BIG(*), tetapi dengan informasi kunci yang baru, pengoptimal melakukannya untuk kami:

Rencana akhir

Rencana akhir dapat menggunakan loop / hash / gabungan bergabung tergantung pada informasi statistik tentang data yang saya tidak memiliki akses. Satu catatan kecil lainnya: Saya berasumsi bahwa indeks suka CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);ada.

Bagaimanapun, hal penting tentang rencana akhir adalah bahwa perkiraan harus jauh lebih baik, dan urutan kompleks dari operasi pengelompokan telah direduksi menjadi Agregat Stream tunggal (yang tidak memerlukan memori dan karenanya tidak dapat tumpah ke disk).

Sulit untuk mengatakan bahwa kinerja sebenarnya akan lebih baik dalam hal ini dengan tabel sementara tambahan, tetapi perkiraan dan pilihan rencana akan jauh lebih tangguh terhadap perubahan volume data dan distribusi dari waktu ke waktu. Itu mungkin lebih berharga dalam jangka panjang daripada peningkatan kinerja kecil hari ini. Bagaimanapun, Anda sekarang memiliki lebih banyak informasi yang menjadi dasar keputusan akhir Anda.

Paul White 9
sumber
9

Perkiraan kardinalitas pada permintaan Anda sebenarnya sangat bagus. Sangat jarang untuk mendapatkan jumlah baris yang diperkirakan untuk mencocokkan jumlah baris yang sebenarnya persis, terutama ketika Anda memiliki banyak ini bergabung. Bergabunglah dengan perkiraan kardinalitas rumit untuk dilakukan optimizer. Satu hal penting yang perlu diperhatikan adalah bahwa jumlah baris yang diperkirakan untuk bagian dalam loop bersarang adalah per eksekusi dari loop itu. Jadi ketika SQL Server mengatakan bahwa 463869 baris akan diambil dengan indeks mencari estimasi nyata dalam kasus ini adalah jumlah eksekusi (2) * 463869 = 927738 yang tidak jauh dari jumlah baris aktual, 1391608. Anehnya, jumlah baris yang diperkirakan mendekati sempurna segera setelah loop bersarang bergabung pada simpul ID 10.

Perkiraan kardinalitas yang buruk sebagian besar merupakan masalah ketika pengoptimal kueri memilih rencana yang salah atau tidak memberikan cukup memori untuk rencana tersebut. Saya tidak melihat tumpahan ke tempdb untuk paket ini, jadi ingatan terlihat baik-baik saja. Untuk bergabung dengan loop bersarang yang Anda panggil, Anda memiliki tabel luar kecil dan tabel dalam diindeks. Apa yang salah dengan itu? Lebih tepatnya, apa yang Anda harapkan dari pengoptimal query lakukan secara berbeda di sini?

Dalam hal meningkatkan kinerja, hal yang menonjol bagi saya adalah bahwa SQL Server menggunakan algoritma hashing untuk mendistribusikan baris paralel yang menghasilkan semuanya berada di utas yang sama:

ketidakseimbangan benang

Akibatnya, satu utas melakukan semua pekerjaan dengan indeks mencari:

mencari ketidakseimbangan benang

Itu berarti bahwa kueri Anda secara efektif tidak berjalan secara paralel sampai partisi ulang mengalirkan operator pada simpul id 9. Apa yang Anda mungkin inginkan adalah partisi round robin sehingga setiap baris berakhir pada utasnya sendiri. Itu akan memungkinkan dua utas untuk melakukan pencarian indeks untuk simpul id 17. Menambahkan TOPoperator yang berlebihan mungkin membuat Anda membuat partisi robin bulat. Saya dapat menambahkan detail di sini jika Anda mau.

Jika Anda benar-benar ingin fokus pada perkiraan kardinalitas, Anda bisa meletakkan baris setelah bergabung pertama ke tabel temp. Jika Anda mengumpulkan statistik di tabel temp yang memberikan optimizer lebih banyak informasi tentang tabel terluar untuk loop bersarang bergabung yang Anda panggil. Itu juga bisa menghasilkan partisi round robin.

Jika Anda tidak menggunakan tanda jejak 4199 atau 2301, Anda dapat mempertimbangkannya. Bendera jejak 4199 menawarkan berbagai perbaikan optimizer, tetapi mereka dapat menurunkan beberapa beban kerja. Trace flag 2301 mengubah beberapa asumsi kardinalitas gabungan dari optimizer kueri dan membuatnya bekerja lebih keras. Dalam kedua kasus, uji dengan cermat sebelum mengaktifkannya.

Joe Obbish
sumber
-2

Saya percaya mendapatkan perkiraan yang lebih baik tentang bergabung itu tidak akan mengubah rencana, kecuali 1,4 mill adalah bagian yang cukup dari tabel untuk membuat pengoptimal memilih scan indeks (bukan cluster) dengan hash atau gabungan gabung. Saya menduga itu tidak akan menjadi masalah di sini, juga tidak benar-benar bermanfaat, tetapi Anda dapat menguji efeknya dengan mengganti gabungan internal terhadap CustomAttributeValues ​​dengan gabungan hash dalam dan gabungan gabungan internal .

Saya juga telah melihat kode secara lebih luas dan tidak dapat melihat cara untuk memperbaikinya - saya tentu saja tertarik untuk terbukti salah. Dan jika Anda ingin memposting logika lengkap tentang apa yang ingin Anda capai, saya akan tertarik pada tampilan lain.

TH
sumber
3
Ada ruang rencana yang sangat besar untuk kueri itu, dengan banyak opsi untuk bergabung dengan pesanan dan bersarang, paralelisme, agregasi lokal / global, dll. Dll. Yang sebagian besar akan dipengaruhi oleh perubahan dalam statistik yang diturunkan (distribusi serta kardinalitas mentah) at plan node 10. Perhatikan juga bahwa petunjuk gabungan harus dihindari karena mereka datang dengan diam OPTION(FORCE ORDER), yang mencegah pengoptimalan pemesanan ulang bergabung dari urutan teks, dan banyak optimasi lainnya selain itu.
Paul White 9
-12

Anda tidak akan membaik dari Pencarian Indeks [non-clustered]. Satu-satunya hal yang lebih baik daripada pencarian indeks non-cluster adalah Pencarian Indeks Clustered.

Juga, saya telah menjadi DBA SQL selama sepuluh tahun terakhir, dan pengembang SQL selama lima tahun sebelumnya, dan dalam pengalaman saya, sangat jarang menemukan peningkatan pada SQL Query dengan mempelajari rencana eksekusi yang tidak dapat Anda lakukan. t menemukan dengan cara lain. Alasan utama untuk menghasilkan rencana pelaksanaan adalah karena sering kali akan menyarankan indeks yang hilang kepada Anda yang dapat Anda tambahkan untuk meningkatkan kinerja.

Keuntungan kinerja utama akan menyesuaikan SQL Query itu sendiri, jika ada inefisiensi di sana. Sebagai contoh, beberapa bulan yang lalu saya mendapatkan fungsi SQL untuk menjalankan 160 kali lebih cepat dengan menulis ulang SELECT UNION SELECTtabel pivot gaya untuk menggunakan PIVOToperator SQL standar .

insert into Variable1 values (?), (?), (?)


select *
    into Object1
    from Variable2
        where Column1 is not null;



select Variable3 = Function1(distinct Column2) 
    from Object2 Object3
        inner join Object1 Object4 on Object3.Column1 = Object4.Column1;



select Object4.Column1
        , Object4.Column3 
    from Object5 Object4
        inner join Object6 Object7
            on Object4.Column1 = Object7.Column4
        inner join Object2 Object8 
            on Object8.Column1 = Object7.Column5
    where Object4.Column6 = Variable4
        and Object7.Column5 in (select Column1 from Object1)
    group by Object4.Column3
        , Object4.Column1
    having Function1(distinct Object8.Column2) = Variable3
    option(recompile);

Jadi mari kita lihat, SELECT * INTOumumnya kurang efisien daripada standar INSERT Object1 (column list) SELECT column list. Jadi saya akan menulis ulang itu. Selanjutnya, jika Function1 didefinisikan tanpa a WITH SCHEMABINDING, menambahkan WITH SCHEMABINDINGklausa akan membuatnya berjalan lebih cepat.

Anda telah memilih banyak alias yang tidak masuk akal, seperti alias Object2 sebagai Object3. Anda harus memilih alias yang lebih baik yang tidak mengaburkan kode. Anda memiliki "Object7.Column5 di (pilih Column1 dari Object1)".

INklausul seperti ini selalu lebih efisien ditulis sebagai EXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5). Mungkin saya seharusnya menulis itu sebaliknya. EXISTSakan selalu setidaknya sebaik IN. Ini tidak selalu lebih baik, tetapi biasanya memang demikian.

Juga, saya meragukannya option(recompile) meningkatkan kinerja permintaan di sini. Saya akan menguji menghapusnya.

Matthew Sontum
sumber
6
Jika pencarian indeks nonclustered mencakup permintaan, itu hampir selalu akan lebih baik daripada pencarian indeks berkerumun, karena menurut definisi, indeks berkerumun memiliki semua kolom di dalamnya, dan indeks nonclustered memiliki kolom lebih sedikit sehingga akan membutuhkan lebih sedikit pencarian halaman (dan lebih sedikit level langkah ke b-tree) untuk mengambil data. Jadi tidak akurat untuk mengatakan bahwa pencarian indeks berkerumun akan selalu lebih baik.
ErikE