Database
 sql >> Base de données >  >> RDS >> Database

Minimiser l'impact de l'élargissement d'une colonne IDENTITY - partie 3

[ Partie 1 | Partie 2 | Partie 3 | Partie 4 ]

Jusqu'à présent dans cette série, j'ai démontré l'impact physique direct sur la page lors de la migration depuis int en bigint , puis parcouru plusieurs des bloqueurs courants de cette opération. Dans cet article, je voulais examiner deux solutions de contournement potentielles :une simple et une incroyablement compliquée.

En toute simplicité

On m'a un peu volé mon tonnerre dans un commentaire sur mon post précédent - Keith Monroe a suggéré que vous pourriez simplement réensemencer le tableau au négatif inférieur lié du type de données entier, doublant votre capacité pour de nouvelles valeurs. Vous pouvez le faire avec DBCC CHECKIDENT :

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

Cela pourrait fonctionner, en supposant que les valeurs de substitution n'ont pas de sens pour les utilisateurs finaux (ou, si c'est le cas, que les utilisateurs ne seront pas paniqués en obtenant soudainement des nombres négatifs). Je suppose que vous pourriez les tromper avec une vue :

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;

Cela signifie que l'utilisateur qui a ajouté ID = -2147483648 verrait en fait +2147483648 , l'utilisateur qui a ajouté ID = -2147483647 verrait +2147483649 , etc. Vous devrez cependant ajuster un autre code pour être sûr de faire le calcul inverse lorsque l'utilisateur transmet cet ID , par exemple

ALTER PROCEDURE dbo.GetRowByID @ID bigintASBEGIN SET NOCOUNT ON ; DÉCLARER @RealID bigint ; SET @RealID =CASE WHEN @ID> 2147483647 THEN @ID - (2147483648*2) + 1 ELSE @ID END ; SELECT ID, @ID /*, autres colonnes */ FROM dbo.TableName WHERE ID =@RealID;ENDGO

Je ne suis pas fou de cet obscurcissement. Du tout. C'est désordonné, trompeur et sujet aux erreurs. Et cela encourage à avoir une visibilité sur les clés de substitution - généralement, IDENTITY les valeurs ne doivent pas être exposées aux utilisateurs finaux, ils ne devraient donc pas se soucier s'il s'agit du client 24, 642, -376 ou de nombres beaucoup plus grands de part et d'autre de zéro.

Cette "solution" suppose également que vous n'avez nulle part de code qui commande par IDENTITY colonne afin de présenter en premier les lignes les plus récemment insérées, ou en déduit que la valeur IDENTITY la plus élevée la valeur doit être la ligne la plus récente. Code qui fait compter sur l'ordre de tri de l'IDENTITY colonne, explicitement ou implicitement (ce qui peut être plus que vous ne le pensez s'il s'agit de l'index clusterisé), ne présentera plus les lignes dans l'ordre attendu - elle affichera toutes les lignes créées après le RESEED , en commençant par le premier, puis il affichera toutes les lignes créées avant le RESEED , en commençant par le premier.

Le principal avantage de cette approche est qu'elle ne vous oblige pas à modifier le type de données et, par conséquent, le RESEED le changement ne nécessite aucune modification des index, des contraintes ou des clés étrangères entrantes.

L'inconvénient - en plus des changements de code mentionnés ci-dessus, bien sûr - est que cela ne vous fait gagner du temps qu'à court terme. Finalement, vous épuiserez également tous les entiers négatifs disponibles. Et ne pensez pas que cela double la durée de vie utile de la version actuelle de la table en termes de temps – dans de nombreux cas, la croissance des données s'accélère et ne reste pas constante. Vous utiliserez donc les 2 milliards de lignes suivantes beaucoup plus rapidement que les 2 premiers milliards.

Un chemin plus difficile

