[ Bagian 1 | Bagian 2 | Bagian 3 | Bagian 4 ]
Di bagian pertama seri ini, kita melihat bagaimana Masalah Halloween berlaku untuk UPDATE
pertanyaan. Untuk rekap singkat, masalahnya adalah bahwa indeks yang digunakan untuk menemukan catatan yang akan diperbarui memiliki kunci yang dimodifikasi oleh operasi pembaruan itu sendiri (alasan bagus lainnya untuk menggunakan kolom yang disertakan dalam indeks daripada memperluas kunci). Pengoptimal kueri memperkenalkan operator Eager Table Spool untuk memisahkan sisi baca dan tulis dari rencana eksekusi untuk menghindari masalah. Dalam postingan ini, kita akan melihat bagaimana masalah mendasar yang sama dapat memengaruhi INSERT
dan DELETE
pernyataan.
Sisipkan Pernyataan
Sekarang kita tahu sedikit tentang kondisi yang memerlukan Perlindungan Halloween, cukup mudah untuk membuat INSERT
contoh yang melibatkan membaca dari dan menulis ke kunci dari struktur indeks yang sama. Contoh paling sederhana adalah menduplikasi baris dalam tabel (di mana menambahkan baris baru pasti mengubah kunci indeks berkerumun):
CREATE TABLE dbo.Demo ( SomeKey integer NOT NULL, CONSTRAINT PK_Demo PRIMARY KEY (SomeKey) ); INSERT dbo.Demo SELECT SomeKey FROM dbo.Demo;
Masalahnya adalah bahwa baris yang baru dimasukkan mungkin ditemui oleh sisi pembacaan dari rencana eksekusi, berpotensi menghasilkan loop yang menambahkan baris selamanya (atau setidaknya sampai beberapa batas sumber daya tercapai). Pengoptimal kueri mengenali risiko ini, dan menambahkan Eager Table Spool untuk menyediakan pemisahan fase yang diperlukan :
Contoh yang lebih realistis
Anda mungkin tidak sering menulis kueri untuk menduplikasi setiap baris dalam tabel, tetapi Anda mungkin menulis kueri di mana tabel target untuk INSERT
juga muncul di suatu tempat di SELECT
ayat. Salah satu contohnya adalah menambahkan baris dari tabel staging yang belum ada di tujuan:
CREATE TABLE dbo.Staging ( SomeKey integer NOT NULL ); -- Sample data INSERT dbo.Staging (SomeKey) VALUES (1234), (1234); -- Test query INSERT dbo.Demo SELECT s.SomeKey FROM dbo.Staging AS s WHERE NOT EXISTS ( SELECT 1 FROM dbo.Demo AS d WHERE d.SomeKey = s.SomeKey );
Rencana eksekusi adalah:
Masalah dalam hal ini agak berbeda, meskipun masih merupakan contoh dari masalah inti yang sama. Tidak ada nilai '1234' di tabel Demo target, tetapi tabel Staging berisi dua entri tersebut. Tanpa pemisahan fase, nilai '1234' pertama yang ditemukan akan berhasil dimasukkan, tetapi pemeriksaan kedua akan menemukan bahwa nilai '1234' sekarang ada dan tidak akan mencoba memasukkannya lagi. Pernyataan secara keseluruhan akan berhasil diselesaikan.
Ini mungkin menghasilkan hasil yang diinginkan dalam kasus khusus ini (dan bahkan mungkin tampak benar secara intuitif) tetapi ini bukan implementasi yang benar. Standar SQL mengharuskan kueri modifikasi data dijalankan seolah-olah tiga fase kendala membaca, menulis, dan memeriksa terjadi sepenuhnya secara terpisah (lihat bagian satu).
Mencari semua baris untuk disisipkan sebagai satu operasi, kita harus memilih kedua baris '1234' dari tabel Staging, karena nilai ini belum ada di target. Oleh karena itu, rencana eksekusi harus mencoba memasukkan keduanya '1234' dari tabel Staging, mengakibatkan pelanggaran kunci utama:
Msg 2627, Level 14, State 1, Line 1Pelanggaran batasan PRIMARY KEY 'PK_Demo'.
Tidak dapat menyisipkan kunci duplikat di objek 'dbo.Demo'.
Nilai kunci duplikat adalah ( 1234).
Pernyataan telah dihentikan.
Pemisahan fase yang disediakan oleh Tabel Spool memastikan bahwa semua pemeriksaan keberadaan diselesaikan sebelum perubahan apa pun dilakukan pada tabel target. Jika Anda menjalankan kueri di SQL Server dengan contoh data di atas, Anda akan menerima pesan kesalahan (benar).
Perlindungan Halloween diperlukan untuk pernyataan INSERT di mana tabel target juga dirujuk dalam klausa SELECT.
Hapus Pernyataan
Kami mungkin mengharapkan Masalah Halloween tidak berlaku untuk DELETE
pernyataan, karena seharusnya tidak masalah jika kita mencoba menghapus baris beberapa kali. Kita dapat memodifikasi contoh tabel staging untuk menghapus baris dari tabel Demo yang tidak ada di Staging:
TRUNCATE TABLE dbo.Demo; TRUNCATE TABLE dbo.Staging; INSERT dbo.Demo (SomeKey) VALUES (1234); DELETE dbo.Demo WHERE NOT EXISTS ( SELECT 1 FROM dbo.Staging AS s WHERE s.SomeKey = dbo.Demo.SomeKey );
Tes ini tampaknya memvalidasi intuisi kita karena tidak ada Table Spool dalam rencana eksekusi:
Jenis DELETE
tidak memerlukan pemisahan fase karena setiap baris memiliki pengenal unik (RID jika tabel adalah heap, kunci indeks berkerumun, dan mungkin uniquifier jika tidak). Pencari baris unik ini adalah kunci stabil – tidak ada mekanisme yang dapat mengubahnya selama pelaksanaan rencana ini, sehingga Masalah Halloween tidak muncul.
HAPUS Perlindungan Halloween
Namun demikian, setidaknya ada satu kasus di mana DELETE
membutuhkan perlindungan Halloween:ketika rencana tersebut merujuk pada baris dalam tabel selain yang sedang dihapus. Ini membutuhkan self-join, biasanya ditemukan ketika hubungan hierarkis dimodelkan. Contoh sederhana ditunjukkan di bawah ini:
CREATE TABLE dbo.Test ( pk char(1) NOT NULL, ref char(1) NULL, CONSTRAINT PK_Test PRIMARY KEY (pk) ); INSERT dbo.Test (pk, ref) VALUES ('B', 'A'), ('C', 'B'), ('D', 'C');
Seharusnya ada referensi kunci asing tabel yang sama yang didefinisikan di sini, tetapi mari kita abaikan desain yang gagal sejenak – struktur dan datanya tetap valid (dan sayangnya cukup umum untuk menemukan kunci asing dihilangkan di dunia nyata). Bagaimanapun, tugas yang ada adalah menghapus setiap baris di mana ref kolom menunjuk ke pk . yang tidak ada nilai. DELETE
alami kueri yang cocok dengan persyaratan ini adalah:
DELETE dbo.Test WHERE NOT EXISTS ( SELECT 1 FROM dbo.Test AS t2 WHERE t2.pk = dbo.Test.ref );
Rencana kuerinya adalah:
Perhatikan paket ini sekarang memiliki Eager Table Spool yang mahal. Pemisahan fase diperlukan di sini karena jika tidak, hasil dapat bergantung pada urutan pemrosesan baris:
Jika mesin eksekusi dimulai dengan baris di mana pk =B, tidak akan menemukan baris yang cocok (ref =A dan tidak ada baris dimana pk =A). Jika eksekusi maka pindah ke baris di mana pk =C, itu juga akan dihapus karena kita baru saja menghapus baris B yang ditunjuk oleh ref kolom. Hasil akhirnya adalah pemrosesan berulang dalam urutan ini akan menghapus semua baris dari tabel, yang jelas-jelas salah.
Di sisi lain, jika mesin eksekusi memproses baris dengan pk =D pertama, ia akan menemukan baris yang cocok (ref =C). Dengan asumsi eksekusi dilanjutkan secara terbalik pk pesanan, satu-satunya baris yang dihapus dari tabel adalah baris di mana pk =B. Ini adalah hasil yang benar (ingat kueri harus dijalankan seolah-olah fase baca, tulis, dan validasi telah terjadi secara berurutan dan tanpa tumpang tindih).
Pemisahan fase untuk validasi batasan
Sebagai tambahan, kita dapat melihat contoh lain dari pemisahan fase jika kita menambahkan batasan kunci asing tabel yang sama ke contoh sebelumnya:
DROP TABLE dbo.Test; CREATE TABLE dbo.Test ( pk char(1) NOT NULL, ref char(1) NULL, CONSTRAINT PK_Test PRIMARY KEY (pk), CONSTRAINT FK_ref_pk FOREIGN KEY (ref) REFERENCES dbo.Test (pk) ); INSERT dbo.Test (pk, ref) VALUES ('B', NULL), ('C', 'B'), ('D', 'C');
Rencana eksekusi untuk INSERT adalah:
Sisipan itu sendiri tidak memerlukan perlindungan Halloween karena paket tidak dibaca dari tabel yang sama (sumber data adalah tabel virtual dalam memori yang diwakili oleh operator Pemindaian Konstan). Namun standar SQL mengharuskan fase 3 (pemeriksaan kendala) terjadi setelah fase penulisan selesai. Untuk alasan ini, pemisahan fase Eager Table Spool ditambahkan ke rencana setelah Indeks Indeks Clustered, dan tepat sebelum setiap baris diperiksa untuk memastikan batasan kunci asing tetap valid.
Jika Anda mulai berpikir bahwa menerjemahkan kueri modifikasi SQL deklaratif berbasis set ke rencana eksekusi fisik iteratif yang kuat adalah bisnis yang rumit, Anda mulai melihat mengapa pemrosesan pembaruan (di mana Perlindungan Halloween hanyalah bagian yang sangat kecil) adalah bagian paling kompleks dari Pemroses Kueri.
Pernyataan DELETE memerlukan Perlindungan Halloween di mana ada self-join dari tabel target.
Ringkasan
Perlindungan Halloween bisa menjadi fitur yang mahal (tetapi perlu) dalam rencana eksekusi yang mengubah data (di mana 'perubahan' mencakup semua sintaks SQL yang menambahkan, mengubah, atau menghapus baris). Perlindungan Halloween diperlukan untuk UPDATE
rencana di mana kunci struktur indeks umum dibaca dan dimodifikasi, untuk INSERT
rencana di mana tabel target direferensikan di sisi membaca rencana, dan untuk DELETE
rencana di mana self-join pada tabel target dilakukan.
Bagian selanjutnya dalam seri ini akan membahas beberapa pengoptimalan Masalah Halloween khusus yang hanya berlaku untuk MERGE
pernyataan.
[ Bagian 1 | Bagian 2 | Bagian 3 | Bagian 4 ]