Business Geek

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

Tag - SQL Server 2005

Fil des billets - Fil des commentaires

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.

jeudi 6 mars 2008

Connection String et Application Name

Je voudrais passer un message à tous les développeurs. Dans tous vos développements, lorsque vous avez des chaînes de connexion...

utilisez la propriété "application name" !!

 

En effet, quand on profile une base de données pour auditer les performances ou pour recenser les utilisateurs en vue d'un migration par exemple, cette information est importante pour voir plus facilement qui fait quoi. Sinon, vous verrez apparaître dans vos traces l'application .NET SqlClient Data Provider qui est tout simplement la valeur par défaut de ADO.NET.

Pour pallier à cela, voilà à quoi doit ressembler votre chaîne de connexion :

Data Source=SERVER;Initial Catalog=DATABASE;Integrated Security=SSPI;Application Name=www.bewise.fr

Merci d'avance pour les gens qui travaillent sur la BD.

dimanche 17 février 2008

Insertion en masse et Triggers

On m'a récemment posé cette question :

j'ai 300.000 enregistrements à insérer et pour chaque insertion, les triggers doivent être exécutés

Quand on parle d'une telle volumétrie d'insertion, on pense tout de suite à de l'insertion en mode BULK. Pour ce faire, nous avons à disposition :

  • un utilitaire en ligne de commande : bcp
  • l'instruction T-SQL : BULK INSERT

Le problème c'est que l'insertion BULK désactive les triggers par défaut. Mais les deux méthodes proposent une option pour ré-activer les triggers.

bcp -h "FIRE_TRIGGERS"

BULK INSERT .... WITH (FIRE_TRIGGERS)

Integration Services utilise intensivement l'insertion BULK via son composant OLE DB Destination avec l'option FastLoad. Les triggers étant désactivés par défaut, il faut les réactiver dans les options avancées du composant. Dans les propriétés, il y en a une appelée FastLoadOptions qui correspond au WITH de l'instruction BULK INSERT. Il suffit juste de rajouter FIRE_TRIGGERS dans la liste (séparé par des virgules).

samedi 16 février 2008

Faire un cumul avec Reporting Services

Erratum :

J'étais fatigué (retour de TechDays ??) quand j'ai écrit cet article car il y a une solution beaucoup plus simple (que je connaissais en plus). Je n'ai pas voulu écouter la petite voix dans ma tête qui me disait "abruti, abruti" quand j'ai cité la fonction Previous. Bref, la solution est :

=RunningValue(Fields!SubTotal.Value, Sum, Nothing)

