Query 100x lebih lambat di SQL Server 2014, Row Count Spool memperkirakan estimasi penyebabnya?

13

Saya memiliki kueri yang berjalan dalam 800 milidetik di SQL Server 2012 dan membutuhkan waktu sekitar 170 detik di SQL Server 2014 . Saya pikir saya mempersempit ini ke perkiraan kardinalitas yang buruk untuk Row Count Spooloperator. Saya sudah membaca sedikit tentang operator spool (misalnya, di sini dan di sini ), tetapi saya masih mengalami kesulitan memahami beberapa hal:

  • Mengapa kueri ini membutuhkan Row Count Spooloperator? Saya pikir itu tidak perlu untuk pembenaran, jadi pengoptimalan spesifik apa yang ingin disediakannya?
  • Mengapa SQL Server memperkirakan bahwa gabungan ke Row Count Spooloperator menghapus semua baris?
  • Apakah ini bug di SQL Server 2014? Jika demikian, saya akan mengajukan di Connect. Tapi saya ingin pemahaman yang lebih dalam dulu.

Catatan: Saya dapat menulis ulang kueri sebagai LEFT JOINatau menambahkan indeks ke tabel untuk mencapai kinerja yang dapat diterima di SQL Server 2012 dan SQL Server 2014. Jadi pertanyaan ini lebih lanjut tentang memahami kueri khusus ini dan merencanakannya secara mendalam dan lebih sedikit tentang cara mengucapkan kueri secara berbeda.


Permintaan lambat

Lihat Pastebin ini untuk skrip pengujian lengkap. Berikut adalah kueri pengujian khusus yang saya lihat:

-- Prune any existing customers from the set of potential new customers
-- This query is much slower than expected in SQL Server 2014 
SELECT *
FROM #potentialNewCustomers -- 10K rows
WHERE cust_nbr NOT IN (
    SELECT cust_nbr
    FROM #existingCustomers -- 1MM rows
)


SQL Server 2014: Perkiraan rencana kueri

SQL Server percaya bahwa Left Anti Semi Joinuntuk Row Count Spoolakan menyaring 10.000 baris ke 1 baris. Karena alasan ini, ia memilih a LOOP JOINuntuk gabungan selanjutnya #existingCustomers.

masukkan deskripsi gambar di sini


SQL Server 2014: Paket kueri yang sebenarnya

Seperti yang diharapkan (oleh semua orang kecuali SQL Server!), Row Count SpoolTidak menghapus baris apa pun. Jadi kita mengulang 10.000 kali ketika SQL Server diharapkan untuk mengulang hanya sekali.

masukkan deskripsi gambar di sini


SQL Server 2012: Perkiraan rencana kueri

Saat menggunakan SQL Server 2012 (atau OPTION (QUERYTRACEON 9481)dalam SQL Server 2014), Row Count Spooltidak mengurangi # estimasi baris dan hash bergabung dipilih, menghasilkan rencana yang jauh lebih baik.

masukkan deskripsi gambar di sini

LEFT JOIN menulis ulang

Untuk referensi, berikut adalah cara saya dapat menulis ulang kueri untuk mencapai kinerja yang baik di semua SQL Server 2012, 2014, dan 2016. Namun, saya masih tertarik pada perilaku spesifik kueri di atas dan apakah itu adalah bug dalam Pengestimasi Kardinalitas SQL Server 2014 yang baru.

