Penyertaan ORDER BY pada kueri yang tidak mengembalikan baris secara drastis memengaruhi kinerja

15

Dengan gabungan tiga tabel sederhana, kinerja permintaan berubah secara drastis saat ORDER BY disertakan bahkan tanpa baris yang dikembalikan. Skenario masalah aktual membutuhkan waktu 30 detik untuk mengembalikan nol baris tetapi instan saat ORDER BY tidak disertakan. Mengapa?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

Saya mengerti bahwa saya bisa memiliki indeks pada bigtable.smallGuidId, tapi, saya percaya itu akan membuat lebih buruk dalam kasus ini.

Berikut skrip untuk membuat / mengisi tabel untuk pengujian. Anehnya, tampaknya penting bahwa smalltable memiliki bidang nvarchar (max). Tampaknya juga penting bahwa saya bergabung di bigtable dengan guid (yang saya kira membuatnya ingin menggunakan pencocokan hash).

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

Saya sudah menguji SQL 2005, 2008 dan 2008R2 dengan hasil yang sama.

Hafthor
sumber

Jawaban:

32

Saya setuju dengan jawaban Martin Smith, tetapi masalahnya bukan hanya statistik, tepatnya. Statistik untuk kolom foreignId (dengan asumsi statistik otomatis diaktifkan) secara akurat menunjukkan bahwa tidak ada baris untuk nilai 3 (hanya ada satu, dengan nilai 7):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

output statistik

SQL Server tahu bahwa hal-hal mungkin telah berubah sejak statistik ditangkap, jadi mungkin ada baris untuk nilai 3 ketika paket dijalankan . Selain itu, jumlah waktu yang dapat dilewati antara kompilasi dan eksekusi rencana (bagaimanapun juga, rencana di-cache untuk digunakan kembali). Seperti kata Martin, SQL Server berisi logika untuk mendeteksi ketika modifikasi yang cukup telah dibuat untuk membenarkan kompilasi ulang setiap rencana cache untuk alasan optimalitas.

Namun, semua ini pada akhirnya tidak penting. Dengan satu pengecualian kasus tepi, optimizer tidak akan pernah memperkirakan jumlah baris yang dihasilkan oleh operasi tabel menjadi nol. Jika secara statis dapat menentukan bahwa output harus selalu nol baris, operasi itu berlebihan dan akan dihapus sepenuhnya.

Model pengoptimal bukannya memperkirakan minimal satu baris. Mempekerjakan heuristik ini cenderung menghasilkan rencana yang lebih baik daripada rata-rata jika estimasi yang lebih rendah dimungkinkan. Sebuah rencana yang menghasilkan estimasi nol-baris pada tahap tertentu tidak akan berguna sejak saat itu dalam aliran pemrosesan, karena tidak akan ada dasar untuk membuat keputusan berbasis biaya (nol baris adalah nol baris tidak peduli apa pun). Jika taksiran ternyata salah, bentuk rencana di atas taksiran nol baris hampir tidak ada kemungkinan masuk akal.

Faktor kedua adalah asumsi pemodelan lain yang disebut Asumsi Kontainmen. Ini pada dasarnya mengatakan bahwa jika kueri bergabung dengan rentang nilai dengan rentang nilai lain, itu karena rentang tumpang tindih. Cara lain untuk mengatakan ini adalah dengan mengatakan bahwa gabungan sedang ditentukan karena baris diharapkan akan dikembalikan. Tanpa alasan ini, biaya umumnya akan diremehkan, menghasilkan rencana yang buruk untuk berbagai pertanyaan umum.

Pada dasarnya, yang Anda miliki di sini adalah kueri yang tidak sesuai dengan model pengoptimal. Tidak ada yang bisa kita lakukan untuk 'meningkatkan' perkiraan dengan indeks multi-kolom atau yang difilter; tidak ada cara untuk mendapatkan perkiraan lebih rendah dari 1 baris di sini. Database nyata mungkin memiliki kunci asing untuk memastikan bahwa situasi ini tidak dapat muncul, tetapi dengan asumsi itu tidak berlaku di sini, kita dibiarkan menggunakan petunjuk untuk memperbaiki kondisi di luar model. Sejumlah pendekatan petunjuk yang berbeda akan bekerja dengan permintaan ini. OPTION (FORCE ORDER)adalah salah satu yang bekerja dengan baik dengan permintaan seperti yang tertulis.