Mais je vous invite à lire la solution proposée ci-dessous car elle est utile pour de aggrégats un peu custom (et puis pour dire que je n'ai pas gratté pour rien).

Article Original

Dans cet article nous allons voir comment faire une somme cumulée dans un rapport. Le principe est simple, j'ai des ventes dans le temps et pour chaque date, je veux le cumul depuis la première date.

Reporting Services n'offre pas cette fonctionnalité en natif mais nous allons mettre en place les élements pour le suporter. Sans vouloir gâcher la surprise, je peux déjà vous annoncer que l'on va passer par du code .NET ajouté au rapport.

Pour commencer, voyons ce que nous offre Reporting Services comme fonction en rapport avec notre besoin. Il y a la fonction Previous qui permet de récupérer des données dans la ligne précédente. Cependant, cela ne marche plus à partir de la 3eme ligne car la fonction ne remonte pas plus haut.

L'idée pour résoudre cette problématique est d'utiliser une variable qui va tenir le cumul depuis la première ligne. A chaque ligne, on ajoute la valeur en cours. Pour garder une variable, on doit passer par le code .NET.

Pour ce faire on passe par la fenêtre de propriétés du rapport, dans l'onglet Code.

Oubliez le confort de l'Intellisense, vous n'êtes pas dans Visual Studio, l'éditeur ressemble plutôt à Notepad. En espérant que Reporting Services 2008 apporte des améliorations dans ce sens (comme pour Integration Services). En plus, vous êtes limité uniquement à VB.NET comme langage.

Quant au code en lui-même il est assez simple. On garde un variable Shared (static) et on fournit une petite fonction pour mettre à jour et retourner le cumul.  Ici j'ai choisi le type Decimal car je souhaite retourner une somme d'argent. Rien de plus simple :

Shared cumul As Decimal = 0.0
Public Shared Function GetCumulativeSum(ByVal currentValue As Decimal) As Decimal
  cumul += currentValue
  Return cumul
End Function

Reste à utiliser ce code dans le rapport. Pour cela, on utilise une Expression dans un élément de notre DataRegion :

=Code!GetCumulativeSum(Fields!SalesAmount.Value)

 

Un des problèmes est que l'on ne peut pas utiliser cette technique de cumul à différents endroits. En effet, on ne repart jamais à zéro. Plutôt que de tenter de réinitialiser le cumul dans la première ligne d'une DataRegion, on préfèrera l'utilisation d'un dictionnaire dans le Custom Code. Voici le code utilisant un dictionnaire :

Shared cumuls As System.Collections.Generic.Dictionary(Of String, Decimal) = _
     New System.Collections.Generic.Dictionary(Of String, Decimal)
Public Shared Function GetCumulativeSum(ByVal currentValue As Decimal, ByVal sumKey As String) As Decimal
  If Not cumuls.ContainsKey(sumKey) Then
    cumuls.Add(sumKey, 0.0)
  End If
  cumuls.Item(sumKey) += currentValue
  Return cumuls.Item(sumKey)
End Function

Dans le rapport, on utilisera l'expression suivante :

=Code!GetCumulativeSum(Fields!SalesAmount.Value, "salesInChart")
...
=Code!GetCumulativeSum(Fields!TaxAmount.Value, "taxInMatrix")

Note : pour faire du C#, il faudra coder la fonction dans une assembly et la référencer dans le rapport.

vendredi 15 février 2008

Compatibilité Management Studio 2008 et SQL Server 2005

J'en avais assez de travailler sur SQL Server 2008 sur une VPC alors j'ai sauté le pas en essayant de l'installer sur mon Vista classique. Pour une raison que j'ignore, la première installation s'est mal passée dans le sens ou il était impossible d'xécuter une requête sur aucune des instances (2005 & 2008).

Je tente un tout bête uninstall / install et miracle, tout marche bien. Les 2 versions cohabitent super bien et l'effet que j'attendais :

J'ai l'intellisense sous SQL Server 2005 grâce à Management Studio 2008 qui arrive à travailler avec une base de données de version inférieure.

Je n'ai évidemment pas tout testé : Full-Text, Broker, connexion IP, etc. mais je vous remonterais les problèmes si j'en rencontre.

A noter que je n'ai rien installé des outils de Business Intelligence (je suis sur un projet et je ne veux rien casser), je vais faire des essais et je reviendrai dessus dans un prochain post.

 

jeudi 14 février 2008

Récupérer uniquement la date d'un champ DateTime

En attendant l'utilisation massive du champ Date (juste la date) de SQL Server 2008, il faut ruser pour récupérer une date sans les heures/minutes depuis un champ DateTime.

2 méthodes s'offrent à nous :

  1. Cast(DATEDIFF(day, 0, monchampDateTime) As DateTime)
  2. Convert(varchar(20), monchampDateTime, 102)

Le premier est plus performant que le second. Le second renvoie un type string.

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é

mardi 29 janvier 2008

Vues indexées et optimisation

Les vues indexées sont un excellent vecteur d'optimisation et j'en suis un des ardents défenseurs !! Je les utilise depuis SQL Server 2000 pour optimiser des requêtes un peu complexes ou fréquemment utilisées. Le but de ce post n'est pas de détailler les vues indexées mais de présenter un problème qui m'est arrivé.

Lors d'une formation SQL Server, je présente comme d'habitude l'intérêt des vues indexées et on décide de faire une petite démo avec AdventureWorks. L'idée est d'optimiser la récupération des descriptions de produits. La vue est la suivante : 

Create View [dbo].[vProdDesc]
WITH SCHEMABINDING
as
select p.name, d.description
from production.product p
    inner join Production.ProductModelProductDescriptionCulture dc
        on dc.ProductModelID = p.ProductModelID
        and cultureid = 'fr'
    inner join production.productdescription d
        on d.ProductDescriptionID = dc.ProductDescriptionID

Je pose un index UNIQUE et CLUSTERED sur la vue et pour montrer l'utilisation de l'index j'exécute un simple SELECT sur la vue :

Select * From dbo.vProdDesc

Le plan d'exécution généré montre clairement que l'index n'est pas utilisé. J'utilise donc tous les grigris SQL en ma possession : DBCC FREEPROCCACHE, sp_recompile, table hint, mais rien n'y fait.

Un petit tour dans l'aide pour voir si je n'ai pas zappé quelquechose et je tombe sur une phrase annodine :

Indexed views can be created in any edition of SQL Server 2005. In SQL Server 2005 Enterprise Edition, the query optimizer automatically considers the indexed view. To use an indexed view in all other editions, the NOEXPAND table hint must be used.

Je n'ai pourtant jamais utiliser ce hint pour activer des vues indexées. Et pourtant, en l'ajoutant à ma requête, le plan d'exécution est correct et tire bénéfice de l'index.

Select * From dbo.vProdDesc WITH (NOEXPAND)

Un coup d'oeil sur la version :

Select @@version

Microsoft SQL Server 2005 - 9.00.3054.00 (Intel X86)   Mar 23 2007 16:28:52   Copyright (c) 1988-2005 Microsoft Corporation  Developer Edition on Windows NT 6.0 (Build 6000: )

Conclusion : l'édition Developer est bien bridée par rapport à la version Enterprise. Et l'effet de bord à cela c'est que l'on ne peut pas utiliser de vue indexées dites "techniques" (ie. utilisées implicitement par l'optimiseur de requêtes) car on ne peut pas préciser l'option NOEXPAND si la vue n'est pas utilisée directement.

Ce que je ne comprends pas c'est qu'il me semblait toujours avoir développé sur des Developer Edition or je n'ai jamais rencontré cet effet de bord. Donc je commence à croire que c'était des Enterprise... Ah le luxe, quand tu nous tiens...

- page 1 de 2