TVP, Merge, CTE

J’ai récemment été confronté à un problème d’architecture SQL et de performances d’opérations CRUD sur SQL SERVER 2008

Pour tout vous dire, il s’agit de mon petit projet WP7 Starcraft 2 (oui je sais, je suis un Geek joueur Smile)

AppScreen01

Il ne devrait pas tarder à pointer le bout de son nez sur le Market Place, mais en attendant, voici un post résumant une des problématiques SQL que j’ai rencontrée !

Voici globalement les données du problème :

  • Je récupère des données provenant d’une source externe
  • Je dois fusionner ces données avec les données existantes de ma base de données. Il faut :
    1. Insérer les données non existantes
    2. Mettre à jour les données existantes
    3. Supprimer les données existantes de ma base de données ne faisant plus partie du scope
  • Je doit traiter pour une clé primaire de 0 à N données associées (N variant de 0 à environ 1000 enregistrements environ)
    • Pour imager le truc, pensez à quelque chose du genre (“Pour chaque client, je récupère de 0 à N Factures)
  • Je dois garder une consitence des données lors de ces opérations
  • Je dois être performance Smile (si si !)

Bien, à partir de là, on peut commencer à réfléchir à une solution.

Première solution : Brut-Force

Oui alors bon celle là, elle est loin d’être excellente, mais c’est la première qui vient à l’esprit:

Je crée une procédure stockée qui prend en paramètre un Record.

Celle ci vérifie si la ligne existe :

  • Si elle existe : Update
  • Si elle n’existe pas : Insert

On éxécute cette requête N fois le nb de records à mettre à jour. (Pour info je mets à jour environ 1 Million de record, faudra pas être pressé Smile with tongue out)

On encapsule ça dans une TransactionScope supportée par MSDTC et roulez !

Bon problème :

  1. On ne gère pas les enregistrements existants obsolètes à supprimer
  2. C’est ANTI-performant …
  3. C’est une solution qui va engendrer un nombre de problèmes incalculables…

Deuxième solution : La solution élégante

Alors, aprés avoir réfléchi à une solution pérenne, voici ce que je vous propose.

0 à N Enregistrements :

Là c’est plutôt simple, on va passer par une TVP : Table Value Parameter.

j’en parle ici pour ceux qui sont intéressés par la découverte de cette formidable avancée Smile

Voici la définition de ma TVP:

CREATE TYPE [dbo].[CharacterLeaguesTvp] AS TABLE(
    [LeagueId] [int] NOT NULL, [RegionId] [nvarchar](5) NOT NULL,
    [CharacterId] [int] NOT NULL, [int] NOT NULL, [Name] [nvarchar](150) NULL,
    [MostPlayedRace] [tinyint] NULL, [DivisionName] [nvarchar](250) NULL, [TeamType] [int] NULL,
    [LeagueType] [int] NULL, [Rank] [tinyint] NULL, [Points] [int] NULL, [VictoriesCount] [int] NULL,
    [LossesCount] [int] NULL, [LastModifiedDate] [datetime] NULL,
    PRIMARY KEY CLUSTERED 
([RegionId] ASC, [LeagueId] ASC, [ZoneId] ASC, [CharacterId] ASC)WITH (IGNORE_DUP_KEY = OFF)
)

Bon la défintion de la procédure stockée devient, du coup, BEAUCOUP plus simple Green with envy

Et elle a la bonne idée de prendre l’ensemble de mes records en un seul appel Open-mouthed smile

ALTER PROCEDURE [dbo].[spInsertOrUpdateCharacterLeague]
(
 @tmpCharacterLeagues  AS CharacterLeaguesTvp readonly
)
as
 
BEGIN
  ....
END

Ok, premier problème résolu, on a UN seul appel à une procédure stockée. Facile de gérer la consitence des données du coup !

Fusion : MERGE

Pour la partie fusion des données, la réponse est assez simple quand on parle juste d’insertion et mise à jour.

On va utiliser l’instruction MERGE de SQL SERVER 2008. De plus avec l’apport de la TVP, rien de plus simple !

