Mengapa menambahkan TOP 1 secara dramatis memperburuk kinerja?

39

Saya punya pertanyaan yang cukup sederhana

SELECT TOP 1 dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Itu memberi saya kinerja yang mengerikan (seperti tidak pernah repot-repot menunggu sampai selesai). Rencana kueri terlihat seperti ini:

masukkan deskripsi gambar di sini

Namun jika saya menghapus TOP 1saya mendapatkan paket yang terlihat seperti ini dan itu berjalan dalam 1-2 detik:

masukkan deskripsi gambar di sini

PK & pengindeksan yang benar di bawah ini.

Fakta bahwa TOP 1perubahan rencana kueri tidak mengejutkan saya, saya hanya sedikit terkejut bahwa itu membuatnya jauh lebih buruk.

Catatan: Saya sudah membaca hasil dari posting ini dan memahami konsep Row Goaldll. Yang saya ingin tahu adalah bagaimana saya bisa mengubah kueri sehingga menggunakan rencana yang lebih baik. Saat ini saya sedang membuang data ke tabel temp lalu menarik baris pertama darinya. Saya bertanya-tanya apakah ada metode yang lebih baik.

Edit Untuk orang-orang yang membaca ini setelah fakta di sini ada beberapa informasi tambahan.

  • Document_Queue - PK / CI adalah D_ID dan memiliki ~ 5k baris.
  • Correspondence_Journal - PK / CI adalah FILE_NUMBER, CORRESPONDENCE_ID dan memiliki ~ 1,4 juta baris.

Ketika saya mulai, tidak ada indeks lain. Saya berakhir dengan satu di Correspondence_Journal (Document_Id, File_Number)

Kenneth Fisher
sumber
1
Apakah Anda memiliki batasan kunci asing yang memberlakukan DOCUMENT_IDhubungan antara dua tabel (atau apakah setiap catatan CORRESPONDENCE_JOURNALmemiliki catatan yang cocok di DOCUMENT_QUEUE)?
Daniel Hutmacher

Jawaban:

28

Coba paksakan hash gabung *

SELECT TOP 1 
       dc.DOCUMENT_ID,
       dc.COPIES,
       dc.REQUESTOR,
       dc.D_ID,
       cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
INNER HASH JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
       AND dc.QUEUE_DATE <= GETDATE()
       AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Pengoptimal mungkin mengira loop akan lebih baik dengan top 1 dan itu masuk akal tetapi pada kenyataannya itu tidak bekerja di sini. Coba tebak di sini, tetapi mungkin perkiraan biaya gulungan itu tidak aktif - menggunakan TEMPDB - Anda mungkin memiliki TEMPDB yang berkinerja buruk.


* Berhati-hatilah dengan petunjuk bergabung , karena mereka memaksa urutan akses tabel rencana agar sesuai dengan urutan tertulis dari tabel dalam kueri (sama seperti jika OPTION (FORCE ORDER)telah ditentukan). Dari tautan dokumentasi:

Ekstrak BOL

Ini mungkin tidak menghasilkan efek yang tidak diinginkan dalam contoh, tetapi secara umum, mungkin sangat baik. FORCE ORDER(tersirat atau eksplisit) adalah petunjuk yang sangat kuat yang melampaui penegakan ketertiban; itu mencegah berbagai teknik pengoptimal diterapkan, termasuk agregasi parsial dan pemesanan ulang.

Sebuah OPTION (HASH JOIN) permintaan petunjuk mungkin kurang mengganggu dalam kasus yang cocok, karena ini tidak berarti FORCE ORDER. Namun, itu berlaku untuk semua gabungan dalam kueri. Solusi lain tersedia.

paparazzo
sumber
1
Sepertinya jawaban yang benar dan satu-satunya perbedaan antara itu dan rencana yang lebih sederhana adalah Sort tambahan di bagian depan.
Kenneth Fisher
3
Tidak yakin saya suka jawaban ini. Petunjuk bergabung sangat invasif. Beberapa perubahan pengindeksan sederhana harus dicoba terlebih dahulu, misalnya indeks pada kolom tanggal.
usr
@ usr Ini adalah join PK sederhana yang berjalan dalam waktu kurang dari satu detik. Taruhan yang cukup aman di sini.
paparazzo
4
Dalam memaksa hash bergabung, Anda memaksa pemindaian tabel besar. Ada opsi yang lebih baik.
Rob Farley
30

Karena Anda mendapatkan paket yang benar ORDER BY, mungkin Anda bisa memutar TOPoperator sendiri ?

