Database
 sql >> Baza danych >  >> RDS >> Database

Minimalizowanie wpływu poszerzenia kolumny TOŻSAMOŚĆ – część 4

[ Część 1 | Część 2 | Część 3 | Część 4 ]

W części 3 tej serii pokazałem dwa obejścia, aby uniknąć poszerzenia IDENTITY kolumna – jedna, która po prostu kupuje Ci czas, i druga, która porzuca IDENTITY całkowicie. To pierwsze zapobiega konieczności zajmowania się zależnościami zewnętrznymi, takimi jak klucze obce, ale drugie nadal nie rozwiązuje tego problemu. W tym poście chciałem szczegółowo opisać podejście, które zastosowałbym, gdybym absolutnie musiał przejść do bigint , potrzebne do zminimalizowania przestojów i miały dużo czasu na planowanie.

Ze względu na wszystkie potencjalne blokery i potrzebę minimalnych zakłóceń, podejście może być postrzegane jako nieco złożone i staje się bardziej skomplikowane, jeśli używane są dodatkowe egzotyczne funkcje (powiedzmy, partycjonowanie, OLTP w pamięci lub replikacja) .

Na bardzo wysokim poziomie podejście polega na stworzeniu zestawu tabel cieni, gdzie wszystkie wstawki kierowane są do nowej kopii tabeli (z większym typem danych), a istnienie dwóch zestawów tabel jest równie przejrzyste jak to możliwe dla aplikacji i jej użytkowników.

Na bardziej szczegółowym poziomie zestaw kroków byłby następujący:

  1. Utwórz kopie w tle tabel z odpowiednimi typami danych.
  2. Zmień procedury składowane (lub kod ad hoc), aby używać bigint dla parametrów. (Może to wymagać modyfikacji poza listą parametrów, takich jak zmienne lokalne, tabele tymczasowe itp., ale w tym przypadku tak nie jest.)
  3. Zmień nazwy starych tabel i utwórz widoki o tych nazwach, które łączą stare i nowe tabele.
    • Te widoki będą miały zamiast wyzwalaczy odpowiednie kierowanie operacji DML do odpowiednich tabel, dzięki czemu dane mogą być nadal modyfikowane podczas migracji.
    • Wymaga to również usunięcia SCHEMABINDING ze wszystkich indeksowanych widoków, istniejących widoków, aby mieć połączenie między nowymi i starymi tabelami, a także modyfikacji procedur opartych na SCOPE_IDENTITY().
  4. Przenieś stare dane do nowych tabel porcjami.
  5. Sprzątanie, składające się z:
    • Porzucanie tymczasowych widoków (co spowoduje porzucenie wyzwalaczy ZAMIAST).
    • Zmienianie nazw nowych tabel z powrotem na oryginalne.
    • Naprawienie procedur składowanych, aby przywrócić SCOPE_IDENTITY().
    • Upuszczanie starych, teraz pustych tabel.
    • Przywracanie SCHEMABINDING do zindeksowanych widoków i ponowne tworzenie indeksów klastrowych.

Prawdopodobnie możesz uniknąć wielu widoków i wyzwalaczy, jeśli możesz kontrolować dostęp do wszystkich danych za pomocą procedur składowanych, ale ponieważ ten scenariusz jest rzadki (i nie można ufać w 100%), pokażę trudniejszą trasę.

Początkowy schemat

Aby to podejście było jak najprostsze, jednocześnie rozwiązując wiele blokerów, o których wspomniałem wcześniej w serii, załóżmy, że mamy następujący 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

Tak więc prosta tabela personelu z klastrowaną kolumną IDENTITY, indeksem nieklastrowym, kolumną obliczoną opartą na kolumnie IDENTITY, widokiem indeksowanym i oddzielną tabelą HR/brud, która ma klucz obcy z powrotem do tabeli personelu (I niekoniecznie zachęcam do tego projektu, po prostu używam go w tym przykładzie). To wszystko sprawia, że ​​problem jest bardziej skomplikowany, niż byłoby, gdybyśmy mieli samodzielną, niezależną tabelę.

Mając ten schemat, prawdopodobnie mamy pewne procedury składowane, które wykonują takie rzeczy, jak CRUD. Są to bardziej ze względu na dokumentację niż cokolwiek; Zamierzam wprowadzić zmiany w podstawowym schemacie tak, aby zmiana tych procedur była minimalna. Ma to na celu symulację faktu, że zmiana ad hoc SQL z aplikacji może nie być możliwa i może nie być konieczna (no cóż, o ile nie używasz ORM, który może wykrywać tabelę w porównaniu z widokiem).

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

Teraz dodajmy 5 wierszy danych do oryginalnych tabel:

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 – nowe tabele

Tutaj utworzymy nową parę tabel, odzwierciedlającą oryginały z wyjątkiem typu danych kolumn EmployeeID, początkowego inicjatora kolumny IDENTITY i tymczasowego przyrostka nazw:

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 – napraw parametry procedury

