sql >> Databáze >  >> RDS >> Database

Minimalizace dopadu rozšíření sloupce IDENTITY – část 4

[ Část 1 | Část 2 | Část 3 | Část 4 ]

V části 3 této série jsem ukázal dvě řešení, jak se vyhnout rozšíření IDENTITY sloupec – jeden, který vám jednoduše kupuje čas, a druhý, který opouští IDENTITY celkem. První vám brání řešit externí závislosti, jako jsou cizí klíče, ale druhý stále tento problém neřeší. V tomto příspěvku jsem chtěl podrobně popsat přístup, který bych zvolil, kdybych nutně potřeboval přejít na bigint , potřeboval minimalizovat prostoje a měl spoustu času na plánování.

Kvůli všem potenciálním blokátorům a potřebě minimálního narušení může být tento přístup považován za trochu složitý a stává se ještě složitějším, pokud se používají další exotické funkce (řekněme dělení, OLTP v paměti nebo replikace) .

Na velmi vysoké úrovni je přístup k vytvoření sady stínových tabulek, kde jsou všechny vložené položky směrovány do nové kopie tabulky (s větším datovým typem) a existence těchto dvou sad tabulek je transparentní co nejvíce pro aplikaci a její uživatele.

Na podrobnější úrovni by sada kroků vypadala takto:

  1. Vytvářejte stínové kopie tabulek se správnými datovými typy.
  2. Upravte uložené procedury (nebo ad hoc kód) tak, aby se pro parametry používal bigint. (To může vyžadovat úpravu nad rámec seznamu parametrů, jako jsou místní proměnné, dočasné tabulky atd., ale v tomto případě tomu tak není.)
  3. Přejmenujte staré tabulky a vytvořte zobrazení s těmito názvy, které spojí staré a nové tabulky.
    • Tato zobrazení budou mít namísto spouštěčů správné nasměrování operací DML do příslušných tabulek, aby bylo možné data během migrace stále upravovat.
    • To také vyžaduje, aby bylo SCHEMABINDING zrušeno ze všech indexovaných zobrazení, existující pohledy měly sjednocení mezi novými a starými tabulkami a aby byly upraveny procedury založené na SCOPE_IDENTITY().
  4. Migrujte stará data do nových tabulek po částech.
  5. Vyčištění, které se skládá z:
    • Odstranění dočasných zobrazení (které MÍSTO spouštěčů zruší).
    • Přejmenování nových tabulek zpět na původní názvy.
    • Oprava uložených procedur pro návrat k SCOPE_IDENTITY().
    • Odstranění starých, nyní prázdných tabulek.
    • Uvedení SCHEMABINDING zpět na indexovaná zobrazení a opětovné vytvoření seskupených indexů.

Pravděpodobně se můžete vyhnout mnoha pohledům a spouštěčům, pokud můžete řídit veškerý přístup k datům prostřednictvím uložených procedur, ale protože tento scénář je vzácný (a nelze mu věřit na 100 %), ukážu vám těžší cestu.

Počáteční schéma

Ve snaze zachovat tento přístup co nejjednodušší a zároveň se stále zabývat mnoha blokátory, které jsem zmínil dříve v sérii, předpokládejme, že máme toto schéma:

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

Takže jednoduchá personální tabulka s seskupeným sloupcem IDENTITY, neshlukovaným indexem, vypočítaným sloupcem založeným na sloupci IDENTITY, indexovaným pohledem a samostatnou tabulkou HR/špína, která má cizí klíč zpět do personální tabulky (I nemusím nutně podporovat tento design, jen jej používám pro tento příklad). To všechno jsou věci, které tento problém komplikují, než kdybychom měli samostatnou nezávislou tabulku.

S tímto schématem máme pravděpodobně nějaké uložené procedury, které dělají věci jako CRUD. Jsou to spíše pro dokumentaci než cokoli jiného; Udělám změny v základním schématu tak, aby změny těchto postupů byly minimální. To má simulovat skutečnost, že změna ad hoc SQL z vašich aplikací nemusí být možná a nemusí být nezbytná (dobře, pokud nepoužíváte ORM, který dokáže detekovat tabulku vs. pohled).

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

Nyní přidejte 5 řádků dat do původních tabulek:

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';

Krok 1 – nové tabulky

Zde vytvoříme novou dvojici tabulek, která bude zrcadlit originály s výjimkou datového typu sloupců ČísloZaměstnance, počátečního zdroje pro sloupec IDENTITA a dočasné přípony u jmen:

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
);

Krok 2 – opravte parametry procedury

