[ Del 1 | Del 2 | Del 3 | Del 4 ]
I del 3 av den här serien visade jag två lösningar för att undvika att utöka en IDENTITY
kolumn – en som helt enkelt köper dig tid och en annan som överger IDENTITY
sammanlagt. Den förra hindrar dig från att behöva hantera externa beroenden såsom främmande nycklar, men den senare tar fortfarande inte upp det problemet. I det här inlägget ville jag beskriva det tillvägagångssätt jag skulle ta om jag absolut behövde gå över till bigint
, behövde minimera stilleståndstiden och hade gott om tid för planering.
På grund av alla potentiella blockerare och behovet av minimala störningar, kan tillvägagångssättet ses som lite komplext, och det blir bara mer så om ytterligare exotiska funktioner används (t.ex. partitionering, In-Memory OLTP eller replikering) .
På en mycket hög nivå är tillvägagångssättet att skapa en uppsättning skuggtabeller, där alla inlägg dirigeras till en ny kopia av tabellen (med den större datatypen), och förekomsten av de två uppsättningarna av tabeller är lika transparenta. som möjligt för applikationen och dess användare.
På en mer detaljerad nivå skulle uppsättningen steg vara som följer:
- Skapa skuggkopior av tabellerna, med rätt datatyper.
- Ändra de lagrade procedurerna (eller ad hoc-koden) för att använda bigint för parametrar. (Detta kan kräva modifiering utöver parameterlistan, såsom lokala variabler, temporära tabeller, etc., men så är inte fallet här.)
- Byt namn på de gamla tabellerna och skapa vyer med de namn som förenar de gamla och nya tabellerna.
- Dessa vyer har istället för utlösare för att korrekt dirigera DML-operationer till lämplig(a) tabell(er), så att data fortfarande kan ändras under migreringen.
- Detta kräver också att SCHEMABINDING tas bort från alla indexerade vyer, att befintliga vyer har kopplingar mellan nya och gamla tabeller och att procedurer som bygger på SCOPE_IDENTITY() ska ändras.
- Migrera gamla data till de nya tabellerna i bitar.
- Städa upp, bestående av:
- Att släppa de tillfälliga vyerna (vilket kommer att ta bort utlösarna I STÄLLET FÖR).
- Döper om de nya tabellerna till de ursprungliga namnen.
- Åtgärdar de lagrade procedurerna för att återgå till SCOPE_IDENTITY().
- Släpp de gamla, nu tomma tabellerna.
- Återställer SCHEMABINDING på indexerade vyer och återskapar klustrade index.
Du kan förmodligen undvika mycket av vyerna och triggers om du kan kontrollera all dataåtkomst genom lagrade procedurer, men eftersom det scenariot är sällsynt (och omöjligt att lita på till 100%), ska jag visa den svårare vägen.
Initialt schema
I ett försök att hålla detta tillvägagångssätt så enkelt som möjligt, samtidigt som vi tar itu med många av blockerarna jag nämnde tidigare i serien, låt oss anta att vi har det här schemat:
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
Så en enkel personaltabell, med en klustrad IDENTITY-kolumn, ett icke-klustrat index, en beräknad kolumn baserad på IDENTITY-kolumnen, en indexerad vy och en separat HR/dirt-tabell som har en främmande nyckel tillbaka till personaltabellen (I Jag uppmuntrar inte nödvändigtvis den designen, jag använder den bara för det här exemplet). Det här är alla saker som gör det här problemet mer komplicerat än det skulle vara om vi hade en fristående, oberoende tabell.
Med det schemat på plats har vi förmodligen några lagrade procedurer som gör saker som CRUD. Dessa är mer för dokumentationens skull än något annat; Jag kommer att göra ändringar i det underliggande schemat så att ändringen av dessa procedurer bör vara minimal. Detta för att simulera det faktum att det kanske inte är möjligt att ändra ad hoc SQL från dina applikationer och kanske inte är nödvändigt (nåja, så länge du inte använder en ORM som kan upptäcka tabell kontra vy).
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
Låt oss nu lägga till 5 rader med data till de ursprungliga tabellerna:
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';
Steg 1 – nya tabeller
Här skapar vi ett nytt tabellpar som speglar originalen förutom datatypen för EmployeeID-kolumnerna, det initiala fröet för IDENTITY-kolumnen och ett tillfälligt suffix på namnen:
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 );
Steg 2 – fixa procedurparametrar
Procedurerna här (och eventuellt din ad hoc-kod, såvida den inte redan använder den större heltalstypen) kommer att behöva en mycket mindre ändring så att de i framtiden kommer att kunna acceptera EmployeeID-värden utanför de övre gränserna för ett heltal. Även om du kan hävda att om du ska ändra dessa procedurer, kan du helt enkelt peka dem mot de nya tabellerna, jag försöker hävda att du kan uppnå det slutliga målet med *minimalt* intrång i det befintliga, permanenta kod.
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
Steg 3 – visningar och utlösare
Tyvärr kan detta inte *allt* göras tyst. Vi kan göra de flesta operationerna parallellt och utan att påverka samtidig användning, men på grund av SCHEMABINDING måste den indexerade vyn ändras och indexet senare återskapas.
Detta gäller för alla andra objekt som använder SCHEMABINDING och refererar till någon av våra tabeller. Jag rekommenderar att du ändrar den till en icke-indexerad vy i början av operationen och att du bara bygger om indexet en gång efter att all data har migrerats, snarare än flera gånger i processen (eftersom tabeller kommer att bytas om flera gånger). Vad jag faktiskt ska göra är att ändra synen så att den nya och gamla versionen av tabellen Anställda förenas under hela processen.
En annan sak vi behöver göra är att ändra den lagrade proceduren Employee_Add till att använda @@IDENTITY istället för SCOPE_IDENTITY(), tillfälligt. Detta beror på att INSTEAD OF-utlösaren som kommer att hantera nya uppdateringar av "Anställda" inte kommer att ha synlighet för SCOPE_IDENTITY()-värdet. Detta förutsätter naturligtvis att tabellerna inte har efterutlösare som kommer att påverka @@IDENTITY. Förhoppningsvis kan du antingen ändra dessa frågor i en lagrad procedur (där du helt enkelt kan peka INSERT mot den nya tabellen), eller så behöver din applikationskod inte förlita sig på SCOPE_IDENTITY() i första hand.
Vi kommer att göra detta under SERIALIZABLE så att inga transaktioner försöker smyga sig in medan objekten är i flux. Det här är en uppsättning operationer som till stor del endast är metadata, så det borde vara snabbt.
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;
Steg 4 – Migrera gamla data till en ny tabell
Vi kommer att migrera data i bitar för att minimera påverkan på både samtidighet och transaktionsloggen, och lånar den grundläggande tekniken från ett gammalt inlägg av mig, "Dela upp stora raderingsoperationer i bitar." Vi kommer också att köra dessa batcher i SERIALIZABLE, vilket betyder att du vill vara försiktig med batchstorleken, och jag har utelämnat felhantering för att förenkla.
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;
Resultat:
Se raderna migrera en efter en
När som helst under den sekvensen kan du testa infogningar, uppdateringar och borttagningar, och de bör hanteras på rätt sätt. När migreringen är klar kan du gå vidare till resten av processen.
Steg 5 – Rensa
En serie steg krävs för att rensa upp de objekt som skapades tillfälligt och för att återställa Employees / EmployeeFile som riktiga, förstklassiga medborgare. Många av dessa kommandon är helt enkelt metadataoperationer – med undantag för att skapa det klustrade indexet i den indexerade vyn, bör de alla vara omedelbara.
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
Vid denna tidpunkt bör allt vara tillbaka till normal drift, även om du kanske vill överväga typiska underhållsaktiviteter efter stora schemaändringar, såsom uppdatering av statistik, återuppbyggnad av index eller vräkning av planer från cachen.
Slutsats
Detta är en ganska komplex lösning på vad som borde vara ett enkelt problem. Jag hoppas att SQL Server vid något tillfälle gör det möjligt att göra saker som att lägga till/ta bort IDENTITY-egenskapen, bygga om index med nya måldatatyper och ändra kolumner på båda sidor av en relation utan att offra relationen. Under tiden skulle jag vara intresserad av att höra om antingen den här lösningen hjälper dig eller om du har ett annat tillvägagångssätt.
Stort shout-out till James Lupolt (@jlupoltsql) för att han hjälpte förnuftet att kontrollera mitt förhållningssätt och satte det på det ultimata testet på ett av hans egna, riktiga bord. (Det gick bra. Tack James!)
—
[ Del 1 | Del 2 | Del 3 | Del 4 ]