Database
 sql >> Database >  >> RDS >> Database

Ridurre al minimo l'impatto dell'ampliamento di una colonna IDENTITY - parte 3

[ Parte 1 | Parte 2 | Parte 3 | Parte 4]

Finora in questa serie ho dimostrato l'impatto fisico diretto sulla pagina durante l'upsize da int a bigint , e quindi ha eseguito l'iterazione attraverso molti dei blocchi comuni per questa operazione. In questo post, ho voluto esaminare due potenziali soluzioni alternative:una semplice e una incredibilmente contorta.

Il modo più semplice

Sono stato derubato un po' del mio fulmine in un commento sul mio post precedente:Keith Monroe ha suggerito che potresti semplicemente riseminare il tavolo al negativo inferiore limite del tipo di dati intero, raddoppiando la capacità di nuovi valori. Puoi farlo con DBCC CHECKIDENT :

DBCC CHECKIDENT(N'dbo.TableName', RESEED, -2147483648);

Questo potrebbe funzionare, supponendo che i valori surrogati non abbiano significato per gli utenti finali (o, se lo fanno, che gli utenti non impazziranno vedendo improvvisamente numeri negativi). Suppongo che potresti ingannarli con una vista:

CREATE VIEW dbo.ViewName
AS
  SELECT ID = CONVERT(bigint, CASE WHEN ID < 0 
    THEN (2147483648*2) - 1 + CONVERT(bigint, ID)
    ELSE ID END) 
  FROM dbo.TableName;

Ciò significa che l'utente che ha aggiunto ID = -2147483648 vedrebbe effettivamente +2147483648 , l'utente che ha aggiunto ID = -2147483647 vedrebbe +2147483649 , e così via. Dovresti modificare l'altro codice per essere sicuro di eseguire il calcolo inverso quando l'utente passa quell'ID , ad es.

ALTER PROCEDURE dbo.GetRowByID
  @ID bigint
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @RealID bigint;
 
  SET @RealID = CASE WHEN @ID > 2147483647 
    THEN @ID - (2147483648*2) + 1
    ELSE @ID END;
 
  SELECT ID, @ID /*, other columns */ 
   FROM dbo.TableName 
   WHERE ID = @RealID;
END
GO

Non vado matto per questo offuscamento. Affatto. È disordinato, fuorviante e soggetto a errori. E incoraggia ad avere visibilità sulle chiavi surrogate, in genere IDENTITY i valori non dovrebbero essere esposti agli utenti finali, quindi a loro non dovrebbe importare se sono clienti 24, 642, -376 o numeri molto più grandi su entrambi i lati dello zero.

Questa "soluzione" presuppone anche che tu non abbia codice da nessuna parte che ordini per IDENTITY colonna per presentare per prime le righe inserite più di recente, o deduce che la IDENTITY più alta il valore deve essere la riga più recente. Codice che fa fare affidamento sull'ordinamento di IDENTITY colonna, in modo esplicito o implicito (che potrebbe essere più di quanto pensi se è l'indice cluster), non presenterà più le righe nell'ordine previsto:mostrerà tutte le righe create dopo il RESEED , iniziando dalla prima, e poi mostrerà tutte le righe create prima del RESEED , a cominciare dal primo.

Il vantaggio principale di questo approccio è che non è necessario modificare il tipo di dati e, di conseguenza, il RESEED change non richiede modifiche agli indici, ai vincoli o alle chiavi esterne in entrata.

Lo svantaggio - oltre alle modifiche al codice sopra menzionate, ovviamente - è che questo ti fa guadagnare tempo solo a breve termine. Alla fine esaurirai anche tutti i numeri interi negativi disponibili. E non pensare che questo raddoppi la vita utile dell'attuale versione della tabella in termini di tempo – in molti casi, la crescita dei dati sta accelerando, non rimanendo costante, quindi utilizzerai i 2 miliardi di righe successivi molto più velocemente dei primi 2 miliardi.

