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: VisitTarget
memiliki satu baris untuk setiap transaksi penjualan, dan VisitSale
memiliki 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:
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!