Kinerja mengerikan bergabung dengan tabel INSERTED dan DELETED dalam sebuah pemicu

12

Saya punya pemicu UPDATE di atas meja yang mengawasi kolom tertentu yang berubah dari satu nilai tertentu ke nilai lainnya. Ketika ini terjadi, itu memperbarui beberapa data terkait di tabel lain melalui pernyataan UPDATE tunggal.

Hal pertama yang dilakukan pemicu adalah memeriksa untuk melihat apakah ada baris yang diperbarui memiliki nilai kolom ini berubah dari nilai yang dimaksud. Ia hanya bergabung dengan INSERTED ke DELETED dan membandingkan nilai di kolom itu. Jika tidak ada yang memenuhi syarat, itu ditebus lebih awal sehingga pernyataan UPDATE tidak berjalan.

IF NOT EXISTS (
    SELECT TOP 1 i.CUSTNMBR
    FROM INSERTED i
        INNER JOIN DELETED d
            ON i.CUSTNMBR = d.CUSTNMBR
    WHERE d.CUSTCLAS = 'Misc'
        AND i.CUSTCLAS != 'Misc'
)
    RETURN

Dalam hal ini, CUSTNMBR adalah kunci utama dari tabel yang mendasarinya. Jika saya melakukan pembaruan besar pada tabel ini (katakanlah, 5000+ baris), pernyataan ini membutuhkan AGES, bahkan jika saya belum menyentuh kolom CUSTCLAS. Saya bisa melihatnya berhenti pada pernyataan ini selama beberapa menit di Profiler.

Rencana eksekusi itu aneh. Ini menunjukkan Scan Dimasukkan dengan 3.714 eksekusi, dan ~ 18.5 juta baris output. Itu berjalan melalui filter pada kolom CUSTCLAS. Ini menggabungkan ini (melalui loop bersarang) ke Scan Dihapus (juga difilter pada CUSTCLAS), yang dieksekusi hanya sekali dan memiliki 5.000 baris output.

Apa hal bodoh yang saya lakukan di sini yang menyebabkan ini? Perhatikan bahwa pemicu harus benar-benar menangani pembaruan multi-baris.

EDIT :

Saya juga mencoba menulis seperti ini (kalau-kalau ada yang melakukan sesuatu yang tidak menyenangkan), tapi tetap saja mengerikan.

DECLARE @CUSTNMBR varchar(31)
SELECT TOP 1 @CUSTNMBR = i.CUSTNMBR
FROM INSERTED i
    INNER JOIN DELETED d
        ON i.CUSTNMBR = d.CUSTNMBR
WHERE d.CUSTCLAS = 'Misc'
    AND i.CUSTCLAS != 'Misc'

IF @CUSTNMBR IS NULL
    RETURN
db2
sumber
Bisakah Anda menyingkirkan "TOP 1"? Saya akan berpikir bahwa itu menyebabkan beberapa overhead yang mungkin tidak diperlukan jika Anda hanya memeriksa untuk melihat apakah ada satu kasus ...
JHFB

Jawaban:

10

Anda bisa mengevaluasi menggunakan eksplisit INNER MERGE JOINatau INNER HASH JOINpetunjuk tetapi mengingat bahwa Anda mungkin menggunakan tabel ini lagi nanti di pemicu Anda mungkin lebih baik hanya memasukkan konten inserteddan deletedtabel ke dalam #temptabel yang diindeks dan sedang dilakukan dengan itu.

Mereka tidak mendapatkan indeks berguna yang dibuat untuk mereka secara otomatis.

