Cara mengoptimalkan kueri yang berjalan lambat di Nested Loops (Inner Join)

39

TL; DR

Karena pertanyaan ini terus mendapatkan pandangan, saya akan meringkasnya di sini sehingga pendatang baru tidak harus menderita sejarah:

JOIN table t ON t.member = @value1 OR t.member = @value2 -- this is slow as hell
JOIN table t ON t.member = COALESCE(@value1, @value2)    -- this is blazing fast
-- Note that here if @value1 has a value, @value2 is NULL, and vice versa

Saya menyadari ini mungkin bukan masalah semua orang, tetapi dengan menyoroti sensitivitas klausa ON, ini mungkin membantu Anda melihat ke arah yang benar. Bagaimanapun teks asli ada di sini untuk antropolog masa depan:

Teks asli

Pertimbangkan permintaan sederhana berikut (hanya 3 tabel yang terlibat)

    SELECT

        l.sku_id AS ProductId,
        l.is_primary AS IsPrimary,
        v1.category_name AS Category1,
        v2.category_name AS Category2,
        v3.category_name AS Category3,
        v4.category_name AS Category4,
        v5.category_name AS Category5

    FROM category c4
    JOIN category_voc v4 ON v4.category_id = c4.category_id and v4.language_code = 'en'

    JOIN category c3 ON c3.category_id = c4.parent_category_id
    JOIN category_voc v3 ON v3.category_id = c3.category_id and v3.language_code = 'en'

    JOIN category c2 ON c2.category_id = c3.category_id
    JOIN category_voc v2 ON v2.category_id = c2.category_id and v2.language_code = 'en'

    JOIN category c1 ON c1.category_id = c2.parent_category_id
    JOIN category_voc v1 ON v1.category_id = c1.category_id and v1.language_code = 'en'

    LEFT OUTER JOIN category c5 ON c5.parent_category_id = c4.category_id
    LEFT OUTER JOIN category_voc v5 ON v5.category_id = c5.category_id and v5.language_code = @lang

    JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
    (
        l.category_id = c4.category_id OR
        l.category_id = c5.category_id
    )

    WHERE c4.[level] = 4 AND c4.version_id = 5

Ini adalah permintaan yang cukup sederhana, satu-satunya bagian yang membingungkan adalah gabungan kategori terakhir, ini cara ini karena kategori level 5 mungkin atau mungkin tidak ada. Di akhir kueri saya mencari info kategori per ID produk (SKU ID), dan di situlah tabel_link yang sangat besar masuk. Akhirnya, tabel #Ids hanyalah tabel temp yang berisi 10'000 Id.

Ketika dieksekusi, saya mendapatkan rencana eksekusi aktual berikut:

Rencana Eksekusi Aktual

Seperti yang Anda lihat, hampir 90% dari waktu dihabiskan di Nested Loops (Inner Join). Berikut informasi tambahan tentang Nested Loops tersebut:

Nested Loops (Inner Join)

Perhatikan bahwa nama tabel tidak sama persis karena saya mengedit nama tabel kueri agar mudah dibaca, tetapi cukup mudah untuk mencocokkan (ads_alt_category = kategori). Apakah ada cara untuk mengoptimalkan permintaan ini? Juga perhatikan bahwa dalam produksi, tabel temp #Ids tidak ada, itu adalah Parameter yang Dihitung Tabel dari 10'000 Id yang sama diteruskan ke Prosedur Tersimpan.

Informasi tambahan:

  • indeks kategori pada category_id dan parent_category_id
  • indeks category_voc pada category_id, language_code
  • indeks category_link di sku_id, category_id

Edit (dipecahkan)

Seperti yang ditunjukkan oleh jawaban yang diterima, masalahnya adalah klausa OR di category_link BERGABUNG. Namun, kode yang disarankan dalam jawaban yang diterima sangat lambat, bahkan lebih lambat dari kode aslinya. Solusi yang jauh lebih cepat dan juga lebih bersih adalah dengan mengganti kondisi JOIN saat ini dengan yang berikut:

JOIN category_link l on l.sku_id IN (SELECT value FROM @p1) AND l.category_id = COALESCE(c5.category_id, c4.category_id)

Tweak menit ini adalah solusi tercepat, diuji terhadap gabungan ganda dari jawaban yang diterima dan juga diuji terhadap CROSS BERLAKU seperti yang disarankan oleh valverij.

Luis Ferrao
sumber
Kami perlu melihat sisa dari rencana kueri.
RBarryYoung
Hanya sebuah komentar: dengan itu banyak ketergantungan bergabung dengan kesalahan estimasi kardinalitas menjadi mungkin. Paling sering, kinerja permintaan digagalkan oleh kardinalitas yang terlalu rendah.
usr
Apakah rencana eksekusi membuat saran untuk indeks? Juga, jangan lupa bahwa Anda dapat mengatur kunci utama dan indeks pada tabel sementara Anda (info lebih lanjut di sini )
@rbarry Jika setelah mencoba solusi saat ini saya tidak mendapatkan apa-apa, saya akan memperbaiki pertanyaan
1
Bagaimana dengan menduplikasi kueri dengan UNION dan menyingkirkan OR