Paul White Reinstate Monica
sumber
21

Masalah mendasar di sini adalah salah satu statistik.

Untuk kedua kueri, taksiran jumlah baris menunjukkan bahwa ia yakin final SELECTakan mengembalikan 1.048.580 baris (jumlah baris yang sama yang diperkirakan ada di bigtable) daripada 0 yang benar-benar terjadi.

Kedua JOINkondisi Anda cocok dan akan mempertahankan semua baris. Mereka akhirnya dihilangkan karena baris tunggal tinytabletidak sesuai dengan t.foreignId=3predikat.

Jika Anda berlari

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

dan lihat perkiraan jumlah baris 1daripada 0dan kesalahan ini menyebar sepanjang rencana. tinytablesaat ini berisi 1 baris. Statistik tidak akan dikompilasi ulang untuk tabel ini sampai terjadi modifikasi 500 baris sehingga baris yang cocok dapat ditambahkan dan tidak akan memicu kompilasi ulang.

Alasan mengapa Bergabung Pesanan berubah ketika Anda menambahkan ORDER BYklausa dan ada varchar(max)kolom di smalltableadalah karena memperkirakan bahwa varchar(max)kolom akan meningkatkan ukuran baris dengan rata-rata 4.000 byte. Kalikan itu dengan baris 1048580 dan itu berarti bahwa operasi semacam akan membutuhkan sekitar 4GB sehingga masuk akal memutuskan untuk melakukan SORToperasi sebelum JOIN.

Anda dapat memaksa ORDER BYkueri untuk mengadopsi ORDER BYstrategi tidak bergabung dengan menggunakan petunjuk seperti di bawah ini.

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

Rencana tersebut menunjukkan operator pengurutan dengan perkiraan biaya sub pohon 12,000perkiraan jumlah baris yang hampir dan salah serta taksiran ukuran data.

Rencana

BTW Saya tidak menemukan mengganti UNIQUEIDENTIFIERkolom dengan yang bilangan bulat mengubah hal-hal dalam pengujian saya.

Martin Smith
sumber
2

Aktifkan tombol Tampilkan Rencana Eksekusi Anda dan Anda dapat melihat apa yang terjadi. Inilah rencana untuk permintaan "lambat": masukkan deskripsi gambar di sini

Dan inilah pertanyaan "cepat": masukkan deskripsi gambar di sini

Lihat itu - jalankan bersama, permintaan pertama ~ 33x lebih "mahal" (rasio 97: 3). SQL mengoptimalkan kueri pertama untuk memesan BigTable berdasarkan datetime, kemudian menjalankan loop kecil "mencari" di atas SmallTable & TinyTable, mengeksekusi mereka 1 juta kali masing-masing (Anda dapat mengarahkan ikon "Clustered Index Seek" untuk mendapatkan lebih banyak statistik). Jadi, pengurutan (27%), dan 2 x 1 juta "mencari" pada tabel kecil (23% dan 46%) adalah bagian terbesar dari kueri mahal. Sebagai perbandingan, non- ORDER BYquery melakukan total 3 scan.

Pada dasarnya, Anda telah menemukan lubang dalam logika pengoptimal SQL untuk skenario khusus Anda. Tetapi seperti yang dinyatakan oleh TysHTTP, jika Anda menambahkan indeks (yang memperlambat insert Anda / memperbarui beberapa), pemindaian Anda menjadi gila dengan cepat.

jklemmack
sumber
2

Apa yang terjadi adalah SQL memutuskan untuk menjalankan pesanan sebelum pembatasan.

Coba ini:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

Ini memberi Anda kinerja yang ditingkatkan (dalam hal ini di mana jumlah hasil yang dikembalikan sangat kecil), tanpa benar-benar membuat kinerja hit dari menambahkan indeks lain. Meskipun aneh ketika pengoptimal SQL memutuskan untuk melakukan pesanan sebelum bergabung, kemungkinan karena jika Anda benar-benar mengembalikan data kemudian mengurutkannya setelah bergabung akan lebih lama daripada menyortir tanpa.

Terakhir, coba jalankan skrip berikut dan lihat apakah statistik dan indeks yang diperbarui memperbaiki masalah yang Anda alami:

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "
Seph
sumber