Business Geek

Aller au contenu | Aller au menu | Aller à la recherche

jeudi 28 août 2008

Agrégat CLR – Nouveauté 2008

En ce moment, je teste pas mal SQL Server 2008 et en particulier les améliorations par rapport à la version précédente. Aujourd’hui, je me suis intéressé aux fonctionnalités de la SQLCLR et j’ai voulu savoir si une des limitations de 2005 était levée.

Le problème était le suivant (le fait que je parle à l’imparfait devrait vous aiguiller sur la réponse) : dans une fonction d’agrégat CLR, il n’était possible de fournir qu’un seul paramètre à la méthode Accumulate. Ceci était pénalisant pour certaines fonctions à double entrée ou pour paramétrer le comportement d’autre.

Par exemple, sur une fonction de concaténation (très utile dans les reports), il était impossible de préciser le caractère à utiliser comme séparateur. Cela a entraîné (chez nous en tout cas) de grands débats sur le choix caractère à utiliser dans le code .NET. Cela impliquait aussi l’utilisation de méthodes de Replace dans les couches supérieures pour traiter le caractère.

Venons-en à la solution que propose SQL Server 2008 maintenant.  Je suis en fait tombé sur la documentation suivante :

CREATE AGGREGATE [ schema_name . ] aggregate_name
        (@param_name <input_sqltype> 
        [ ,...n ] )

J’ai été intrigué et admiratif sur le ..n dans la liste de paramètres qui suppose que les fonctions d’agrégat de 2008 supportent le multi-paramètre.
Je me suis empressé de modifier quelque peu l’agrégat de concaténation des exemples de SQL Server et j’ai modifié la méthode Accumulate de cette façon :

[Microsoft.SqlServer.Server.SqlUserDefinedAggregate(…)]
public struct Concat {
    […]
    public void Accumulate(SqlString Value, SqlString separator)
    {
        if (!isFirst)
            sb.Append(separator);
        else
            isFirst = false;
        sb.Append(Value.Value);
    }

Après avoir intégré l’Assembly dans ma base, je mappe la fonction d’agrégat :

CREATE AGGREGATE [dbo].[Concat] (
  @value [nvarchar](4000),
  @separator [nvarchar](10))
RETURNS[nvarchar](4000)
EXTERNAL NAME [BewiseUtils].[Bewise.SqlServer.Concat]

Et le test final :

SELECT dbo.Concat(LastName, ', ')
FROM Person.Person

Et voila le travail… Chacun peu choisir son séparateur dans la fonction de concaténation.

dimanche 3 août 2008

Récupérer la taille d'une base de données

ll existe différentes techniques pour récupérer la taille d'une base de données. Je vous en livre ici une que j'utilise. Elle se base sur la vue système sys.database_files qui nous renseigne sur les fichiers de la base de données.

SELECT dbname, [ROWS]*8 as Donnees, [LOG]*8 as Journaux
FROM
   (SELECT DB_NAME() as dbname, type_desc, size
   FROM sys.database_files) as p
PIVOT
   (SUM(size) FOR type_desc IN ([ROWS], [LOG])) as pvt

Grâce à la fonction PIVOT, j'aggrège les fichiers de données et journaux. Vous noterez aussi que je multiplie la taille par 8 car la valeur de cette vue correspond à un nombre de pages (8KB). Nous obtenons donc une taille en kilo-octets.

Pour connaître les tailles de toutes les bases de données d'un serveur, on peut utiliser se script combiné avec la fonction système sp_MSforeachDB

CREATE TABLE #TMP_DB_Sizes (
  dbname sysname,
  datasizeinkb int,
  logsizeinkb int)

EXECUTE sp_msforeachdb '
INSERT INTO #TMP_DB_Sizes
SELECT dbname, [ROWS]*8 as Donnees, [LOG]*8 as Journaux
FROM
   (SELECT ''[?]'' as dbname, type_desc, size
   FROM [?].sys.database_files) as p
