[ 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 1Istruzione 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]