Zde uvedené procedury (a potenciálně váš ad hoc kód, pokud již nepoužívá typ většího celého čísla) budou vyžadovat velmi malou změnu, aby v budoucnu mohly přijímat hodnoty EmployeeID za horní hranicí celého čísla. I když byste mohli tvrdit, že pokud se chystáte změnit tyto postupy, můžete je jednoduše nasměrovat na nové tabulky, snažím se tvrdit, že konečného cíle můžete dosáhnout s *minimálním* zásahem do stávajícího, trvalého kód.

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

Krok 3 – zobrazení a spouštění

Bohužel to nelze *všechno* provést potichu. Většinu operací můžeme provádět paralelně a bez ovlivnění souběžného použití, ale kvůli SCHEMABINDING musí být indexovaný pohled změněn a index později znovu vytvořen.

To platí pro všechny ostatní objekty, které používají SCHEMABINDING a odkazují na některou z našich tabulek. Doporučuji jej změnit na neindexovaný pohled na začátku operace a index pouze znovu vytvořit jednou po migraci všech dat, nikoli několikrát v procesu (protože tabulky budou přejmenovány několikrát). Ve skutečnosti to, co udělám, je změnit pohled na spojení nové a staré verze tabulky Zaměstnanci po dobu trvání procesu.

Jedna další věc, kterou musíme udělat, je změnit uloženou proceduru Employee_Add tak, aby dočasně používala @@IDENTITY namísto SCOPE_IDENTITY(). Důvodem je, že spouštěč NAMÍSTO OF, který bude zpracovávat nové aktualizace „Zaměstnanci“, nebude mít viditelnost hodnoty SCOPE_IDENTITY(). To samozřejmě předpokládá, že tabulky nemají po triggery, které ovlivní @@IDENTITY. Doufejme, že tyto dotazy můžete buď změnit v uložené proceduře (kde byste mohli jednoduše namířit INSERT na novou tabulku), nebo kód vaší aplikace nemusí v první řadě spoléhat na SCOPE_IDENTITY().

Uděláme to pod SERIALIZABLE, aby se žádné transakce nepokoušely proniknout dovnitř, když jsou objekty v pohybu. Jedná se o sadu operací převážně pouze s metadaty, takže by to mělo být rychlé.

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;

Krok 4 – Migrace starých dat do nové tabulky

Budeme migrovat data po kouscích, abychom minimalizovali dopad na souběžnost i protokol transakcí, přičemž si vypůjčíme základní techniku ​​z mého starého příspěvku „Rozdělit velké operace mazání na kousky“. Tyto dávky budeme provádět také v SERIALIZABLE, což znamená, že budete chtít být opatrní s velikostí dávky a kvůli stručnosti jsem vynechal zpracování chyb.

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;

Výsledky:

Podívejte se, jak se řádky migrují jeden po druhém

Kdykoli během této sekvence můžete otestovat vložení, aktualizace a odstranění a mělo by se s nimi zacházet odpovídajícím způsobem. Po dokončení migrace můžete přejít ke zbytku procesu.

Krok 5 – Vyčištění

Je vyžadována řada kroků k vyčištění objektů, které byly dočasně vytvořeny, a k obnovení Employees / EmployeeFile jako správných, prvotřídních občanů. Většina těchto příkazů jsou jednoduše operace s metadaty – s výjimkou vytváření seskupeného indexu v indexovaném pohledu by měly být všechny okamžité.

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

V tuto chvíli by se vše mělo vrátit do normálního provozu, i když možná budete chtít zvážit typické činnosti údržby po velkých změnách schématu, jako je aktualizace statistik, přestavba indexů nebo vyřazení plánů z mezipaměti.

Závěr

Toto je docela složité řešení toho, co by měl být jednoduchý problém. Doufám, že v určitém okamžiku SQL Server umožní dělat věci, jako je přidání/odebrání vlastnosti IDENTITY, opětovné sestavení indexů s novými cílovými datovými typy a změna sloupců na obou stranách vztahu bez obětování vztahu. Mezitím by mě zajímalo, jestli vám toto řešení pomůže, nebo jestli máte jiný přístup.

Velká výzva Jamesi Lupoltovi (@jlupoltsql) za to, že mi pomohl prověřit můj přístup zdravým rozumem a podrobil ho konečné zkoušce na jednom z jeho vlastních skutečných stolů. (Dopadlo to dobře. Díky Jamesi!)

[ Část 1 | Část 2 | Část 3 | Část 4 ]


  1. Jak rozdělit okno dotazu v SQL Server Management Studio (SSMS) – SQL Server / TSQL výukový program, část 13

  2. Spark Dataframes UPSERT to Postgres Table

  3. Co může způsobit občasné chyby ORA-12519 (TNS:nebyla nalezena žádná vhodná obsluha).

  4. Typ data bez času v Oracle