Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

IGNORE_DUP_KEY lebih lambat pada indeks berkerumun

IGNORE_DUP_KEY opsi untuk indeks unik menentukan bagaimana SQL Server merespons upaya untuk INSERT nilai duplikat:Ini hanya berlaku untuk tabel (bukan tampilan) dan hanya untuk sisipan. Setiap bagian sisipan dari MERGE pernyataan mengabaikan IGNORE_DUP_KEY pengaturan indeks.

Ketika IGNORE_DUP_KEY adalah OFF , duplikat pertama yang ditemukan menghasilkan kesalahan , dan tidak ada baris baru yang disisipkan.

Ketika IGNORE_DUP_KEY adalah ON , baris yang disisipkan yang akan melanggar keunikan akan dibuang. Baris yang tersisa berhasil dimasukkan. Peringatan pesan dipancarkan alih-alih kesalahan:

Kunci duplikat diabaikan.

Ringkasan Artikel

IGNORE_DUP_KEY opsi indeks dapat ditentukan untuk indeks unik berkerumun dan tidak berkerumun. Menggunakannya pada indeks berkerumun dapat menghasilkan kinerja yang jauh lebih buruk daripada untuk indeks unik nonclustered.

Besar kecilnya perbedaan performa bergantung pada seberapa banyak pelanggaran keunikan yang ditemui selama INSERT operasi. Semakin banyak pelanggaran, semakin buruk kinerja indeks unik berkerumun sebagai perbandingan. Jika tidak ada pelanggaran sama sekali, penyisipan indeks berkerumun bahkan dapat bekerja lebih baik.

Sisipkan indeks unik yang dikelompokkan

Untuk indeks unik berkerumun dengan IGNORE_DUP_KEY diatur, duplikat ditangani oleh mesin penyimpanan .

Banyak pekerjaan yang terlibat dalam menyisipkan setiap baris dilakukan sebelum duplikat terdeteksi. Misalnya, Sisipkan Indeks Berkelompok operator menavigasi ke bawah indeks b-tree berkerumun ke titik di mana baris baru akan pergi, mengambil kait halaman dan hierarki kunci biasa, sebelum menemukan kunci duplikat.

Saat kondisi kunci duplikat terdeteksi, kesalahan dibangkitkan. Alih-alih membatalkan eksekusi dan mengembalikan kesalahan ke klien, kesalahan ditangani secara internal. Baris bermasalah tidak dimasukkan, dan eksekusi berlanjut, mencari baris berikutnya untuk disisipkan. Jika baris tersebut menemukan kunci duplikat, kesalahan lain akan muncul dan ditangani, dan seterusnya.

Pengecualian sangat mahal untuk melempar dan menangkap. Sejumlah besar duplikat akan sangat memperlambat eksekusi.

Insert indeks unik nonclustered

Untuk indeks unik nonclustered dengan IGNORE_DUP_KEY ditetapkan, duplikat ditangani oleh pemroses kueri . Duplikat terdeteksi, dan peringatan dikeluarkan, sebelum setiap penyisipan dicoba.

Pemroses kueri menghapus duplikat dari aliran penyisipan, memastikan bahwa tidak ada duplikat yang terlihat oleh mesin penyimpanan. Akibatnya, tidak ada kesalahan pelanggaran kunci unik yang muncul atau ditangani secara internal.

Trade-off

Ada trade-off antara biaya mendeteksi dan menghapus kunci duplikat dalam rencana eksekusi, versus biaya melakukan pekerjaan terkait penyisipan yang signifikan, dan membuang dan menangkap kesalahan saat duplikat ditemukan.

Jika duplikat diperkirakan sangat jarang , solusi mesin penyimpanan (indeks berkerumun) mungkin lebih efisien. Ketika duplikat kurang jarang, pendekatan prosesor kueri kemungkinan akan membayar dividen. Titik persilangan yang tepat akan bergantung pada faktor-faktor seperti efisiensi waktu proses dari komponen rencana eksekusi yang digunakan untuk mendeteksi dan menghapus duplikat.

Bagian selanjutnya dari artikel ini memberikan demo dan melihat lebih detail mengapa pendekatan mesin penyimpanan dapat berkinerja sangat buruk.

