Hash join antara tabel master / detail menghasilkan perkiraan kardinalitas terlalu rendah


9

Ketika bergabung dengan tabel master ke tabel detail, bagaimana saya bisa mendorong SQL Server 2014 untuk menggunakan perkiraan kardinalitas dari tabel (detail) yang lebih besar sebagai perkiraan kardinalitas dari output gabungan?

Misalnya, ketika bergabung dengan baris induk 10K ke baris detail 100K, saya ingin SQL Server memperkirakan gabungan pada 100K baris - sama dengan perkiraan jumlah baris detail. Bagaimana saya harus menyusun kueri dan / atau tabel dan / atau indeks saya untuk membantu penaksir SQL Server memanfaatkan fakta bahwa setiap baris detail selalu memiliki baris master yang sesuai? (Artinya gabungan antara mereka tidak boleh mengurangi perkiraan kardinalitas.)

Ini lebih detailnya. Basis data kami memiliki pasangan master / detail tabel: VisitTargetmemiliki satu baris untuk setiap transaksi penjualan, dan VisitSalememiliki satu baris untuk setiap produk dalam setiap transaksi. Ini adalah hubungan satu-ke-banyak: satu baris TargetTarget untuk rata-rata 10 baris VisitSale.

Tabelnya terlihat seperti ini: (Saya menyederhanakan hanya kolom yang relevan untuk pertanyaan ini)

-- "master" table
CREATE TABLE VisitTarget
(
  VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  SaleDate date NOT NULL,
  StoreId int NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate 
    ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);

