[ Teil 1 | Teil 2 | Teil 3 | Teil 4 ]
In Teil 3 dieser Serie habe ich zwei Problemumgehungen gezeigt, um das Erweitern einer IDENTITY
zu vermeiden Spalte – eine, die Ihnen einfach Zeit verschafft, und eine andere, die IDENTITY
aufgibt insgesamt. Ersteres verhindert, dass Sie sich mit externen Abhängigkeiten wie Fremdschlüsseln befassen müssen, aber Letzteres löst dieses Problem immer noch nicht. In diesem Beitrag wollte ich detailliert beschreiben, wie ich vorgehen würde, wenn ich unbedingt zu bigint
wechseln müsste , musste Ausfallzeiten minimieren und hatte viel Zeit für die Planung.
Aufgrund all der potenziellen Blocker und der Notwendigkeit einer minimalen Störung kann der Ansatz als etwas komplex angesehen werden, und es wird nur noch komplizierter, wenn zusätzliche exotische Funktionen verwendet werden (z. B. Partitionierung, In-Memory-OLTP oder Replikation). .
Auf einer sehr hohen Ebene besteht der Ansatz darin, einen Satz Schattentabellen zu erstellen, bei dem alle Einfügungen zu einer neuen Kopie der Tabelle (mit dem größeren Datentyp) geleitet werden und die Existenz der beiden Tabellensätze ebenso transparent ist wie möglich für die Anwendung und ihre Benutzer.
Auf einer detaillierteren Ebene würden die Schritte wie folgt aussehen:
- Erstellen Sie Schattenkopien der Tabellen mit den richtigen Datentypen.
- Ändern Sie die gespeicherten Prozeduren (oder den Ad-hoc-Code), um bigint für Parameter zu verwenden. (Dies kann eine Änderung über die Parameterliste hinaus erfordern, wie lokale Variablen, temporäre Tabellen usw., aber das ist hier nicht der Fall.)
- Benennen Sie die alten Tabellen um und erstellen Sie Ansichten mit diesen Namen, die die alten und neuen Tabellen vereinen.
- Diese Ansichten haben anstelle von Triggern DML-Vorgänge ordnungsgemäß an die entsprechende(n) Tabelle(n) geleitet, sodass Daten während der Migration weiterhin geändert werden können.
- Dies erfordert auch, dass SCHEMABINDING aus allen indizierten Ansichten gelöscht wird, dass bestehende Ansichten Vereinigungen zwischen neuen und alten Tabellen haben und dass Prozeduren, die auf SCOPE_IDENTITY() angewiesen sind, geändert werden müssen.
- Migrieren Sie die alten Daten in Blöcken in die neuen Tabellen.
- Aufräumen, bestehend aus:
- Löschen der temporären Ansichten (wodurch die INSTEAD OF-Trigger gelöscht werden).
- Umbenennung der neuen Tabellen zurück in die ursprünglichen Namen.
- Korrektur der gespeicherten Prozeduren, um zu SCOPE_IDENTITY() zurückzukehren.
- Löschen der alten, jetzt leeren Tabellen.
- SCHEMABINDING wieder auf indizierte Ansichten setzen und Clustered-Indizes neu erstellen.
Sie können wahrscheinlich einen Großteil der Ansichten und Trigger vermeiden, wenn Sie den gesamten Datenzugriff durch gespeicherte Prozeduren steuern können, aber da dieses Szenario selten ist (und man ihm nicht hundertprozentig vertrauen kann), werde ich den schwierigeren Weg zeigen.
Anfangsschema
Um diesen Ansatz so einfach wie möglich zu halten und gleichzeitig viele der Blocker, die ich zuvor in der Serie erwähnt habe, anzugehen, nehmen wir an, wir haben dieses Schema:
CREATE TABLE dbo.Employees ( EmployeeID int IDENTITY(1,1) PRIMARY KEY, Name nvarchar(64) NOT NULL, LunchGroup AS (CONVERT(tinyint, EmployeeID % 5)) ); GO CREATE INDEX EmployeeName ON dbo.Employees(Name); GO CREATE VIEW dbo.LunchGroupCount WITH SCHEMABINDING AS SELECT LunchGroup, MemberCount = COUNT_BIG(*) FROM dbo.Employees GROUP BY LunchGroup; GO CREATE UNIQUE CLUSTERED INDEX LGC ON dbo.LunchGroupCount(LunchGroup); GO CREATE TABLE dbo.EmployeeFile ( EmployeeID int NOT NULL PRIMARY KEY FOREIGN KEY REFERENCES dbo.Employees(EmployeeID), Notes nvarchar(max) NULL ); GO
Eine einfache Personaltabelle mit einer geclusterten IDENTITY-Spalte, einem nicht geclusterten Index, einer berechneten Spalte basierend auf der IDENTITY-Spalte, einer indizierten Ansicht und einer separaten HR/Dirt-Tabelle, die einen Fremdschlüssel zurück zur Personaltabelle (I befürworte dieses Design nicht unbedingt, sondern verwende es nur für dieses Beispiel). Dies sind alles Dinge, die dieses Problem komplizierter machen, als wenn wir eine eigenständige, unabhängige Tabelle hätten.
Mit diesem Schema haben wir wahrscheinlich einige gespeicherte Prozeduren, die Dinge wie CRUD tun. Diese dienen eher der Dokumentation als irgendetwas anderem; Ich werde Änderungen am zugrunde liegenden Schema vornehmen, sodass die Änderung dieser Prozeduren minimal sein sollte. Dies soll die Tatsache simulieren, dass das Ändern von Ad-hoc-SQL aus Ihren Anwendungen möglicherweise nicht möglich und möglicherweise nicht erforderlich ist (naja, solange Sie kein ORM verwenden, das Tabellen vs. Ansichten erkennen kann).
CREATE PROCEDURE dbo.Employee_Add @Name nvarchar(64), @Notes nvarchar(max) = NULL AS BEGIN SET NOCOUNT ON; INSERT dbo.Employees(Name) VALUES(@Name); INSERT dbo.EmployeeFile(EmployeeID, Notes) VALUES(SCOPE_IDENTITY(),@Notes); END GO CREATE PROCEDURE dbo.Employee_Update @EmployeeID int, @Name nvarchar(64), @Notes nvarchar(max) AS BEGIN SET NOCOUNT ON; UPDATE dbo.Employees SET Name = @Name WHERE EmployeeID = @EmployeeID; UPDATE dbo.EmployeeFile SET Notes = @Notes WHERE EmployeeID = @EmployeeID; END GO CREATE PROCEDURE dbo.Employee_Get @EmployeeID int AS BEGIN SET NOCOUNT ON; SELECT e.EmployeeID, e.Name, e.LunchGroup, ed.Notes FROM dbo.Employees AS e INNER JOIN dbo.EmployeeFile AS ed ON e.EmployeeID = ed.EmployeeID WHERE e.EmployeeID = @EmployeeID; END GO CREATE PROCEDURE dbo.Employee_Delete @EmployeeID int AS BEGIN SET NOCOUNT ON; DELETE dbo.EmployeeFile WHERE EmployeeID = @EmployeeID; DELETE dbo.Employees WHERE EmployeeID = @EmployeeID; END GO
Lassen Sie uns nun 5 Datenzeilen zu den ursprünglichen Tabellen hinzufügen:
EXEC dbo.Employee_Add @Name = N'Employee1', @Notes = 'Employee #1 is the best'; EXEC dbo.Employee_Add @Name = N'Employee2', @Notes = 'Fewer people like Employee #2'; EXEC dbo.Employee_Add @Name = N'Employee3', @Notes = 'Jury on Employee #3 is out'; EXEC dbo.Employee_Add @Name = N'Employee4', @Notes = '#4 is moving on'; EXEC dbo.Employee_Add @Name = N'Employee5', @Notes = 'I like #5';
Schritt 1 – neue Tabellen
Hier erstellen wir ein neues Tabellenpaar, das die Originale widerspiegelt, mit Ausnahme des Datentyps der Spalte „EmployeeID“, des Ausgangswerts für die Spalte „IDENTITY“ und eines temporären Suffixes für die Namen:
CREATE TABLE dbo.Employees_New ( EmployeeID bigint IDENTITY(2147483648,1) PRIMARY KEY, Name nvarchar(64) NOT NULL, LunchGroup AS (CONVERT(tinyint, EmployeeID % 5)) ); GO CREATE INDEX EmployeeName_New ON dbo.Employees_New(Name); GO CREATE TABLE dbo.EmployeeFile_New ( EmployeeID bigint NOT NULL PRIMARY KEY FOREIGN KEY REFERENCES dbo.Employees_New(EmployeeID), Notes nvarchar(max) NULL );
Schritt 2 – Verfahrensparameter festlegen
Die Prozeduren hier (und möglicherweise Ihr Ad-hoc-Code, es sei denn, er verwendet bereits den größeren Integer-Typ) benötigen eine sehr geringfügige Änderung, damit sie in Zukunft EmployeeID-Werte über die oberen Grenzen einer Ganzzahl hinaus akzeptieren können. Während Sie argumentieren könnten, dass Sie, wenn Sie diese Verfahren ändern, sie einfach auf die neuen Tische richten könnten, versuche ich zu argumentieren, dass Sie das ultimative Ziel mit *minimalem* Eingriff in das Bestehende erreichen können, dauerhaft Code.
ALTER PROCEDURE dbo.Employee_Update @EmployeeID bigint, -- only change @Name nvarchar(64), @Notes nvarchar(max) AS BEGIN SET NOCOUNT ON; UPDATE dbo.Employees SET Name = @Name WHERE EmployeeID = @EmployeeID; UPDATE dbo.EmployeeFile SET Notes = @Notes WHERE EmployeeID = @EmployeeID; END GO ALTER PROCEDURE dbo.Employee_Get @EmployeeID bigint -- only change AS BEGIN SET NOCOUNT ON; SELECT e.EmployeeID, e.Name, e.LunchGroup, ed.Notes FROM dbo.Employees AS e INNER JOIN dbo.EmployeeFile AS ed ON e.EmployeeID = ed.EmployeeID WHERE e.EmployeeID = @EmployeeID; END GO ALTER PROCEDURE dbo.Employee_Delete @EmployeeID bigint -- only change AS BEGIN SET NOCOUNT ON; DELETE dbo.EmployeeFile WHERE EmployeeID = @EmployeeID; DELETE dbo.Employees WHERE EmployeeID = @EmployeeID; END GO
Schritt 3 – Aufrufe und Auslöser
Leider kann dies nicht *alles* im Stillen geschehen. Wir können die meisten Operationen parallel und ohne Beeinträchtigung der gleichzeitigen Nutzung ausführen, aber wegen SCHEMABINDING muss die indizierte Ansicht geändert und der Index später neu erstellt werden.
Dies gilt für alle anderen Objekte, die SCHEMABINDING verwenden und auf eine unserer Tabellen verweisen. Ich empfehle, sie zu Beginn des Vorgangs in eine nicht indizierte Ansicht zu ändern und den Index nur einmal neu zu erstellen, nachdem alle Daten migriert wurden, und nicht mehrmals im Prozess (da Tabellen mehrmals umbenannt werden). Tatsächlich werde ich die Ansicht ändern, um die neue und alte Version der Employees-Tabelle für die Dauer des Prozesses zu vereinen.
Eine andere Sache, die wir tun müssen, ist, die gespeicherte Prozedur Employee_Add so zu ändern, dass sie vorübergehend @@IDENTITY anstelle von SCOPE_IDENTITY() verwendet. Dies liegt daran, dass der INSTEAD OF-Trigger, der neue Aktualisierungen für „Employees“ handhabt, keine Sichtbarkeit des SCOPE_IDENTITY()-Werts hat. Dies setzt natürlich voraus, dass die Tabellen keine After-Trigger haben, die sich auf @@IDENTITY auswirken. Hoffentlich können Sie diese Abfragen entweder innerhalb einer gespeicherten Prozedur ändern (wo Sie einfach mit INSERT auf die neue Tabelle zeigen könnten), oder Ihr Anwendungscode muss sich nicht von vornherein auf SCOPE_IDENTITY() verlassen.
Wir werden dies unter SERIALIZABLE tun, damit sich keine Transaktionen einschleichen, während die Objekte im Fluss sind. Dies ist eine Reihe von weitgehend reinen Metadaten-Operationen, daher sollte es schnell gehen.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION; GO -- first, remove schemabinding from the view so we can change the base table ALTER VIEW dbo.LunchGroupCount --WITH SCHEMABINDING -- this will silently drop the index -- and will temp. affect performance AS SELECT LunchGroup, MemberCount = COUNT_BIG(*) FROM dbo.Employees GROUP BY LunchGroup; GO -- rename the tables EXEC sys.sp_rename N'dbo.Employees', N'Employees_Old', N'OBJECT'; EXEC sys.sp_rename N'dbo.EmployeeFile', N'EmployeeFile_Old', N'OBJECT'; GO -- the view above will be broken for about a millisecond -- until the following union view is created: CREATE VIEW dbo.Employees WITH SCHEMABINDING AS SELECT EmployeeID = CONVERT(bigint, EmployeeID), Name, LunchGroup FROM dbo.Employees_Old UNION ALL SELECT EmployeeID, Name, LunchGroup FROM dbo.Employees_New; GO -- now the view will work again (but it will be slower) CREATE VIEW dbo.EmployeeFile WITH SCHEMABINDING AS SELECT EmployeeID = CONVERT(bigint, EmployeeID), Notes FROM dbo.EmployeeFile_Old UNION ALL SELECT EmployeeID, Notes FROM dbo.EmployeeFile_New; GO CREATE TRIGGER dbo.Employees_InsteadOfInsert ON dbo.Employees INSTEAD OF INSERT AS BEGIN SET NOCOUNT ON; -- just needs to insert the row(s) into the new copy of the table INSERT dbo.Employees_New(Name) SELECT Name FROM inserted; END GO CREATE TRIGGER dbo.Employees_InsteadOfUpdate ON dbo.Employees INSTEAD OF UPDATE AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; -- need to cover multi-row updates, and the possibility -- that any row may have been migrated already UPDATE o SET Name = i.Name FROM dbo.Employees_Old AS o INNER JOIN inserted AS i ON o.EmployeeID = i.EmployeeID; UPDATE n SET Name = i.Name FROM dbo.Employees_New AS n INNER JOIN inserted AS i ON n.EmployeeID = i.EmployeeID; COMMIT TRANSACTION; END GO CREATE TRIGGER dbo.Employees_InsteadOfDelete ON dbo.Employees INSTEAD OF DELETE AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; -- a row may have been migrated already, maybe not DELETE o FROM dbo.Employees_Old AS o INNER JOIN deleted AS d ON o.EmployeeID = d.EmployeeID; DELETE n FROM dbo.Employees_New AS n INNER JOIN deleted AS d ON n.EmployeeID = d.EmployeeID; COMMIT TRANSACTION; END GO CREATE TRIGGER dbo.EmployeeFile_InsteadOfInsert ON dbo.EmployeeFile INSTEAD OF INSERT AS BEGIN SET NOCOUNT ON; INSERT dbo.EmployeeFile_New(EmployeeID, Notes) SELECT EmployeeID, Notes FROM inserted; END GO CREATE TRIGGER dbo.EmployeeFile_InsteadOfUpdate ON dbo.EmployeeFile INSTEAD OF UPDATE AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; UPDATE o SET Notes = i.Notes FROM dbo.EmployeeFile_Old AS o INNER JOIN inserted AS i ON o.EmployeeID = i.EmployeeID; UPDATE n SET Notes = i.Notes FROM dbo.EmployeeFile_New AS n INNER JOIN inserted AS i ON n.EmployeeID = i.EmployeeID; COMMIT TRANSACTION; END GO CREATE TRIGGER dbo.EmployeeFile_InsteadOfDelete ON dbo.EmployeeFile INSTEAD OF DELETE AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; DELETE o FROM dbo.EmployeeFile_Old AS o INNER JOIN deleted AS d ON o.EmployeeID = d.EmployeeID; DELETE n FROM dbo.EmployeeFile_New AS n INNER JOIN deleted AS d ON n.EmployeeID = d.EmployeeID; COMMIT TRANSACTION; END GO -- the insert stored procedure also has to be updated, temporarily ALTER PROCEDURE dbo.Employee_Add @Name nvarchar(64), @Notes nvarchar(max) = NULL AS BEGIN SET NOCOUNT ON; INSERT dbo.Employees(Name) VALUES(@Name); INSERT dbo.EmployeeFile(EmployeeID, Notes) VALUES(@@IDENTITY, @Notes); -------^^^^^^^^^^------ change here END GO COMMIT TRANSACTION;
Schritt 4 – Alte Daten in neue Tabelle migrieren
Wir werden Daten in Blöcken migrieren, um die Auswirkungen sowohl auf die Parallelität als auch auf das Transaktionsprotokoll zu minimieren, wobei wir uns die grundlegende Technik aus einem alten Beitrag von mir leihen:„Große Löschvorgänge in Blöcke aufteilen“. Wir werden diese Stapel auch in SERIALIZABLE ausführen, was bedeutet, dass Sie mit der Stapelgröße vorsichtig sein sollten, und ich habe der Kürze halber die Fehlerbehandlung weggelassen.
CREATE TABLE #batches(EmployeeID int); DECLARE @BatchSize int = 1; -- for this demo only -- your optimal batch size will hopefully be larger SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; WHILE 1 = 1 BEGIN INSERT #batches(EmployeeID) SELECT TOP (@BatchSize) EmployeeID FROM dbo.Employees_Old WHERE EmployeeID NOT IN (SELECT EmployeeID FROM dbo.Employees_New) ORDER BY EmployeeID; IF @@ROWCOUNT = 0 BREAK; BEGIN TRANSACTION; SET IDENTITY_INSERT dbo.Employees_New ON; INSERT dbo.Employees_New(EmployeeID, Name) SELECT o.EmployeeID, o.Name FROM #batches AS b INNER JOIN dbo.Employees_Old AS o ON b.EmployeeID = o.EmployeeID; SET IDENTITY_INSERT dbo.Employees_New OFF; INSERT dbo.EmployeeFile_New(EmployeeID, Notes) SELECT o.EmployeeID, o.Notes FROM #batches AS b INNER JOIN dbo.EmployeeFile_Old AS o ON b.EmployeeID = o.EmployeeID; DELETE o FROM dbo.EmployeeFile_Old AS o INNER JOIN #batches AS b ON b.EmployeeID = o.EmployeeID; DELETE o FROM dbo.Employees_Old AS o INNER JOIN #batches AS b ON b.EmployeeID = o.EmployeeID; COMMIT TRANSACTION; TRUNCATE TABLE #batches; -- monitor progress SELECT total = (SELECT COUNT(*) FROM dbo.Employees), original = (SELECT COUNT(*) FROM dbo.Employees_Old), new = (SELECT COUNT(*) FROM dbo.Employees_New); -- checkpoint / backup log etc. END DROP TABLE #batches;
Ergebnisse:
Sehen Sie, wie die Zeilen einzeln migriert werden
Während dieser Sequenz können Sie jederzeit Einfügungen, Aktualisierungen und Löschungen testen, und sie sollten entsprechend gehandhabt werden. Sobald die Migration abgeschlossen ist, können Sie mit dem Rest des Prozesses fortfahren.
Schritt 5 – Aufräumen
Eine Reihe von Schritten ist erforderlich, um die vorübergehend erstellten Objekte zu bereinigen und Employees / EmployeeFile als ordnungsgemäße, erstklassige Bürger wiederherzustellen. Viele dieser Befehle sind einfache Metadatenoperationen – mit Ausnahme der Erstellung des Clustered-Index für die indizierte Ansicht sollten sie alle sofort ausgeführt werden.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION; -- drop views and restore name of new tables DROP VIEW dbo.EmployeeFile; --v DROP VIEW dbo.Employees; -- this will drop the instead of triggers EXEC sys.sp_rename N'dbo.Employees_New', N'Employees', N'OBJECT'; EXEC sys.sp_rename N'dbo.EmployeeFile_New', N'EmployeeFile', N'OBJECT'; GO -- put schemabinding back on the view, and remove the union ALTER VIEW dbo.LunchGroupCount WITH SCHEMABINDING AS SELECT LunchGroup, MemberCount = COUNT_BIG(*) FROM dbo.Employees GROUP BY LunchGroup; GO -- change the procedure back to SCOPE_IDENTITY() ALTER PROCEDURE dbo.Employee_Add @Name nvarchar(64), @Notes nvarchar(max) = NULL AS BEGIN SET NOCOUNT ON; INSERT dbo.Employees(Name) VALUES(@Name); INSERT dbo.EmployeeFile(EmployeeID, Notes) VALUES(SCOPE_IDENTITY(), @Notes); END GO COMMIT TRANSACTION; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- drop the old (now empty) tables -- and create the index on the view -- outside the transaction DROP TABLE dbo.EmployeeFile_Old; DROP TABLE dbo.Employees_Old; GO -- only portion that is absolutely not online CREATE UNIQUE CLUSTERED INDEX LGC ON dbo.LunchGroupCount(LunchGroup); GO
An diesem Punkt sollte alles wieder normal funktionieren, obwohl Sie vielleicht typische Wartungsaktivitäten nach größeren Schemaänderungen in Betracht ziehen möchten, wie z. B. das Aktualisieren von Statistiken, das Neuerstellen von Indizes oder das Entfernen von Plänen aus dem Cache.
Schlussfolgerung
Dies ist eine ziemlich komplexe Lösung für ein eigentlich einfaches Problem. Ich hoffe, dass SQL Server es irgendwann ermöglicht, Dinge wie das Hinzufügen/Entfernen der IDENTITY-Eigenschaft, das Neuerstellen von Indizes mit neuen Zieldatentypen und das Ändern von Spalten auf beiden Seiten einer Beziehung zu tun, ohne die Beziehung zu opfern. In der Zwischenzeit würde mich interessieren, ob Ihnen diese Lösung weiterhilft oder ob Sie einen anderen Ansatz haben.
Großes Dankeschön an James Lupolt (@jlupoltsql) dafür, dass er mir dabei geholfen hat, meinen Ansatz zu überprüfen und ihn an einem seiner eigenen, echten Tische auf die ultimative Probe gestellt hat. (Es lief gut. Danke James!)
—
[ Teil 1 | Teil 2 | Teil 3 | Teil 4 ]