-- Re-writing with LEFT JOIN yields much better performance in 2012/2014/2016
SELECT n.*
FROM #potentialNewCustomers n
LEFT JOIN (SELECT 1 AS test, cust_nbr FROM #existingCustomers) c
    ON c.cust_nbr = n.cust_nbr
WHERE c.test IS NULL

masukkan deskripsi gambar di sini

Geoff Patterson
sumber

Jawaban:

10

Mengapa kueri ini membutuhkan operator Row Count Spool? ... pengoptimalan spesifik apa yang ingin disediakannya?

The cust_nbrkolom dalam #existingCustomersadalah nullable. Jika benar-benar berisi nol, respons yang benar di sini adalah mengembalikan nol baris ( NOT IN (NULL,...) akan selalu menghasilkan set hasil kosong.).

Jadi kueri dapat dianggap sebagai

SELECT p.*
FROM   #potentialNewCustomers p
WHERE  NOT EXISTS (SELECT *
                   FROM   #existingCustomers e1
                   WHERE  p.cust_nbr = e1.cust_nbr)
       AND NOT EXISTS (SELECT *
                       FROM   #existingCustomers e2
                       WHERE  e2.cust_nbr IS NULL) 

Dengan spool rowcount di sana untuk menghindari keharusan mengevaluasi

EXISTS (SELECT *
        FROM   #existingCustomers e2
        WHERE  e2.cust_nbr IS NULL) 

Lebih dari sekali.

Sepertinya ini adalah kasus di mana perbedaan kecil dalam asumsi dapat membuat perbedaan kinerja yang cukup besar.

Setelah memperbarui satu baris seperti di bawah ini ...

UPDATE #existingCustomers
SET    cust_nbr = NULL
WHERE  cust_nbr = 1;

... kueri diselesaikan dalam waktu kurang dari satu detik. Baris diperhitungkan dalam versi aktual dan perkiraan paket sekarang hampir tepat.

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT *
FROM   #potentialNewCustomers
WHERE  cust_nbr NOT IN (SELECT cust_nbr
                        FROM   #existingCustomers 
                       ) 

masukkan deskripsi gambar di sini

Baris nol adalah keluaran seperti dijelaskan di atas.

Histogram Statistik dan ambang pembaruan otomatis dalam SQL Server tidak cukup granular untuk mendeteksi perubahan baris tunggal semacam ini. Dapat diperdebatkan jika kolom tersebut nullable mungkin masuk akal untuk bekerja atas dasar itu mengandung setidaknya satu NULLbahkan jika histogram statistik saat ini tidak menunjukkan bahwa ada.

Martin Smith
sumber
9

Mengapa kueri ini membutuhkan operator Row Count Spool? Saya pikir itu tidak perlu untuk pembenaran, jadi pengoptimalan spesifik apa yang ingin disediakannya?

Lihat jawaban menyeluruh Martin untuk pertanyaan ini. Poin kuncinya adalah bahwa jika satu baris dalam NOT INis NULL, logika boolean bekerja sedemikian rupa sehingga "respons yang benar adalah mengembalikan nol baris". The Row Count Spooloperator mengoptimalkan ini (diperlukan) logika.

Mengapa SQL Server memperkirakan bahwa bergabung dengan operator Row Count Spool menghapus semua baris?

Microsoft menyediakan kertas putih yang sangat baik tentang Penaksir Kardinalitas SQL 2014 . Dalam dokumen ini, saya menemukan informasi berikut:

CE baru mengasumsikan bahwa nilai-nilai yang dipertanyakan memang ada dalam dataset bahkan jika nilainya jatuh dari kisaran histogram. CE baru dalam contoh ini menggunakan frekuensi rata-rata yang dihitung dengan mengalikan kardinalitas tabel dengan kepadatan.

Seringkali, perubahan seperti itu sangat bagus; itu sangat meringankan masalah kunci naik dan biasanya menghasilkan rencana kueri yang lebih konservatif (perkiraan baris lebih tinggi) untuk nilai yang berada di luar kisaran berdasarkan histogram statistik.

Namun, dalam kasus khusus ini, dengan asumsi bahwa suatu NULLnilai akan ditemukan mengarah pada asumsi bahwa bergabung dengan Row Count Spoolakan menyaring semua baris dari #potentialNewCustomers. Dalam kasus di mana sebenarnya ada NULLderetan, ini adalah perkiraan yang benar (seperti yang terlihat dalam jawaban Martin). Namun, dalam kasus di mana kebetulan tidak ada NULLbaris, efeknya bisa sangat buruk karena SQL Server menghasilkan estimasi pasca-bergabung 1 baris terlepas dari berapa banyak baris input yang muncul. Ini dapat menyebabkan pilihan bergabung yang sangat buruk di sisa rencana kueri.

Apakah ini bug di SQL 2014? Jika demikian, saya akan mengajukan di Connect. Tapi saya ingin pemahaman yang lebih dalam dulu.

Saya pikir itu berada di daerah abu-abu antara bug dan asumsi yang berdampak pada kinerja atau pembatasan Cardinality Estimator baru SQL Server. Namun, kekhasan ini dapat menyebabkan regresi substansial dalam kinerja relatif terhadap SQL 2012 dalam kasus spesifik dari NOT INklausa nullable yang tidak memiliki NULLnilai.

Oleh karena itu, saya telah mengajukan masalah Connect sehingga tim SQL menyadari potensi implikasi dari perubahan ini ke Cardinality Estimator.

Pembaruan: Kami menggunakan CTP3 sekarang untuk SQL16, dan saya mengkonfirmasi bahwa masalah tidak terjadi di sana.

Geoff Patterson
sumber
5

Jawaban Martin Smith dan jawaban -diri Anda telah menjawab semua poin utama dengan benar, saya hanya ingin menekankan area untuk pembaca masa depan:

Jadi pertanyaan ini lebih tentang memahami permintaan khusus dan rencana ini secara mendalam dan lebih sedikit tentang bagaimana cara mengucapkan pertanyaan secara berbeda.

Tujuan kueri yang disebutkan adalah:

-- Prune any existing customers from the set of potential new customers

Persyaratan ini mudah diungkapkan dalam SQL, dalam beberapa cara. Yang mana yang dipilih adalah masalah gaya seperti yang lainnya, tetapi spesifikasi permintaan masih harus ditulis untuk mengembalikan hasil yang benar dalam semua kasus. Ini termasuk akuntansi untuk nol.

Mengekspresikan persyaratan logis sepenuhnya:

  • Kembalikan pelanggan potensial yang belum menjadi pelanggan
  • Daftarkan setiap pelanggan potensial paling banyak sekali
  • Kecualikan pelanggan potensial nol dan yang ada (apa pun artinya pelanggan nol)

Kami kemudian dapat menulis kueri yang cocok dengan persyaratan tersebut menggunakan sintaks mana pun yang kami inginkan. Sebagai contoh:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr NOT IN
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

Ini menghasilkan rencana eksekusi yang efisien, yang mengembalikan hasil yang benar:

Rencana eksekusi

Kami dapat menyatakan NOT INsebagai <> ALLatau NOT = ANYtanpa memengaruhi rencana atau hasil:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr <> ALL
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );
WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    NOT DPNNC.cust_nbr = ANY
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