Demo

Skrip berikut membuat tabel sementara dengan sejuta baris. Ini memiliki 1.000 nilai unik dan 1.000 baris untuk setiap nilai unik. Kumpulan data ini akan digunakan sebagai sumber data untuk disisipkan ke dalam tabel dengan konfigurasi indeks yang berbeda.

DROP TABLE IF EXISTS #Data;
GO
CREATE TABLE #Data (c1 integer NOT NULL);
GO
SET NOCOUNT ON;
SET STATISTICS XML OFF;
 
DECLARE
    @Loop integer = 1,
    @N integer = 1;
 
WHILE @N <= 1000
BEGIN
    SET @Loop = 1;
 
    BEGIN TRANSACTION;
 
        -- Add 1,000 copies of the current loop value
        WHILE @Loop <= 50
        BEGIN
            INSERT #Data 
                (c1) 
            VALUES 
                (@N), (@N), (@N), (@N), (@N),
                (@N), (@N), (@N), (@N), (@N),
                (@N), (@N), (@N), (@N), (@N),
                (@N), (@N), (@N), (@N), (@N);
 
            SET @Loop += 1;
        END;
 
    COMMIT TRANSACTION;
 
    SET @N += 1;
END;
 
CREATE CLUSTERED INDEX cx 
ON #Data (c1) 
WITH (MAXDOP = 1);

Dasar

Penyisipan berikut ke dalam variabel tabel dengan indeks berkerumun non-unik membutuhkan waktu sekitar 900 md :

DECLARE @T table 
(
    c1 integer NOT NULL
        INDEX cuq CLUSTERED (c1)
);
 
INSERT @T 
    (c1) 
SELECT 
    D.c1 
FROM #Data AS D;

Perhatikan kurangnya IGNORE_DUP_KEY pada variabel tabel target.

Indeks unik yang dikelompokkan

Memasukkan data yang sama ke berkelompok yang unik indeks dengan IGNORE_DUP_KEY atur ON membutuhkan waktu sekitar 15.900 md — hampir 18 kali lebih buruk:

DECLARE @T table 
(
    c1 integer NOT NULL
        UNIQUE CLUSTERED 
        WITH (IGNORE_DUP_KEY = ON)
);
 
INSERT @T 
    (c1) 
SELECT 
    D.c1 
FROM #Data AS D;

Indeks unik nonclustered

Memasukkan data ke nonclustered unique yang unik indeks dengan IGNORE_DUP_KEY atur ON membutuhkan waktu sekitar 700 md :

DECLARE @T table 
(
    c1 integer NOT NULL
        UNIQUE NONCLUSTERED
        WITH (IGNORE_DUP_KEY = ON)
);
 
INSERT @T 
    (c1) 
SELECT 
    D.c1 
FROM #Data AS D;

Ringkasan kinerja

Tes dasar membutuhkan waktu 900 md untuk menyisipkan semua satu juta baris. Pengujian indeks nonclustered membutuhkan waktu 700 md untuk memasukkan hanya 1.000 kunci yang berbeda. Pengujian indeks berkerumun membutuhkan waktu 15.900 md untuk menyisipkan 1.000 baris unik yang sama.

Pengujian ini sengaja dibuat untuk menyoroti kinerja buruk dari implementasi mesin penyimpanan, dengan menghasilkan 999 unit kerja yang terbuang (latches, locks, error handling) untuk setiap baris yang berhasil.

Pesan yang dimaksud bukanlah IGNORE_DUP_KEY akan selalu berkinerja buruk pada indeks berkerumun, hanya saja mungkin, dan mungkin ada perbedaan besar antara indeks berkerumun dan tidak berkerumun.

Rencana Eksekusi Indeks Tergugus

Tidak banyak yang bisa dilihat dalam rencana penyisipan indeks berkerumun:

Ada 1.000.000 baris yang diteruskan ke Sisipkan Indeks Berkelompok operator, yang ditampilkan sebagai 'mengembalikan' 1.000 baris. Menggali detail rencana, kita dapat melihat:

  • 1.244.008 pembacaan logika pada operator penyisipan.
  • Sebagian besar waktu eksekusi dihabiskan di Sisipkan operator.
  • 11 md dari SOS_SCHEDULER_YIELD menunggu (yaitu tidak ada yang lain menunggu).

