Mengapa ini lebih cepat dan aman untuk digunakan? (DI MANA huruf pertama ada dalam alfabet)

10

Singkatnya, kami memperbarui tabel kecil orang dengan nilai dari tabel orang yang sangat besar. Dalam pengujian baru-baru ini, pembaruan ini membutuhkan waktu sekitar 5 menit untuk dijalankan.

Kami menemukan apa yang tampaknya seperti optimasi paling konyol yang mungkin, yang tampaknya bekerja dengan sempurna! Permintaan yang sama sekarang berjalan dalam waktu kurang dari 2 menit dan menghasilkan hasil yang sama, dengan sempurna.

Ini pertanyaannya. Baris terakhir ditambahkan sebagai "optimisasi". Mengapa penurunan tajam dalam waktu permintaan? Apakah kita melewatkan sesuatu? Bisakah ini menimbulkan masalah di masa depan?

UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
    AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')

Catatan teknis: Kami menyadari bahwa daftar surat yang akan diuji mungkin memerlukan beberapa surat lagi. Kami juga menyadari margin kesalahan yang jelas ketika menggunakan "PERBEDAAN".

Rencana kueri (reguler): https://www.brentozar.com/pastetheplan/?id=rypV84y7V
Rencana kueri (dengan "optimasi"): https://www.brentozar.com/pastetheplan/?id=r1aC2my7E

JohnF
sumber
4
Balasan kecil untuk catatan teknis Anda: AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AIharus melakukan apa yang Anda inginkan di sana tanpa mengharuskan Anda untuk membuat daftar semua karakter dan memiliki kode yang sulit dibaca
Erik A
Apakah Anda memiliki baris di mana kondisi terakhir di WHEREsalah? Catatan khusus bahwa perbandingan mungkin peka terhadap huruf besar-kecil.
jpmc26
@ErikvonAsmuth membuat poin yang bagus. Tapi, hanya catatan teknis kecil: untuk SQL Server 2008 dan 2008 R2, yang terbaik adalah menggunakan versi "100" collations (jika tersedia untuk budaya / lokal yang digunakan). Jadi itu akan terjadi Latin1_General_100_CI_AI. Dan untuk SQL Server 2012 dan yang lebih baru (paling tidak melalui SQL Server 2019), yang terbaik adalah menggunakan kumpulan yang didukung karakter tambahan dalam versi tertinggi untuk lokal yang digunakan. Jadi itu Latin1_General_100_CI_AI_SCdalam kasus ini. Versi> 100 (hanya bahasa Jepang sejauh ini) tidak memiliki (atau membutuhkan) _SC(mis Japanese_XJIS_140_CI_AI.).
Solomon Rutzky

Jawaban:

9

Itu tergantung pada data di tabel Anda, indeks Anda, .... Sulit dikatakan tanpa bisa membandingkan rencana eksekusi / statistik waktu + io +.

Perbedaan yang saya harapkan adalah pemfilteran ekstra terjadi sebelum GABUNG di antara dua tabel. Dalam contoh saya, saya mengubah pembaruan untuk memilih untuk menggunakan kembali tabel saya.

Rencana eksekusi dengan "optimasi" masukkan deskripsi gambar di sini

Rencana eksekusi

Anda dengan jelas melihat operasi filter terjadi, dalam data pengujian saya tidak ada catatan di mana disaring dan akibatnya tidak ada perbaikan di mana dilakukan.

Rencana eksekusi, tanpa "optimasi" masukkan deskripsi gambar di sini

Rencana eksekusi

Filter hilang, yang berarti bahwa kita harus bergantung pada gabungan untuk menyaring catatan yang tidak dibutuhkan.

Alasan lain Alasan / konsekuensi lain dari mengubah kueri bisa jadi, bahwa rencana eksekusi baru dibuat ketika mengubah kueri, yang kebetulan lebih cepat. Contoh dari ini adalah mesin memilih operator Bergabung yang berbeda, tetapi itu hanya menebak pada titik ini.

EDIT:

Klarifikasi setelah mendapatkan dua paket permintaan:

Permintaan membaca 550M Baris dari tabel besar, dan menyaringnya. masukkan deskripsi gambar di sini

Berarti predikat adalah yang melakukan sebagian besar penyaringan, bukan predikat pencarian. Menghasilkan data yang sedang dibaca, tetapi tidak terlalu dikembalikan.

Membuat sql server menggunakan indeks yang berbeda (rencana kueri) / menambahkan indeks bisa menyelesaikannya.

Jadi mengapa kueri pengoptimalan tidak memiliki masalah yang sama?

Karena rencana kueri yang berbeda digunakan, dengan pemindaian alih-alih pencarian.

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

Tanpa melakukan apa pun, tetapi hanya mengembalikan 4M baris untuk bekerja dengannya.

Perbedaan selanjutnya

Mengabaikan perbedaan pembaruan (tidak ada yang diperbarui pada kueri yang dioptimalkan) kecocokan hash digunakan pada kueri yang dioptimalkan:

masukkan deskripsi gambar di sini

Alih-alih loop bersarang bergabung pada yang tidak dioptimalkan:

masukkan deskripsi gambar di sini

Nested loop adalah yang terbaik ketika satu meja kecil dan yang lainnya besar. Karena keduanya dekat dengan ukuran yang sama, saya berpendapat bahwa pertandingan hash adalah pilihan yang lebih baik dalam kasus ini.

Gambaran

Kueri yang dioptimalkan masukkan deskripsi gambar di sini

