Bagaimana mengisyaratkan banyak-ke-banyak bergabung di SQL Server?

9

Saya memiliki 3 "besar" tabel yang bergabung pada sepasang kolom (keduanya int).

  • Table1 memiliki ~ 200 juta baris
  • Table2 memiliki ~ 1,5 juta baris
  • Table3 memiliki ~ 6 juta baris

Setiap tabel memiliki indeks berkerumun di Key1, Key2, dan kemudian satu kolom lagi. Key1memiliki kardinalitas rendah dan sangat miring. Itu selalu dirujuk dalam WHEREklausa. Key2tidak pernah disebutkan dalam WHEREklausa. Setiap bergabung adalah banyak-ke-banyak.

Masalahnya adalah dengan estimasi kardinalitas. Estimasi output masing-masing bergabung menjadi lebih kecil, bukan lebih besar . Ini menghasilkan estimasi akhir dari ratusan yang rendah ketika hasil aktual mencapai jutaan.

Apakah ada cara bagi saya untuk memberi petunjuk kepada CE agar membuat perkiraan yang lebih baik?

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;

Solusi yang saya coba:

  • Membuat statistik multi-kolom pada Key1,Key2
  • Membuat banyak statistik yang difilter aktif Key1(Ini membantu sedikit, tapi saya berakhir dengan ribuan statistik yang dibuat pengguna dalam database.)

Rencana eksekusi bertopeng (maaf atas masking buruk)

Dalam kasus yang saya lihat, hasilnya memiliki 9 juta baris. CE yang baru memperkirakan 180 baris; legacy CE memperkirakan 6100 baris.

Berikut ini contoh yang dapat direproduksi:

DROP TABLE IF EXISTS #Table1, #Table2, #Table3;
CREATE TABLE #Table1 (Key1 INT NOT NULL, Key2 INT NOT NULL, T1Key3 INT NOT NULL, CONSTRAINT pk_t1 PRIMARY KEY CLUSTERED (Key1, Key2, T1Key3));
CREATE TABLE #Table2 (Key1 INT NOT NULL, Key2 INT NOT NULL, T2Key3 INT NOT NULL, CONSTRAINT pk_t2 PRIMARY KEY CLUSTERED (Key1, Key2, T2Key3));
CREATE TABLE #Table3 (Key1 INT NOT NULL, Key2 INT NOT NULL, T3Key3 INT NOT NULL, CONSTRAINT pk_t3 PRIMARY KEY CLUSTERED (Key1, Key2, T3Key3));

-- Table1 
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2),
     DataSize (Key1, NumberOfRows)
     AS (SELECT 1, 2000 UNION
         SELECT 2, 10000 UNION
         SELECT 3, 25000 UNION
         SELECT 4, 50000 UNION
         SELECT 5, 200000)
INSERT INTO #Table1
SELECT Key1
     , Key2 = ROW_NUMBER() OVER (PARTITION BY Key1, T1Key3 ORDER BY Number)
     , T1Key3
FROM DataSize
     CROSS APPLY (SELECT TOP(NumberOfRows) 
                         Number
                       , T1Key3 = Number%(Key1*Key1) + 1 
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smaller number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table2
SELECT DISTINCT 
       Key1
     , Key2
     , T2Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1*10) 
                         T2Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smallest number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table3
SELECT DISTINCT 
       Key1
     , Key2
     , T3Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1) 
                         T3Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;


DROP TABLE IF EXISTS #a;
SELECT col = 1 
INTO #a
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
WHERE t1.Key1 = 1;

DROP TABLE IF EXISTS #b;
SELECT col = 1 
INTO #b
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN #Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;
Steven Hibble
sumber

Jawaban:

5

Supaya jelas, pengoptimal sudah tahu bahwa itu adalah banyak bergabung. Jika Anda memaksa gabungan bergabung dan melihat rencana yang diperkirakan, Anda bisa melihat properti untuk operator gabungan yang memberi tahu Anda jika gabungan itu bisa banyak-ke-banyak. Masalah yang perlu Anda selesaikan di sini adalah menumbuhkan perkiraan kardinalitas, mungkin sehingga Anda mendapatkan rencana kueri yang lebih efisien untuk bagian dari kueri yang Anda tinggalkan.

Hal pertama yang akan saya coba adalah memasukkan hasil join dari Object3dan Object5ke tabel temp. Untuk paket yang Anda poskan, itu hanya satu kolom pada 51393 baris, jadi tidak akan memakan banyak ruang di tempdb. Anda dapat mengumpulkan statistik lengkap di tabel temp dan itu saja mungkin cukup untuk mendapatkan perkiraan kardinalitas akhir yang cukup akurat. Mengumpulkan statistik lengkap tentang Object1dapat membantu juga. Perkiraan kardinalitas seringkali menjadi lebih buruk ketika Anda menelusuri dari rencana dari kanan ke kiri.

