Sebagian besar database harus menggunakan kunci asing untuk menegakkan integritas referensial (RI) jika memungkinkan. Namun, keputusan ini lebih dari sekadar memutuskan untuk menggunakan batasan FK dan membuatnya. Ada sejumlah pertimbangan yang harus diperhatikan untuk memastikan database Anda bekerja semulus mungkin.
Artikel ini membahas satu pertimbangan yang tidak mendapat banyak publisitas:Untuk meminimalkan pemblokiran , Anda harus berpikir hati-hati tentang indeks yang digunakan untuk menegakkan keunikan di sisi induk dari hubungan kunci asing tersebut.
Ini berlaku baik Anda menggunakan penguncian baca berkomitmen atau berbasis versi membaca isolasi snapshot berkomitmen (RCSI). Keduanya dapat mengalami pemblokiran saat hubungan kunci asing diperiksa oleh mesin SQL Server.
Di bawah isolasi snapshot (SI), ada peringatan ekstra. Masalah penting yang sama dapat menyebabkan kegagalan transaksi yang tidak terduga (dan bisa dibilang tidak logis) karena konflik pembaruan yang jelas.
Artikel ini terdiri dari dua bagian. Bagian pertama melihat pemblokiran kunci asing di bawah penguncian, baca berkomitmen dan baca isolasi snapshot berkomitmen. Bagian kedua mencakup konflik pembaruan terkait di bawah isolasi snapshot.
1. Memblokir Pemeriksaan Kunci Asing
Mari kita lihat dulu bagaimana pengaruh desain indeks saat pemblokiran terjadi karena pemeriksaan kunci asing.
Demo berikut harus dijalankan di bawah baca komitmen isolasi. Untuk SQL Server, defaultnya adalah mengunci read commit; Azure SQL Database menggunakan RCSI sebagai default. Jangan ragu untuk memilih mana yang Anda suka, atau jalankan skrip satu kali untuk setiap setelan untuk memverifikasi sendiri bahwa perilakunya sama.
-- Use locking read committed ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT OFF; -- Or use row-versioning read committed ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT ON;
Buat dua tabel yang dihubungkan oleh hubungan kunci asing:
CREATE TABLE dbo.Parent ( ParentID integer NOT NULL, ParentNaturalKey varchar(10) NOT NULL, ParentValue integer NOT NULL, CONSTRAINT [PK dbo.Parent ParentID] PRIMARY KEY (ParentID), CONSTRAINT [AK dbo.Parent ParentNaturalKey] UNIQUE (ParentNaturalKey) ); CREATE TABLE dbo.Child ( ChildID integer NOT NULL, ChildNaturalKey varchar(10) NOT NULL, ChildValue integer NOT NULL, ParentID integer NULL, CONSTRAINT [PK dbo.Child ChildID] PRIMARY KEY (ChildID), CONSTRAINT [AK dbo.Child ChildNaturalKey] UNIQUE (ChildNaturalKey), CONSTRAINT [FK dbo.Child to dbo.Parent] FOREIGN KEY (ParentID) REFERENCES dbo.Parent (ParentID) );
Tambahkan baris ke tabel induk:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 100; INSERT dbo.Parent ( ParentID, ParentNaturalKey, ParentValue ) VALUES ( @ParentID, @ParentNaturalKey, @ParentValue );
Pada koneksi kedua , perbarui atribut tabel induk non-kunci ParentValue
di dalam transaksi, tetapi jangan melakukan itu dulu:
DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 200; BEGIN TRANSACTION; UPDATE dbo.Parent SET ParentValue = @ParentValue WHERE ParentID = @ParentID;
Jangan ragu untuk menulis predikat pembaruan menggunakan kunci alami jika Anda mau, tidak ada bedanya untuk tujuan kami saat ini.
Kembali ke koneksi pertama , coba tambahkan catatan anak:
DECLARE @ChildID integer = 101, @ChildNaturalKey varchar(10) = 'CNK1', @ChildValue integer = 999, @ParentID integer = 1; INSERT dbo.Child ( ChildID, ChildNaturalKey, ChildValue, ParentID ) VALUES ( @ChildID, @ChildNaturalKey, @ChildValue, @ParentID );
Pernyataan sisipan ini akan memblokir , apakah Anda memilih penguncian atau pembuatan versi baca komitmen isolasi untuk tes ini.
Penjelasan
Rencana eksekusi untuk penyisipan catatan anak adalah:
Setelah memasukkan baris baru ke tabel anak, rencana eksekusi memeriksa batasan kunci asing. Pemeriksaan dilewati jika id induk yang dimasukkan adalah null (dicapai melalui predikat 'pass through' di semi join kiri). Dalam kasus ini, id induk yang ditambahkan bukan nol, jadi kunci asing memeriksa adalah dilakukan.
SQL Server memverifikasi batasan kunci asing dengan mencari baris yang cocok di tabel induk. Mesin tidak dapat menggunakan versi baris untuk melakukannya — harus dipastikan bahwa data yang diperiksa adalah data komitmen terbaru , bukan versi lama. Mesin memastikan hal ini dengan menambahkan READCOMMITTEDLOCK
internal internal petunjuk tabel untuk pemeriksaan kunci asing di tabel induk.
Hasil akhirnya adalah SQL Server mencoba untuk mendapatkan kunci bersama pada baris yang sesuai di tabel induk, yang memblokir karena sesi lain memegang kunci mode eksklusif yang tidak kompatibel karena pembaruan yang belum dikomit.
Agar jelas, petunjuk penguncian internal hanya berlaku untuk pemeriksaan kunci asing. Sisa paket masih menggunakan RCSI, jika Anda memilih implementasi tingkat isolasi read-commit tersebut.
Menghindari pemblokiran
Komit atau kembalikan transaksi terbuka di sesi kedua, lalu setel ulang lingkungan pengujian:
DROP TABLE IF EXISTS dbo.Child, dbo.Parent;
Buat tabel pengujian lagi, tetapi kali ini alih-alih menerima default, kami memilih untuk membuat kunci utama nonclustered dan batasan unik dikelompokkan:
CREATE TABLE dbo.Parent ( ParentID integer NOT NULL, ParentNaturalKey varchar(10) NOT NULL, ParentValue integer NOT NULL, CONSTRAINT [PK dbo.Parent ParentID] PRIMARY KEY NONCLUSTERED (ParentID), CONSTRAINT [AK dbo.Parent ParentNaturalKey] UNIQUE CLUSTERED (ParentNaturalKey) ); CREATE TABLE dbo.Child ( ChildID integer NOT NULL, ChildNaturalKey varchar(10) NOT NULL, ChildValue integer NOT NULL, ParentID integer NULL, CONSTRAINT [PK dbo.Child ChildID] PRIMARY KEY NONCLUSTERED (ChildID), CONSTRAINT [AK dbo.Child ChildNaturalKey] UNIQUE CLUSTERED (ChildNaturalKey), CONSTRAINT [FK dbo.Child to dbo.Parent] FOREIGN KEY (ParentID) REFERENCES dbo.Parent (ParentID) );
Tambahkan baris ke tabel induk seperti sebelumnya:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 100; INSERT dbo.Parent ( ParentID, ParentNaturalKey, ParentValue ) VALUES ( @ParentID, @ParentNaturalKey, @ParentValue );
Dalam sesi kedua , jalankan pembaruan tanpa melakukan lagi. Saya menggunakan kunci alami kali ini hanya untuk variasi — tidak penting untuk hasilnya. Gunakan kunci pengganti lagi jika Anda mau.
DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 200; BEGIN TRANSACTION UPDATE dbo.Parent SET ParentValue = @ParentValue WHERE ParentNaturalKey = @ParentNaturalKey;
Sekarang jalankan kembali sisipan anak pada sesi pertama :
DECLARE @ChildID integer = 101, @ChildNaturalKey varchar(10) = 'CNK1', @ChildValue integer = 999, @ParentID integer = 1; INSERT dbo.Child ( ChildID, ChildNaturalKey, ChildValue, ParentID ) VALUES ( @ChildID, @ChildNaturalKey, @ChildValue, @ParentID );
Kali ini sisipan anak tidak memblokir . Ini benar apakah Anda menjalankan isolasi baca berkomitmen berbasis penguncian atau versi. Itu bukan salah ketik atau kesalahan:RCSI tidak membuat perbedaan di sini.
Penjelasan
Rencana eksekusi untuk sisipan rekaman anak kali ini sedikit berbeda:
Semuanya sama seperti sebelumnya (termasuk READCOMMITTEDLOCK
yang tidak terlihat petunjuk) kecuali pemeriksaan kunci asing sekarang menggunakan nonclustered indeks unik yang menerapkan kunci utama tabel induk. Pada pengujian pertama, indeks ini dikelompokkan.
Jadi mengapa kali ini kita tidak mendapatkan pemblokiran?
Pembaruan tabel induk yang belum dikomit di sesi kedua memiliki kunci eksklusif pada indeks berkerumun baris karena tabel dasar sedang dimodifikasi. Perubahan pada ParentValue
kolom tidak mempengaruhi kunci utama nonclustered pada ParentID
, sehingga baris indeks nonclustered tidak terkunci .
Oleh karena itu, pemeriksaan kunci asing dapat memperoleh kunci bersama yang diperlukan pada indeks kunci utama nonclustered tanpa pertentangan, dan penyisipan tabel anak berhasil segera .
Saat primer di-cluster, pemeriksaan kunci asing memerlukan kunci bersama pada sumber daya yang sama (baris indeks berkerumun) yang dikunci secara eksklusif oleh pernyataan pembaruan.
Perilaku ini mungkin mengejutkan, tetapi bukan bug . Memberikan kunci asing memeriksa metode aksesnya sendiri yang dioptimalkan menghindari pertikaian kunci yang tidak perlu secara logis. Tidak perlu memblokir pencarian kunci asing karena ParentID
atribut tidak terpengaruh oleh pembaruan bersamaan.
2. Konflik Pembaruan yang Dapat Dihindari
Jika Anda menjalankan tes sebelumnya di bawah level Snapshot Isolation (SI), hasilnya akan sama. Baris anak menyisipkan blok ketika kunci yang direferensikan diterapkan oleh indeks berkerumun , dan tidak memblokir saat penegakan kunci menggunakan nonclustered indeks unik.
Ada satu perbedaan potensial yang penting saat menggunakan SI. Di bawah isolasi baca berkomitmen (penguncian atau RCSI), sisipan baris anak akhirnya berhasil setelah pembaruan di sesi kedua melakukan atau memutar kembali. Menggunakan SI, ada risiko transaksi dibatalkan karena konflik pembaruan yang jelas.
Ini sedikit lebih sulit untuk didemonstrasikan karena transaksi snapshot tidak dimulai dengan BEGIN TRANSACTION
pernyataan — dimulai dengan akses data pengguna pertama setelah titik itu.
Skrip berikut menyiapkan demonstrasi SI, dengan tabel dummy tambahan yang digunakan hanya untuk memastikan transaksi snapshot benar-benar dimulai. Ini menggunakan variasi pengujian di mana kunci utama yang direferensikan diterapkan menggunakan berkelompok . yang unik indeks (default):
ALTER DATABASE CURRENT SET ALLOW_SNAPSHOT_ISOLATION ON; GO DROP TABLE IF EXISTS dbo.Dummy, dbo.Child, dbo.Parent; GO CREATE TABLE dbo.Dummy ( x integer NULL ); CREATE TABLE dbo.Parent ( ParentID integer NOT NULL, ParentNaturalKey varchar(10) NOT NULL, ParentValue integer NOT NULL, CONSTRAINT [PK dbo.Parent ParentID] PRIMARY KEY (ParentID), CONSTRAINT [AK dbo.Parent ParentNaturalKey] UNIQUE (ParentNaturalKey) ); CREATE TABLE dbo.Child ( ChildID integer NOT NULL, ChildNaturalKey varchar(10) NOT NULL, ChildValue integer NOT NULL, ParentID integer NULL, CONSTRAINT [PK dbo.Child ChildID] PRIMARY KEY (ChildID), CONSTRAINT [AK dbo.Child ChildNaturalKey] UNIQUE (ChildNaturalKey), CONSTRAINT [FK dbo.Child to dbo.Parent] FOREIGN KEY (ParentID) REFERENCES dbo.Parent (ParentID) );
Menyisipkan baris induk:
DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 100; INSERT dbo.Parent ( ParentID, ParentNaturalKey, ParentValue ) VALUES ( @ParentID, @ParentNaturalKey, @ParentValue );
Masih dalam sesi pertama , mulai transaksi snapshot:
-- Session 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; -- Ensure snapshot transaction is started SELECT COUNT_BIG(*) FROM dbo.Dummy AS D;
Dalam sesi kedua (berjalan pada tingkat isolasi apa pun):
-- Session 2 DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 200; BEGIN TRANSACTION; UPDATE dbo.Parent SET ParentValue = @ParentValue WHERE ParentID = @ParentID;
Mencoba menyisipkan baris anak di blok sesi pertama seperti yang diharapkan:
-- Session 1 DECLARE @ChildID integer = 101, @ChildNaturalKey varchar(10) = 'CNK1', @ChildValue integer = 999, @ParentID integer = 1; INSERT dbo.Child ( ChildID, ChildNaturalKey, ChildValue, ParentID ) VALUES ( @ChildID, @ChildNaturalKey, @ChildValue, @ParentID );
Perbedaan terjadi saat kita mengakhiri transaksi di sesi kedua. Jika kita memutarnya kembali , sisipan baris anak sesi pertama berhasil diselesaikan .
Jika kita malah berkomitmen transaksi terbuka:
-- Session 2 COMMIT TRANSACTION;
Sesi pertama melaporkan konflik pembaruan dan memutar kembali:
Penjelasan
Konflik pembaruan ini terjadi meskipun faktanya kunci asing divalidasi tidak diubah dengan pembaruan sesi kedua.
Alasannya pada dasarnya sama seperti pada rangkaian tes pertama. Saat indeks berkerumun digunakan untuk penegakan kunci yang direferensikan, transaksi snapshot bertemu baris yang telah dimodifikasi sejak dimulai. Ini tidak diperbolehkan dalam isolasi snapshot.
Saat kunci diterapkan menggunakan indeks nonclustered , transaksi snapshot hanya melihat baris indeks nonclustered yang tidak dimodifikasi, sehingga tidak ada pemblokiran, dan tidak ada 'konflik pembaruan' yang terdeteksi.
Ada banyak keadaan lain di mana isolasi snapshot dapat melaporkan konflik pembaruan yang tidak terduga, atau kesalahan lainnya. Lihat artikel saya sebelumnya untuk contoh.
Kesimpulan
Ada banyak pertimbangan yang harus dipertimbangkan ketika memilih indeks berkerumun untuk tabel baris-toko. Masalah yang dijelaskan di sini hanyalah faktor lain untuk mengevaluasi.
Ini terutama benar jika Anda akan menggunakan isolasi snapshot. Tidak ada yang menikmati transaksi yang dibatalkan , terutama yang bisa dibilang tidak logis. Jika Anda akan menggunakan RCSI, pemblokiran saat membaca untuk memvalidasi kunci asing mungkin tidak terduga, dan dapat menyebabkan kebuntuan.
bawaan untuk PRIMARY KEY
kendalanya adalah membuat indeks pendukungnya sebagai berkelompok , kecuali indeks atau batasan lain dalam definisi tabel secara eksplisit tentang pengelompokan. Merupakan kebiasaan yang baik untuk menjadi eksplisit tentang maksud desain Anda, jadi saya mendorong Anda untuk menulis CLUSTERED
atau NONCLUSTERED
setiap saat.
Indeks duplikat?
Mungkin ada saat-saat ketika Anda secara serius mempertimbangkan, untuk alasan yang masuk akal, memiliki indeks berkerumun dan indeks tidak berkerumun dengan kunci yang sama .
Tujuannya mungkin untuk memberikan akses baca yang optimal untuk kueri pengguna melalui berkelompok index (menghindari pencarian kunci), sekaligus mengaktifkan validasi pemblokiran minimal (dan konflik pembaruan) untuk kunci asing melalui nonclustered yang ringkas indeks seperti yang ditunjukkan di sini.
Ini dapat dicapai, tetapi ada beberapa hambatan yang harus diwaspadai:
-
Diberikan lebih dari satu indeks target yang sesuai, SQL Server tidak menyediakan cara untuk menjamin indeks mana yang akan digunakan untuk penegakan kunci asing.
Dan Guzman mendokumentasikan pengamatannya di Rahasia Pengikatan Indeks Kunci Asing, tetapi ini mungkin tidak lengkap, dan bagaimanapun juga tidak didokumentasikan, sehingga dapat berubah .
Anda dapat mengatasinya dengan memastikan hanya ada satu target indeks pada saat kunci asing dibuat, tetapi hal itu memperumit masalah, dan mengundang masalah di masa mendatang jika batasan kunci asing dihapus dan dibuat ulang.
-
Jika Anda menggunakan sintaks kunci asing singkatan, SQL Server akan hanya ikat batasan ke kunci utama , apakah itu nonclustered atau clustered.
Cuplikan kode berikut menunjukkan perbedaan terakhir:
CREATE TABLE dbo.Parent ( ParentID integer NOT NULL UNIQUE CLUSTERED ); -- Shorthand (implicit) syntax -- Fails with error 1773 CREATE TABLE dbo.Child ( ChildID integer NOT NULL PRIMARY KEY NONCLUSTERED, ParentID integer NOT NULL REFERENCES dbo.Parent ); -- Explicit syntax succeeds CREATE TABLE dbo.Child ( ChildID integer NOT NULL PRIMARY KEY NONCLUSTERED, ParentID integer NOT NULL REFERENCES dbo.Parent (ParentID) );
Orang-orang telah terbiasa mengabaikan sebagian besar konflik baca-tulis di bawah RCSI dan SI. Semoga artikel ini memberi Anda sesuatu yang ekstra untuk dipikirkan saat menerapkan desain fisik untuk tabel yang terkait dengan kunci asing.