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

Minimering af virkningen af ​​at udvide en IDENTITY-søjle – del 3

[ Del 1 | Del 2 | Del 3 | Del 4 ]

Indtil videre i denne serie har jeg demonstreret den direkte fysiske indvirkning på siden, når der skal opgraderes fra int til bigint , og gentog derefter flere af de almindelige blokkere til denne operation. I dette indlæg ville jeg undersøge to potentielle løsninger:en enkel og en utrolig indviklet.

Den nemme måde

Jeg blev frarøvet min torden en lille smule i en kommentar til mit tidligere indlæg – Keith Monroe foreslog, at du bare kunne flytte bordet til det nederste negativ bundet af heltalsdatatypen, hvilket fordobler din kapacitet for nye værdier. Du kan gøre dette med DBCC CHECKIDENT :

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

Dette kunne fungere, forudsat at surrogatværdierne ikke har betydning for slutbrugere (eller, hvis de gør, at brugere ikke bliver skræmt af pludselig at få negative tal). Jeg formoder, at du kunne narre dem med en udsigt:

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

Det betyder, at den bruger, der tilføjede ID = -2147483648 ville faktisk se +2147483648 , brugeren, der tilføjede ID = -2147483647 ville se +2147483649 , og så videre. Du bliver dog nødt til at justere en anden kode for at være sikker på at foretage den omvendte beregning, når brugeren indtaster det ID , f.eks.

ÆNDRINGSPROCEDURE dbo.GetRowByID @ID bigintASBEGIN SÆT NOCOUNT ON; DECLARE @RealID bigint; SET @RealID =CASE WHEN @ID> 2147483647 THEN @ID - (2147483648*2) + 1 ANDET @ID END; SELECT ID, @ID /*, other columns */ FROM dbo.TableName WHERE ID =@RealID;ENDGO

Jeg er ikke vild med denne sløring. Overhovedet. Det er rodet, vildledende og udsat for fejl. Og det tilskynder til at have synlighed i surrogatnøgler – generelt IDENTITY værdier bør ikke eksponeres for slutbrugere, så de burde virkelig være ligeglade med, om de er kunde 24, 642, -376 eller meget større tal på begge sider af nul.

Denne "løsning" forudsætter også, at du ikke har kode nogen steder, der bestilles efter IDENTITY kolonne for at præsentere de senest indsatte rækker først, eller udlede, at den højeste IDENTITY værdien skal være den nyeste række. Kode, der gør stole på sorteringsrækkefølgen af ​​IDENTITY kolonne, enten eksplicit eller implicit (hvilket måske er mere, end du tror, ​​hvis det er det klyngede indeks), vil ikke længere præsentere rækkerne i forventet rækkefølge – den vil vise alle rækkerne, der er oprettet efter RESEED , startende med den første, og derefter vil den vise alle de rækker, der er oprettet før RESEED , begyndende med den første.

Den primære fordel ved denne tilgang er, at den ikke kræver, at du ændrer datatypen, og som følge heraf er RESEED ændring kræver ingen ændringer af indekser, begrænsninger eller indgående fremmednøgler.

Ulempen – udover kodeændringerne nævnt ovenfor, selvfølgelig – er, at dette kun køber dig tid på kort sigt. Til sidst vil du også udtømme alle de tilgængelige negative heltal. Og tro ikke, at dette fordobler brugstiden for den aktuelle version af tabellen med hensyn til tid – i mange tilfælde accelererer datavæksten og forbliver ikke konstant, så du vil bruge de næste 2 milliarder rækker meget hurtigere end de første 2 milliarder.

En sværere måde

En anden fremgangsmåde, du kan tage, er at stoppe med at bruge en IDENTITY kolonne helt; i stedet kan du konvertere til at bruge en SEQUENCE . Du kan oprette en ny bigint kolonne, skal du indstille standardværdien til den næste værdi fra en SEQUENCE , opdater alle disse værdier med værdierne fra den oprindelige kolonne (i batches, hvis det er nødvendigt), slip den oprindelige kolonne og omdøb den nye kolonne. Lad os oprette denne fiktive tabel og indsætte en enkelt række:

CREATE TABLE dbo.SequenceDemo( ID int IDENTITY(1,1), x char(1), CONSTRAINT PK_SD_Identity PRIMÆR NØGLE KLUSTERET (ID));GO INSERT dbo.SequenceDemo(x) VALUES('x');

Dernæst opretter vi en SEQUENCE der starter lige ud over den øvre grænse af en int:

OPRET SEKVENS dbo.BeyondIntAS bigintSTART MED 2147483648 FORØG MED 1;

Dernæst de ændringer i tabellen, der er nødvendige for at skifte til at bruge SEQUENCE for den nye kolonne:

BEGIN TRANSAKTIONEN; -- tilføj en ny "identitet"-kolonne:ÆNDRINGSTABEL dbo.SequenceDemo ADD ID2 bigint;GO -- sæt den nye kolonne lig med de eksisterende identitetsværdier-- for store tabeller skal du muligvis gøre dette i batches:OPDATERING dbo.SequenceDemo SET ID2 =ID; -- gør det nu ikke nullbart og tilføj standarden fra vores SEQUENCE:ALTER TABLE dbo.SequenceDemo ALTER COLUMN ID2 bigint NOT NULL;ALTER TABEL dbo.SequenceDemo TILFØJ BEGRÆNSNING DF_SD_Identity DEFAULT NÆSTE VÆRDI FOR dbo.BeyondInt FOR ID2; -- nødt til at droppe den eksisterende PK (og eventuelle indekser):ÆNDRINGSTABEL dbo.SequenceDemo DROP CONSTRAINT PK_SD_Identity; -- slip den gamle kolonne og omdøb den nye:ALTER TABLE dbo.SequenceDemo DROP COLUMN ID;EXEC sys.sp_rename N'dbo.SequenceDemo.ID2', N'ID', 'COLUMN'; -- sæt nu PK'en op igen:ÆNDRINGSTABEL dbo.SequenceDemo TILFØJ BEGRÆNSNING PK_SD_Identity PRIMÆR NØGLE KLYNGET (ID); FORBAGE TRANSAKTION;

I dette tilfælde vil den næste indsættelse give følgende resultater (bemærk at SCOPE_IDENTITY() returnerer ikke længere en gyldig værdi):

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

Hvis tabellen er stor, og du skal opdatere den nye kolonne i batches i stedet for ovenstående one-shot transaktion, som jeg har beskrevet her – så brugerne kan interagere med tabellen i mellemtiden – skal du have en trigger på plads for at tilsidesætte SEQUENCE værdi for eventuelle nye rækker, der er indsat, så de fortsat matcher det, der udlæses til enhver kaldekode. (Dette forudsætter også, at du stadig har lidt plads i heltalområdet til at fortsætte med at acceptere nogle opdateringer; ellers, hvis du allerede har opbrugt rækkevidden, bliver du nødt til at tage lidt nedetid – eller bruge den nemme løsning ovenfor på kort sigt .)

Lad os droppe alt og starte forfra, så tilføjer vi bare den nye kolonne:

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 PRIMÆR NØGLE CLUSTERED (ID));GO INSERT dbo .SequenceDemo(x) VALUES('x');GO CREATE SEQUENCE dbo.BeyondIntAS bigintSTART MED 2147483648 FORØG MED 1;GO ÆNDRINGSTABEL dbo.SequenceDemo ADD ID2 bigint;GO

Og her er udløseren, vi tilføjer:

OPRET TRIGGER dbo.After_SequenceDemoON dbo.SequenceDemoAFTER INSERTASBEGIN UPDATE sd SET sd.ID2 =sd.ID FRA dbo.SequenceDemo AS sd INNER JOIN indsat AS i ON sd.ID =i.ID;END

Denne gang vil den næste indsættelse fortsætte med at generere rækker i det nederste område af heltal for begge kolonner, indtil alle de allerede eksisterende værdier er blevet opdateret, og resten af ​​ændringerne er blevet foretaget:

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

Nu kan vi fortsætte med at opdatere den eksisterende ID2 værdier, mens nye rækker fortsætter med at blive indsat inden for det nedre område:

INDSTIL ANTAL TIL; DEKLARE @r INT =1; MENS @r> 0BEGIN START TRANSAKTION; OPDATERING TOP (10000) dbo.SequenceDemo SET ID2 =ID HVOR ID2 ER NULL; SET @r =@@ RÆKEL; FORBAGE TRANSAKTION; -- CHECKPOINT; -- hvis det er enkelt -- BACKUP LOG ... -- hvis fuldEND

Når vi har opdateret alle de eksisterende rækker, kan vi fortsætte med resten af ​​ændringerne og derefter slippe triggeren:

BEGIN TRANSACTION;ALTER TABLE dbo.SequenceDemo ALTER COLUMN ID2 BIGINT NOT NULL;ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT DF_SD_Identity STANDARD NÆSTE VÆRDI FOR dbo.BeyondInt FOR ID2;ALTER TABLE dbo.SequenceDemo TABLE dbo.SequenceDequence; DROP COLUMN ID;EXEC sys.sp_rename N'dbo.SequenceDemo.ID2', N'ID', 'COLUMN';ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT PK_SD_Identity PRIMÆR NØGLE CLUSTERED (ID);DROP TRIGGER dbo.InsteadACTIONmof; 

Nu vil den næste indsættelse generere disse værdier:

