Modification de la clé primaire d'IDENTITY en colonne persistante calculée à l'aide de COALESCE

10

Dans une tentative de découpler une application de notre base de données monolithique, nous avons essayé de changer les colonnes INT IDENTITY de diverses tables pour qu'elles soient une colonne calculée PERSISTED qui utilise COALESCE. Fondamentalement, nous avons besoin de l'application découplée pour pouvoir mettre à jour la base de données pour les données communes partagées entre de nombreuses applications tout en permettant aux applications existantes de créer des données dans ces tables sans avoir besoin de modifier le code ou la procédure.

Donc, essentiellement, nous sommes passés d'une définition de colonne de;

PkId INT IDENTITY(1,1) PRIMARY KEY

à;

PkId AS AS COALESCE(old_id, external_id, new_id) PERSISTED NOT NULL,
old_id INT NULL, -- Values here are from existing records of PkId before table change
external_id INT NULL,
new_id INT IDENTITY(2000000,1) NOT NULL

Dans tous les cas, le PkId est également une CLÉ PRIMAIRE et dans tous les cas sauf un, il est CLUSTERED. Toutes les tables ont les mêmes clés et index étrangers que précédemment. En substance, le nouveau format permet au PkId d'être fourni par l'application découplée (comme external_id), mais permet également au PkId d'être la valeur de la colonne IDENTITY, permettant ainsi au code existant qui repose sur la colonne IDENTITY via l'utilisation de SCOPE_IDENTITY et @@ IDENTITY de travailler comme avant.

Le problème que nous avons rencontré est que nous avons rencontré quelques requêtes qui s'exécutaient dans un délai acceptable pour s'éteindre complètement. Les plans de requête générés utilisés par ces requêtes ne ressemblent en rien à ce qu'ils étaient auparavant.

Étant donné que la nouvelle colonne est une CLÉ PRIMAIRE, le même type de données qu'avant, et PERSISTE, je m'attendais à ce que les requêtes et les plans de requête se comportent de la même manière qu'auparavant. Le PkId INT COMPUTED PERSISTED INT devrait-il essentiellement se comporter de la même manière qu'une définition INT explicite en termes de la façon dont SQL Server produira le plan d'exécution? Y a-t-il d'autres problèmes probables avec cette approche que vous pouvez voir?

Le but de ce changement était censé nous permettre de changer la définition de la table sans avoir besoin de modifier les procédures et le code existants. Compte tenu de ces problèmes, je ne pense pas que nous puissions adopter cette approche.

Mr Moose
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Paul White 9

Réponses:

4

PREMIER

Vous n'avez probablement pas besoin de trois colonnes: old_id, external_id, new_id. La new_idcolonne, étant un IDENTITY, aura une nouvelle valeur générée pour chaque ligne, même lorsque vous l'insérez dans external_id. Mais, entre old_idet external_id, ceux-ci sont à peu près mutuellement exclusifs: soit il y a déjà une old_idvaleur, soit cette colonne, dans la conception actuelle, sera juste NULLsi vous utilisez external_idou new_id. Étant donné que vous n'ajouterez pas un nouvel identifiant "externe" à une ligne qui existe déjà (c'est-à-dire qui a une old_idvaleur), et qu'il n'y aura pas de nouvelles valeurs à entrer old_id, alors il peut y avoir une colonne utilisée aux deux fins.

Donc, débarrassez-vous de la external_idcolonne et renommez-la old_iden quelque chose comme old_or_external_idou autre. Cela ne devrait nécessiter aucune modification réelle de quoi que ce soit, tout en réduisant une partie de la complication. Au plus, vous devrez peut-être appeler la colonne external_id, même si elle contient des "anciennes" valeurs, si le code d'application est déjà écrit pour être inséré dans external_id.

Cela réduit la nouvelle structure à être juste:

PkId AS AS COALESCE(old_or_external_id, new_id, -1) PERSISTED NOT NULL,
old_or_external_id INT NULL, -- values from existing record OR passed in from app
new_id INT IDENTITY(2000000, 1) NOT NULL

Maintenant, vous n'avez ajouté que 8 octets par ligne au lieu de 12 octets (en supposant que vous n'utilisez pas l' SPARSEoption ou la compression de données). Et vous n'avez pas eu besoin de changer de code, de code T-SQL ou d'application.

SECONDE