Paket kueri yang dioptimalkan memiliki parallellism, menggunakan gabungan hash, dan perlu melakukan lebih sedikit penyaringan IO residual. Itu juga menggunakan bitmap untuk menghilangkan nilai-nilai kunci yang tidak bisa menghasilkan baris gabungan apa pun. (Juga tidak ada yang diperbarui)

Kueri yang tidak dioptimalkan masukkan deskripsi gambar di sini Rencana kueri yang tidak Dioptimalkan tidak memiliki parallellism, menggunakan gabungan loop bersarang, dan perlu melakukan penyaringan IO residual pada catatan 550M. (Juga pembaruan sedang terjadi)

Apa yang dapat Anda lakukan untuk meningkatkan kueri yang tidak dioptimalkan?

  • Mengubah indeks untuk memiliki first_name & last_name dalam daftar kolom kunci:

    CREATE INDEX IX_largeTableOfPeople_birth_date_first_name_last_name di dbo.largeTableOfPeople (birth_date, first_name, last_name) termasuk (id)

Tetapi karena penggunaan fungsi dan tabel ini menjadi besar ini mungkin bukan solusi yang optimal.

  • Memperbarui statistik, menggunakan kompilasi ulang untuk mencoba dan mendapatkan rencana yang lebih baik.
  • Menambahkan OPSI (HASH JOIN, MERGE JOIN)ke kueri
  • ...

Data uji + Pertanyaan yang digunakan

CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));


set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @i += 1;
END


set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @j += 1;
END


SET STATISTICS IO, TIME ON;

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å');

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')




drop table #largeTableOfPeople;
drop table #smallTableOfPeople;
Randi Vertongen
sumber
8

Tidak jelas bahwa permintaan kedua sebenarnya merupakan peningkatan.

Paket eksekusi berisi QueryTimeStats yang menunjukkan perbedaan yang jauh lebih dramatis daripada yang dinyatakan dalam pertanyaan.

Rencana lambat memiliki waktu berlalu 257,556 ms(4 menit 17 detik). Rencana cepat memiliki waktu yang telah berlalu 190,992 ms(3 menit 11 detik) meskipun berjalan dengan tingkat paralelisme 3.

Apalagi rencana kedua berjalan dalam database di mana tidak ada pekerjaan yang harus dilakukan setelah bergabung.

Paket Pertama

masukkan deskripsi gambar di sini

Paket kedua

masukkan deskripsi gambar di sini

Sehingga waktu tambahan bisa dijelaskan dengan pekerjaan yang diperlukan untuk memperbarui 3,5 juta baris (pekerjaan yang diperlukan dalam operator pembaruan untuk menemukan baris ini, mengunci halaman, menulis pembaruan ke halaman dan log transaksi tidak dapat diabaikan)

Jika ini sebenarnya dapat direproduksi ketika membandingkan suka dengan suka maka penjelasannya adalah bahwa Anda beruntung dalam hal ini.

Filter dengan 37 INkondisi hanya menghilangkan 51 baris dari 4.008.334 dalam tabel tetapi pengoptimal menganggap itu akan menghilangkan lebih banyak

masukkan deskripsi gambar di sini

   LEFT(TRIM(largeTbl.last_name), 1) IN ( 'a', 'à', 'á', 'b',
                                          'c', 'd', 'e', 'è',
                                          'é', 'f', 'g', 'h',
                                          'i', 'j', 'k', 'l',
                                          'm', 'n', 'o', 'ô',
                                          'ö', 'p', 'q', 'r',
                                          's', 't', 'u', 'ü',
                                          'v', 'w', 'x', 'y',
                                          'z', 'æ', 'ä', 'ø', 'å' ) 

Perkiraan kardinalitas yang salah seperti itu biasanya merupakan hal yang buruk. Dalam hal ini menghasilkan rencana yang berbeda bentuk (dan paralel) yang tampaknya (?) Bekerja lebih baik untuk Anda meskipun tumpahan hash yang disebabkan oleh perkiraan yang terlalu rendah.

Tanpa TRIMSQL Server dapat mengkonversi ini ke interval rentang dalam histogram kolom dasar dan memberikan perkiraan yang jauh lebih akurat tetapi dengan TRIMitu hanya resor untuk menebak.

Sifat tebakannya bisa beragam, tetapi perkiraan untuk satu predikat tunggal LEFT(TRIM(largeTbl.last_name), 1)dalam beberapa keadaan * hanya diperkirakan table_cardinality/estimated_number_of_distinct_column_values.

Saya tidak yakin persis keadaan apa - ukuran data tampaknya berperan. Saya dapat mereproduksi ini dengan tipe data panjang tetap yang lebar seperti di sini tetapi mendapat tebakan berbeda, lebih tinggi, dengan varchar(yang hanya menggunakan tebakan datar 10% dan diperkirakan 100.000 baris). @ Solomon Rutzky menunjukkan bahwa jika varchar(100)diisi dengan spasi tambahan seperti yang terjadi untuk charestimasi yang lebih rendah digunakan

The INdaftar diperluas ke ORdan SQL Server menggunakan backoff eksponensial dengan maksimum 4 predikat dipertimbangkan. Jadi 219.707perkiraannya adalah sebagai berikut.

DECLARE @TableCardinality FLOAT = 4008334, 
        @DistinctColumnValueEstimate FLOAT = 34207

DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)

SELECT @TableCardinality * ( 1 - (
@NotSelectivity * 
SQRT(@NotSelectivity) * 
SQRT(SQRT(@NotSelectivity)) * 
SQRT(SQRT(SQRT(@NotSelectivity)))
))
Martin Smith
sumber