sql >> Databasteknik >  >> RDS >> Database

Minimera effekten av att bredda en IDENTITY-kolumn – del 4

[ 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:

  1. Skapa skuggkopior av tabellerna, med rätt datatyper.
  2. Ä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.)
  3. 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.
  4. Migrera gamla data till de nya tabellerna i bitar.
  5. 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 ]


  1. En introduktion till Databas High Availability för MySQL &MariaDB

  2. MariaDB ROWNUM() Förklarad

  3. Alibaba moln

  4. Begränsning av främmande nyckel kan orsaka cykler eller flera kaskadvägar?