Poursuivant dans cette voie de simplification, regardons ce qu'il nous reste:

  • La old_or_external_idcolonne a déjà des valeurs, ou recevra une nouvelle valeur de l'application, ou restera comme NULL.
  • Le new_idaura toujours une nouvelle valeur générée, mais cette valeur ne sera utilisée que si la old_or_external_idcolonne l'est NULL.

Il n'y a jamais un moment où vous auriez besoin de valeurs à la fois dans old_or_external_idet new_id. Oui, il y aura des moments où les deux colonnes ont des valeurs car elles new_idsont an IDENTITY, mais ces new_idvaleurs sont ignorées. Encore une fois, ces deux champs s'excluent mutuellement. Et maintenant?

Maintenant, nous pouvons voir pourquoi nous en avions besoin external_iden premier lieu. Étant donné qu'il est possible d'insérer dans une IDENTITYcolonne à l'aide de SET IDENTITY_INSERT {table_name} ON;, vous pouvez vous passer de tout changement de schéma et ne modifier votre code d'application que pour encapsuler les INSERTinstructions / opérations dans SET IDENTITY_INSERT {table_name} ON;et les SET IDENTITY_INSERT {table_name} OFF;instructions. Vous devez ensuite déterminer la plage de départ pour réinitialiser la IDENTITYcolonne (pour les valeurs nouvellement générées) car elle devra être bien au-dessus des valeurs que le code d'application va insérer car l'insertion d'une valeur plus élevée entraînera la prochaine valeur générée automatiquement. être supérieur à la valeur MAX actuelle. Mais vous pouvez toujours insérer une valeur inférieure à la valeur IDENT_CURRENT .

La combinaison des colonnes old_or_external_idet new_idn'augmente pas non plus les chances de se retrouver dans une situation de valeurs qui se chevauchent entre les valeurs générées automatiquement et les valeurs générées par l'application, car l'intention d'avoir les colonnes 2, voire 3, est de les combiner en une valeur de clé primaire, et ce sont toujours des valeurs uniques.

Dans cette approche, il vous suffit de:

  • Laissez les tableaux comme:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Cela ajoute 0 octet à chaque ligne, au lieu de 8, voire 12.

  • Déterminez la plage de départ des valeurs générées par l'application. Celles-ci seront supérieures à la valeur MAX actuelle dans chaque tableau, mais inférieures à ce qui deviendra la valeur minimale pour les valeurs générées automatiquement.
  • Déterminez à quelle valeur la plage générée automatiquement doit commencer. Il devrait y avoir beaucoup d'espace entre la valeur MAX actuelle et beaucoup d'espace pour croître, sachant que la limite supérieure est légèrement supérieure à 2,14 milliards. Vous pouvez ensuite définir cette nouvelle valeur de départ minimale via DBCC CHECKIDENT .
  • Wrap code de l' application dans INSERTs SET IDENTITY_INSERT {table_name} ON;et SET IDENTITY_INSERT {table_name} OFF;déclarations.

DEUXIÈME, partie B

