Archive

Archive for the ‘SGBDR’ Category

Instruction MERGE

Ça fait un petit moment maintenant que je vois des instructions de ce genre, dans des scripts de données :

IF EXISTS (SELECT * FROM Client WHERE Nom = 'Legrand' AND Prenom = 'Lucas')
	BEGIN
		PRINT 'UPDATE'
		UPDATE Client
		SET Email = 'lucas.legrand@live.com'
		WHERE Nom = 'Legrand' AND Prenom = 'Lucas'
	END
ELSE
	BEGIN
		PRINT 'INSERT'
		INSERT INTO Client
		VALUES ('Legrand', 'Lucas',
				'lucas.legrand@live.com',
				'107 rue de Lille, 75007 PARIS')
	END

Le gros problème, c’est que s’il y a beaucoup de données (surtout de références), ça va multiplier les blocs comme des petits pains (ce qui peut être sympa, mais ce n’est pas le sujet).

C’est là que le sauveur arrive, sous la forme de l’instruction MERGE.

Pour obtenir l’équivalent avec MERGE, on écrira :

MERGE Client AS target
USING (VALUES 
   ('lucas.legrand@live.com', 'Legrand', 'Lucas', '107 rue de Lille, 75007 PARIS'),
   ('julienne.jeremi@hotmail.fr', 'Jeremi', 'Julienne', '146 rue de Lyon, 75012 PARIS'))
	AS source (Email, Nom, Prenom, Adresse)
ON (target.Nom = source.Nom AND target.Prenom = source.Prenom)
WHEN MATCHED
	THEN UPDATE
		SET target.Email = source.Email
WHEN NOT MATCHED BY target
	THEN INSERT (Nom, Prenom, Email, Adresse)
	VALUES (source.Nom, source.Prenom, source.Email, source.Adresse);

-- Ou, en utilisant une structure (temporaire ou non) existante
	
CREATE TABLE #temp (
   Nom varchar(50) NULL,
   Prenom varchar(50) NULL,
   Email varchar(50) NULL,
   Adresse varchar(250) NULL)

INSERT INTO #temp
VALUES 
   ('lucas.legrand@live.com', 'Legrand', 'Lucas', '107 rue de Lille, 75007 PARIS'),
   ('julienne.jeremi@hotmail.fr', 'Jeremi', 'Julienne', '146 rue de Lyon, 75012 PARIS');
