Business Geek

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

samedi 30 août 2008

Transactions - REPEATABLE READ

Voici le problème que je vais traiter : je veux pouvoir m'assurer dans une transaction qu'un jeu d'enregistrements ne sera pas touché pendant que j'opère une série de modifications. Par exemple, je ne veux pas qu'on puisse modifier une série d'adresses pendant que je mets à jour le contact qui les possède. Pour être plus concret, je veux bloquer les adresses 31 et 52 pendant que je modifie le contact John Smith. Par exemple :

BEGIN TRAN

SELECT * INTO #TempTable FROM Person.Address Where AddressID in (31,52)

UPDATE Person.Person Set Title = 'Lord' Where LastName = 'Smith' and FirstName = 'John'
UPDATE Person.PersonPhone Set [...]

Par défaut, le niveau d'isolation de transaction de SQL Server est READ COMMITED. En d'autres termes, cela signifie que l'on ne peut lire ou modifier que des données qui ont validées (comitées pour reprendre le barbarisme franglais). Selon le moteur de transactions, les adresses peuvent être modifiées pendant que ma transaction a lieu.

Techniquement, dans le moteur SQL, cela se traduit par des locks partagés (Shared) sur la table Address qui durent le temps du premier SELECT et des locks exclusifs (eXclusive) sur les tables modifiées. Une fois le SELECT passé, les locks Shared sont libérés, laissant la place pour des modifications. En revanche les locks exclusifs sont maintenus jusqu'au COMMIT (ou ROLLBACK), empêchant les modifications. On peut le vérifier en regardant les verrous sur les tables en question :

SET TRANSACTION ISOLATION LEVEL READ COMMITTED --mode par défaut

BEGIN TRAN

SELECT *
FROM Person.Address
WHERE AddressID IN (31,52)

select resource_type, resource_description, request_mode, request_type
from sys.dm_tran_locks

ROLLBACK

 

 

Lors de la consultation des locks, il n'y en a aucun donc une instruction parallèle peut modifier les adresses. Nous allons donc utiliser un autre niveau de transaction pour changer le comportement du moteur.

En choisissant le niveau juste au dessus, REPEATABLE READ, le moteur va poser des locks Shared et les maintenir le temps de la transaction. C'est ce que l'on appelle des lectures répétables. Pour modifier un enregistrement, le moteur doit poser un lock exclusif et donc doit attendre de n'avoir plus de locks Shared. Voyons le comportement des verrous, pour cela, il suffit de changer le niveau d'isolation dans la première ligne : 

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

BEGIN TRAN

SELECT *
FROM Person.Address
WHERE AddressID IN (31,52)

select resource_type, resource_description, request_mode, request_type
from sys.dm_tran_locks

ROLLBACK

 

 

On voit bien que des locks Shared (S et IS) sont maintenus après le SELECT. Ceci garantie que ces enregistrements ne seront pas modifiés le temps de ma transaction. De plus, du fait que ce soit des locks Shared, une autre transaction peut lire ces enregistrements.

 

Bien évidemment, la gestion des locks est très fine dans SQL Server et se complexifie avec l'ensemble des requêtes à un instant T. SQL Server va tenter de verrouiller le niveau le plus fin et le moins contraignant pour respecter vos niveaux d'isolation et garantir la transactionalité. Et tout ceci se combine avec les différents objets liés à la table (index, allocations, etc.).

 

Vous aurez noté au passage l'utilisation de la vue système sys.dm_tran_locks qui remplace depuis 2005 l'ancien sp_lock et qui vous aidera à comprendre le fonctionnement du moteur transactionnel et à minimiser la contention sur vos bases.

Bonne optimisation...

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.

mercredi 13 août 2008

Indexation - Gérer la fragmentation

Je continue ma série sur les index avant de faire une petite pause pour préparer une traduction d'article sur les clauses WHERE dynamiques d'un collègue MVP suédois.

Dans ce post, je vais m'intéresser à la fragmentation des index. J'espère que le terme fragmentation rappelera à certains le temps où on passait notre samedi après midi à défragmenter nos disques de 40Mo (oui, méga-octets) pour gagner quelques octets et installer Phantasmagoria.