Une variation de l'approche notée directement ci-dessus consisterait à insérer les valeurs du code d'application commençant par -1 et descendant à partir de là. Cela laisse les IDENTITYvaleurs comme étant les seules à augmenter . L'avantage ici est que non seulement vous ne compliquez pas le schéma, vous n'avez pas non plus à vous soucier de rencontrer des ID qui se chevauchent (si les valeurs générées par l'application se retrouvent dans la nouvelle plage générée automatiquement). Ceci n'est qu'une option si vous n'utilisez pas déjà des valeurs d'ID négatives (et il semble assez rare que les gens utilisent des valeurs négatives sur les colonnes générées automatiquement, cela devrait donc être une possibilité probable dans la plupart des situations).

Dans cette approche, il vous suffit de:

  • Laissez les tableaux comme:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Cela ajoute 0 octet à chaque ligne, au lieu de 8, voire 12.

  • La plage de départ pour les valeurs générées par l'application sera -1.
  • Wrap code de l' application dans INSERTs SET IDENTITY_INSERT {table_name} ON;et SET IDENTITY_INSERT {table_name} OFF;déclarations.

Ici, vous devez toujours le faire IDENTITY_INSERT, mais: vous n'ajoutez pas de nouvelles colonnes, vous n'avez pas besoin de "réamorcer" les IDENTITYcolonnes et vous n'avez aucun risque futur de chevauchements.

DEUXIÈME, 3e partie

Une dernière variante de cette approche serait de permuter éventuellement les IDENTITYcolonnes et d'utiliser à la place des séquences . La raison de cette approche est de pouvoir avoir les valeurs d'insertion de code d'application qui sont: positives, au-dessus de la plage générée automatiquement (pas en dessous), et pas nécessaire SET IDENTITY_INSERT ON / OFF.

Dans cette approche, il vous suffit de:

  • Créer des séquences à l'aide de CREATE SEQUENCE
  • Copiez la IDENTITYcolonne dans une nouvelle colonne qui n'a pas la IDENTITYpropriété, mais qui a une DEFAULTcontrainte à l'aide de la fonction NEXT VALUE FOR :

    PkId INT PRIMARY KEY CONSTRAINT [DF_TableName_NextID] DEFAULT (NEXT VALUE FOR...)

    Cela ajoute 0 octet à chaque ligne, au lieu de 8, voire 12.

  • La plage de départ pour les valeurs générées par l'application sera bien supérieure à ce que vous pensez que les valeurs générées automatiquement approcheront.
  • Wrap code de l' application dans INSERTs SET IDENTITY_INSERT {table_name} ON;et SET IDENTITY_INSERT {table_name} OFF;déclarations.

CEPENDANT , en raison de l'exigence que le code avec SCOPE_IDENTITY()ou @@IDENTITYfonctionne toujours correctement, le passage aux séquences n'est pas actuellement une option car il semble qu'il n'y ait pas d'équivalent de ces fonctions pour les séquences :-(. Triste!

Solomon Rutzky
la source
Merci beaucoup pour votre réponse. Vous soulevez quelques points qui ont été discutés ici en interne. Malheureusement, certains d'entre eux ne fonctionneront pas pour nous pour deux raisons. Notre base de données est assez ancienne et quelque peu fragile et fonctionne sous le mode de compatibilité 2005, les séquences sont donc épuisées. Notre poussée de données d'application se produit via un outil de chargement de données qui obtient de nouveaux enregistrements à partir des files d'attente de Service Broker et les pousse via plusieurs threads. IDENTITY_INSERT ne peut être utilisé que pour une table par session, et la pensée actuelle est que notre architecture ne peut pas répondre à cela sans changement significatif. Je teste votre première suggestion maintenant.
M. Moose
@MrMoose Oui, j'ai mis à jour ma réponse pour inclure plus d'informations sur les séquences à la fin. Cela ne fonctionnerait pas dans votre situation de toute façon. Et je me posais des questions sur les problèmes de concurrence potentiels avec IDENTITY_INSERT, mais je ne l'ai pas testé. Je ne suis pas sûr que l'option # 1 va résoudre votre problème global, c'était juste une observation pour réduire la complexité inutile. Pourtant, si vous avez plusieurs threads insérant de nouveaux ID "externes", comment garantissez-vous qu'ils sont uniques?
Solomon Rutzky
@MrMoose En fait, concernant " IDENTITY_INSERT ne peut être utilisé que pour une table par session ", quel est exactement le problème ici? 1) vous ne pouvez insérer que dans une table à la fois, donc vous la désactivez pour TableA avant de l'insérer dans TableB, et 2) je viens de tester et contrairement à ce que j'avais pensé, il n'y a pas de problèmes de concurrence - j'ai pu avoir IDENTITY_INSERT ONpour le même tableau en deux sessions et a été insérer dans les deux sans aucun problème.
Solomon Rutzky
1
Comme vous l'avez suggéré, le changement 1 n'a fait aucune différence. L'ID que nous utiliserons sera attribué en dehors de la base de données actuelle et utilisé pour relier les enregistrements. Il se pourrait bien que ma compréhension des sessions ne soit pas tout à fait correcte, donc IDENTITY_INSERT pourrait fonctionner. Il me faudra un peu de temps pour enquêter là-dessus, donc je ne pourrai pas faire rapport avant un petit moment. Merci encore pour la contribution. C'est très apprécié.
M. Moose
1
Je pense que votre suggestion d'utiliser IDENTITY_INSERT (avec une valeur de départ élevée pour les applications existantes) fonctionnera bien. Aaron Bertrand a fourni une réponse ici avec un bon petit exemple sur le tester avec la concurrence. Nous avons modifié notre outil de chargement de données pour pouvoir gérer les tables qui doivent spécifier des valeurs d'identité et nous passerons à d'autres tests dans les semaines à venir.
M. Moose