Bereits im August habe ich einen Beitrag über meine Schema-Swap-Methodik für T-SQL Tuesday geschrieben. Der Ansatz ermöglicht es Ihnen im Wesentlichen, eine Kopie einer Tabelle (z. B. einer Art Nachschlagetabelle) im Hintergrund zu laden, um die Interferenz mit den Benutzern zu minimieren:Sobald die Hintergrundtabelle auf dem neuesten Stand ist, ist alles erforderlich, um die aktualisierten Daten zu liefern für Benutzer ist eine Unterbrechung, die lang genug ist, um eine Metadatenänderung zu übernehmen.
In diesem Beitrag erwähnte ich zwei Vorbehalte, die die Methodik, für die ich mich im Laufe der Jahre eingesetzt habe, derzeit nicht berücksichtigt:Fremdschlüsselbeschränkungen und Statistiken . Es gibt eine Vielzahl anderer Funktionen, die diese Technik ebenfalls beeinträchtigen können. Einer, der kürzlich im Gespräch auftauchte:Auslöser . Und es gibt noch andere:Identitätsspalten , Primärschlüsseleinschränkungen , Standardeinschränkungen , Einschränkungen prüfen , Einschränkungen, die UDFs referenzieren , Indizes , Aufrufe (einschließlich indizierter Aufrufe , die SCHEMABINDING
erfordern ) und Partitionen . Ich werde mich heute nicht mit all diesen befassen, aber ich dachte, ich würde ein paar testen, um genau zu sehen, was passiert.
Ich gebe zu, dass meine ursprüngliche Lösung im Grunde eine Momentaufnahme eines armen Mannes war, ohne all die Probleme, die gesamte Datenbank und die Lizenzierungsanforderungen von Lösungen wie Replikation, Spiegelung und Verfügbarkeitsgruppen. Dies waren schreibgeschützte Kopien von Tabellen aus der Produktion, die mithilfe von T-SQL und der Schema-Swap-Technik „gespiegelt“ wurden. Sie brauchten also keine dieser ausgefallenen Tasten, Einschränkungen, Auslöser und anderen Funktionen. Aber ich sehe, dass die Technik in mehr Szenarien nützlich sein kann, und in diesen Szenarien können einige der oben genannten Faktoren ins Spiel kommen.
Lassen Sie uns also ein einfaches Tabellenpaar mit mehreren dieser Eigenschaften einrichten, einen Schemaaustausch durchführen und sehen, was bricht. :-)
Zuerst die Schemas:
CREATE SCHEMA prep; GO CREATE SCHEMA live; GO CREATE SCHEMA holder; GO
Jetzt ist die Tabelle im live
Schema, einschließlich eines Triggers und einer 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
Nun wiederholen wir dasselbe für die Kopie der Tabelle in prep
. Außerdem benötigen wir eine zweite Kopie des Triggers, da wir in prep
keinen Trigger erstellen können Schema, das auf eine Tabelle in live
verweist , oder umgekehrt. Wir setzen die Identität absichtlich auf einen höheren Startwert und einen anderen Standardwert für int_column
(damit wir besser nachverfolgen können, mit welcher Kopie der Tabelle wir es nach mehreren Schema-Austauschen wirklich zu tun haben):
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
Lassen Sie uns nun ein paar Zeilen in jede Tabelle einfügen und die Ausgabe beobachten:
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;
Ergebnisse:
id | int_column | udf_column | berechnete_Spalte |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
Ergebnisse von live.t1
id | int_column | udf_column | berechnete_Spalte |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
Ergebnisse von prep.t1
Und im Nachrichtenbereich:
live.triglive.trig
prep.trig
prep.trig
Lassen Sie uns nun einen einfachen Schemaaustausch durchführen:
-- 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;
Und wiederholen Sie dann die Übung:
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;
Die Ergebnisse in den Tabellen scheinen in Ordnung zu sein:
id | int_column | udf_column | berechnete_Spalte |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
3 | 1 | 20 | 2 |
4 | 1 | 20 | 2 |
Ergebnisse von live.t1
id | int_column | udf_column | berechnete_Spalte |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
1002 | 2 | 20 | 3 |
1003 | 2 | 20 | 3 |
Ergebnisse von prep.t1
Aber das Meldungsfenster listet die Triggerausgabe in der falschen Reihenfolge auf:
prep.trigprep.trig
live.trig
live.trig
Sehen wir uns also alle Metadaten an. Hier ist eine Abfrage, die schnell alle Identitätsspalten, Trigger, Primärschlüssel, Standard- und Prüfeinschränkungen für diese Tabellen überprüft und sich auf das Schema des zugeordneten Objekts, den Namen und die Definition (und den Seed / letzten Wert für Identitätsspalten):
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');
Die Ergebnisse weisen auf ein ziemliches Metadaten-Chaos hin:
geben Sie | einSchema | Name | Definition |
---|---|---|---|
Überprüfen | vorbereiten | ck_live | ([int_column]>(0)) |
Überprüfen | leben | ck_prep | ([int_column]>(1)) |
Standard | vorbereiten | df_live1 | ((1)) |
Standard | vorbereiten | df_live2 | ([dbo].[udf]()) |
Standard | leben | df_prep1 | ((2)) |
Standard | leben | df_prep2 | ([dbo].[udf]()) |
Trigger | vorbereiten | trig_live | CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END |
Trigger | leben | trig_prep | CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END |
Identität | vorbereiten | Seed =1 | letzter_wert =4 |
Identität | leben | Seed =1000 | letzter_Wert =1003 |
Primärschlüssel | vorbereiten | pk_live | |
Primärschlüssel | leben | pk_prep |
Metadaten Ente-Ente-Gans
Die Probleme mit den Identitätsspalten und Einschränkungen scheinen kein großes Problem zu sein. Auch wenn die Objekte gemäß den Katalogansichten *scheinbar* auf die falschen Objekte verweisen, funktioniert die Funktionalität – zumindest für einfache Einfügungen – so, wie Sie es erwarten würden, wenn Sie sich nie die Metadaten angesehen hätten.
Das große Problem ist der Trigger – wenn ich für einen Moment vergesse, wie trivial ich dieses Beispiel gemacht habe, referenziert es in der realen Welt wahrscheinlich die Basistabelle nach Schema und Name. In diesem Fall, wenn es am falschen Tisch befestigt ist, kann es schief gehen. Wechseln wir zurück:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
(Sie können die Metadatenabfrage erneut ausführen, um sich davon zu überzeugen, dass alles wieder normal ist.)
Jetzt ändern wir den Trigger *nur* im live
Version, um tatsächlich etwas Nützliches zu tun (na ja, "nützlich" im Kontext dieses Experiments):
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
Jetzt fügen wir eine Zeile ein:
INSERT live.t1 DEFAULT VALUES;
Ergebnisse:
id msg ---- ---------- 5 live.trig
Führen Sie dann den Austausch erneut durch:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
Und fügen Sie eine weitere Zeile ein:
INSERT live.t1 DEFAULT VALUES;
Ergebnisse (im Nachrichtenbereich):
prep.trig
Uh-oh. Wenn wir diesen Schemaaustausch einmal pro Stunde durchführen, dann tut der Trigger 12 Stunden lang jeden Tag nicht das, was wir erwarten, da er mit der falschen Kopie der Tabelle verknüpft ist! Lassen Sie uns nun die "Prep"-Version des Triggers ändern:
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
Ergebnis:
Nachricht 208, Ebene 16, Zustand 6, Prozedur trig_prep, Zeile 1Ungültiger Objektname 'prep.trig_prep'.
Nun, das ist definitiv nicht gut. Da wir uns in der Metadaten-werden-ausgetauscht-Phase befinden, gibt es kein solches Objekt; die Trigger sind jetzt live.trig_prep
und prep.trig_live
. Noch verwirrt? Ich auch. Versuchen wir also Folgendes:
EXEC sp_helptext 'live.trig_prep';
Ergebnisse:
CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
Na, ist das nicht lustig? Wie ändere ich diesen Trigger, wenn seine Metadaten nicht einmal richtig in seiner eigenen Definition widergespiegelt werden? Versuchen wir Folgendes:
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
Ergebnisse:
Msg 2103, Ebene 15, Zustand 1, Prozedur trig_prep, Zeile 1Trigger „live.trig_prep“ kann nicht geändert werden, da sein Schema sich vom Schema der Zieltabelle oder -ansicht unterscheidet.
Das ist natürlich auch nicht gut. Es scheint, dass es keine gute Möglichkeit gibt, dieses Szenario zu lösen, bei dem die Objekte nicht in ihre ursprünglichen Schemas zurückversetzt werden. Ich könnte diesen Trigger so ändern, dass er gegen live.t1
ist :
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
Aber jetzt habe ich zwei Trigger, die in ihrem Haupttext sagen, dass sie gegen live.t1
arbeiten , aber nur dieser wird tatsächlich ausgeführt. Ja, mir schwirrt der Kopf (und Michael J. Swarts (@MJSwart) in diesem Blogbeitrag auch). Und beachten Sie, dass ich, um dieses Durcheinander zu beseitigen, nachdem ich Schemas wieder zurückgetauscht habe, die Trigger mit ihren ursprünglichen Namen löschen kann:
DROP TRIGGER live.trig_live; DROP TRIGGER prep.trig_prep;
Wenn ich DROP TRIGGER live.trig_prep;
versuche , zum Beispiel erhalte ich einen Objekt nicht gefunden Fehler.
Auflösungen?
Eine Problemumgehung für das Triggerproblem besteht darin, den CREATE TRIGGER
dynamisch zu generieren Code und löschen und erstellen Sie den Trigger als Teil des Austauschs neu. Lassen Sie uns zuerst einen Trigger auf die *aktuelle* Tabelle in live
zurücksetzen (Sie können in Ihrem Szenario entscheiden, ob Sie überhaupt einen Auslöser für die prep
benötigen Version der Tabelle überhaupt):
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
Nun ein kurzes Beispiel dafür, wie unser neuer Schemaaustausch funktionieren würde (und Sie müssen dies möglicherweise anpassen, um mit jedem Auslöser umzugehen, wenn Sie mehrere Auslöser haben, und es für das Schema auf dem prep
wiederholen Version, wenn Sie auch dort einen Trigger pflegen müssen. Achten Sie besonders darauf, dass der folgende Code der Kürze halber davon ausgeht, dass es nur *einen* Trigger auf live.t1
gibt .
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;
Eine andere (weniger wünschenswerte) Problemumgehung wäre, den gesamten Schemaaustauschvorgang zweimal durchzuführen, einschließlich aller Vorgänge, die gegen den prep
ausgeführt werden Version der Tabelle. Was den Zweck des Schemaaustauschs weitgehend zunichte macht:die Zeit zu verkürzen, in der Benutzer nicht auf die Tabelle(n) zugreifen können, und ihnen die aktualisierten Daten mit minimaler Unterbrechung zu liefern.