Une autre approche que vous pourriez adopter consiste à cesser d'utiliser une IDENTITY colonne tout à fait ; à la place, vous pouvez convertir en utilisant une SEQUENCE . Vous pouvez créer un nouveau bigint colonne, définissez la valeur par défaut sur la valeur suivante à partir d'une SEQUENCE , mettez à jour toutes ces valeurs avec les valeurs de la colonne d'origine (par lots si nécessaire), supprimez la colonne d'origine et renommez la nouvelle colonne. Créons ce tableau fictif et insérons une seule ligne :

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

Ensuite, nous allons créer une SEQUENCE qui commence juste au-delà de la limite supérieure d'un int :

CRÉER SÉQUENCE dbo.BeyondIntAS bigintCOMMENCER AVEC 2147483648 INCREMENTER DE 1 ;

Ensuite, les changements dans le tableau nécessaires pour passer à l'utilisation de la SEQUENCE pour la nouvelle colonne :

COMMENCER LA TRANSACTION ; -- ajouter une nouvelle colonne "identity" :ALTER TABLE dbo.SequenceDemo ADD ID2 bigint;GO -- définir la nouvelle colonne égale aux valeurs d'identité existantes -- pour les grandes tables, il peut être nécessaire de le faire par lots :UPDATE dbo.SequenceDemo ENSEMBLE ID2 =ID ; - maintenant, rendez-le non nullable et ajoutez la valeur par défaut de notre 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; -- besoin de supprimer le PK existant (et tous les index):ALTER TABLE dbo.SequenceDemo DROP CONSTRAINT PK_SD_Identity; -- supprimez l'ancienne colonne et renommez la nouvelle:ALTER TABLE dbo.SequenceDemo DROP COLUMN ID;EXEC sys.sp_rename N'dbo.SequenceDemo.ID2', N'ID', 'COLUMN'; -- maintenant remettre le PK en place :ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID); COMMIT TRANSACTION ;

Dans ce cas, la prochaine insertion donnerait les résultats suivants (notez que SCOPE_IDENTITY() ne renvoie plus de valeur valide) :

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

Si la table est grande et que vous devez mettre à jour la nouvelle colonne par lots au lieu de la transaction unique ci-dessus, comme je l'ai décrit ici - permettant aux utilisateurs d'interagir avec la table en attendant - vous aurez besoin d'un déclencheur en place pour remplacer la SEQUENCE valeur pour toutes les nouvelles lignes qui sont insérées, afin qu'elles continuent à correspondre à ce qui est sorti pour tout code appelant. (Cela suppose également que vous avez encore de la place dans la plage d'entiers pour continuer à accepter certaines mises à jour ; sinon, si vous avez déjà épuisé la plage, vous devrez prendre un certain temps d'arrêt - ou utiliser la solution simple ci-dessus à court terme .)

Laissons tout tomber et recommençons, puis ajoutons simplement la nouvelle colonne :

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.BeyondIntAS bigintSTART WITH 2147483648 INCREMENT BY 1;GO ALTER TABLE dbo.SequenceDemo ADD ID2 bigint;GO

Et voici le déclencheur que nous ajouterons :

CREATE TRIGGER dbo.After_SequenceDemoON dbo.SequenceDemoAFTER INSERTASBEGIN UPDATE sd SET sd.ID2 =sd.ID FROM dbo.SequenceDemo AS sd INNER JOIN inséré AS i ON sd.ID =i.ID;END

Cette fois, la prochaine insertion continuera à générer des lignes dans la plage inférieure d'entiers pour les deux colonnes, jusqu'à ce que toutes les valeurs préexistantes aient été mises à jour et que le reste des modifications ait été validé :

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

Maintenant, nous pouvons continuer à mettre à jour le ID2 existant tandis que de nouvelles lignes continuent d'être insérées dans la plage inférieure :

SET NOCOUNT ON ; DÉCLARER @r INT =1 ; TANT QUE @r> 0BEGIN COMMENCE LA TRANSACTION ; UPDATE TOP (10000) dbo.SequenceDemo SET ID2 =ID WHERE ID2 IS NULL ; SET @r =@@ROWCOUNT ; ENGAGER LA TRANSACTION ; -- POINT DE CONTRÔLE ; -- si simple -- BACKUP LOG ... -- si completEND

