[ Deel 1 | Deel 2 | Deel 3 | Deel 4 ]
In deel 3 van deze serie liet ik twee oplossingen zien om te voorkomen dat een IDENTITY . groter wordt kolom – een die je gewoon tijd kost, en een andere die IDENTITY verlaat allemaal samen. De eerste voorkomt dat je te maken krijgt met externe afhankelijkheden zoals externe sleutels, maar de laatste lost dat probleem nog steeds niet op. In dit bericht wilde ik de aanpak beschrijven die ik zou volgen als ik absoluut naar bigint moest verhuizen. , moest de uitvaltijd tot een minimum beperken en had voldoende tijd om te plannen.
Vanwege alle potentiële blokkers en de noodzaak van minimale verstoring, kan de aanpak als een beetje complex worden beschouwd, en het wordt alleen maar meer als extra exotische functies worden gebruikt (bijvoorbeeld partitionering, In-Memory OLTP of replicatie) .
Op een zeer hoog niveau is de benadering om een set schaduwtabellen te maken, waarbij alle invoegingen naar een nieuwe kopie van de tabel worden geleid (met het grotere gegevenstype), en het bestaan van de twee sets tabellen is even transparant mogelijk aan de applicatie en zijn gebruikers.
Op een meer gedetailleerd niveau zou de reeks stappen als volgt zijn:
- Maak schaduwkopieën van de tabellen, met de juiste gegevenstypen.
- Wijzig de opgeslagen procedures (of ad-hoccode) om bigint voor parameters te gebruiken. (Hierdoor kunnen wijzigingen nodig zijn die verder gaan dan de parameterlijst, zoals lokale variabelen, tijdelijke tabellen, enz., maar dit is hier niet het geval.)
- Hernoem de oude tabellen en maak weergaven met die namen die de oude en nieuwe tabellen verenigen.
- Die weergaven hebben in plaats van triggers om DML-bewerkingen op de juiste manier naar de juiste tabel(len) te leiden, zodat gegevens nog steeds kunnen worden gewijzigd tijdens de migratie.
- Hiervoor moet ook SCHEMABINDING worden verwijderd uit geïndexeerde weergaven, bestaande weergaven moeten verbindingen hebben tussen nieuwe en oude tabellen en procedures die afhankelijk zijn van SCOPE_IDENTITY() moeten worden gewijzigd.
- Migreer de oude gegevens in delen naar de nieuwe tabellen.
- Opruimen, bestaande uit:
- De tijdelijke weergaven verwijderen (waardoor de IN PLAATS VAN triggers worden verwijderd).
- De nieuwe tabellen hernoemen naar de originele namen.
- De opgeslagen procedures herstellen om terug te keren naar SCOPE_IDENTITY().
- De oude, nu lege tafels laten vallen.
- SCHEMABINDING terugzetten op geïndexeerde weergaven en opnieuw geclusterde indexen maken.
U kunt waarschijnlijk veel van de weergaven en triggers vermijden als u alle gegevenstoegang kunt regelen via opgeslagen procedures, maar aangezien dat scenario zeldzaam is (en onmogelijk 100% te vertrouwen), zal ik de moeilijkere route laten zien.
Initieel schema
Laten we aannemen dat we dit schema hebben om deze aanpak zo eenvoudig mogelijk te houden, terwijl we toch veel van de blokkers aanpakken die ik eerder in de serie noemde:
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 Dus een eenvoudige personeelstabel, met een geclusterde IDENTITY-kolom, een niet-geclusterde index, een berekende kolom op basis van de IDENTITY-kolom, een geïndexeerde weergave en een afzonderlijke HR/vuiltabel met een externe sleutel terug naar de personeelstabel (ik ik moedig dat ontwerp niet per se aan, ik gebruik het alleen voor dit voorbeeld). Dit zijn allemaal dingen die dit probleem ingewikkelder maken dan het zou zijn als we een op zichzelf staande, onafhankelijke tabel hadden.
Met dat schema hebben we waarschijnlijk enkele opgeslagen procedures die dingen als CRUD doen. Deze zijn meer ter documentatie dan wat dan ook; Ik ga wijzigingen aanbrengen in het onderliggende schema, zodat het wijzigen van deze procedures minimaal zou moeten zijn. Dit is om het feit te simuleren dat het wijzigen van ad-hoc SQL vanuit uw applicaties misschien niet mogelijk is, en misschien niet nodig is (nou ja, zolang u geen ORM gebruikt die tabel versus weergave kan detecteren).
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 Laten we nu 5 rijen met gegevens toevoegen aan de originele tabellen:
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';
Stap 1 – nieuwe tabellen
Hier maken we een nieuw paar tabellen, waarbij de originelen worden gespiegeld, behalve het gegevenstype van de EmployeeID-kolommen, de initiële seed voor de IDENTITY-kolom en een tijdelijk achtervoegsel op de 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
); Stap 2 – procedureparameters corrigeren
De procedures hier (en mogelijk uw ad-hoccode, tenzij deze al het grotere integer-type gebruikt) zullen een zeer kleine wijziging nodig hebben, zodat ze in de toekomst EmployeeID-waarden boven de bovengrenzen van een geheel getal kunnen accepteren. Hoewel je zou kunnen beweren dat als je deze procedures gaat wijzigen, je ze gewoon naar de nieuwe tafels kunt wijzen, probeer ik te beweren dat je het uiteindelijke doel kunt bereiken met *minimale* inbreuk op de bestaande, permanente 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 Stap 3 – weergaven en triggers
Helaas kan dit niet *alles* stilzwijgend worden gedaan. We kunnen de meeste bewerkingen parallel uitvoeren zonder het gelijktijdig gebruik te beïnvloeden, maar vanwege de SCHEMABINDING moet de geïndexeerde weergave worden gewijzigd en de index later opnieuw worden gemaakt.
Dit geldt voor alle andere objecten die SCHEMABINDING gebruiken en verwijzen naar een van onze tabellen. Ik raad aan om het aan het begin van de bewerking te wijzigen in een niet-geïndexeerde weergave en de index slechts één keer opnieuw op te bouwen nadat alle gegevens zijn gemigreerd, in plaats van meerdere keren in het proces (aangezien tabellen meerdere keren worden hernoemd). Wat ik in feite ga doen, is de weergave wijzigen om de nieuwe en oude versies van de tabel Werknemers voor de duur van het proces samen te voegen.
Een ander ding dat we moeten doen, is de opgeslagen procedure Employee_Add wijzigen om tijdelijk @@IDENTITY te gebruiken in plaats van SCOPE_IDENTITY(). Dit komt omdat de INSTEAD OF-trigger die nieuwe updates voor "Werknemers" afhandelt, geen zichtbaarheid heeft van de SCOPE_IDENTITY()-waarde. Dit veronderstelt natuurlijk dat de tabellen geen after-triggers hebben die van invloed zijn op @@IDENTITY. Hopelijk kunt u deze query's wijzigen in een opgeslagen procedure (waarbij u de INSERT eenvoudig naar de nieuwe tabel kunt wijzen), of uw toepassingscode hoeft in de eerste plaats niet op SCOPE_IDENTITY() te vertrouwen.
We gaan dit doen onder SERIALIZABLE, zodat er geen transacties proberen binnen te sluipen terwijl de objecten in beweging zijn. Dit is een reeks grotendeels alleen metadata-bewerkingen, dus het zou snel moeten gaan.
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; Stap 4 – Migreer oude gegevens naar nieuwe tabel
We gaan gegevens in brokken migreren om de impact op zowel gelijktijdigheid als het transactielogboek te minimaliseren, waarbij we de basistechniek lenen van een oude post van mij, "Break grote verwijderingsbewerkingen in brokken." We gaan deze batches ook in SERIALIZABLE uitvoeren, wat betekent dat je voorzichtig moet zijn met de batchgrootte, en ik heb de foutafhandeling voor de beknoptheid weggelaten.
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; Resultaten:
Bekijk de rijen één voor één migreren
Op elk moment tijdens die reeks kunt u invoegingen, updates en verwijderingen testen en deze moeten op de juiste manier worden behandeld. Zodra de migratie is voltooid, kunt u doorgaan naar de rest van het proces.
Stap 5 – Opruimen
Er is een reeks stappen nodig om de objecten die tijdelijk zijn gemaakt op te schonen en om Employees / EmployeeFile te herstellen als echte, eersteklas burgers. Veel van deze commando's zijn gewoon metadatabewerkingen - met uitzondering van het maken van de geclusterde index op de geïndexeerde weergave, zouden ze allemaal onmiddellijk moeten zijn.
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 Op dit punt zou alles weer normaal moeten werken, hoewel u misschien typische onderhoudsactiviteiten wilt overwegen na grote schemawijzigingen, zoals het bijwerken van statistieken, het opnieuw opbouwen van indexen of het verwijderen van plannen uit de cache.
Conclusie
Dit is een vrij complexe oplossing voor wat een eenvoudig probleem zou moeten zijn. Ik hoop dat SQL Server het op een gegeven moment mogelijk maakt om dingen te doen zoals het toevoegen/verwijderen van de IDENTITY-eigenschap, het opnieuw opbouwen van indexen met nieuwe doelgegevenstypen en het wijzigen van kolommen aan beide zijden van een relatie zonder de relatie op te offeren. In de tussentijd zou ik graag willen horen of deze oplossing je helpt, of dat je een andere aanpak hebt.
Een dikke pluim voor James Lupolt (@jlupoltsql) voor het helpen van mijn gezond verstand om mijn aanpak te controleren en het tot de ultieme test te brengen op een van zijn eigen, echte tafels. (Het ging goed. Bedankt James!)
—
[ Deel 1 | Deel 2 | Deel 3 | Deel 4 ]