Jawaban:

17

Masalahnya muncul di bagian kode ini:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

ordalam kondisi join selalu mencurigakan. Satu saran adalah untuk membagi ini menjadi dua gabungan:

JOIN category_link l1 on l1.sku_id in (SELECT value FROM #Ids) and l1.category_id = cr.category_id
left outer join
category_link l1 on l2.sku_id in (SELECT value FROM #Ids) and l2.category_id = cr.category_id

Anda kemudian harus mengubah sisa kueri untuk menangani ini. . . coalesce(l1.sku_id, l2.sku_id)misalnya dalam selectklausa.

Gordon Linoff
sumber
Dengan jumlah penyaringan yang dilakukan pada yang tertentu bergabung, saya juga akan menguji mengubah JOINke CROSS APPLYdengan INberalih ke EXISTSdalam APPLY's WHEREklausa.
Terima kasih Gordon, saya akan menguji hal pertama ini di pagi hari. @Valverij, saya tidak terbiasa dengan cross apply, bisakah Anda menjelaskan solusi Anda lebih lanjut, mungkin dalam Jawaban yang tepat, jadi saya dapat memberikan suara jika ternyata itu adalah skenario tercepat?
3
Saya menerima jawaban ini karena itu adalah jawaban pertama yang mengarahkan saya ke masalah. Namun solusi yang disarankan sangat lambat, bahkan lebih lambat dari kode aslinya. Namun, mengetahui bahwa klausa OR adalah masalahnya, cukup menggantinya dengan ON l.category_id = ISNULL(c5.category_id, c4.category_idmelakukan trik.
Luis Ferrao
1
@LuisFerrao. . . Terima kasih atas informasi tambahannya. Penting untuk diketahui bahwa coalesce()mendorong pengoptimal ke arah yang benar.
Gordon Linoff
9

Seperti yang disebutkan pengguna lain, gabungan ini kemungkinan penyebabnya:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

Selain membaginya menjadi beberapa gabungan, Anda juga dapat mencoba a CROSS APPLY

CROSS APPLY (
    SELECT [some column(s)]
    FROM category_link x
    WHERE EXISTS(SELECT value FROM #Ids WHERE value = x.sku_id)
    AND (x.category_id = c4.category_id OR x.category_id = c5.category_id)        
) l

Dari tautan MSDN di atas:

Fungsi bernilai tabel bertindak sebagai input kanan dan ekspresi tabel luar bertindak sebagai input kiri. Input kanan dievaluasi untuk setiap baris dari input kiri dan baris yang dihasilkan digabungkan untuk hasil akhir .

Pada dasarnya, APPLYini seperti subquery yang memfilter rekaman di kanan terlebih dahulu, dan kemudian menerapkannya ke seluruh kueri Anda.

Artikel ini melakukan pekerjaan yang sangat baik untuk menjelaskan apa itu dan kapan menggunakannya: http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

Penting untuk dicatat, bagaimanapun, bahwa CROSS APPLYtidak selalu berkinerja lebih cepat daripada INNER JOIN. Dalam banyak situasi, mungkin akan hampir sama. Dalam kasus yang jarang terjadi, saya benar-benar melihatnya lebih lambat (sekali lagi, ini semua tergantung pada struktur tabel Anda dan permintaan itu sendiri).

Sebagai aturan umum, jika saya menemukan diri saya bergabung ke meja dengan pernyataan kondisional terlalu banyak, maka saya cenderung condong ke arah APPLY

Juga catatan yang menyenangkan: OUTER APPLYakan bertindak seperti aLEFT JOIN

Juga, harap perhatikan pilihan saya untuk digunakan EXISTSdaripada IN. Ketika melakukan INsubquery, ingatlah bahwa itu akan mengembalikan set seluruh hasil, bahkan setelah telah menemukan nilai Anda. Dengan EXISTS, meskipun, itu akan menghentikan subquery begitu menemukan kecocokan.

valverij
sumber
Saya menguji solusi ini secara menyeluruh. Saat Anda menulisnya, ini sangat lambat, tetapi Anda lupa menerapkan saran yang Anda gunakan untuk memulai pesan Anda. Mengganti AND x.cat = c4.cat OR x.cat = c5.catdengan x.cat = ISNULL(c5.cat, c4.cat)dan menyingkirkan klausa IN menjadikan ini solusi tercepat kedua, dan layak mendapat upvote, karena cukup informatif.
Luis Ferrao
Terima kasih. Baris IN sebenarnya tidak seharusnya ada di sana (tidak bisa memutuskan untuk menggunakan IN atau bertahan dengan OR), saya akan menghapusnya.
valverij