Pour l’utilisation du Merge, je vous redirige ici.

Voici le corps de la méthode d’insertion / mise à jour :

  MERGE tCharacterLeague AS target
  USING @tmpCharacterLeagues AS source 
 
  ON (target.CharacterId = source.CharacterId and 
      target.LeagueId = source.LeagueId And 
      target.ZoneId = source.ZoneId and 
      target.RegionId = source.RegionId)    
  WHEN MATCHED THEN 
        UPDATE SET LastModifiedDate = source.LastModifiedDate,
                   Name = source.Name,
                   MostPlayedRace = source.MostPlayedRace,
                   [Rank] = source.[Rank],
                   Points = Source.Points,
                   VictoriesCount = Source.VictoriesCount,
                   LossesCount = source.LossesCount
  WHEN NOT MATCHED By TARGET THEN    
    INSERT (RegionId, CharacterId, ZoneId, LeagueId, Name, MostPlayedRace, [Rank], Points,
            VictoriesCount, LossesCount, LastModifiedDate)
    VALUES (Source.RegionId, Source.CharacterId, Source.ZoneId, Source.LeagueId, 
            Source.Name, Source.MostPlayedRace, Source.[Rank], Source.Points,
            Source.VictoriesCount, Source.LossesCount, Source.LastModifiedDate)

Ok là on est vraiment pas mal, en 1 appel, on fusionne TOUS les enregistrements existants et les nouveaux.

Reste à gérer les suppressions.

Là j’étais parti sur un truc assez simple de prime abord : On rajoute l’instruction de suppression sur le Merge

WHEN NOT MATCHED By Source THEN    
    
Delete;

Et ça marche … un peu trop

En effet, le Delete agit sur L’ENSEMBLE de la table, du coup TOUS les enregistrements de ma base (même ceux qui ne correspondent pas à ma clé primaire) sont impactées et donc supprimés !

Du coup je me retrouve avec une base .. vide! (enfin presque, il reste les éléments insérés / updatés)

Bon, il faut donc réduire le scope de mon merge “à la base”

Rien de plus simple avec une CTE !

Ce qui donne au final :

-- Utilisation d'une CTE pour réduire le scope, surtout pour le delete
  WITH TargetCharacterLeague AS 
 (
      SELECT CL.*
      FROM dbo.CharacterLeague CL
      Inner Join @tmpTableLeague T 
        on T.LeagueId = CL.LeagueId And T.ZoneId = CL.ZoneId and T.RegionId = CL.RegionId
  )
  MERGE TargetCharacterLeague AS target
  USING @tmpCharacterLeagues AS source 
 
  ON (target.CharacterId = source.CharacterId and 
      target.LeagueId = source.LeagueId And 
      target.ZoneId = source.ZoneId and 
      target.RegionId = source.RegionId)
    
  WHEN MATCHED THEN 
        UPDATE SET LastModifiedDate = source.LastModifiedDate,
                   Name = source.Name,
                   MostPlayedRace = source.MostPlayedRace,
                   [Rank] = source.[Rank],
                   Points = Source.Points,
                   VictoriesCount = Source.VictoriesCount,
                   LossesCount = source.LossesCount
  WHEN NOT MATCHED By TARGET THEN    
    INSERT (RegionId, CharacterId, ZoneId, LeagueId, Name, MostPlayedRace, [Rank], Points,
            VictoriesCount, LossesCount, LastModifiedDate)
    VALUES (Source.RegionId, Source.CharacterId, Source.ZoneId, Source.LeagueId, 
            Source.Name, Source.MostPlayedRace, Source.[Rank], Source.Points,
            Source.VictoriesCount, Source.LossesCount, Source.LastModifiedDate)
  WHEN NOT MATCHED By Source THEN    
    Delete;

 

Et voilà, une utilisation à la fois élégante et performante des outils mis à votre disposition avec SQL SERVER 2008 !

Bon Merge , bonne CTE, bonne TVP etc etc !