Tidak ada yang benar-benar menjelaskan 15.900 md waktu yang telah berlalu.

Mengapa performa sangat buruk

Jelas bahwa rencana ini harus melakukan banyak pekerjaan untuk setiap baris:

  • Menavigasi tingkat indeks b-tree berkerumun, mengunci dan mengunci saat berjalan, untuk menemukan titik penyisipan untuk record baru.
  • Jika salah satu halaman indeks yang diperlukan tidak ada dalam memori, halaman tersebut perlu diambil dari disk.
  • Buat baris b-tree baru di memori.
  • Siapkan catatan log.
  • Jika duplikat kunci ditemukan (bukan catatan hantu), angkat kesalahan, tangani kesalahan itu secara internal, lepaskan baris saat ini, dan lanjutkan pada titik yang sesuai dalam kode untuk memproses baris kandidat berikutnya.

Itu semua cukup banyak pekerjaan, dan ingat itu semua terjadi untuk setiap baris .

Bagian yang ingin saya fokuskan adalah peningkatan dan penanganan kesalahan, karena ini sangat mahal. Aspek lainnya yang disebutkan di atas sudah dibuat semurah mungkin dengan menggunakan variabel tabel dan tabel sementara dalam demo.

Pengecualian

Hal pertama yang ingin saya lakukan adalah menunjukkan bahwa Sisipkan Indeks Berkelompok operator benar-benar memunculkan pengecualian ketika menemukan kunci duplikat.

Salah satu cara untuk menunjukkan ini secara langsung adalah dengan melampirkan debugger dan menangkap jejak tumpukan pada titik pengecualian dilemparkan:

Poin penting di sini adalah melempar dan menangkap pengecualian sangat mahal.

Memantau SQL Server menggunakan Windows Performance Recorder saat pengujian berjalan, dan menganalisis hasilnya di Windows Performance Analyzer menunjukkan:

Hampir semua waktu eksekusi kueri dihabiskan di sqlmin!IndexDataSetSession::InsertRowInternal seperti yang diharapkan untuk kueri yang tidak melakukan banyak hal lain kecuali menyisipkan baris.

Kejutannya adalah bahwa 45% dari waktu tersebut dihabiskan untuk memunculkan pengecualian melalui sqlmin!RaiseDuplicateKeyException dan 47% lainnya dihabiskan di blok tangkap pengecualian terkait (ntdll!RcConsolidateFrames hierarki) .

Ringkasnya:Menaikkan dan menangkap pengecualian merupakan 92% dari waktu eksekusi dari kueri penyisipan indeks berkerumun pengujian kami.

Masalah pengumpulan data

Pembaca yang jeli mungkin melihat jumlah yang signifikan – sekitar 12% – pengecualian meningkatkan waktu yang dihabiskan di sqlmin!DumpKey dalam grafik Windows Performance Analyzer. Ini perlu ditelusuri dengan cepat, bersama dengan beberapa item terkait.

Sebagai bagian dari memunculkan pengecualian, SQL Server harus mengumpulkan beberapa data yang hanya tersedia pada saat kesalahan terjadi. Nomor kesalahan yang terkait dengan pengecualian kunci duplikat adalah 2627. Teks pesan di sys.messages untuk nomor kesalahan itu adalah:

Pelanggaran batasan %ls '%.*ls'. Tidak dapat menyisipkan kunci duplikat di objek '%.*ls'. Nilai kunci duplikat adalah %ls.

Informasi untuk mengisi penanda tempat itu perlu dikumpulkan pada saat kesalahan muncul — itu tidak akan tersedia nanti! Itu berarti mencari dan memformat jenis batasan, namanya, nama lengkap objek target, dan nilai kunci spesifik. Semua itu membutuhkan waktu.

Jejak tumpukan berikut menunjukkan server memformat nilai kunci duplikat sebagai string Unicode selama DumpKey hubungi:

Penanganan pengecualian juga melibatkan pengambilan jejak tumpukan:

SQL Server juga merekam informasi tentang pengecualian (termasuk bingkai tumpukan) dalam buffer cincin kecil, seperti yang ditunjukkan berikut ini:

Anda dapat melihat entri buffer cincin tersebut menggunakan perintah seperti:

SELECT TOP (10)
    date_time = 
        DATEADD
        (
            MILLISECOND, 
            DORB.[timestamp] - DOSI.ms_ticks, 
            SYSDATETIME()
        ),
    record = CONVERT(xml, DORB.record)
FROM sys.dm_os_ring_buffers AS DORB
CROSS JOIN sys.dm_os_sys_info AS DOSI
WHERE 
    DORB.ring_buffer_type = N'RING_BUFFER_EXCEPTION'
ORDER BY 
    DORB.[timestamp] DESC;

Contoh rekaman xml untuk pengecualian kunci duplikat berikut. Perhatikan bingkai tumpukan:

<Record id="4611442" type="RING_BUFFER_EXCEPTION" time="93079430">
  <Exception>
    <Task address="0x00000245B5E1FC28" />
    <Error>2627</Error>
    <Severity>14</Severity>
    <State>1</State>
    <UserDefined>0</UserDefined>
    <Origin>0</Origin>
  </Exception>
  <Stack>
    <frame id="0">0X00007FFAC659E80A</frame>
    <frame id="1">0X00007FFACBAC0EFD</frame>
    <frame id="2">0X00007FFACBAA1252</frame>
    <frame id="3">0X00007FFACBA9E040</frame>
    <frame id="4">0X00007FFACAB55D53</frame>
    <frame id="5">0X00007FFACAB55C06</frame>
    <frame id="6">0X00007FFACB3E3D0B</frame>
    <frame id="7">0X00007FFAC92020EC</frame>
    <frame id="8">0X00007FFACAB5B2FA</frame>
    <frame id="9">0X00007FFACABA3B9B</frame>
    <frame id="10">0X00007FFACAB3D89F</frame>
    <frame id="11">0X00007FFAC6A9D108</frame>
    <frame id="12">0X00007FFAC6AB2BBF</frame>
    <frame id="13">0X00007FFAC6AB296F</frame>
    <frame id="14">0X00007FFAC6A9B7D0</frame>
    <frame id="15">0X00007FFAC6A9B233</frame>
  </Stack>
</Record>

Semua pekerjaan latar belakang ini terjadi untuk setiap pengecualian. Dalam pengujian kami, itu berarti terjadi 999.000 kali — sekali untuk setiap baris yang menemukan pelanggaran kunci duplikat.

Ada banyak cara untuk melihatnya, misalnya dengan menjalankan pelacakan Profiler menggunakan Pengecualian acara di Kesalahan dan Peringatan kelas. Dalam kasus pengujian kami, ini akan akhirnya menghasilkan 999.000 baris dengan TextData elemen seperti ini:

Pelanggaran batasan KUNCI UNIK 'UQ__#AC166DE__3213663B8B6E2E0E'
Tidak dapat menyisipkan kunci duplikat di objek 'dbo.@T'.
Nilai kunci duplikat adalah (173).

Melampirkan Profiler berarti bahwa setiap kejadian penanganan pengecualian memperoleh banyak overhead tambahan, karena data tambahan yang dibutuhkan dikumpulkan dan diformat. Data default yang disebutkan sebelumnya selalu dikumpulkan, meskipun tidak ada yang secara aktif menggunakan informasi tersebut.

Untuk lebih jelasnya:Angka kinerja yang dilaporkan dalam artikel ini semuanya diperoleh tanpa debugger terpasang, dan tidak ada pemantauan lain yang aktif.

Rencana Eksekusi Indeks Nonclustered

Meskipun jauh lebih cepat, rencana penyisipan indeks nonclustered sedikit lebih rumit, jadi saya akan membaginya menjadi dua bagian.

Tema umumnya adalah bahwa rencana ini lebih cepat karena menghilangkan duplikat sebelum mencoba memasukkannya ke dalam tabel target.

Bagian 1

Pertama, sisi kanan dari rencana indeks nonclustered:

Bagian rencana ini menolak setiap baris yang memiliki kecocokan kunci di tabel target untuk indeks unik dengan IGNORE_DUP_KEY atur ON .