PIVOT
   (SUM(size) FOR type_desc IN ([ROWS], [LOG])) as pvt'

select * from #TMP_DB_Sizes

drop table #TMP_DB_Sizes

J'utilise une table temporaire pour n'avoir qu'un jeu de résultat. Vous noterez l'utilisation de '[?]' pour injecter le nom de la base en cours dans le script

Il existe bien évidemment d'autres solutions, venant de 2000 ou de 2005, certaines apportant d'autres informations comme le pourcentage d'espace inutilisé.

Bon audit !!

Transactions imbriquées et SAVE TRANSACTION

Lorsque l'on travaille avec des transactions imbriquées, on veut parfois pour voir annuler une transaction intermédiaire mais garder les précédentes opérations.

En T-SQL, l'opération de ROLLBACK annule l'ensemble des niveaux de transactions (scopes). Pour s'en convaincre, cette citation de la documentation (ROLLBACK) :

When nesting transactions, this same statement rolls back all inner transactions to the outermost BEGIN TRANSACTION statement.

Le code suivant montre un exemple du problème. Le premier ROLLBACK annule tout depuis la transaction A. La variable @@trancount permet de voir qu'il n'y a plus de transaction en cours

BEGIN TRAN A
-------on démarre le premier niveau de transaction

INSERT INTO [AdventureWorks].[dbo].[truc]([a],[b])
VALUES ('transaction A','transaction A')

select @@trancount --returns 1

BEGIN TRAN B
-------on démarre le deuxième niveau de transaction

INSERT INTO [AdventureWorks].[dbo].[truc]([a], [b])
VALUES ('transaction B','transaction B')

select @@trancount --returns 2

ROLLBACK TRANSACTION B
--on annule la transaction intermédiaire

select @@trancount
--returns 0 : plus de transaction en cours

COMMIT TRAN A --génère une erreur
--Msg 6401, Level 16, State 1, Line 19
--Cannot roll back B. No transaction or savepoint of that name was found.

Ce comportement ne nous permet donc pas d'annuler une partie du traitement tout en gardant le reste. Heureusement, l'instruction SAVE TRANSACTION nous permet de corriger le tir. SAVE TRANSACTION permet de mettre un point de sauvegarde dans la transaction en cours pour pouvoir y revenir par la suite, avec l'instruction ROLLBACK.

Voici le script modifié. A la fin, la ligne A est insérée mais pas la B.

BEGIN TRAN A
-------on démarre le premier niveau de transaction

INSERT INTO [AdventureWorks].[dbo].[truc]([a],[b])
VALUES ('transaction A','transaction A')

select @@trancount --returns 1

BEGIN TRAN B
-------on démarre le deuxième niveau de transaction

SAVE TRANSACTION B_Saved
--on met un point de sauvegarde

INSERT INTO [AdventureWorks].[dbo].[truc]([a], [b])
VALUES ('transaction B','transaction B')

select @@trancount --returns 2

ROLLBACK TRANSACTION B_Saved

COMMIT B
--on force quand même un commit pour valider la transaction B

select @@trancount --returns 1

COMMIT TRAN A

A noter que l'on doit quand même faire un COMMIT de la transaction enfant car le retour à un point de sauvegarde ne termine pas la transaction.

jeudi 24 avril 2008

De l'utilisation de NOLOCK

J'ai eu il y a quelques temps la question suivante :

j'aimerais faire comprendre à mes développeurs l'intéret de positionner des NOLOCK dans les requêtes de sélection, peux-tu m'aider pour faire l'argumentaire

Pour rappel ou pour ceux qui ne voient pas de quoi je parle, NOLOCK est un Query Hint qui permet de passer le mode d'isolation au niveau le plus bas sur la lecture d'une table. Il s'utilise de la façon suivante :

Select * From MaTable WITH (NOLOCK)

Voici la réponse que j'ai donné :

Premièrement et c'est le plus important, utiliser NOLOCK n'est pas « naturel », dans le sens où la base de donnnées est là pour garantir les accès concurrents et gérer des lecture/écriture dans une optique transactionnelle (contraintes ACID1) et ceux même pour un énorme volume de transactions.

