Kembali pada bulan Agustus saya menulis posting tentang metodologi pertukaran skema saya untuk T-SQL Selasa. Pendekatan ini pada dasarnya memungkinkan Anda untuk malas memuat salinan tabel (misalnya, semacam tabel pencarian) di latar belakang untuk meminimalkan gangguan dengan pengguna:setelah tabel latar belakang diperbarui, semua yang diperlukan untuk mengirimkan data yang diperbarui bagi pengguna merupakan gangguan yang cukup lama untuk melakukan perubahan metadata.
Dalam posting itu, saya menyebutkan dua peringatan bahwa metodologi yang saya perjuangkan selama bertahun-tahun saat ini tidak memenuhi:kendala kunci asing dan statistik . Ada sejumlah fitur lain yang dapat mengganggu teknik ini juga. Salah satu yang muncul dalam percakapan baru-baru ini:pemicu . Dan masih ada lagi:kolom identitas , batasan kunci utama , batasan default , periksa batasan , batasan yang mereferensikan UDF , indeks , tampilan (termasuk tampilan yang diindeks , yang memerlukan SCHEMABINDING
), dan partisi . Saya tidak akan membahas semua ini hari ini, tetapi saya pikir saya akan menguji beberapa untuk melihat apa yang sebenarnya terjadi.
Saya akan mengakui bahwa solusi asli saya pada dasarnya adalah snapshot orang miskin, tanpa semua kerepotan, seluruh database, dan persyaratan lisensi solusi seperti replikasi, mirroring, dan Grup Ketersediaan. Ini adalah salinan tabel hanya-baca dari produksi yang sedang "dicerminkan" menggunakan T-SQL dan teknik pertukaran skema. Jadi mereka tidak memerlukan kunci mewah, batasan, pemicu, dan fitur lainnya. Tetapi saya melihat bahwa teknik ini dapat berguna dalam lebih banyak skenario, dan dalam skenario tersebut beberapa faktor di atas dapat berperan.
Jadi mari kita siapkan sepasang tabel sederhana yang memiliki beberapa properti ini, lakukan pertukaran skema, dan lihat apa yang rusak. :-)
Pertama, skema:
CREATE SCHEMA prep; GO CREATE SCHEMA live; GO CREATE SCHEMA holder; GO
Sekarang, tabel di live
skema, termasuk pemicu dan UDF:
CREATE FUNCTION dbo.udf() RETURNS INT AS BEGIN RETURN (SELECT 20); END GO CREATE TABLE live.t1 ( id INT IDENTITY(1,1), int_column INT NOT NULL DEFAULT 1, udf_column INT NOT NULL DEFAULT dbo.udf(), computed_column AS CONVERT(INT, int_column + 1), CONSTRAINT pk_live PRIMARY KEY(id), CONSTRAINT ck_live CHECK (int_column > 0) ); GO CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END GO
Sekarang, kita ulangi hal yang sama untuk salinan tabel di prep
. Kami juga memerlukan salinan pemicu kedua, karena kami tidak dapat membuat pemicu di prep
skema yang mereferensikan tabel di live
, atau sebaliknya. Kami sengaja akan menetapkan identitas ke seed yang lebih tinggi dan nilai default yang berbeda untuk int_column
(untuk membantu kami melacak dengan lebih baik salinan tabel mana yang benar-benar kami tangani setelah beberapa pertukaran skema):
CREATE TABLE prep.t1 ( id INT IDENTITY(1000,1), int_column INT NOT NULL DEFAULT 2, udf_column INT NOT NULL DEFAULT dbo.udf(), computed_column AS CONVERT(INT, int_column + 1), CONSTRAINT pk_prep PRIMARY KEY(id), CONSTRAINT ck_prep CHECK (int_column > 1) ); GO CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END GO
Sekarang, mari kita masukkan beberapa baris ke dalam setiap tabel dan amati hasilnya:
SET NOCOUNT ON; INSERT live.t1 DEFAULT VALUES; INSERT live.t1 DEFAULT VALUES; INSERT prep.t1 DEFAULT VALUES; INSERT prep.t1 DEFAULT VALUES; SELECT * FROM live.t1; SELECT * FROM prep.t1;
Hasil:
Hasil dari live.t1
Hasil dari prep.t1
Dan di panel pesan:
live.triglive.trig
prep.trig
prep.trig
Sekarang, mari kita lakukan pertukaran skema sederhana:
-- assume that you do background loading of prep.t1 here BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
Dan kemudian ulangi latihan ini:
SET NOCOUNT ON; INSERT live.t1 DEFAULT VALUES; INSERT live.t1 DEFAULT VALUES; INSERT prep.t1 DEFAULT VALUES; INSERT prep.t1 DEFAULT VALUES; SELECT * FROM live.t1; SELECT * FROM prep.t1;
Hasil di tabel tampak oke:
Hasil dari live.t1
Hasil dari prep.t1
Tetapi panel pesan mencantumkan keluaran pemicu dalam urutan yang salah:
prep.trigprep.trig
live.trig
live.trig
Jadi, mari gali semua metadata. Berikut adalah kueri yang akan dengan cepat memeriksa semua kolom identitas, pemicu, kunci utama, default, dan memeriksa batasan untuk tabel ini, dengan fokus pada skema objek terkait, nama, dan definisi (dan nilai seed / terakhir untuk kolom identitas):
SELECT [type] = 'Check', [schema] = OBJECT_SCHEMA_NAME(parent_object_id), name, [definition] FROM sys.check_constraints WHERE OBJECT_SCHEMA_NAME(parent_object_id) IN (N'live',N'prep') UNION ALL SELECT [type] = 'Default', [schema] = OBJECT_SCHEMA_NAME(parent_object_id), name, [definition] FROM sys.default_constraints WHERE OBJECT_SCHEMA_NAME(parent_object_id) IN (N'live',N'prep') UNION ALL SELECT [type] = 'Trigger', [schema] = OBJECT_SCHEMA_NAME(parent_id), name, [definition] = OBJECT_DEFINITION([object_id]) FROM sys.triggers WHERE OBJECT_SCHEMA_NAME(parent_id) IN (N'live',N'prep') UNION ALL SELECT [type] = 'Identity', [schema] = OBJECT_SCHEMA_NAME([object_id]), name = 'seed = ' + CONVERT(VARCHAR(12), seed_value), [definition] = 'last_value = ' + CONVERT(VARCHAR(12), last_value) FROM sys.identity_columns WHERE OBJECT_SCHEMA_NAME([object_id]) IN (N'live',N'prep') UNION ALL SELECT [type] = 'Primary Key', [schema] = OBJECT_SCHEMA_NAME([parent_object_id]), name, [definition] = '' FROM sys.key_constraints WHERE OBJECT_SCHEMA_NAME([object_id]) IN (N'live',N'prep');
Hasil menunjukkan kekacauan metadata:
CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END
CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
Metadata bebek-bebek-angsa
Masalah dengan kolom identitas dan batasan tampaknya tidak menjadi masalah besar. Meskipun objek *tampaknya* menunjuk ke objek yang salah menurut tampilan katalog, fungsionalitas – setidaknya untuk sisipan dasar – beroperasi seperti yang Anda harapkan jika Anda belum pernah melihat metadata.
Masalah besar adalah dengan pemicu – lupa sejenak betapa sepele saya membuat contoh ini, di dunia nyata, mungkin referensi tabel dasar dengan skema dan nama. Dalam hal ini, ketika dilampirkan ke meja yang salah, semuanya bisa berjalan ... yah, salah. Mari beralih kembali:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
(Anda dapat menjalankan kueri metadata lagi untuk meyakinkan diri sendiri bahwa semuanya kembali normal.)
Sekarang mari kita ubah pemicunya *hanya* di live
versi untuk benar-benar melakukan sesuatu yang berguna (yah, "berguna" dalam konteks eksperimen ini):
ALTER TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN SELECT i.id, msg = 'live.trig' FROM inserted AS i INNER JOIN live.t1 AS t ON i.id = t.id; END GO
Sekarang mari kita sisipkan satu baris:
INSERT live.t1 DEFAULT VALUES;
Hasil:
id msg ---- ---------- 5 live.trig
Kemudian lakukan swap lagi:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
Dan masukkan baris lain:
INSERT live.t1 DEFAULT VALUES;
Hasil (di panel pesan):
prep.trig
Uh oh. Jika kita melakukan pertukaran skema ini sekali dalam satu jam, maka selama 12 jam setiap hari, pemicunya tidak melakukan apa yang kita harapkan, karena ini terkait dengan salinan tabel yang salah! Sekarang mari kita ubah versi pemicu "persiapan":
ALTER TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN SELECT i.id, msg = 'prep.trig' FROM inserted AS i INNER JOIN prep.t1 AS t ON i.id = t.id; END GO
Hasil:
Pesan 208, Level 16, Status 6, Prosedur trig_prep, Baris 1Nama objek 'prep.trig_prep' tidak valid.
Yah, itu pasti tidak baik. Karena kita berada dalam fase metadata-is-swapped, tidak ada objek seperti itu; pemicunya sekarang live.trig_prep
dan prep.trig_live
. Bingung belum? Gerakan mengungkap kekerasan seksual demi menghapuskannya. Jadi mari kita coba ini:
EXEC sp_helptext 'live.trig_prep';
Hasil:
CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
Nah, bukankah itu lucu? Bagaimana cara mengubah pemicu ini ketika metadatanya bahkan tidak tercermin dengan benar dalam definisinya sendiri? Mari kita coba ini:
ALTER TRIGGER live.trig_prep ON prep.t1 FOR INSERT AS BEGIN SELECT i.id, msg = 'prep.trig' FROM inserted AS i INNER JOIN prep.t1 AS t ON i.id = t.id; END GO
Hasil:
Msg 2103, Level 15, Status 1, Prosedur trig_prep, Baris 1Tidak dapat mengubah pemicu 'live.trig_prep' karena skemanya berbeda dari skema tabel atau tampilan target.
Ini juga tidak baik, jelas. Tampaknya tidak ada cara yang baik untuk menyelesaikan skenario ini yang tidak melibatkan pertukaran objek kembali ke skema aslinya. Saya dapat mengubah pemicu ini menjadi melawan live.t1
:
ALTER TRIGGER live.trig_prep ON live.t1 FOR INSERT AS BEGIN SELECT i.id, msg = 'live.trig' FROM inserted AS i INNER JOIN live.t1 AS t ON i.id = t.id; END GO
Tapi sekarang saya memiliki dua pemicu yang mengatakan, dalam teks isi mereka, bahwa mereka beroperasi melawan live.t1
, tetapi hanya yang ini yang benar-benar mengeksekusi. Ya, kepala saya berputar (dan begitu juga Michael J. Swart (@MJSwart) di posting blog ini). Dan perhatikan bahwa, untuk membersihkan kekacauan ini, setelah menukar skema kembali, saya dapat menghapus pemicu dengan nama aslinya:
DROP TRIGGER live.trig_live; DROP TRIGGER prep.trig_prep;
Jika saya mencoba DROP TRIGGER live.trig_prep;
, misalnya, saya mendapatkan kesalahan objek tidak ditemukan.
Resolusi?
Solusi untuk masalah pemicu adalah membuat CREATE TRIGGER
secara dinamis kode, dan jatuhkan dan buat ulang pemicu, sebagai bagian dari swap. Pertama, mari kita kembalikan trigger ke tabel *saat ini* di live
(Anda dapat memutuskan dalam skenario Anda jika Anda membutuhkan pemicu di prep
versi tabel sama sekali):
CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN SELECT i.id, msg = 'live.trig' FROM inserted AS i INNER JOIN live.t1 AS t ON i.id = t.id; END GO
Sekarang, contoh cepat tentang bagaimana skema swap baru kami akan bekerja (dan Anda mungkin harus menyesuaikan ini untuk menangani setiap pemicu, jika Anda memiliki beberapa pemicu, dan ulangi untuk skema di prep
versi, jika Anda perlu mempertahankan pemicu di sana juga. Berhati-hatilah agar kode di bawah ini, untuk singkatnya, mengasumsikan bahwa hanya ada *satu* pemicu di live.t1
.
BEGIN TRANSACTION; DECLARE @sql1 NVARCHAR(MAX), @sql2 NVARCHAR(MAX); SELECT @sql1 = N'DROP TRIGGER live.' + QUOTENAME(name) + ';', @sql2 = OBJECT_DEFINITION([object_id]) FROM sys.triggers WHERE [parent_id] = OBJECT_ID(N'live.t1'); EXEC sp_executesql @sql1; -- drop the trigger before the transfer ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; EXEC sp_executesql @sql2; -- re-create it after the transfer COMMIT TRANSACTION;
Solusi lain (yang kurang diinginkan) adalah melakukan seluruh operasi pertukaran skema dua kali, termasuk operasi apa pun yang terjadi terhadap prep
versi tabel. Yang sebagian besar mengalahkan tujuan pertukaran skema di tempat pertama:mengurangi waktu pengguna tidak dapat mengakses tabel dan memberi mereka data yang diperbarui dengan gangguan minimal.