Martin Smith
sumber
Oke, ini mempercepatnya, namun ada potensi untuk memicu eksekusi cascading. Jika saya menggunakan nama tabel temp yang sama (#i, #d) di setiap pemicu, mereka konflik. Apakah ada solusi yang lebih baik / lebih aman daripada hanya menggunakan nama tabel temp yang berbeda di setiap pemicu?
db2
Dapat mengevaluasi menggunakan variabel tabel (dengan kunci utama yang ditentukan CUSTNMBRuntuk membuat indeks berkerumun unik) dan menggunakan OPTION (RECOMPILE)petunjuk untuk mendapatkannya dengan mempertimbangkan jumlah baris atau mungkin hanya menggunakan konvensi penamaan tertentu seperti#i_dbo_YourTable
Martin Smith
Saya pikir saya akan setuju untuk penamaan mereka seperti #trigger_name_i. Jika saya menggunakan variabel tabel, saya harus semakin mengacaukan kode dengan CREATE TABLEs eksplisit. Kami memiliki pemicu cascading, tetapi bukan pemicu rekursif, jadi saya pikir saya akan aman ...
db2
Saya merekomendasikan variabel tabel bukan tabel temp untuk tujuan ini; variabel tabel masih dapat memiliki indeks primer dan sekunder (unik), mereka secara otomatis dibersihkan ketika pemicu keluar dan variabel tabel dicakup hanya untuk eksekusi pemicu (itu tidak akan bertentangan dengan variabel tabel lainnya dengan nama yang sama lebih tinggi atau lebih rendah pada tumpukan panggilan). Untuk menghemat overhead kode definisi tabel, tentukan jenis tabel untuk masing-masing dan gunakan nama jenis untuk mendeklarasikan variabel tabel.
Chris Smith
@ ChrisSmith yang sering Anda perlukan juga OPTION (RECOMPILE)agar kardinalitas diperhitungkan.
Martin Smith
10

Saya tahu ini telah dijawab tetapi baru saja muncul sebagai baru-baru ini aktif dan saya telah mengalami ini juga untuk tabel dengan jutaan baris. Meskipun tidak mengabaikan jawaban yang diterima, setidaknya saya dapat menambahkan bahwa pengalaman saya menunjukkan bahwa faktor kunci dalam kinerja Pemicu ketika melakukan tes serupa (melihat apakah satu atau lebih kolom benar-benar memiliki nilai berubah) adalah apakah kolom tersebut atau tidak yang diuji sebenarnya adalah bagian dari UPDATEpernyataan itu. Saya menemukan bahwa membandingkan kolom antara tabel inserteddan deletedyang sebenarnya bukan bagian dari UPDATEpernyataan memberikan hambatan besar pada kinerja yang sebaliknya tidak ada jika bidang tersebut adalah bagian dariUPDATEpernyataan (terlepas dari nilai mereka sebenarnya sedang diubah). Mengapa semua pekerjaan itu (yaitu kueri untuk membandingkan bidang N di seluruh baris X) untuk menentukan apakah ada yang berubah jika Anda dapat secara logis mengesampingkan kemungkinan salah satu kolom tersebut diubah, yang jelas tidak mungkin jika tidak ada. dalam SETklausa UPDATEpernyataan.

Solusi yang saya gunakan adalah menggunakan fungsi UPDATE () yang hanya berfungsi di dalam Pemicu. Fungsi bawaan ini memberi tahu Anda jika kolom ditentukan dalam UPDATEpernyataan dan dapat digunakan untuk keluar dari Pemicu jika kolom yang Anda khawatirkan bukan bagian dari UPDATE. Ini dapat digunakan bersama dengan a SELECTuntuk menentukan apakah kolom-kolom itu, dengan asumsi bahwa mereka ada di UPDATE, memiliki perubahan yang sebenarnya. Saya memiliki kode di atas beberapa pemicu audit yang terlihat seperti:

-- exit on updates that do not update the only 3 columns we ETL
IF (
     EXISTS(SELECT 1 FROM DELETED) -- this is an UPDATE (Trigger is AFTER INSERT, UPDATE)
     AND (
            NOT (UPDATE(Column3) OR UPDATE(Column7)
                 OR UPDATE(Column11)) -- the columns we care about are not being updated
            OR NOT EXISTS(
                        SELECT 1
                        FROM INSERTED ins
                        INNER JOIN DELETED del
                                ON del.KeyField1 = ins.KeyField1
                                AND del.KeyField2 = ins.KeyField2
                        WHERE ins.Column3 <> del.Column3
                                 COLLATE Latin1_General_100_CS_AS -- case-sensitive compare
                        OR    ISNULL(ins.Column7, -99) <> 
                                 ISNULL(del.Column7, -99) -- NULLable INT field
                        OR    ins.[Column11] <> del.[Column11] -- NOT NULL INT field
                      )
          )
    )
BEGIN
    RETURN;
END;

Logika ini akan berlanjut ke pemicu lainnya jika:

  1. Operasi adalah INSERT
  2. Setidaknya salah satu bidang yang relevan ada di SETklausa UPDATE dan setidaknya satu dari kolom di satu baris telah berubah

The NOT (UPDATE...) OR NOT EXISTS()mungkin terlihat aneh atau mundur, tetapi dirancang untuk menghindari melakukan SELECTpada inserteddan deletedtabel jika tidak ada kolom yang relevan adalah bagian dari UPDATE.

Bergantung pada kebutuhan Anda, fungsi COLUMNS_UPDATED () adalah pilihan lain untuk menentukan kolom mana yang merupakan bagian dari UPDATEpernyataan.

Solomon Rutzky
sumber
1
Poin bagus bahwa mereka harus memeriksa UPDATE(CUSTCLAS)dan melewatkan semuanya jika salah (+1). Saya tidak berpikir Anda benar bahwa kolom yang tidak diperbarui tidak tersedia di versi baris seperti yang diperbarui.
Martin Smith
@ MartinSmith, bagaimana cara membuktikannya dengan cara apa pun? Meskipun, mungkin tidak masalah jika perilaku itu dapat diprediksi dengan cara yang saya temukan. Saya hanya tahu bahwa ini adalah perbedaan kinerja yang drastis dengan melakukan SELECT yang sama, BERGABUNG antara TERMASUK dan DIHAPUS, memeriksa bidang untuk perbedaan yang sebenarnya, tergantung pada apakah bidang di WHERE berada di SET UPDATE atau tidak. Perilaku yang saya lihat konsisten, maka teori saya, tetapi akan baik / menarik untuk mengetahui alasan sebenarnya. Saya menduga bahwa bidang yang tidak ada dalam SET harus kembali ke tabel dasar untuk nilainya.
Solomon Rutzky
Saya telah melihat struktur ini sebelumnya. Saya tidak ingat apakah saya menemukan cara yang baik untuk melakukannya atau saya hanya menggunakan sebuah mudah menemukan mampu tali dan pencarian yang melelahkan melalui tempdbdenganDBCC PAGE
Martin Smith
BAIK. Pada contoh dengan file tunggal berukuran minimal tempdbsaya baru saja mencoba skrip ini , menempelkan output ke notepad dan mencari "EEEEEE". Saya melihat output di screenshot di sini . Catat sebelum dan sesudah versi kedua kolom di kedua baris. Mungkin ada banyak cara yang lebih mudah tetapi cukup untuk tujuan saya di sini!
Martin Smith
Meskipun sebenarnya ada string EEEEEE panjang lainnya di tempdbhalaman tidak di sebelah BBBBBBatau DDDDDD. Mungkin harus melakukan penyelidikan lagi! Meskipun mungkin ini karena REPLICATEpanggilan.
Martin Smith
2

Saya mungkin mencoba menulis ulang menggunakan jika ada

IF EXISTS (SELECT TOP 1 i.CUSTNMBR     
            FROM INSERTED i         
            INNER JOIN DELETED d             
            ON i.CUSTNMBR = d.CUSTNMBR and d.custclass = 'Misc'  
            WHERE d.CUSTCLAS <>i.CUSTCLAS)    
BEGIN

--do your triggerstuff here
END
HLGEM
sumber
1

http://dave.brittens.org/blog/writing-well-behaved-triggers.html

Menurut Dave, Anda harus menggunakan tabel temp atau variabel tabel dengan indeks, karena tabel virtual INSERTED / DELETED tidak memilikinya. Jika Anda memiliki kemungkinan pemicu rekursif, maka Anda harus menggunakan variabel tabel untuk menghindari tabrakan nama.

Semoga seseorang menemukan ini bermanfaat karena pos aslinya beberapa waktu yang lalu ...

Keith
sumber
-1

Kode berikut dapat meningkatkan kinerja pemicu ini. Saya tidak tahu tipe data yang benar dari kolom [custclass] sehingga Anda perlu menyesuaikannya.

DECLARE @i AS TABLE (CUSTNMBR VARCHAR(31) NOT NULL PRIMARY KEY, custclass VARCHAR(10) NOT NULL)
DECLARE @d AS TABLE (CUSTNMBR VARCHAR(31) NOT NULL PRIMARY KEY, custclass VARCHAR(10) NOT NULL)
INSERT INTO @i SELECT CUSTNMBR, custclass FROM inserted
INSERT INTO @d SELECT CUSTNMBR, custclass FROM deleted
IF NOT EXISTS
  (SELECT * FROM @i AS i INNER JOIN @d AS d ON d.CUSTNMBR = i.CUSTNMBR
   WHERE i.custclass <> d.custclass) RETURN

Perhatikan bahwa Anda dapat memasukkan kolom tambahan dalam ini dalam salinan memori dari tabel yang dimasukkan dan dihapus jika Anda membutuhkannya dalam kode pemicu Anda. Kunci utama pada tabel ini akan sangat meningkatkan kinerja gabungan saat memperbarui lebih dari beberapa baris sekaligus. Semoga berhasil!

Dony
sumber