SELECT DOCUMENT_ID, COPIES, REQUESTOR, D_ID, FILE_NUMBER
FROM (
    SELECT dc.DOCUMENT_ID,
           dc.COPIES,
           dc.REQUESTOR,
           dc.D_ID,
           cj.FILE_NUMBER,
           ROW_NUMBER() OVER (ORDER BY cj.FILE_NUMBER) AS _rownum
    FROM DOCUMENT_QUEUE dc
    INNER JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
    WHERE dc.QUEUE_DATE <= GETDATE()
      AND dc.PRINT_LOCATION = 2
) AS sub
WHERE _rownum=1;

Dalam pikiran saya, rencana kueri untuk di ROW_NUMBER()atas harus sama dengan jika Anda memiliki ORDER BY. Rencana kueri sekarang harus memiliki Segmen, Proyek Urutan dan akhirnya operator Filter, sisanya harus seperti rencana bagus Anda.

Daniel Hutmacher
sumber
3
Sebenarnya sementara itu memang memberikan operator teratas (dan banyak hal lainnya (proyek urutan, segmen, dan urut)) masih berjalan subsecond. Saya akan memberikan jawaban yang benar untuk @ frisbee karena ini adalah yang pertama dan lebih sederhana. Jawaban yang bagus.
Kenneth Fisher
10
@KennethFisher, jawaban frisbee lebih sederhana, tetapi cara sledgehammer menggerakkan paku akhir lebih sederhana dari palu pembingkaian standar. Itu juga datang dengan banyak risiko, terutama jika dibiarkan untuk jangka panjang. Saya tidak akan menggunakan petunjuk seperti itu kecuali dalam pengujian atau mungkin, mungkin pengecualian pinggiran.
Steve Mangiameli
@SteveMangiameli Dalam kasus khusus ini hanya ada satu yang bergabung sehingga sejumlah masalah hilang. Saya menyadari risiko menggunakan petunjuk bergabung (atau petunjuk permintaan). Saya hanya berpikir itu dibenarkan dalam kasus ini.
Kenneth Fisher
5
@KennethFisher Imo, risiko utama dari petunjuk permintaan adalah bahwa ketika data Anda tumbuh atau berubah, rencana permintaan yang Anda terapkan dapat menjadi lebih buruk daripada apa yang ditemukan oleh sistem sendiri. Anda telah melihat bagaimana kesalahan kecil dalam rencana dapat berdampak serius pada kinerja. Menggunakan petunjuk dalam produksi menyatakan, "Saya tahu rencana ini akan selalu, selalu menjadi yang terbaik karena saya sangat memahami perencana dan bagaimana data saya akan berperilaku selama masa permintaan ini dalam produksi." Saya belum pernah seyakin itu tentang kueri.
jpmc26
29

Sunting: +1 berfungsi dalam situasi ini karena ternyata itu FILE_NUMBERadalah versi string nol-empuk bilangan bulat. Solusi yang lebih baik di sini untuk string adalah menambahkan ''(string kosong), karena menambahkan nilai dapat mempengaruhi urutan, atau untuk angka untuk menambahkan sesuatu yang konstan tetapi berisi fungsi non-deterministik, seperti sign(rand()+1). Gagasan 'melanggar semacam itu' masih berlaku di sini, hanya saja metode saya tidak ideal.

+1

Tidak, maksudku aku tidak setuju dengan apa pun, maksudku itu sebagai solusi. Jika Anda mengubah kueri untuk ORDER BY cj.FILE_NUMBER + 1maka TOP 1akan berperilaku berbeda.

Anda lihat, dengan sasaran baris kecil di tempat untuk permintaan yang dipesan, sistem akan mencoba untuk mengkonsumsi data dalam rangka, untuk menghindari memiliki operator Urutkan. Ini juga akan menghindari membangun tabel hash, dengan memperkirakan bahwa mungkin tidak perlu melakukan terlalu banyak pekerjaan untuk menemukan baris pertama. Dalam kasus Anda, ini salah - dari ketebalan panah-panah itu, sepertinya ia harus mengkonsumsi banyak data untuk menemukan satu kecocokan.

Ketebalan panah-panah itu menunjukkan bahwa DOCUMENT_QUEUEtabel (DQ) Anda jauh lebih kecil dari CORRESPONDENCE_JOURNALtabel (CJ) Anda. Dan bahwa rencana terbaik sebenarnya adalah memeriksa melalui baris DQ sampai baris CJ ditemukan. Memang, itulah yang akan dilakukan oleh Pengoptimal Kueri (QO) jika tidak ada sial ORDER BYdi sana, itu didukung oleh indeks penutup pada CJ.

