Database
 sql >> Datenbank >  >> RDS >> Database

Schema Switch-A-Roo:Teil 2

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.trig
live.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.trig
prep.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:

ein
geben Sie Schema 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 1
Ungü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 1
Trigger „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.