Anda mungkin mengharapkan untuk melihat Anti Semi Join di sini, tetapi SQL Server tidak memiliki infrastruktur yang diperlukan untuk mengeluarkan peringatan kunci duplikat yang diperlukan dengan Anti Semi Join operator. (Jika itu belum masuk akal, itu akan segera terjadi.)

Sebagai gantinya, kami mendapatkan paket dengan sejumlah fitur menarik:

  • Pemindaian Indeks Berkelompok adalah Ordered:True untuk memberikan masukan ke Merge Left Semi Join diurutkan berdasarkan kolom c1 di #Data meja.
  • Pemindaian Indeks dari variabel tabel adalah Ordered:False
  • Urutkan mengurutkan baris demi kolom c1 dalam variabel tabel. Pesanan ini bisa saja disediakan oleh dipesan scan indeks variabel tabel pada c1 , tetapi pengoptimal memutuskan Urutkan adalah cara termurah untuk memberikan tingkat Perlindungan Halloween yang diperlukan.
  • Variabel tabel Pemindaian Indeks memiliki UPDLOCK internal internal dan SERIALIZABLE petunjuk diterapkan untuk memastikan stabilitas target selama eksekusi rencana.
  • The Merge Left Semi Join memeriksa kecocokan dalam variabel tabel untuk setiap nilai c1 dikembalikan dari #Data meja. Tidak seperti semi join biasa, ia memancarkan setiap baris yang diterima pada input atasnya. Ini menetapkan bendera di kolom penyelidikan untuk menunjukkan apakah baris saat ini menemukan kecocokan atau tidak. Kolom probe dipancarkan dari Merge Left Semi Join sebagai ekspresi bernama Expr1012 .
  • Pernyataan operator memeriksa nilai kolom probe Expr1012 . Saat pertama kali melihat baris dengan nilai kolom probe non-null (menunjukkan bahwa ditemukan kecocokan kunci indeks), ia mengeluarkan “Kunci duplikat diabaikan” pesan.
  • Pernyataan hanya melewati baris di mana kolom probe nol. Ini menghilangkan baris masuk yang akan menghasilkan kesalahan kunci duplikat.

Itu semua mungkin tampak rumit, tetapi pada dasarnya sesederhana menetapkan bendera jika kecocokan ditemukan, mengeluarkan peringatan saat pertama kali bendera ditetapkan, dan hanya meneruskan baris ke sisipan yang belum ada di tabel target .

Bagian 2

Bagian kedua dari rencana mengikuti Menegaskan operator:

Bagian rencana sebelumnya menghapus baris yang cocok dengan tabel target. Bagian rencana ini menghapus duplikat dalam set sisipan .

Misalnya, bayangkan tidak ada baris dalam tabel target di mana c1 = 1 . Kami mungkin masih menyebabkan kesalahan kunci duplikat jika kami mencoba menyisipkan dua baris dengan c1 = 1 dari tabel sumber. Kita perlu menghindarinya untuk menghormati semantik IGNORE_DUP_KEY = ON .

Aspek ini ditangani oleh Segmen dan Atas operator.

Segmen operator menetapkan tanda baru (berlabel Segment1015 ) ketika menemukan baris dengan nilai baru untuk c1 . Karena baris disajikan dalam c1 pesanan (terima kasih kepada Gabungkan yang mempertahankan pesanan ), paket dapat mengandalkan semua baris dengan c1 yang sama nilai tiba dalam aliran yang berdekatan.

Atas operator meneruskan satu baris untuk setiap grup duplikat, seperti yang ditunjukkan oleh Segmen bendera. Jika Atas operator menemukan lebih dari satu baris untuk Segmen yang sama grup (c1 nilai), itu memancarkan “Kunci duplikat diabaikan” peringatan, jika itu adalah pertama kalinya rencana mengalami kondisi itu.

Efek bersih dari semua ini adalah hanya satu baris yang diteruskan ke operator penyisipan untuk setiap nilai unik c1 , dan peringatan dibuat jika diperlukan.