Cependant, sans aller jusqu'à dire que c'est un constat d'échec que de l'utiliser, le hint NOLOCK est intéressant dans certains contextes.
Notamment les sites Web qui font beaucoup d'accès en lecture pour faire principalement de l'affichage. Dans ce cas, en effet, avoir une donnée non commitée n'est (en général) pas critique, sauf pour certains sites (bourses, état de stock, etc.). Cela permet de soulager la base de données et de booster l'affichage des pages.

Mais pour moi, le NOLOCK n'est qu'une réponse technique d'urgence et ne sert juste qu'à cacher la poussière sous le tapis. Il faut comprendre pourquoi l'ajout d'une ligne dans une facture bloque l'affichage du top 10 des vendeurs et essayer d'y répondre par la conception de l'application (cache, portée des transactions, etc.) ou de la base (normalisation/dénormalisation).
J'insiste qu'il y a un risque à utiliser NOLOCK à tout va, car on peut récupérer des données "dirty" et les réécrire par la suite et perdre la cohérence (aCid); à l'application d'adresser cette contrainte.

Donc pour conclure :

  • le NOLOCK est une réponse technique intéressante pour optimiser l'affichage de données NON CRITIQUES
  • l'utilisation systématique du NOLOCK n'est pas une bonne chose car elle cache un réel problème transactionnel. Et surtout on ne respecte plus la "norme" ACID.

Si tu utilises SQL Server 2005, il faut te tourner vers les autres niveaux de verrou comme le Snapshot (READ_COMMITTED_SNAPSHOT) qui apporte la souplesse en lecture tout en garantissant les transactions (comme le fait Oracle). Un bon argument pour passer à 2005.

1ACID: Atomicité, Cohérence, Isolation, Durabilité

jeudi 27 mars 2008

De l'instruction COALESCE

Ce petit post pour faire découvrir ou, je l'espère,  redécouvrir, une instruction T-SQL : COALESCE. La définition de cette instruction est :

retourne la première valeur non nulle.

Quand je parle de cette instruction que j'utilise depuis 2001, on me dit toujours : ben t'as qu'à utiliser ISNULL... Ce n'est pas faux car ISNULL fait la même chose mais uniquement entre 2 valeurs.

Récemment, je suis tombé sur ce code T-SQL, assez illisible : 

