Saya punya pertanyaan seperti berikut:
DELETE FROM tblFEStatsBrowsers WHERE BrowserID NOT IN (
SELECT DISTINCT BrowserID FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID IS NOT NULL
)
tblFEStatsBrowsers telah mendapatkan 553 baris.
tblFEStatsPaperHits telah mendapat baris 47.974.301.
tblFEStatsBrowsers:
CREATE TABLE [dbo].[tblFEStatsBrowsers](
[BrowserID] [smallint] IDENTITY(1,1) NOT NULL,
[Browser] [varchar](50) NOT NULL,
[Name] [varchar](40) NOT NULL,
[Version] [varchar](10) NOT NULL,
CONSTRAINT [PK_tblFEStatsBrowsers] PRIMARY KEY CLUSTERED ([BrowserID] ASC)
)
tblFESatsPaperHits:
CREATE TABLE [dbo].[tblFEStatsPaperHits](
[PaperID] [int] NOT NULL,
[Created] [smalldatetime] NOT NULL,
[IP] [binary](4) NULL,
[PlatformID] [tinyint] NULL,
[BrowserID] [smallint] NULL,
[ReferrerID] [int] NULL,
[UserLanguage] [char](2) NULL
)
Ada indeks berkerumun di tblFEStatsPaperHits yang tidak termasuk BrowserID. Karena itu, melakukan query dalam akan membutuhkan pemindaian tabel penuh dari tblFEStatsPaperHits - yang sepenuhnya OK.
Saat ini, pemindaian penuh dieksekusi untuk setiap baris di tblFEStatsBrowsers, yang berarti saya telah mendapatkan 553 pemindaian tabel penuh dari tblFEStatsPaperHits.
Menulis ulang menjadi WHERE EXISTS tidak mengubah rencana:
DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
)
Namun, seperti yang disarankan oleh Adam Machanic, menambahkan opsi HASH JOIN menghasilkan rencana eksekusi yang optimal (hanya satu pemindaian tblFEStatsPaperHits):
DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
) OPTION (HASH JOIN)
Sekarang ini bukan pertanyaan bagaimana memperbaikinya - saya bisa menggunakan OPTION (HASH JOIN) atau membuat tabel temp secara manual. Saya lebih bertanya-tanya mengapa pengoptimal kueri akan menggunakan paket yang saat ini dilakukannya.
Karena QO tidak memiliki statistik pada kolom BrowserID, saya menduga itu mengasumsikan yang terburuk - 50 juta nilai yang berbeda, sehingga membutuhkan meja kerja dengan memori / tempdb yang cukup besar. Dengan demikian, cara teraman adalah melakukan pemindaian untuk setiap baris di tblFEStatsBrowsers. Tidak ada hubungan kunci asing antara kolom BrowserID di dua tabel, sehingga QO tidak dapat mengurangi informasi dari tblFEStatsBrowsers.
Apakah ini, sesederhana kedengarannya, alasannya?
Perbarui 1
Untuk memberikan beberapa statistik: OPSI (BERGABUNG
DENGAN HASH ): 208,711 bacaan logis (12 scan)
OPSI (BERGABUNG
DENGAN LOOP, HASH GROUP): 11.008.698 bacaan logis (~ pindai per BrowserID (339))
Tidak ada opsi:
11.008.775 pembacaan logis (~ scan per BrowserID (339))
Perbarui 2
Jawaban luar biasa, kalian semua - terima kasih! Sulit untuk memilih satu saja. Meskipun Martin adalah yang pertama dan Remus memberikan solusi yang sangat baik, saya harus memberikannya kepada Kiwi untuk mengetahui detailnya :)
sumber
Jawaban:
Dengan kata lain, pertanyaannya adalah mengapa rencana berikut terlihat paling murah bagi pengoptimal, dibandingkan dengan alternatif (yang ada banyak ).
Sisi dalam dari gabungan pada dasarnya menjalankan kueri dari formulir berikut untuk setiap nilai berkorelasi
BrowserID
:Perhatikan bahwa perkiraan jumlah baris adalah 185.220 (bukan 289.013 ) karena perbandingan kesetaraan tidak termasuk
NULL
(kecualiANSI_NULLS
adaOFF
). Perkiraan biaya rencana di atas adalah 206,8 unit.Sekarang mari kita tambahkan
TOP (1)
klausa:Perkiraan biaya sekarang 0,00452 unit. Penambahan operator fisik Top menetapkan tujuan baris 1 baris di operator Top. Pertanyaannya kemudian menjadi bagaimana memperoleh 'sasaran baris' untuk Pemindaian Indeks Berkelompok; yaitu, berapa banyak baris yang harus diproses oleh pemindaian sebelum satu baris cocok dengan
BrowserID
predikat?Informasi statistik yang tersedia menunjukkan 166
BrowserID
nilai berbeda (1 / [Semua Kepadatan] = 1 / 0,006024096 = 166). Biaya mengasumsikan bahwa nilai-nilai yang berbeda didistribusikan secara seragam di atas baris fisik, sehingga tujuan baris pada Pemindaian Indeks Cluster diatur ke 166,302 (memperhitungkan perubahan kardinalitas tabel sejak statistik sampel dikumpulkan).Perkiraan biaya pemindaian diharapkan 166 baris ini tidak sangat besar (bahkan dieksekusi 339 kali, sekali untuk setiap perubahan
BrowserID
) - menunjukkan Clustered Indeks Scan perkiraan biaya 1,3219 unit, menunjukkan efek skala dari tujuan baris. Biaya operator yang tidak dihitung untuk I / O dan CPU masing-masing ditampilkan sebagai 153.931 , dan 52.8698 :Dalam praktiknya, sangat tidak mungkin bahwa 166 baris pertama yang dipindai dari indeks (dalam urutan apa pun yang akan dikembalikan) akan berisi masing-masing nilai yang mungkin
BrowserID
. Namun demikian,DELETE
paket tersebut dihitung biayanya dengan total 1.40921 unit, dan dipilih oleh pengoptimal karena alasan itu. Bart Duncan menunjukkan contoh lain dari jenis ini dalam posting terbaru berjudul Row Goals Gone Rogue .Menarik juga untuk dicatat bahwa operator Top dalam rencana eksekusi tidak terkait dengan Anti Semi Join (khususnya Martin yang disebut "korsleting"). Kita dapat mulai melihat dari mana datangnya Top dengan terlebih dahulu menonaktifkan aturan eksplorasi yang disebut GbAggToConstScanOrTop :
Rencana itu memiliki perkiraan biaya 364.912 , dan menunjukkan bahwa Top mengganti Grup Dengan Agregat (pengelompokan berdasarkan kolom yang berkorelasi
BrowserID
). Agregat bukan karena redundanDISTINCT
dalam teks kueri: agregat yang dapat diperkenalkan oleh dua aturan eksplorasi, LASJNtoLASJNonDist dan LASJOnLclDist . Menonaktifkan keduanya juga menghasilkan rencana ini:Rencana itu memiliki perkiraan biaya 40729,3 unit.
Tanpa transformasi dari Group By ke Top, pengoptimal 'secara alami' memilih rencana bergabung hash dengan
BrowserID
agregasi sebelum anti bergabung:Dan tanpa batasan MAXDOP 1, rencana paralel:
Cara lain untuk 'memperbaiki' permintaan asli adalah dengan membuat indeks yang hilang pada
BrowserID
laporan rencana eksekusi. Loop bersarang bekerja paling baik ketika sisi bagian dalam diindeks. Memperkirakan kardinalitas untuk setengah bergabung adalah tantangan di saat terbaik. Tidak memiliki pengindeksan yang tepat (tabel besar bahkan tidak memiliki kunci unik!) Tidak akan membantu sama sekali.Saya menulis lebih banyak tentang ini di Row Goals, Bagian 4: The Anti Join Anti Pattern .
sumber
Ketika saya menjalankan skrip Anda untuk membuat basis data hanya statistik dan kueri dalam pertanyaan saya mendapatkan paket berikut.
Tabel Kardinalitas yang ditunjukkan dalam rencana adalah
tblFEStatsPaperHits
:48063400
tblFEStatsBrowsers
:339
Jadi diperkirakan perlu melakukan pemindaian
tblFEStatsPaperHits
sebanyak 339 kali. Setiap pemindaian memiliki predikat berkorelasitblFEStatsBrowsers.BrowserID=tblFEStatsPaperHits.BrowserID AND tblFEStatsPaperHits.BrowserID IS NOT NULL
yang didorong ke dalam operator pemindaian.Rencana itu tidak berarti bahwa akan ada 339 pemindaian penuh. Karena berada di bawah operator anti semi join segera setelah baris pertama yang cocok pada setiap pemindaian ditemukan, ia dapat membuat hubungan pendek sisanya. Perkiraan biaya subtree untuk simpul ini adalah
1.32603
dan seluruh paket dihitung biayanya1.41337
.Untuk Gabung Hash itu memberikan rencana di bawah ini
Paket keseluruhan dihitung biayanya
418.415
(sekitar 300 kali lebih mahal daripada paket loop bersarang) dengan pemindaian indeks berkerumun tunggal penuh dengantblFEStatsPaperHits
biaya206.8
saja. Bandingkan ini dengan1.32603
perkiraan untuk 339 pemindaian parsial yang diberikan sebelumnya (Biaya pemindaian parsial rata-rata =0.003911592
).Jadi ini akan menunjukkan bahwa setiap pemindaian parsial biayanya 53.000 kali lebih murah daripada pemindaian penuh. Jika penetapan biaya harus linier dengan jumlah baris maka itu berarti bahwa diasumsikan bahwa rata-rata hanya perlu memproses 900 baris pada setiap iterasi sebelum menemukan baris yang cocok dan dapat membuat hubungan pendek.
Saya tidak berpikir penetapan biaya skala dalam cara linier itu. Saya pikir mereka juga memasukkan beberapa elemen biaya startup tetap. Mencoba berbagai nilai
TOP
dalam kueri berikut147
memberikan perkiraan biaya subtree terdekat ke0.003911592
pada0.0039113
. Either way jelas bahwa itu mendasarkan penetapan biaya pada asumsi bahwa setiap pemindaian hanya perlu memproses sebagian kecil dari tabel, dalam urutan ratusan baris daripada jutaan.Saya tidak yakin matematika apa yang mendasari asumsi ini dan itu tidak benar-benar cocok dengan perkiraan jumlah baris di sisa rencana (236 estimasi baris yang keluar dari loop bersarang bergabung akan menyiratkan bahwa ada 236 kasus di mana tidak ada baris yang cocok ditemukan sama sekali dan pemindaian penuh diperlukan). Saya berasumsi ini hanya kasus di mana asumsi pemodelan yang dibuat jatuh agak dan meninggalkan rencana loop bersarang secara signifikan di bawah biaya.
sumber
Dalam buku saya, bahkan satu pemindaian baris 50M tidak dapat diterima ... Trik saya yang biasa adalah untuk mewujudkan nilai yang berbeda dan mendelegasikan mesin dengan menjaganya agar tetap terbaru:
Ini memberi Anda indeks terwujud satu baris per BrowserID, sehingga tidak perlu memindai 50 juta baris. Mesin akan mempertahankannya untuk Anda dan QO akan menggunakannya 'apa adanya' dalam pernyataan yang Anda poskan (tanpa petunjuk atau permintaan penulisan ulang).
Kelemahannya tentu saja pertengkaran. Setiap operasi menyisipkan atau menghapus di
tblFEStatsPaperHits
(dan saya kira adalah tabel logging dengan sisipan berat) harus membuat serial akses ke BrowserID yang diberikan. Ada beberapa cara yang membuat ini bisa dilakukan (pembaruan tertunda, 2 tahapan logging dll) jika Anda bersedia membelinya.sumber