Jadi, jika Anda menjatuhkan ORDER BYsepenuhnya, saya berharap Anda akan mendapatkan rencana yang melibatkan Nested Loop, iterasi di atas baris di DQ, mencari ke CJ untuk memastikan baris ada. Dan dengan TOP 1, ini akan berhenti setelah satu baris ditarik.

Tetapi jika Anda benar-benar membutuhkan baris pertama dalam FILE_NUMBERurutan, maka Anda dapat menipu sistem untuk mengabaikan indeks yang tampaknya (salah) sangat membantu, dengan melakukan ORDER BY CJ.FILE_NUMBER+1- yang kita tahu akan menjaga urutan yang sama seperti sebelumnya, tetapi yang penting QO tidak. QO akan fokus untuk menyelesaikan keseluruhan, sehingga operator Sort N Top dapat puas. Metode ini harus menghasilkan rencana yang berisi operator Compute Scalar untuk menghitung nilai pemesanan, dan operator Sort N Top untuk mendapatkan baris pertama. Tetapi di sebelah kanan ini, Anda harus melihat Nested Loop yang bagus, melakukan banyak hal di CJ. Dan kinerja yang lebih baik daripada berlari melalui tabel besar baris yang tidak cocok dengan apa pun di DQ.

Pertandingan Hash tidak selalu buruk, tetapi jika rangkaian baris yang Anda kembali dari DQ jauh lebih kecil dari CJ (seperti yang saya perkirakan), maka Pertandingan Hash akan memindai lebih banyak CJ dari yang dibutuhkan.

Catatan: Saya menggunakan +1 sebagai ganti +0 karena pengoptimal kueri cenderung mengenali bahwa +0 tidak mengubah apa pun. Tentu saja, hal yang sama mungkin berlaku untuk +1, jika tidak sekarang, maka di beberapa titik di masa mendatang.

Rob Farley
sumber
7

Saya telah membaca hasil dari posting ini dan memahami konsep Row Goal dll. Yang saya ingin tahu adalah bagaimana saya bisa mengubah kueri sehingga menggunakan rencana yang lebih baik

Menambahkan OPTION (QUERYTRACEON 4138)mematikan efek tujuan baris untuk kueri itu saja, tanpa terlalu menentukan tentang rencana akhir, dan mungkin akan menjadi cara paling sederhana / paling langsung.

Jika menambahkan petunjuk ini memberi Anda kesalahan izin (diperlukan untuk DBCC TRACEON), Anda bisa menerapkannya menggunakan panduan paket:

Menggunakan QUERYTRACEONpanduan dalam rencana oleh spaghettidba

... atau cukup gunakan prosedur tersimpan:

Izin Apa yang QUERYTRACEONDibutuhkan? oleh Kendra Little

Martin Smith
sumber
3

Versi SQL Server yang lebih baru menawarkan opsi yang berbeda (dan bisa dibilang lebih baik) untuk berurusan dengan kueri yang mendapatkan kinerja suboptimal ketika pengoptimal mampu menerapkan pengoptimalan sasaran baris. SQL Server 2016 SP1 memperkenalkan DISABLE_OPTIMIZER_ROWGOAL USE HINTyang memiliki efek yang sama dengan jejak flag 4138. Jika Anda tidak pada versi itu, Anda juga dapat mempertimbangkan menggunakan OPTIMIZE FORpetunjuk kueri untuk mendapatkan paket permintaan yang dirancang untuk mengembalikan semua baris, bukan hanya 1. Kueri di bawah ini akan mengembalikan hasil yang sama dengan yang ada di pertanyaan tetapi tidak akan dibuat dengan tujuan hanya mendapatkan 1 baris.

DECLARE @top INT = 1;

SELECT TOP (@top) dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
OPTION (OPTIMIZE FOR (@top = 987654321));
Joe Obbish
sumber
2

Karena Anda sedang melakukan TOP(1), saya sarankan untuk membuat ORDER BYdeterministik sebagai permulaan. Paling tidak ini akan memastikan hasil yang dapat diprediksi secara fungsional (selalu berguna untuk pengujian regresi). Sepertinya Anda perlu menambahkan DC.D_IDdan CJ.CORRESPONDENCE_IDuntuk itu.

Ketika melihat rencana kueri, kadang-kadang saya menemukan petunjuk untuk menyederhanakan kueri: Mungkin pilih semua baris dc yang relevan ke tabel temp di muka, untuk menghilangkan masalah dengan estimasi kardinalitas pada QUEUE_DATEdan PRINT_LOCATION. Ini harus cepat mengingat jumlah baris yang rendah. Anda kemudian dapat menambahkan indeks ke tabel temp ini jika perlu tanpa mengubah tabel permanen.

Simon Birch
sumber