Une fois que nous avons mis à jour toutes les lignes existantes, nous pouvons continuer avec le reste des modifications, puis supprimer le déclencheur :

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_SequenceDemoCOMMIT TRANSACTION; 

Maintenant, la prochaine insertion générera ces valeurs :

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

Si vous avez du code qui repose sur SCOPE_IDENTITY() , @@IDENTITY , ou IDENT_CURRENT() , il devrait également changer, car ces valeurs ne sont plus renseignées après une insertion - bien que le OUTPUT La clause devrait continuer à fonctionner correctement dans la plupart des scénarios. Si vous avez besoin de votre code pour continuer à croire que la table génère une IDENTITY valeur, alors vous pouvez utiliser un déclencheur pour simuler cela - mais il ne pourra remplir que @@IDENTITY à l'insertion, pas SCOPE_IDENTITY() . Cela peut encore nécessiter des modifications, car dans la plupart des cas, vous ne voulez pas vous fier à @@IDENTITY pour quoi que ce soit (donc, si vous allez apporter des modifications, supprimez toutes les hypothèses sur une IDENTITY colonne du tout).

CREATE TRIGGER dbo.FakeIdentityON dbo.SequenceDemoINSTEAD OF INSERTASBEGIN SET NOCOUNT ON ; DECLARE @lowestID bigint =(SELECT MIN(id) FROM inséré); DECLARE @sql nvarchar(max) =N'DECLARE @foo TABLE(ID bigint IDENTITY(' + CONVERT(varchar(32), @lowestID) + N',1));'; SELECT @sql +=N'INSÉRER @foo VALEURS PAR DÉFAUT ;' DE inséré ; EXEC sys.sp_executesql @sql; INSERT dbo.SequenceDemo(ID, x) SELECT ID, x FROM inséré;END

Maintenant, la prochaine insertion générera ces valeurs :

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

Avec cette solution de contournement, vous devrez toujours gérer d'autres contraintes, index et tables avec des clés étrangères entrantes. Les contraintes locales et les index sont assez simples, mais je traiterai de la situation plus complexe avec les clés étrangères dans la prochaine partie de cette série.

Un qui ne fonctionnera pas, mais j'aimerais qu'il le fasse

ALTER TABLE SWITCH peut être un moyen très puissant d'effectuer des modifications de métadonnées difficiles à accomplir autrement. Et contrairement à la croyance populaire, cela n'implique pas seulement le partitionnement et n'est pas limité à Enterprise Edition. Le code suivant fonctionnera sur Express, et est une méthode que les gens ont utilisée pour ajouter ou supprimer l'IDENTITY propriété sur une table (encore une fois, sans tenir compte des clés étrangères et de tous ces autres bloqueurs embêtants).

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

Cela fonctionne car les types de données et la possibilité de valeur nulle correspondent exactement, et aucune attention n'est accordée à l'IDENTITY attribut. Essayez de mélanger les types de données, cependant, et les choses ne fonctionnent pas si bien :

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 ;

Cela se traduit par :

Msg 4944, Niveau 16, État 1
L'instruction ALTER TABLE SWITCH a échoué car la colonne 'ID' a un type de données int dans la table source 'dbo.SourceTable' qui est différent de son type bigint dans la table cible 'dbo.TrySwitch'.

Ce serait fantastique si un SWITCH l'opération pourrait être utilisée dans un scénario comme celui-ci, où la seule différence de schéma n'a en fait *nécessité* aucun changement physique pour s'adapter (encore une fois, comme je l'ai montré dans la partie 1, les données sont réécrites sur de nouvelles pages, même si il n'est pas nécessaire de le faire).

Conclusion

Ce message a étudié deux solutions de contournement potentielles pour vous faire gagner du temps avant de modifier votre IDENTITY existant colonne, ou en abandonnant IDENTITY tout à fait en ce moment en faveur d'une SEQUENCE . Si aucune de ces solutions de contournement ne vous convient, veuillez regarder la partie 4, où nous aborderons ce problème de front.

[ Partie 1 | Partie 2 | Partie 3 | Partie 4 ]