Rencana eksekusi sekarang telah menghilangkan semua potensi pelanggaran kunci duplikat, jadi Sisipkan Tabel yang tersisa dan Indeks Sisipan operator dapat dengan aman menyisipkan baris ke heap dan indeks nonclustered tanpa takut kesalahan kunci duplikat.

Ingat bahwa UPDLOCK dan SERIALIZABLE petunjuk yang diterapkan ke tabel target memastikan bahwa set tidak dapat berubah selama eksekusi. Dengan kata lain, pernyataan bersamaan tidak dapat mengubah tabel target sehingga kesalahan kunci duplikat akan terjadi di Sisipkan operator. Itu bukan masalah di sini karena kami menggunakan variabel tabel pribadi, tetapi SQL Server masih menambahkan petunjuk sebagai ukuran keamanan umum.

Tanpa petunjuk tersebut, proses bersamaan dapat menambahkan baris ke tabel target yang akan menghasilkan pelanggaran kunci duplikat, meskipun pemeriksaan dilakukan oleh bagian 1 dari rencana. SQL Server perlu memastikan bahwa hasil pemeriksaan keberadaan tetap valid.

Pembaca yang penasaran dapat melihat beberapa fitur yang dijelaskan di atas dengan mengaktifkan tanda pelacakan 3604 dan 8607 untuk melihat pohon keluaran pengoptimal:

PhyOp_RestrRemap
    PhyOp_StreamUpdate(INS TBL: @T, iid 0x2 as IDX, Sort(QCOL: .c1, )), {
            - COL: Bmk10001013 = COL: Bmk1000 
            - COL: c11014 = QCOL: .c1} 
        PhyOp_StreamUpdate(INS TBL: @T, iid 0x0 as TBLInsLocator(COL: Bmk1000  ) REPORT-COUNT), {
                - QCOL: .c1= QCOL: [D].c1} 
            PhyOp_GbTop Group(QCOL: [D].c1,) WARN-DUP
                PhyOp_StreamCheck (WarnIgnoreDuplicate TABLE) 
                    PhyOp_MergeJoin x_jtLeftSemi M-M, Probe COL: Expr1012  ( QCOL: [D].c1) = ( QCOL: .c1)
                        PhyOp_Range TBL: #Data(alias TBL: D)(1) ASC
                        PhyOp_Sort +s -d QCOL: .c1
                            PhyOp_Range TBL: @T(2) ASC Hints( UPDLOCK SERIALIZABLE FORCEDINDEX )
                        ScaOp_Comp x_cmpIs
                            ScaOp_Identifier QCOL: [D].c1
                            ScaOp_Identifier QCOL: .c1
                    ScaOp_Logical x_lopIsNotNull
                        ScaOp_Identifier COL: Expr1012 

Pemikiran Terakhir

IGNORE_DUP_KEY opsi indeks bukanlah sesuatu yang akan sering digunakan kebanyakan orang. Namun, menarik untuk melihat bagaimana fungsi ini diterapkan, dan mengapa ada perbedaan kinerja yang besar antara IGNORE_DUP_KEY pada indeks berkerumun dan tidak berkerumun.

Dalam banyak kasus, itu akan membayar untuk mengikuti jejak prosesor kueri dan mencari untuk menulis kueri yang menghilangkan duplikat secara eksplisit, daripada mengandalkan IGNORE_DUP_KEY . Dalam contoh kita, itu berarti menulis:

DECLARE @T table 
(
    c1 integer NOT NULL
        UNIQUE CLUSTERED -- no IGNORE_DUP_KEY!
);
 
INSERT @T 
    (c1) 
SELECT DISTINCT -- Remove duplicates
    D.c1 
FROM #Data AS D;

Ini dijalankan dalam waktu sekitar 400 md , sebagai catatan saja.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Menyederhanakan Pengujian Unit Prosedur Tersimpan Utama Yang Juga Memanggil Prosedur Utilitas

  2. T-SQL Selasa #67 :Pencadangan dan Pemulihan Baru Acara yang Diperpanjang

  3. 4 Cara Mendapatkan Definisi Tampilan menggunakan Transact-SQL

  4. Solusi tantangan generator seri angka – Bagian 1

  5. Langsung ke Memulai Pengembangan Basis Data Berbasis Tes (TDDD)