Procedury tutaj (i potencjalnie twój kod ad hoc, chyba że używa już większego typu liczb całkowitych) będą wymagały bardzo niewielkiej zmiany, aby w przyszłości były w stanie akceptować wartości EmployeeID wykraczające poza górne granice liczby całkowitej. Chociaż możesz argumentować, że jeśli zamierzasz zmienić te procedury, możesz po prostu wskazać je na nowe tabele, staram się udowodnić, że możesz osiągnąć ostateczny cel przy *minimalnej* ingerencji w istniejące, stałe 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

Krok 3 – widoki i wyzwalacze

Niestety, nie można *wszystko* zrobić po cichu. Większość operacji możemy wykonywać równolegle i bez wpływu na współbieżne użycie, ale ze względu na SCHEMABINDING, indeksowany widok musi zostać zmieniony, a indeks później odtworzony.

Odnosi się to do wszystkich innych obiektów, które używają SCHEMABINDING i odwołują się do jednej z naszych tabel. Polecam zmienić go na widok nieindeksowany na początku operacji i po prostu odbudować indeks raz po migracji wszystkich danych, a nie wielokrotnie w trakcie procesu (ponieważ nazwy tabel będą wielokrotnie zmieniane). W rzeczywistości zamierzam zmienić widok, aby połączyć nową i starą wersję tabeli Pracownicy na czas trwania procesu.

Jeszcze jedna rzecz, którą musimy zrobić, to zmienić procedurę składowaną Employee_Add tak, aby tymczasowo używała @@IDENTITY zamiast SCOPE_IDENTITY(). Dzieje się tak, ponieważ wyzwalacz INSTEAD OF, który będzie obsługiwał nowe aktualizacje „Pracowników”, nie będzie miał widoczności wartości SCOPE_IDENTITY(). To oczywiście zakłada, że ​​tabele nie mają wyzwalaczy after, które będą miały wpływ na @@IDENTITY. Miejmy nadzieję, że możesz zmienić te zapytania wewnątrz procedury składowanej (gdzie możesz po prostu wskazać INSERT na nową tabelę) lub kod aplikacji nie musi w pierwszej kolejności polegać na SCOPE_IDENTITY().

Zrobimy to w opcji SERIALIZABLE, aby żadne transakcje nie próbowały się wkraść, gdy obiekty są w ruchu. Jest to zestaw operacji w dużej mierze opartych wyłącznie na metadanych, więc powinien być szybki.

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 – Przenieś stare dane do nowej tabeli

Zamierzamy migrować dane porcjami, aby zminimalizować wpływ zarówno na współbieżność, jak i dziennik transakcji, zapożyczając podstawową technikę ze starego mojego posta „Rozbijaj duże operacje usuwania na porcje”. Zamierzamy wykonać te partie również w SERIALIZABLE, co oznacza, że ​​będziesz chciał być ostrożny z rozmiarem partii, a dla zwięzłości pominąłem obsługę błędów.

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;

Wyniki:

Zobacz migrację wierszy jeden po drugim

W dowolnym momencie tej sekwencji możesz testować wstawki, aktualizacje i usunięcia i należy je odpowiednio obsługiwać. Po zakończeniu migracji możesz przejść do pozostałej części procesu.

Krok 5 – Posprzątaj

Wymagana jest seria kroków, aby wyczyścić obiekty, które zostały tymczasowo utworzone i przywrócić Employees / EmployeeFile jako właściwych obywateli pierwszej klasy. Wiele z tych poleceń to po prostu operacje na metadanych – z wyjątkiem tworzenia indeksu klastrowego w widoku indeksowanym, wszystkie powinny być natychmiastowe.

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

W tym momencie wszystko powinno wrócić do normalnego działania, chociaż warto rozważyć typowe czynności konserwacyjne po poważnych zmianach w schemacie, takie jak aktualizowanie statystyk, odbudowywanie indeksów lub usuwanie planów z pamięci podręcznej.

Wniosek

To dość złożone rozwiązanie tego, co powinno być prostym problemem. Mam nadzieję, że w pewnym momencie SQL Server umożliwi takie rzeczy jak dodanie/usunięcie właściwości IDENTITY, przebudowanie indeksów z nowymi docelowymi typami danych i zmianę kolumn po obu stronach relacji bez poświęcania relacji. W międzyczasie chciałbym usłyszeć, czy to rozwiązanie Ci pomoże, czy też masz inne podejście.

Wielkie podziękowania dla Jamesa Lupolta (@jlupoltsql) za pomoc w sprawdzeniu mojego podejścia i poddaniu go ostatecznemu testowi na jednym z jego własnych, prawdziwych stołów. (Poszło dobrze. Dzięki James!)

[ Część 1 | Część 2 | Część 3 | Część 4 ]


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Używanie ODBC z Salesforce i Active Directory Federation Services (ADFS) Single Sign On (SSO)

  2. Połącz aplikacje ODBC w systemie Windows z Zoho CRM

  3. 12 najczęściej używanych operatorów SQL

  4. Jak obliczyć współczynnik retencji w SQL?

  5. Historia poleceń w isql