ISNULL(matable.Col1, ISNULL(maTable.col2, ISNULL(DefTable.Col1, @ValeurParDefaut))

En "correction", j'ai proposé :

COALESCE(matable.Col1, maTable.col2, DefTable.Col1, @ValeurParDefaut)

Voila, ça fait la même chose mais c'est plus propre et plus simple à maintenir.

dimanche 9 mars 2008

Ne faites pas confiance au designer de requête

Chez presque tous mes clients, tout le monde utilise le Query Designer pour écrire un SELECT en T-SQL, que ce soit pour faire une vue, une simple recherche ou encore pour créer un Dataset dans Reporting Services. Nombre de mes collègues utilise cet outil également.

A chaque fois je fais la même remarque qui me fait passer pour un vieux con has been :

Evitez d'utiliser le designer de requête ou bien refaite une passe sur le T-SQL généré.

Mes arguments étant les suivants : ordre des jointures, lisibilité du code généré et une expérience avec la bête ayant développée ma mauvaise impression à son égard.

Mais depuis SQL Server 2005, j'ai dû souvent avaler mon chapeau car la qualité de l'optimiseur de requêtes faisait passer mon argumentaire pour un discours de bonimenteur.

Mais j'ai enfin trouvé un cas reproductible où le designer de requête ne ramène pas le résultat escompté et où l'optimiseur de requêtes ne peut pas corriger le tir. En plus, pour appuyer mon discours, je tiens à préciser que cette imprécision du designer a conduit à des erreurs dans des rapports de synthèse.

Explication de texte :

Nous allons travailler avec AdventureWorks. Mon besoin est le suivant : je veux récupérer les produits bleus et, si j'ai l'info, afficher leur prix en juillet 2002. Il y a 26 produits bleus dans la table Product.

 Voici la requête fournie par l'éditeur de requête :

SELECT Production.Product.ProductNumber, Production.Product.Name, Production.ProductCostHistory.StandardCost AS Cost200112
FROM Production.Product LEFT OUTER JOIN
Production.ProductCostHistory ON Production.Product.ProductID = Production.ProductCostHistory.ProductI
WHERE (Production.ProductCostHistory.StartDate = CONVERT(DATETIME, '2002-07-01 00:00:00', 102)) AND
(Production.Product.Color = N'Blue')

Et en résultat, je n'ai qu'un seul produit bleu au lieu de 26. Le problème est que le designer ajoute la condition de la date à l'ensemble du jeu de résultat, or s'il n'y a pas de prix disponible à la date demandée, la valeur sera NULL à cause de la jointure externe.

Pour corriger le problème, il faut remonter la condition sur la jointure pour filter la table ProductCostHistory avant de faire la jointure.

ON Production.Product.ProductID = Production.ProductCostHistory.ProductID AND
(Production.ProductCostHistory.StartDate = CONVERT(DATETIME, '2002-07-01 00:00:00', 102))

 Ainsi, le LEFT JOIN est correctement interprété à l'exécution et on obtient bien les 26 produits bleus. On utilisait cette  technique pour optimiser sous SQL Server 2000 car il arrivait que l'optimiseur ordonne mal les opérations dans le plan d'exécution.

Si on réouvre la requête avec l'éditeur, il représente bien la condition mais de façon assez... bizarre. D'ailleurs, je ne saurais même pas le refaire à la souris. 

Aussi, le designer écrit les tables de la clause FROM dans l'ordre où vous posez les tables sur la surface de travail, ce qui lui fait mettre un RIGHT JOIN au lieu d'une lecture plus naturelle avec un LEFT JOIN. Ceci entraîne une maintenance plus complexe, déjà que se plonger dans une grosse requête n'est pas forcément trivial...

Autre problème que j'ai rencontré quelques fois, quand il y a trop de jointures, LEFT, RIGHT et INNER, en essayant de les mettre bout à bout il arrive qu'il se rate sur le résultat global (bien souvent avec des données non remontées) :-(.

En conclusion, même si le designer vous fait gagner du temps de rédaction, pensez à vérifier les jointures (ordre, condition) et en particulier les jointures externes.

samedi 2 février 2008

TABLESAMPLE, une alternative à ORDER BY NEWID()

Juste un petit post pour présenter une instruction TSQL utile à connaître. Quand vous voulez obtenir un jeu d'enregistrement d'une table de façon aléatoire, il était commode d'utiliser la fonction NEWID (qui génère un GUID) dans une clause ORDER BY.

select top 1000 * from person.contact order by newid()

Le problème c'est que l'on arrive vite à des performances médiocres, surtout avec des volumes de données importants. L'instruction TABLESAMPLE sert à récupérer un échantillon d'enregistrements de façon aléatoire et un petit bench nous montre que cette technique enterre la première.

select * from person.contact tablesample (1000 rows)

Remarques

L'instruction TABLESAMPLE ne ramène pas le nombre exact d'enregistrement : c'est parce que le moteur de requêtes choisit aléatoirement des pages (8ko) de l'index clustered. Et une fois une page choisie, il remonte tous les enregistrements qui s'y trouvent. En fonction de la fragmentation et de la taille de l'enregistrement, vous aurez plus ou moins de lignes remontées.

L'instruction TABLESAMPLE fait toujours un SCAN dans le plan d'exécution : c'est parce que le requêteur attribue une valeur aléatoire à chaque page de l'index clustered ou HEAP et choisit ensuite les pages en fonctions de ces valeurs. Le scan est donc obligatoire.

 

PS : merci à Matthieu de Bordeaux de m'en avoir parlé