sql >> Database >  >> RDS >> Database

Het minimaliseren van de impact van het verbreden van een IDENTITEIT-kolom - deel 4

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

  1. Maak schaduwkopieën van de tabellen, met de juiste gegevenstypen.
  2. 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.)
  3. 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.
  4. Migreer de oude gegevens in delen naar de nieuwe tabellen.
  5. 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 ]


  1. Afbeeldingen toewijzen aan Tree View-knooppunten-2

  2. Een array invoegen met behulp van Sequel gem in PostgreSQL

  3. Hoe u alle weergaven in een PostgreSQL-database kunt weergeven

  4. Wijzig het datumformaat voor de huidige sessie in SQL Server