Un modo più difficile

Un altro approccio che potresti adottare è smettere di usare un IDENTITY colonna del tutto; invece potresti convertire usando una SEQUENCE . Potresti creare un nuovo bigint colonna, imposta il valore predefinito sul valore successivo da una SEQUENCE , aggiorna tutti questi valori con i valori della colonna originale (in batch se necessario), elimina la colonna originale e rinomina la nuova colonna. Creiamo questa tabella fittizia e inseriamo una sola riga:

CREATE TABLE dbo.SequenceDemo
(
  ID int IDENTITY(1,1),
  x char(1),
  CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID)
);
GO
 
INSERT dbo.SequenceDemo(x) VALUES('x');

Successivamente, creeremo una SEQUENCE che inizia appena oltre il limite superiore di un int:

CREATE SEQUENCE dbo.BeyondInt
AS bigint
START WITH 2147483648 INCREMENT BY 1;

Successivamente, le modifiche nella tabella necessarie per passare all'utilizzo della SEQUENCE per la nuova colonna:

BEGIN TRANSACTION;
 
-- add a new "identity" column:
ALTER TABLE dbo.SequenceDemo ADD ID2 bigint;
GO
 
-- set the new column equal to the existing identity values
-- for large tables, may need to do this in batches:
UPDATE dbo.SequenceDemo SET ID2 = ID;
 
-- now make it not nullable and add the default from our SEQUENCE:
ALTER TABLE dbo.SequenceDemo ALTER COLUMN ID2 bigint NOT NULL;
ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT DF_SD_Identity DEFAULT NEXT VALUE FOR dbo.BeyondInt FOR ID2;
 
-- need to drop the existing PK (and any indexes):
ALTER TABLE dbo.SequenceDemo DROP CONSTRAINT PK_SD_Identity;
 
-- drop the old column and rename the new one:
ALTER TABLE dbo.SequenceDemo DROP COLUMN ID;
EXEC sys.sp_rename N'dbo.SequenceDemo.ID2', N'ID', 'COLUMN';
 
-- now put the PK back up:
ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID);
 
COMMIT TRANSACTION;

In questo caso, l'inserimento successivo produrrebbe i seguenti risultati (notare che SCOPE_IDENTITY() non restituisce più un valore valido):

INSERT dbo.SequenceDemo(x) VALUES('y');
SELECT Si = SCOPE_IDENTITY();
SELECT ID, x FROM dbo.SequenceDemo;
 
/* results
 
Si
----
NULL
 
ID           x
----------   -
1            x
2147483648   y           */

Se la tabella è grande e devi aggiornare la nuova colonna in batch invece della transazione one-shot sopra, come ho descritto qui, consentendo agli utenti di interagire con la tabella nel frattempo, dovrai disporre di un trigger in atto per sovrascrivere la SEQUENCE valore per tutte le nuove righe inserite, in modo che continuino a corrispondere a ciò che viene restituito a qualsiasi codice chiamante. (Ciò presuppone anche che tu abbia ancora spazio nell'intervallo intero per continuare ad accettare alcuni aggiornamenti; altrimenti, se hai già esaurito l'intervallo, dovrai prenderti dei tempi di inattività o utilizzare la semplice soluzione sopra a breve termine .)

Lasciamo perdere tutto e ricominciamo, quindi aggiungiamo semplicemente la nuova colonna:

DROP TABLE dbo.SequenceDemo;
DROP SEQUENCE dbo.BeyondInt;
GO
 
CREATE TABLE dbo.SequenceDemo
(
  ID int IDENTITY(1,1),
  x char(1),
  CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID)
);
GO
 
INSERT dbo.SequenceDemo(x) VALUES('x');
GO
 
CREATE SEQUENCE dbo.BeyondInt
AS bigint
START WITH 2147483648 INCREMENT BY 1;
GO
 