Jika itu tidak berhasil, Anda dapat mencoba ENABLE_QUERY_OPTIMIZER_HOTFIXESpetunjuk kueri jika Anda belum mengaktifkannya di tingkat database atau server. Microsoft mengunci perbaikan kinerja yang memengaruhi paket untuk SQL Server 2016 di belakang pengaturan itu. Beberapa di antaranya terkait dengan perkiraan kardinalitas, jadi mungkin Anda akan beruntung dan salah satu perbaikannya akan membantu dengan kueri Anda. Anda juga dapat mencoba menggunakan penaksir kardinalitas lama dengan FORCE_LEGACY_CARDINALITY_ESTIMATIONpetunjuk kueri. Kumpulan data tertentu mungkin mendapatkan taksiran yang lebih baik dengan legacy CE.

Sebagai upaya terakhir, Anda dapat secara manual meningkatkan perkiraan kardinalitas dengan faktor apa pun yang Anda suka menggunakan MANY()fungsi Adam Machanic . Saya membicarakannya di jawaban lain tetapi sepertinya tautannya sudah mati. Jika Anda tertarik, saya bisa mencoba menggali sesuatu.

Joe Obbish
sumber
make_parallelFungsi Adam digunakan untuk membantu mengurangi masalah. Saya akan melihat many. Sepertinya bantuan band yang cukup kotor.
Steven Hibble
2

Statistik SQL Server hanya berisi histogram untuk kolom terkemuka dari objek statistik. Oleh karena itu, Anda bisa membuat statistik yang difilter yang memberikan histogram nilai untuk Key2, tetapi hanya di antara baris dengan Key1 = 1. Membuat statistik yang difilter ini pada setiap tabel memperbaiki perkiraan dan mengarah ke perilaku yang Anda harapkan untuk kueri pengujian: setiap gabungan baru tidak memengaruhi perkiraan kardinalitas akhir (dikonfirmasi di SQL 2016 SP1 dan SQL 2017).

-- Note: Add "WITH FULLSCAN" to each if you want a perfect 20,000 row estimate
CREATE STATISTICS st_#Table1 ON #Table1 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table2 ON #Table2 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table3 ON #Table3 (Key2) WHERE Key1 = 1

Tanpa statistik yang difilter ini, SQL Server akan mengambil pendekatan berbasis heuristik untuk memperkirakan kardinalitas bergabung Anda. Papan tulis berikut berisi deskripsi tingkat tinggi yang baik dari beberapa heuristik yang digunakan SQL Server: Mengoptimalkan Rencana Kueri Anda dengan Penaksir Kardinalitas SQL Server 2014 .

Misalnya, menambahkan USE HINT('ASSUME_JOIN_PREDICATE_DEPENDS_ON_FILTERS')petunjuk ke kueri Anda akan mengubah heuristik kontainment join untuk mengasumsikan beberapa korelasi (daripada independensi) antara Key1predikat dan Key2predikat join, yang mungkin bermanfaat untuk kueri Anda. Untuk kueri pengujian akhir, petunjuk ini meningkatkan perkiraan kardinalitas dari 1,175menjadi 7,551, tetapi masih agak malu dengan 20,000perkiraan baris yang benar yang dihasilkan dengan statistik yang difilter.

Pendekatan lain yang kami gunakan dalam situasi yang serupa adalah mengekstraksi subset data yang relevan ke tabel #temp. Apalagi sekarang versi SQL Server yang lebih baru tidak lagi bersemangat menulis tabel #temp ke disk , kami sudah mendapatkan hasil yang baik dengan pendekatan ini. Deskripsi Anda tentang banyak-ke-banyak Anda bergabung menyiratkan bahwa setiap tabel #temp individu dalam kasus Anda akan relatif kecil (atau setidaknya lebih kecil dari hasil akhir yang ditetapkan), sehingga pendekatan ini mungkin patut dicoba.

DROP TABLE IF EXISTS #Table1_extract, #Table2_extract, #Table3_extract, #c
-- Extract only the subset of rows that match the filter predicate
-- (Or better yet, extract only the subset of columns you need!)
SELECT * INTO #Table1_extract FROM #Table1 WHERE Key1 = 1
SELECT * INTO #Table2_extract FROM #Table2 WHERE Key1 = 1
SELECT * INTO #Table3_extract FROM #Table3 WHERE Key1 = 1
-- Now perform the join on those extracts, removing the filter predicate
SELECT col = 1
INTO #c 
FROM #Table1_extract t1
JOIN #Table2_extract t2
    ON t1.Key2 = t2.Key2
JOIN #Table3_extract t3
    ON t1.Key2 = t3.Key2
Geoff Patterson
sumber
Kami menggunakan statistik yang difilter secara luas, tetapi kami membuatnya satu per Key1nilai pada setiap tabel. Kami sekarang memiliki ribuan dari mereka.
Steven Hibble
2
@StevenHibble Poin bagus bahwa ribuan statistik yang difilter dapat membuat manajemen sulit. (Kami juga telah melihat bahwa ini berdampak negatif pada waktu kompilasi paket.) Mungkin tidak cocok dengan kasus penggunaan Anda, tetapi saya juga menambahkan pendekatan tabel #temp lain yang telah kami gunakan dengan sukses beberapa kali.
Geoff Patterson
-1

Sebuah jangkauan. Tidak ada dasar nyata selain mencoba.

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key2 = t2.Key2
      AND t1.Key1 = 1
      AND t2.Key1 = 1
     JOIN Table3 t3
       ON t2.Key2 = t3.Key2
      AND t3.Key1 = 1;
paparazzo
sumber