-- "detail" table
CREATE TABLE VisitSale
(
  VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  VisitTargetId int NOT NULL,
  SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
  StoreId int NOT NULL, -- denormalized; copied from VisitTarget
  ItemId int NOT NULL,
  SaleQty int NOT NULL,
  SalePrice decimal(9,2) NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate 
  ON VisitSale (SaleDate)
  INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale 
  WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId 
  FOREIGN KEY (VisitTargetId)
  REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
  CHECK CONSTRAINT FK_VisitSale_VisitTargetId;

Untuk alasan kinerja, kami telah sebagian dinetralisasi dengan menyalin kolom pemfilteran yang paling umum (mis. SaleDate) Dari tabel master ke setiap baris tabel detail, dan kemudian kami menambahkan indeks penutup pada kedua tabel untuk mendukung kueri yang difilter dengan tanggal dengan lebih baik. Ini bekerja sangat baik untuk mengurangi I / O saat menjalankan query tanggal-difilter, tapi saya pikir pendekatan ini menyebabkan masalah estimasi kardinalitas ketika bergabung dengan tabel master dan detail bersama-sama.

Saat kami bergabung dengan dua tabel ini, kueri terlihat seperti ini:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt 
    JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
    vs.SaleDate BETWEEN '20170101' and '20171231'
    and vt.SaleDate BETWEEN '20170101' and '20171231'
    -- more filtering goes here, e.g. by store, by product, etc. 

Filter tanggal pada tabel detail ( VisitSale) berlebihan. Itu ada di sana untuk mengaktifkan berurutan I / O (alias Index Seek operator) pada tabel detail untuk kueri yang difilter menurut rentang tanggal.

Rencana untuk jenis pertanyaan ini terlihat seperti ini:

masukkan deskripsi gambar di sini

Rencana aktual kueri dengan masalah yang sama dapat ditemukan di sini .

Seperti yang Anda lihat, estimasi kardinalitas untuk gabungan (tooltip di kiri bawah dalam gambar) lebih dari 4x terlalu rendah: 2.1M sebenarnya vs 0.5M diperkirakan. Ini menyebabkan masalah kinerja (mis. Tumpah ke tempdb), terutama ketika kueri ini adalah subquery yang digunakan dalam kueri yang lebih kompleks.

Tetapi perkiraan jumlah baris untuk setiap cabang gabungan dekat dengan jumlah baris aktual. Setengah bagian atas dari gabungan adalah 100 ribu aktual vs 164 ribu yang diperkirakan. Setengah bagian bawah dari gabungan adalah 2.1M baris aktual vs. 3.7M diperkirakan. Distribusi ember hash juga terlihat bagus. Pengamatan ini menunjukkan kepada saya bahwa statistik OK untuk setiap tabel, dan bahwa masalahnya adalah estimasi kardinalitas gabungan.

Pada awalnya saya berpikir bahwa masalahnya adalah SQL Server mengharapkan kolom SaleDate di setiap tabel independen, padahal sebenarnya keduanya identik. Jadi saya mencoba menambahkan perbandingan kesetaraan untuk tanggal Penjualan ke kondisi bergabung atau klausa WHERE, misalnya

ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate

atau

WHERE vt.SaleDate = vs.SaleDate

Ini tidak berhasil. Bahkan membuat perkiraan kardinalitas lebih buruk! Jadi baik SQL Server tidak menggunakan petunjuk kesetaraan atau yang lain adalah akar penyebab masalah.

Punya ide untuk memecahkan masalah dan semoga memperbaiki masalah estimasi kardinalitas ini? Tujuan saya adalah agar kardinalitas gabungan master / detail diperkirakan sama dengan estimasi untuk input yang lebih besar ("tabel detail") dari gabungan tersebut.

Jika itu penting, kami menjalankan SQL Server 2014 Enterprise SP2 CU8 build 12.0.5557.0 pada Windows Server. Tidak ada tanda jejak diaktifkan. Tingkat kompatibilitas basis data adalah SQL Server 2014. Kami melihat perilaku yang sama pada beberapa SQL Server yang berbeda, sehingga sepertinya tidak akan menjadi masalah khusus server.

Ada pengoptimalan di Pengukur Kardinalitas SQL Server 2014 yang persis perilaku yang saya cari:

CE baru, bagaimanapun, menggunakan algoritma yang lebih sederhana yang mengasumsikan bahwa ada asosiasi gabungan satu-ke-banyak antara tabel besar dan meja kecil. Ini mengasumsikan bahwa setiap baris dalam tabel besar sama persis dengan satu baris di tabel kecil. Algoritma ini mengembalikan perkiraan ukuran input yang lebih besar sebagai gabungan kardinalitas.

Idealnya saya bisa mendapatkan perilaku ini, di mana perkiraan kardinalitas untuk bergabung akan sama dengan perkiraan untuk tabel besar, meskipun meja "kecil" saya masih akan mengembalikan lebih dari 100 ribu baris!

Jawaban:


6

Dengan asumsi bahwa tidak ada peningkatan yang dapat diperoleh dengan melakukan sesuatu pada statistik atau menggunakan legacy CE, maka cara paling mudah untuk mengatasi masalah Anda adalah dengan mengubah Anda INNER JOINmenjadi LEFT OUTER JOIN:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitSale vs
    LEFT OUTER JOIN VisitTarget vt on vt.VisitTargetId = vs.VisitTargetId
            AND vt.SaleDate BETWEEN '20170101' and '20171231'
WHERE vs.SaleDate BETWEEN '20170101' and '20171231'

Jika Anda memiliki kunci asing di antara tabel, Anda selalu memfilter pada SaleDaterentang yang sama untuk kedua tabel, dan SaleDateselalu cocok di antara tabel maka hasil kueri Anda tidak boleh berubah. Mungkin aneh untuk menggunakan gabungan luar seperti ini, tetapi menganggapnya sebagai pengoptimal permintaan kueri bahwa bergabung dengan VisitTargettabel tidak akan pernah mengurangi jumlah baris yang dikembalikan oleh permintaan. Sayangnya, kunci asing tidak mengubah perkiraan kardinalitas sehingga terkadang Anda harus menggunakan trik seperti ini. (Saran Microsoft Connect: Buat estimasi pengoptimal lebih akurat dengan menggunakan metadata ).

Ada kemungkinan bahwa menulis kueri dalam formulir ini tidak akan berfungsi dengan baik tergantung pada apa lagi yang terjadi dalam kueri setelah bergabung. Anda bisa mencoba menggunakan tabel temp untuk menahan hasil antara dari hasil yang ditetapkan dengan perkiraan kardinalitas yang paling penting.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.