W sierpniu napisałem post na temat mojej metodologii schema-swap dla T-SQL we wtorek. Podejście to zasadniczo pozwala na leniwe ładowanie kopii tabeli (powiedzmy pewnego rodzaju tabeli przeglądowej) w tle, aby zminimalizować ingerencję użytkowników:gdy tabela w tle jest aktualna, wszystko, co jest wymagane do dostarczenia zaktualizowanych danych dla użytkowników jest przerwą na tyle długą, aby wprowadzić zmianę metadanych.
W tym poście wspomniałem o dwóch zastrzeżeniach, których metodologia, której broniłem przez lata, obecnie nie spełnia:ograniczeń związanych z kluczami zagranicznymi i statystyki . Istnieje wiele innych funkcji, które również mogą zakłócać tę technikę. Jeden, który ostatnio pojawił się w rozmowie:wyzwalacze . Są też inne:kolumny tożsamości , podstawowe ograniczenia klucza , domyślne ograniczenia , sprawdź ograniczenia , ograniczenia odnoszące się do UDF , indeksy , wyświetlenia (w tym widoki indeksowane , które wymagają SCHEMABINDING
) i partycje . Nie zamierzam dzisiaj zajmować się wszystkimi tymi wszystkimi, ale pomyślałem, że przetestuję kilka, aby zobaczyć dokładnie, co się stanie.
Przyznam, że moje oryginalne rozwiązanie było w zasadzie migawką biednego człowieka, bez wszystkich kłopotów, całej bazy danych i wymagań licencyjnych związanych z rozwiązaniami, takimi jak replikacja, dublowanie i grupy dostępności. Były to kopie tabel tylko do odczytu z produkcji, które były „dublowane” przy użyciu T-SQL i techniki wymiany schematów. Nie potrzebowali więc żadnego z tych wymyślnych kluczy, ograniczeń, wyzwalaczy i innych funkcji. Widzę jednak, że technika ta może być użyteczna w większej liczbie scenariuszy i w takich sytuacjach niektóre z powyższych czynników mogą mieć znaczenie.
Skonfigurujmy więc prostą parę tabel, które mają kilka z tych właściwości, wykonaj zamianę schematów i zobaczmy, co się psuje. :-)
Najpierw schematy:
CREATE SCHEMA prep; GO CREATE SCHEMA live; GO CREATE SCHEMA holder; GO
Teraz tabela w live
schemat, w tym wyzwalacz i 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
Teraz powtarzamy to samo dla kopii tabeli w prep
. Potrzebujemy również drugiej kopii wyzwalacza, ponieważ nie możemy utworzyć wyzwalacza w prep
schemat, który odwołuje się do tabeli w live
, lub odwrotnie. Celowo ustawimy tożsamość na wyższy ziarno i inną wartość domyślną dla int_column
(aby pomóc nam lepiej śledzić, z którą kopią tabeli mamy do czynienia po wielu zamianach schematów):
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
Teraz wstawmy kilka wierszy do każdej tabeli i obserwujmy wynik:
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;
Wyniki:
id | int_column | kolumna_udf | computed_column |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
Wyniki z live.t1
id | int_column | kolumna_udf | computed_column |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
Wyniki z prep.t1
A w okienku wiadomości:
live.triglive.trig
prep.trig
prep.trig
Przeprowadźmy teraz prostą zamianę schematu:
-- 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;
A potem powtórz ćwiczenie:
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;
Wyniki w tabelach wydają się w porządku:
id | int_column | kolumna_udf | computed_column |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
3 | 1 | 20 | 2 |
4 | 1 | 20 | 2 |
Wyniki z live.t1
id | int_column | kolumna_udf | computed_column |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
1002 | 2 | 20 | 3 |
1003 | 2 | 20 | 3 |
Wyniki z prep.t1
Ale okienko komunikatów wyświetla dane wyjściowe wyzwalacza w niewłaściwej kolejności:
prep.trigprep.trig
live.trig
live.trig
Zajmijmy się więc wszystkimi metadanymi. Oto zapytanie, które szybko sprawdzi wszystkie kolumny tożsamości, wyzwalacze, klucze podstawowe, ograniczenia domyślne i sprawdzające dla tych tabel, koncentrując się na schemacie skojarzonego obiektu, nazwie i definicji (oraz początkowej/ostatniej wartości dla kolumny tożsamości):
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');
Wyniki wskazują na dość bałagan metadanych:
typ | schemat | imię | definicja |
---|---|---|---|
Sprawdź | przygotuj | ck_live | ([int_column]>(0)) |
Sprawdź | na żywo | ck_prep | ([int_column]>(1)) |
Domyślne | przygotuj | df_live1 | ((1)) |
Domyślne | przygotuj | df_live2 | ([dbo].[udf]()) |
Domyślne | na żywo | df_prep1 | ((2)) |
Domyślne | na żywo | df_prep2 | ([dbo].[udf]()) |
Wyzwalacz | przygotuj | trig_live | CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END |
Wyzwalacz | na żywo | trig_prep | CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END |
Tożsamość | przygotuj | nasiona =1 | ostatnia_wartość =4 |
Tożsamość | na żywo | nasiona =1000 | ostatnia_wartość =1003 |
Klucz główny | przygotuj | pk_live | |
Klucz główny | na żywo | pk_prep |
Metadane kaczka-kaczka-gęś
Problemy z kolumnami tożsamości i ograniczeniami nie wydają się być dużym problemem. Mimo że obiekty *wydają się* wskazywać niewłaściwe obiekty zgodnie z widokami katalogu, funkcjonalność – przynajmniej dla podstawowych wstawek – działa tak, jak można by się spodziewać, gdybyś nigdy nie zaglądał do metadanych.
Duży problem dotyczy wyzwalacza – zapominając na chwilę, jak trywialny zrobiłem ten przykład, w prawdziwym świecie prawdopodobnie odwołuje się do tabeli bazowej poprzez schemat i nazwę. W takim przypadku, gdy jest przymocowany do niewłaściwego stołu, wszystko może pójść… cóż, źle. Wróćmy z powrotem:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
(Możesz ponownie uruchomić zapytanie o metadane, aby przekonać się, że wszystko wróciło do normy.)
Teraz zmieńmy wyzwalacz *tylko* na live
wersji, aby zrobić coś użytecznego (cóż, "użytecznego" w kontekście tego eksperymentu):
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
Teraz wstawmy wiersz:
INSERT live.t1 DEFAULT VALUES;
Wyniki:
id msg ---- ---------- 5 live.trig
Następnie ponownie wykonaj zamianę:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
I wstaw kolejny wiersz:
INSERT live.t1 DEFAULT VALUES;
Wyniki (w okienku wiadomości):
prep.trig
O o. Jeśli wykonujemy tę zamianę schematu raz na godzinę, to przez 12 godzin każdego dnia wyzwalacz nie robi tego, czego oczekujemy, ponieważ jest powiązany z niewłaściwą kopią tabeli! Teraz zmieńmy wersję „prep” wyzwalacza:
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
Wynik:
Msg 208, Poziom 16, Stan 6, Procedura trig_prep, Wiersz 1Nieprawidłowa nazwa obiektu „prep.trig_prep”.
Cóż, to zdecydowanie nie jest dobre. Ponieważ jesteśmy w fazie wymiany metadanych, nie ma takiego obiektu; wyzwalacze to teraz live.trig_prep
i prep.trig_live
. Zdezorientowany? Ja też. Spróbujmy więc tego:
EXEC sp_helptext 'live.trig_prep';
Wyniki:
CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
Cóż, czy to nie zabawne? Jak zmienić ten wyzwalacz, gdy jego metadane nie są nawet odpowiednio odzwierciedlone w jego własnej definicji? Spróbujmy tego:
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
Wyniki:
Msg 2103, Poziom 15, Stan 1, Procedura trig_prep, Wiersz 1Nie można zmienić wyzwalacza „live.trig_prep”, ponieważ jego schemat różni się od schematu tabeli docelowej lub widoku.
To też oczywiście nie jest dobre. Wygląda na to, że tak naprawdę nie ma dobrego sposobu rozwiązania tego scenariusza, który nie wymaga zamiany obiektów z powrotem na ich oryginalne schematy. Mógłbym zmienić ten wyzwalacz, aby był przeciwko 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
Ale teraz mam dwa wyzwalacze, które mówią w swoim tekście, że działają przeciwko live.t1
, ale tylko ten faktycznie jest wykonywany. Tak, moja głowa się kręci (tak jak Michael J. Swart (@MJSwart) w tym poście na blogu). I zauważ, że aby posprzątać ten bałagan, po ponownej zamianie schematów mogę usunąć wyzwalacze z ich oryginalnymi nazwami:
DROP TRIGGER live.trig_live; DROP TRIGGER prep.trig_prep;
Jeśli spróbuję DROP TRIGGER live.trig_prep;
, na przykład otrzymuję błąd nie znaleziono obiektu.
Rezolucje?
Obejściem problemu z wyzwalaczem jest dynamiczne generowanie CREATE TRIGGER
kod i upuść i ponownie utwórz wyzwalacz w ramach wymiany. Najpierw ustawmy wyzwalacz z powrotem na *bieżącej* tabeli w live
(możesz zdecydować w swoim scenariuszu, czy potrzebujesz wyzwalacza w prep
wersja tabeli w ogóle):
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
Teraz krótki przykład tego, jak działa nasza nowa zamiana schematów (w przypadku wielu wyzwalaczy może być konieczne dostosowanie tego, aby poradzić sobie z każdym wyzwalaczem, i powtórzyć to dla schematu w prep
wersji, jeśli tam też musisz zachować wyzwalacz. Zwróć szczególną uwagę, aby poniższy kod, dla zwięzłości, zakładał, że istnieje tylko *jeden* wyzwalacz na 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;
Innym (mniej pożądanym) obejściem byłoby dwukrotne wykonanie całej operacji zamiany schematu, łącznie z operacjami wykonywanymi na prep
wersja tabeli. Co w dużej mierze niweczy cel zamiany schematów:skrócenie czasu, w którym użytkownicy nie mogą uzyskać dostępu do tabel i dostarczanie im zaktualizowanych danych przy minimalnej przerwie.