Le principe est le même, l'idée est de "ranger" un index pour gagner un peu de place et surtout qu'il soit plus performant. La fragmentation est un processus "naturel", dû aux modifications des données. La base vit, grandit et donc il faut opérer de la maintenance régulièrement.

On a 2 types de fragmentation :

  • celle qui touche le niveau feuille de l'index et qui correspond à un ordre sur le disque différent de l'ordre logique dans l'index. Le résultat est trop d'accès disque lors de scans sur ce niveau
  • celle qui touche l'arbre de l'index et qui correspond à des pages trop vides ou trop pleines

On peut utiliser 2 approches pour traiter avec la fragmentation :

  • Traiter systématiquement l'ensemble de sa base de données. C'est la méthode que j'utilise pour des petites volumétries et quand on dispose d'une fenêtre de tir pour faire les opérations de maintenance sans gêner personne (par exemple une base qui est utilisée avec des horaires de bureau).
  • Traiter uniquement les index fragmentés et de façon intelligente. C'est la méthode que j'utilise pour des grosses volumétries et des bases en accès 24/7.

Pour le premier cas, on fait juste un plan de maintenance dans lequel on demande une réorganisation ou une reconstruction des index sur toute la base. Sauf cas contraire, je fais un REORGANIZE tous les soirs et un REBUILD le week-end (cela ne reste qu'une stratégie parmi d'autres). Pour les puristes, les instructions T-SQL sont :

ALTER INDEX IX_abcd ON maTable REORGANIZE

ALTER INDEX IX_abcd ON maTable REBUILD

Pour le second cas, il faut déterminer si un index a besoin d'une reconstruction ou d'une réorganisation et connaître la volumétrie (et par conséquent le temps que ça va prendre) pour limiter l'impact sur la production.

On connait la fragmentation d'un index avec la vue système sys.dm_db_index_physical_stats et plus précisément le champ avg_fragmentation_in_percent. Ainsi on peut connaître pour chaque index son niveau de fragmentation :

SELECT DB_NAME(database_id), OBJECT_NAME(object_id), index_id, avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats(
   DB_ID('MaBase'),
   OBJECT_ID('MaTable'),
   NULL, NULL, NULL)

Je ne vais pas rentrer ici dans le détail du pourcentage remonté par SQL Server, je vais lui faire confiance pour le calcul (note : SQL Server 2008 est plus précis dans le calcul car il prend en compte plus de cas).
Sur l'action a mener en fonction de la fragmentation, on trouve toutes les stratégies. Selon Microsoft, on réorganise pour une fragmentation < 30% et on reconstruit au-delà. Moi je n'ai pas de seuil arrêté, il m'arrive de faire un REBUILD quand la fragmentation est > 10%.

Voila, ce post est loin d'être exhaustif car je ne parle pas de la fragmentation par niveau de l'index, du paramètre FILL_FACTOR ou de la reconstruction ONLINE qui permet de reconstruire un index avec peu d'impact sur la production (version Enterprise uniquement).

PS : pour connaître le nom de l'index visé (la requête ci-dessus ne remonte que l'id), il va falloir faire une jointure sur sys.indexes.

Mise à Jour : pour illustrer le problème de la fragmentation d'un index, voici 2 images montrant un index "propre" et un index fragmenté.

Index non-fragmenté

Index fragmenté

Et comme il faut rendre à César ce qui lui appatient, voici mes sources : http://technet.microsoft.com/en-us/magazine/cc671165.aspx

mardi 12 août 2008

BACKUP WITH COPY_ONLY

Trop souvent je vois en consulting les développeurs faire eux-mêmes des backups de la base de production pour les remonter sur un environnement de développement. Et bien évidemment, mauvaise habitude, ils le font avec le wizard de Management Studio.

Le souci c'est que la version 2005 de cet outil ne prend pas en charge une option essentielle pour la création de ce type de backup : WITH COPY_ONLY.

Cette option permet de ne pas casser la stratégie de backup que le DBA, votre collègue ou même vous, auriez mis en place. Sans cette option, SQL Server garde des traces de la sauvegarde et vide le LOG, comme si elle était une continuité du plan en cours.