INSERT dbo.SequenceDemo(x) VALUES('z');SELECT Si =SCOPE_IDENTITY();SELECT ID, x FROM dbo.SequenceDemo; /* resultater Si----NULL ID x---------- -1 x2 y2147483648 z */

Hvis du har kode, der er afhængig af SCOPE_IDENTITY() , @@IDENTITY , eller IDENT_CURRENT() , ville det også skulle ændres, da disse værdier ikke længere udfyldes efter en indsættelse – selvom OUTPUT klausulen skal fortsætte med at fungere korrekt i de fleste scenarier. Hvis du har brug for din kode for at fortsætte med at tro, genererer tabellen en IDENTITY værdi, så kan du bruge en trigger til at forfalske dette – men det ville kun være i stand til at udfylde @@IDENTITY ved indsættelse, ikke SCOPE_IDENTITY() . Dette kan stadig kræve ændringer, fordi du i de fleste tilfælde ikke ønsker at stole på @@IDENTITY for noget (så hvis du vil foretage ændringer, skal du fjerne alle antagelser om en IDENTITY kolonne overhovedet).

CREATE TRIGGER dbo.FakeIdentityON dbo.SequenceDemoINSTEAD OF INSERTASBEGIN SET NOCOUNT ON; DECLARE @lowestID bigint =(SELECT MIN(id) FROM indsat); DECLARE @sql nvarchar(max) =N'DECLARE @foo TABLE(ID bigint IDENTITY(' + CONVERT(varchar(32), @lowestID) + N',1));'; VÆLG @sql +=N'INSERT @foo STANDARDVÆRDIER;' FRA indsat; EXEC sys.sp_executesql @sql; INSERT dbo.SequenceDemo(ID, x) SELECT ID, x FROM indsat;END

Nu vil den næste indsættelse generere disse værdier:

INSERT dbo.SequenceDemo(x) VALUES('a');SELECT Si =SCOPE_IDENTITY(), Ident =@@IDENTITY;SELECT ID, x FROM dbo.SequenceDemo; /* resultater Si Ident---- -----NULL 2147483649 ID x---------- -1 x2 y2147483648 z2147483649 a */

Med denne løsning vil du stadig skulle håndtere andre begrænsninger, indekser og tabeller med indgående fremmednøgler. Lokale begrænsninger og indekser er ret ligetil, men jeg vil behandle den mere komplekse situation med fremmednøgler i den næste del af denne serie.

En, der ikke virker, men jeg ville ønske, den ville

ALTER TABLE SWITCH kan være en meget effektiv måde at lave nogle metadataændringer på, som ellers er svære at opnå. Og i modsætning til hvad mange tror, ​​involverer dette ikke kun partitionering og er ikke begrænset til Enterprise Edition. Følgende kode fungerer på Express, og er en metode, folk har brugt til at tilføje eller fjerne IDENTITY ejendom på et bord (igen, uden at tage højde for fremmednøgler og alle de andre irriterende blokkere).

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

Dette virker, fordi datatyperne og nullbarheden matcher nøjagtigt, og der er ikke taget hensyn til IDENTITY attribut. Prøv dog at blande datatyper, og tingene fungerer ikke så godt:

CREATE TABLE dbo.SourceTable( ID int IDENTITY(1,1) NOT NULL); CREATE TABLE dbo.TrySwitch( ID bigint IDENTITY(1,1) NOT NULL); ÆNDRINGSTABEL dbo.SourceTable SKIFTER TIL dbo.TrySwitch;

Dette resulterer i:

Msg 4944, niveau 16, tilstand 1
ALTER TABLE SWITCH-sætning mislykkedes, fordi kolonne 'ID' har datatypen int i kildetabellen 'dbo.SourceTable', som er forskellig fra dens type bigint i måltabellen 'dbo.TrySwitch'.

Det ville være fantastisk, hvis en SWITCH operation kunne bruges i et scenarie som dette, hvor den eneste forskel i skemaet faktisk ikke *krævede* nogen fysiske ændringer for at imødekomme (igen, som jeg viste i del 1, bliver data omskrevet til nye sider, selvom det er ikke nødvendigt at gøre det).

Konklusion

Dette indlæg undersøgte to potentielle løsninger til enten at købe dig tid, før du ændrer din eksisterende IDENTITY kolonne eller opgive IDENTITY helt lige nu til fordel for en SEQUENCE . Hvis ingen af ​​disse løsninger er acceptable for dig, så se venligst del 4, hvor vi vil løse dette problem direkte.

[ Del 1 | Del 2 | Del 3 | Del 4 ]


  1. Oracle SQL - max() med NULL-værdier

  2. Hvordan statement_timestamp() virker i PostgreSQL

  3. Hvordan Random() virker i PostgreSQL

  4. Skal jeg bruge en inline varchar(max) kolonne eller gemme den i en separat tabel?