ALTER TABLE dbo.SequenceDemo ADD ID2 bigint;
GO

Ed ecco il trigger che aggiungeremo:

CREATE TRIGGER dbo.After_SequenceDemo
ON dbo.SequenceDemo
AFTER INSERT
AS
BEGIN
  UPDATE sd SET sd.ID2 = sd.ID
    FROM dbo.SequenceDemo AS sd
    INNER JOIN inserted AS i
    ON sd.ID = i.ID;
END

Questa volta, l'inserimento successivo continuerà a generare righe nell'intervallo inferiore di interi per entrambe le colonne, fino a quando tutti i valori preesistenti non saranno stati aggiornati e il resto delle modifiche non saranno state salvate:

INSERT dbo.SequenceDemo(x) VALUES('y');
SELECT Si = SCOPE_IDENTITY();
SELECT ID, ID2, x FROM dbo.SequenceDemo;
 
/* results
 
Si
----
2
 
ID    ID2   x
----  ----  --
1     NULL  x
2     2     y          */

Ora possiamo continuare ad aggiornare l'ID2 esistente valori mentre le nuove righe continuano a essere inserite nell'intervallo inferiore:

SET NOCOUNT ON;
 
DECLARE @r INT = 1;
 
WHILE @r > 0
BEGIN
  BEGIN TRANSACTION;
 
  UPDATE TOP (10000)
    dbo.SequenceDemo
    SET ID2 = ID WHERE ID2 IS NULL;
 
  SET @r = @@ROWCOUNT;
 
  COMMIT TRANSACTION;
 
  -- CHECKPOINT;    -- if simple
  -- BACKUP LOG ... -- if full
END

Dopo aver aggiornato tutte le righe esistenti, possiamo continuare con il resto delle modifiche e quindi rilasciare il trigger:

BEGIN TRANSACTION;
ALTER TABLE dbo.SequenceDemo ALTER COLUMN ID2 BIGINT NOT NULL;
ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT DF_SD_Identity DEFAULT NEXT VALUE FOR dbo.BeyondInt FOR ID2;
ALTER TABLE dbo.SequenceDemo DROP CONSTRAINT PK_SD_Identity;
ALTER TABLE dbo.SequenceDemo DROP COLUMN ID;
EXEC sys.sp_rename N'dbo.SequenceDemo.ID2', N'ID', 'COLUMN';
ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID);
DROP TRIGGER dbo.InsteadOf_SequenceDemo
COMMIT TRANSACTION;

Ora, il prossimo inserto genererà questi valori:

INSERT dbo.SequenceDemo(x) VALUES('z');
SELECT Si = SCOPE_IDENTITY();
SELECT ID, x FROM dbo.SequenceDemo;
 
/* results
 
Si
----
NULL
 
ID            x
----------    -
1             x
2             y
2147483648    z          */

Se hai un codice che si basa su SCOPE_IDENTITY() , @@IDENTITY o IDENT_CURRENT() , dovrebbe anche cambiare, poiché quei valori non vengono più popolati dopo un inserimento, sebbene OUTPUT La clausola dovrebbe continuare a funzionare correttamente nella maggior parte degli scenari. Se hai bisogno del tuo codice per continuare a credere che la tabella generi un IDENTITY valore, quindi potresti utilizzare un trigger per falsificarlo, tuttavia sarebbe solo in grado di popolare @@IDENTITY all'inserimento, non SCOPE_IDENTITY() . Questo potrebbe comunque richiedere modifiche, perché nella maggior parte dei casi non vuoi fare affidamento su @@IDENTITY per qualsiasi cosa (quindi, se hai intenzione di apportare modifiche, rimuovi tutte le ipotesi su un IDENTITY colonna).