La conséquence à cela est que si votre base plante entre cette manipulation et le prochain BACKUP FULL "planifié", il manquera à l'administrateur des pièces du puzzle pour remonter la base puisque votre manip sera considérée comme le dernier backup complet valide. Et comme en général, ce fichier part à la poubelle...vous risquez de ne pas pouvoir remonter votre base.

Bien sûr, l'idée n'est pas de brider ces chers développeurs mais de leur enseigner l'art des choses bien faites. La solution :

  • apprendre le T-SQL

BACKUP DATABASE DataWarehouse TO DISK = 'c:\temp\dw.bak' WITH COPY_ONLY, NAME = 'Copie pour Dev'

  • Utiliser Management Studio 2008 qui inclut cette option dans le wizard

Indexation - Faites la chasse aux index HEAP

Un premier post qui j'espère me conduira à une série sur l'indexation dans SQL Server. Je traiterai les points un peu dans le désordre mais n'est-ce pas là l'attrait d'un blog ? Je sais que je ne commence pas par les bases mais bon, j'attend vos questions :-).

Un index de type HEAP indique en réalité le fait qu'il n'y ait aucun index sur la table et surtout pas d'index CLUSTERED. C'est à dire que seul le CREATE TABLE a été exécuté, sans aucune option, sans PRIMARY KEY.

Cela signifie que les données sont rangées n'importe comment ou plutôt dans l'ordre de leur création. C'est comme si je prenais un annuaire et que je mélangais les villes et les noms.

La conséquence d'avoir ce type d'indexation dans sa base de données est très souvent d'avoir des performances catastrophiques lors de la selection de données. D'autant plus quand la volumétrie augmente. Ceci vient du fait que l'opération qui est faite sur la table pour récupérer un jeu d'enregistrement est un TABLE SCAN (aussi appelé Full Scan).

Pour en revenir à mon annuaire, c'est comme si je vous demandais de me trouver toutes les personnes qui habitent au 12, rue des saules, quelque soit la ville. Vous seriez obligé de lire (scanner) toutes les pages.

Pour limiter cela, on peut mettre un index clustered ou une PRIMARY KEY (équivalent d'un index unique non null) pour optimiser la recherche.

Pour retrouver toutes les tables concernées :

select OBJECT_NAME(sys.indexes.object_id)
from sys.indexes
  inner join sys.tables on sys.tables.object_id = sys.indexes.object_id
where sys.indexes.[type] = 0 --0 = HEAP

Donc faite un petit check-up de vos bases et vérifier vos tables stockées sur un index HEAP... Et pourquoi pas les mettre sur un index CLUSTERED... mais je reviendrai sur ce point ;-)

 

 

 

 

 

jeudi 7 août 2008

SQL Server 2008 est RTM...

...depuis quelques heures déjà.


Désolé de ne pas plus m'étendre plus sur cette nouvelle mais je suis chez un client en plein audit de perfs alors je développerai ce soir, pendant l'installation ;-)

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 26 juin 2008

Comparaison SQL Server 2008 et Oracle 11g

Un très bon article forcément partisan mais qui permet de lister les atouts de SQL Server 2008 (et pourquoi pas même 2005) par rapport à son grand rival Oracle.

Le livre blanc est accessible à cette adresse : SQL Server 2008 Compared to Oracle Database 11g

Moi j'aime bien la comparaison des prix ;-)

jeudi 12 juin 2008

De belles références

Non, ce n'est pas un jeu de mot pour présenter une nouvelle Assembly. Je parle ici de références clients qui utilisent avec succès SQL Server.
Le tout présenté dans une petite application Silverlight.

Le site est ici : http://www.microsoft.com/france/sql/ilslontfait/

Les références sont :

  • JCDecaux (plus connu sous le nom de Velib') : 400 000 locations par jour
  • Premier Bankcard : un datawarehouse de 20 To (27 milliards de lignes)
  • AFP : 4000 utilisateurs en simultanés
  • Xiti : 17 milliards d'enregistrements par mois
  • AXA : Business Intelligence temps réel
  • Nancy-Université : réduction des coûts de SAP

- page 1 de 5