-- A noter : le ";" pour séparer les deux instructions
MERGE Client AS target
USING (SELECT Email, Nom, Prenom, Adresse FROM #temp) AS source
ON (target.Nom = source.Nom AND target.Prenom = source.Prenom)
WHEN MATCHED
	THEN UPDATE
		SET target.Email = source.Email
WHEN NOT MATCHED BY target
	THEN INSERT (Nom, Prenom, Email, Adresse)
	VALUES (source.Nom, source.Prenom, source.Email, source.Adresse);
	
DROP TABLE #temp

-- Fonctionne aussi avec les CTE
WITH clients AS (SELECT TOP 10 Nom, Prenom, Email, Adresse FROM Client)
MERGE Client AS target
USING (SELECT Email, Nom, Prenom, Adresse FROM clients) AS source
ON (target.Nom = source.Nom AND target.Prenom = source.Prenom)
WHEN MATCHED
	THEN UPDATE
		SET target.Email = source.Email
WHEN NOT MATCHED BY target
	THEN INSERT (Nom, Prenom, Email, Adresse)
	VALUES (source.Nom, source.Prenom, source.Email, source.Adresse);

Je ne vais pas détailler les trois blocs puisqu’ils font des choses équivalents, mais attirer l’attention sur le premier et en particulier les lignes 7, et 10.

Pour la ligne 6 : c’est une jointure tout ce qu’il y a de plus banal, comme celle que l’on peut réaliser avec l’instruction « JOIN » (ou « INNER JOIN », c’est la même chose).

Pour les lignes 7 et 10 : on peut voir qu’il est possible de réaliser des actions si les lignes correspondent à la jointure.

Ainsi, il est possible de faire des actions :

  • Si elles correspondent : « WHEN MATCHED », il est alors possible d’utiliser les instructions UPDATE ou DELETE
  • Si elles ne correspondent pas : « WHEN NOT MATCHED BY « , il est alors possible d’utiliser les instructions UPDATE et DELETE

Par défaut, l’action dans le « NOT MATCHED » s’applique à ce qui est spécifié après le « BY ».
Ainsi, il ne faut pas non plus le spécifier par la suite.

Note : dans le 2nd cas, il FAUT ajouter le point-virgule pour séparer l’instruction MERGE de la création/alimentation de la table temporaire.
Si ce n’est pas fait, vous aurez droit à une erreur :

Msg 325, Level 15, State 1, Line 6
Syntaxe incorrecte près de 'MERGE'. Vous devrez peut-être affecter au niveau de compatibilité de la base de données actuelle une valeur plus élevée pour activer cette fonctionnalité. Reportez-vous à l'aide relative à l'option SET COMPATIBILITY_LEVEL de ALTER DATABASE.
Msg 156, Level 15, State 1, Line 7
Syntaxe incorrecte vers le mot clé 'AS'.
Catégories :Développement, Sql Server

SQL-Server – RANK et ROW_NUMBER

Récemment, j’ai du faire un petit exercice relativement simple : importer des données en base depuis un fichier CSV.
Avec le bulk insert, c’est assez simple :

BULK INSERT #Bulk
FROM 'C:\Temp\FichierAImporter.csv' 
WITH 
(
	FIELDTERMINATOR = ';',
	ROWTERMINATOR = '\n',
	CODEPAGE = 'ACP',
	ROWS_PER_BATCH = 35000
)

Là où ça s’est compliqué, c’est la génération des identifiants fonctionnels avant leur insertion dans les « vraies » tables.

Pour simplifier un peu, voici le cas.
Je dois importer les nouvelles adresses de clients.
J’ai deux champs : Client et Adresse qui sont chargées depuis le fichier CSV.

Mais surtout, j’ai deux identifiants fonctionnels à créer.
Le premier est un identifiant interne, de la forme « FR00000000 » dont la partie numérique est incrémentée pour chaque entrée. Le prochain identifiant est lisible depuis une table.
Le second identifiant est externe, c’est un numérique incrémenté par client.
C’est à dire que le client #1 possèdant déjà 50 adresses, la suivants sera la 51, la prochaine pour le client #2 est la 6ème.

La bonne vieille méthode, c’est de faire des boucles pour alimenter tout le bazar.
Mais comme on est dans une base de données, c’est quand même nettement mieux de faire de l’ensembliste.
C’est là que RANK et ROW_NUMBER viennent à la rescousse !

Lire la suite…

Catégories :Développement, SGBDR, Sql Server

Optimisations C#, Entity Framework et Sql

Ça fait un petit moment maintenant que je fais des tests de charges.
Le code n’était pas toujours optimisé au mieux, ce qui m’a posé divers problèmes : timeouts sur la base de données, explosions des requêtes Linq to Entities, Entity Framework à la ramasse…

Bref, j’ai du modifier pas mal de choses pour arriver à un résultat plus acceptable.
Dans ce billet, ce sera juste la situation générale et les liens vers les billets détaillant la résolution.

Dans un premier temps, voici grosso modo l’algorithme :

  1. Préparation du traitement
    1. Récupération des informations pour paramétrage
    2. Récupération de données de références pour les modifier et créer de nouvelles données
    3. Création en base des données de test
    4. Création d’un fichier en entrée
  2. Lancement du traitement
    1. Lecture du fichier
    2. Conversion du fichier en objets
    3. Validation des lignes
    4. Récupération des données complémentaires (connecté à la BDD)
    5. Création des nouvelles entrées (déconnecté)
    6. Insertion en base de données (transactionnel)
  3. Validation du test
    1. Récupération des lignes générées en BDD
    2. Validation des lignes

Si pour un test sur 500 lignes (quelques secondes), ça passe pas trop mal, sur 50.000, c’était pas franchement le même succès (près de 10 minutes).

Les points majeurs d’achoppement se situaient sur la récupération de la donnée de référence (point 1.2); insertion en base des données de test (les 50.000 lignes, point 1.3); récupération des données complémentaires (point 2.4) et enfin l’insertion en base de données des lignes traitées (2.6).

Pour les points 1.3 et 2.6, c’est le Bulk Insert qui m’a sauvé.
J’en ai parlé ici : [C#] Entity Framework et Bulk Insert.

Pour le point 1.2, c’est du SQL avec le hint FORCESEEK : [C#-TSQL] FORCESEEK et Entity Framework.

Et enfin, pour le point 2.4, c’est les requêtes compilées d’EF 5 : [C# EF5] Requêtes Compilées.

Au final, 50.000 lignes sont maintenant traitées en moins de 3 minutes (le scénario complet).
C’est plutôt pas mal, d’autant que le DBA m’a demandé de mettre quelques temporisation pour laisser respirer un peu la base de données (et permettre aux autres applications de tourner) ^^

Catégories :.Net, Développement, Sql Server

Copier des données d’une base à une autre

Récemment, j’ai eu besoin d’alimenter une base de données locale avec les données d’une autre base de données.
La contrainte est que la base de données source est un SQL Server 2008 R2, la base de données cible un SQL Server Express 2005.
Donc, on élimine de suite le backup/restore.

Après, il y a sans doute des outils ô combien puissant pour faire ça.
Mais…j’aime bien me faire les miens.

Du coup, j’ai réutilisé une couche d’accès aux données que j’avais déjà (que j’avais partiellement donné ici : [C# SqlServer] Moteur de requêtage) ainsi que le bulk insert (dont j’avais parlé ici [C#] Bulk Insert).

En modifiant le premier pour intégrer le deuxième, ça m’a donné le résultat escompté en un temps record.

Lire la suite…

Catégories :.Net, C#, Développement, Sql Server

SQL Server Profiler

Aujourd’hui, on va discuter d’un outil assez important que tous développeur qui peut être amené à faire du T-SQL (sous SQL Server) devrait connaître.
Bon, je dis ça, mais y a pas si longtemps, je ne le connaissais pas.
Mais c’est un outil ô combien pratique qui peut permettre de surveiller ce qui se passe sur une base de données et aider à résoudre des problèmes de performances. Et, accessoirement, il est plus « humainement » lisible qu’un plan d’exécution (enfin, ça, c’est mon avis…).

Bref, c’est outil, c’est SQL Server Profiler (oui, le titre spolait déjà le billet !).

Lire la suite…

Catégories :Développement, SGBDR, Sql Server

[C#] Bulk Insert

Voici la problématique que j’ai eu récemment : pour mes tests automatisés, j’ai besoin d’insérer des données.
Jusque là, tout va bien.
Pour des tests de charge, j’ai besoin de beaucoup de données.

Sur le ring, j’ai donc :
Côté droit, ma table, près de 70.000.000 de lignes, plus de 80 colonnes.
Côté gauche, mes données, presque 5.000 lignes à insérer.

A la base, mon moteur de gestion de base de données gère les insertions ligne par ligne (il n’a pas été fait, à l’origine, pour du test de charge).
Du coup, pour plusieurs lignes, c’est géré via une basique boucle.
Mais voilà, avec cette méthode, j’ai un rythme d’environ 500 lignes par minute.
Pas terrible, terrible…

Alors, comment faire ?
Le Bulk Insert à la rescousse !

Lire la suite…

Catégories :.Net, C#, Développement, Sql Server

Traiter les données dans le code ou en base ?

Cela fait plusieurs fois que je vois de gros traitements de manipulation de données directement dans le code.

A chaque fois, il y a eu des variations, mais en règle générale, c’est un algorithme plus ou moins comme cela :

Récupération des données depuis la base de données.
Pour chaque ligne récupérée.
     Si le champX est égale à "variable1", alors on lance la procédure "procedure1" pour cette ligne.
     Si le champX est égale à "variable2", alors on lance la procédure "procedure2" pour cette ligne.
     Si le champX est égale à "variable3", alors on lance la procédure "procedure3" pour cette ligne.
     Sinon, on lance la procédure "procedure4" pour cette ligne.

Le tout avec du if, else if, esle (ou un switch, c’est selon), parfois d’autres boucles à l’intérieur de la première (forception, foreachception ou whileception, si je puis dire).

Comme je l’ai dis, il y a des variantes, mais cependant, si les causes sont différentes, l’effet reste le même : c’est pas performant.
Voir pire, ça occasionne des timeout vis-à-vis de la base de données.

Mais, alors, que faire ?
Le traitement ensembliste. En base de données.
Voilà, c’est tout.

Alors, je sais que parfois, la base ne doit être utilisée que pour stocker les données (sic…) et que le code métier ne doit se trouver qu’en un seul et unique endroit (sic bis…), que la base de données ne doit pas contenir d’intelligence (sic ter…).

Mais parfois (souvent ?), c’est juste très con de vouloir procéder comme cela.
Bon, l’avantage, c’est quand même que c’est tellement à la ramasse qu’optimiser n’est pas très compliqué… (en fait, l’étape « ohmondieucesthorrible » est plus longue à gérer).

Quand on a un SGBDR, il ne faut pas oublier que le G, c’est pour Gestion.
C’est autant de la gestion des base de données que de données elles-mêmes.
En somme, les SGBDR sont fait pour traiter de la données !!!

Comment on fait, du coup ?
A chaque fois que j’ai eu ce problème, il a été résolu avec des tables temporaires.
En somme, récupération des données vers une table temporaire, puis traitement des données depuis cette table en reprenant les conditions mentionnées plus haut.
Donc, j’ai du :

UPDATE #MaTable
SET ChampY = "Valeur1"
WHERE ChampX = "variable1"

Et là, miraculeusement (ou pas), au lieu de traiter les lignes une par une, le SGBDR va traiter toutes les lignes.

Après, il faut quand même réfléchir un peu à comment on peut procéder, mais il est tout à fait possible de gérer les données de cette manière.

Un exemple ?
Là, j’ai un traitement qui doit gérer 70.000 lignes.

C’est pas une volumétrie très importante, mais le traitement via code dure 20 minutes avant que la connexion à la base de données ne parte en timeout.
Avec les traitements ensemblistes, la même volume de données (c’est à dire le MÊME cas de test) dure…environ 15 secondes.

Oui, oui, 15 secondes.
Et même en purgeant le cache de SqlServer (pour info).
DBCC DROPCLEANBUFFERS
DBCC FREEPROCCACHE

Sur d’autres cas de tests, le gain de temps varie d’un facteur 1 (pour les très petits volumes) à 10 (pour les volumes plus importants et mixtes).
Le coup des 15 secondes est sur un scénario assez précis et j’avoue que…eh bien, je m’attendais pas à un tel gain ! ^^

J’ai pourtant 8 insertions de différents types (et différentes provenances) dans ma table temporaire, 18 updates différents puis insertion des données (conditionnées) dans une table physique et modification de deux autres tables (dont l’un compte plus de 67 millions d’entrée).

 
 
Alors OUI aux traitements de masse des données dans la base de données et NON aux mêmes traitements dans le code !!!

Catégories :Développement, Humeur, SGBDR