Atau menggunakan NOT EXISTS:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE 
    NOT EXISTS
    (
        SELECT * 
        FROM #existingCustomers AS EC
        WHERE
            EC.cust_nbr = DPNNC.cust_nbr
            AND EC.cust_nbr IS NOT NULL
    );

Tidak ada keajaiban tentang ini, atau apa pun yang secara khusus tidak menyenangkan tentang penggunaan IN,, ANYatau ALL- kita hanya perlu menulis kueri dengan benar, sehingga akan selalu menghasilkan hasil yang benar.

Bentuk paling ringkas menggunakan EXCEPT:

SELECT 
    PNC.cust_nbr 
FROM #potentialNewCustomers AS PNC
WHERE 
    PNC.cust_nbr IS NOT NULL
EXCEPT
SELECT
    EC.cust_nbr 
FROM #existingCustomers AS EC
WHERE 
    EC.cust_nbr IS NOT NULL;

Ini menghasilkan hasil yang benar juga, meskipun rencana eksekusi mungkin kurang efisien karena tidak adanya pemfilteran bitmap:

Rencana eksekusi non-bitmap

Pertanyaan asli menarik karena memperlihatkan masalah yang mempengaruhi kinerja dengan implementasi pemeriksaan nol yang diperlukan. Inti dari jawaban ini adalah bahwa menulis kueri dengan benar juga menghindari masalah.

Paul White 9
sumber