CREATE TRIGGER dbo.FakeIdentity
ON dbo.SequenceDemo
INSTEAD OF INSERT
AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @lowestID bigint = (SELECT MIN(id) FROM inserted);
  DECLARE @sql nvarchar(max) = N'DECLARE @foo TABLE(ID bigint IDENTITY(' 
    + CONVERT(varchar(32), @lowestID) + N',1));';
  SELECT @sql += N'INSERT @foo DEFAULT VALUES;' FROM inserted;
  EXEC sys.sp_executesql @sql;
  INSERT dbo.SequenceDemo(ID, x) SELECT ID, x FROM inserted;
END

Ora, il prossimo inserto genererà questi valori:

INSERT dbo.SequenceDemo(x) VALUES('a');
SELECT Si = SCOPE_IDENTITY(), Ident = @@IDENTITY;
SELECT ID, x FROM dbo.SequenceDemo;
 
/* results
 
Si      Ident
----    -----
NULL    2147483649
 
ID            x
----------    -
1             x
2             y
2147483648    z
2147483649    a         */

Con questa soluzione alternativa, dovresti comunque gestire altri vincoli, indici e tabelle con chiavi esterne in ingresso. I vincoli e gli indici locali sono piuttosto semplici, ma tratterò la situazione più complessa con le chiavi esterne nella prossima parte di questa serie.

Uno che non funzionerà, ma vorrei che lo facesse

ALTER TABLE SWITCH può essere un modo molto efficace per apportare alcune modifiche ai metadati che altrimenti sarebbero difficili da realizzare. E contrariamente alla credenza popolare, questo non implica solo il partizionamento e non è limitato all'edizione Enterprise. Il codice seguente funzionerà su Express ed è un metodo utilizzato dalle persone per aggiungere o rimuovere IDENTITY proprietà su un tavolo (di nuovo, senza tenere conto delle chiavi esterne e di tutti quegli altri fastidiosi blocchi).

CREATE TABLE dbo.WithIdentity
(
  ID int IDENTITY(1,1) NOT NULL
);
 
CREATE TABLE dbo.WithoutIdentity
(
  ID int NOT NULL
);
 
ALTER TABLE dbo.WithIdentity SWITCH TO dbo.WithoutIdentity;
GO
 
DROP TABLE dbo.WithIdentity;
EXEC sys.sp_rename N'dbo.WithoutIdentity', N'dbo.WithIdentity', 'OBJECT';

Funziona perché i tipi di dati e la capacità dei valori nulli corrispondono esattamente e non viene prestata alcuna attenzione a IDENTITY attributo. Prova a mescolare i tipi di dati, però, e le cose non funzionano molto bene:

CREATE TABLE dbo.SourceTable
(
  ID int IDENTITY(1,1) NOT NULL
);
 
CREATE TABLE dbo.TrySwitch
(
  ID bigint IDENTITY(1,1) NOT NULL
);
 
ALTER TABLE dbo.SourceTable SWITCH TO dbo.TrySwitch;

Ciò si traduce in:

Msg 4944, livello 16, stato 1
Istruzione ALTER TABLE SWITCH non riuscita perché la colonna 'ID' ha il tipo di dati int nella tabella di origine 'dbo.SourceTable' che è diverso dal suo tipo bigint nella tabella di destinazione 'dbo.TrySwitch'.

Sarebbe fantastico se un SWITCH l'operazione potrebbe essere utilizzata in uno scenario come questo, in cui l'unica differenza nello schema in realtà non *richiede* alcuna modifica fisica per adattarsi (di nuovo, come ho mostrato nella parte 1, i dati vengono riscritti su nuove pagine, anche se non è necessario farlo).

Conclusione

Questo post ha esaminato due potenziali soluzioni alternative per farti guadagnare tempo prima di modificare il tuo IDENTITY esistente colonna o abbandonando IDENTITY del tutto in questo momento a favore di una SEQUENCE . Se nessuna di queste soluzioni alternative è accettabile per te, guarda la parte 4, dove affronteremo questo problema frontalmente.

[ Parte 1 | Parte 2 | Parte 3 | Parte 4]