Archives de l’auteur : rudib

optimize for adhoc workloads

SQL Server maintient un cache de plans d’exécution. Ces plans permettent au moteur de ne pas ré-optimiser les requêtes si elles sont exécutées une deuxième fois. C’est valable bien sûr pour les procédures stockées, mais aussi pour les requêtes ad-hoc. Le problème avec les requêtes ad-hoc (celles qui sont générées dans le code de l’application au lieu de faire un appel de procédure stockée), c’est que le plan concerne une requête, et non pas une requête paramétrée. En d’autres termes, ces deux requêtes :

SELECT * FROM dbo.Client WHERE Nom = 'MAUGHAN';
SELECT * FROM dbo.Client WHERE Nom = 'LLOYD';

Vont générer deux plans en mémoire (probablement identiques), parce que SQL Server ne paramétrise pas la requête automatiquement, car le plan pourrait être différent selon le nombre de valeur à retourner par paramètre.
Nous pouvons voir la taille occupée par ces plans qui ne sont utilisés qu’une fois à l’aide de la requête suivante :

SELECT SUM(CAST(cp.size_in_bytes as bigint)) / 1024 / 1024 as adhoc_once_mb
FROM sys.dm_exec_cached_plans AS cp
WHERE cp.cacheobjtype = N'Compiled Plan'
AND cp.objtype = N'Adhoc'
AND cp.usecounts = 1
OPTION (RECOMPILE);

Pour économiser cette mémoire, une option de l’instance disponible à partir de SQL Server 2008 peut être activée. Son nom est « optimize for adhoc workloads ». Vous la trouvez dans les propriétés de l’instance, ou vous pouvez la modifier comme suit :

SP_CONFIGURE 'show advanced options',1
RECONFIGURE
GO

SP_CONFIGURE 'optimize for ad hoc workloads',1
RECONFIGURE
GO

Cette option modifie le comportement de SQL Server de la manière suivante : lorsqu’un plan de requête ad-hoc est calculé, il n’est plus directement stocké dans le cache, seul l’est un résumé du plan (un stub) qui permettra de reconnaître le même plan ensuite. Si le même plan doit être calculé une deuxième fois, il sera cette fois gardé en cache.
Je vous recommande d’activer cette option systématiquement sur tous vos serveurs SQL.
Voici une requête synoptique pour vous rendre compte de la situation sur votre serveur:

SELECT
    cacheobjtype,
    objtype,
    'total' as [type],
    SUM(CAST(size_in_bytes as bigint)) / 1024 / 1014 as size_in_mb
FROM sys.dm_exec_cached_plans
GROUP BY cacheobjtype, objtype
UNION ALL
SELECT
    cacheobjtype,
    objtype,
    '1x',
    SUM(CAST(size_in_bytes as bigint)) / 1024 / 1014 as size_in_mb
FROM sys.dm_exec_cached_plans
WHERE usecounts = 1
GROUP BY cacheobjtype, objtype
ORDER BY cacheobjtype, objtype, [type];

Modifier les colonnes TEXT en VARCHAR(MAX)

Le type de données TEXT est déprécié depuis SQL Server 2005. Outre les problèmes de gestion dans le code et sa non compatibilité avec plusieurs fonctions de chaînes, et est gourmand en espace disque et ralentit les opérations de lecture et d’écriture, parce que le moteur de stockage doit créer une allocation spécifique aux LOB pour chaque ligne insérée.
Le type qui le remplace s’appelle VARCHAR(MAX), et l’allocation dans la page ou en LOB se fait dynamiquement selon le contenu inséré dans la colonne. Dans la pratique, cela prend beaucoup moins d’espace en base.

Voici un code se basant sur une vue de catalogue, pour générer les instructions ALTER TABLE pour convertir les types de données, et ensuite pour reconstruire les tables (à partir de SQL Server 2008). Vous pouvez au besoin modifier le code pour accommoder des colonnes NTEXT, et IMAGE (à remplacer par VARBINARY(MAX)) si vous en avez.

SELECT
    'ALTER TABLE ['+TABLE_SCHEMA+'].['+TABLE_NAME+'] ALTER COLUMN ['+COLUMN_NAME+'] VARCHAR(MAX) '
    +CASE IS_NULLABLE WHEN 'YES' THEN 'NULL' ELSE 'NOT NULL' END + ';'
FROM INFORMATION_SCHEMA.COLUMNS
WHERE DATA_TYPE = 'text';

SELECT
    'ALTER TABLE ['+TABLE_SCHEMA+'].['+TABLE_NAME+'] REBUILD;'
FROM INFORMATION_SCHEMA.COLUMNS
WHERE DATA_TYPE = 'text'
GROUP BY TABLE_SCHEMA, TABLE_NAME;

Surveillance de l’activité disque

La requête suivante retourne l’activité disque enregistrée par SQL Server depuis le démarrage de l’instance, par fichier de base de données.

SELECT
    DB_NAME(mf.database_id) as db,
    mf.name,
    LEFT(mf.physical_name, 2) as disque,
    fs.num_of_reads, fs.num_of_bytes_read,
    fs.num_of_writes, fs.num_of_bytes_written,
    fs.io_stall, fs.io_stall_read_ms, fs.io_stall_write_ms
FROM sys.dm_io_virtual_file_stats(null,null) AS fs
INNER JOIN sys.master_files AS mf
ON fs.database_id = mf.database_id
AND fs.[file_id] = mf.[file_id]

informations de session

La requête suivante vous indique les sessions ouvertes sur SQL Server, avec nom de la machine client, le login, type d’authentification (SQL ou Windows), et le nom du programme tel qu’indiqué dans la chaîne de connexion.

SELECT
    c.session_id,
    s.login_name,
    CASE c.auth_scheme
        WHEN 'sql' THEN 'SQL'
        ELSE 'Windows'
    END as Authentification_mode,
    s.host_name,
    s.program_name
FROM sys.dm_exec_connections c
JOIN sys.dm_exec_sessions s ON c.session_id = s.session_id

In-Memory OLTP sur machine virtuelle VirtualBox

Lorsque j’ai voulu créer une table In-Memory dans SQL Server 2014 CTP2, dans une machine virtuelle virtualBox, j’ai obetnu la message d’erreur suivant de la part de SQL Server :

Msg 41342, Level 15, State 1, Line 5

The model of the processor on the system does not support creating filegroups with MEMORY_OPTIMIZED_DATA. This error typically occurs with older processors. See SQL Server Books Online for information on supported models.

J’ai trouvé la réponse ici : mattsql.wordpress.com/2013/07/08/in-memory-oltp-with-sql-server-2014/

qui est: lancer ceci en ligne de commande :

VBoxManage setextradata [vmname] VBoxInternal/CPUM/CMPXCHG16B 1

et redémarrer la machine virtuelle.

voir les requêtes en cours d’exécution

Cette requête utilise la vue de gestion dynamique sys.dm_exec_requests pour lister les requêtes en cours d’exécution avec quelques informations utiles.

SELECT
    s.host_name,
    t.text,
    r.start_time,
    r.status,
    r.total_elapsed_time,
    r.logical_reads,
    r.granted_query_memory
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE s.session_id > 50;

Contraintes non trustées

Une contrainte CHECK est marque comme étant digne de confiance si elle a été créée avec l’option WITH CHECK (valeur par défaut), c’est-à-dire qu’elle vérifie les données existantes dans la table à la création. Une contrainte marquée comme non digne de confiance ne pourra pas être utilisée par l’optimiseur pour éliminer certains cas dans la recherche. Cela vaut donc la peine de vérifier que vos contraintes sont dignes de confiance.
Voici une requête qui liste toutes les contraintes CHECK de votre base de données qui ne sont pas dignes de confiance :

SELECT
    OBJECT_NAME(parent_object_id) AS NomTable,
    name AS NomContrainte
FROM sys.objects
WHERE type_desc = 'CHECK_CONSTRAINT'
AND OBJECTPROPERTY([object_id], 'CnstIsNotTrusted') = 1
ORDER BY NomTable, NomContrainte;

Si vous voulez les rendre dignes de confiance, vous pouvez utiliser une instruction comme celle qui suit :

ALTER TABLE CDRRating WITH CHECK CHECK CONSTRAINT NomDeLaContrainte;
ALTER TABLE CDRRating WITH CHECK CHECK CONSTRAINT ALL; -- pour toutes les contraintes de la table

recherche dans les plans en mémoire

Voici quelques requêtes permettant de faire des recherches dans les plans d’exécution gardés en mémoire dans le cache de plans. Ces plans sont représentés en XML, donc nous utilisons du XQuery pour effectuer les recherches.

Recherche des plans parallélisés :

SELECT TOP 10
p.*,
q.*,
qs.*,
cp.plan_handle
FROM
sys.dm_exec_cached_plans cp
CROSS apply sys.dm_exec_query_plan(cp.plan_handle) p
CROSS apply sys.dm_exec_sql_text(cp.plan_handle) AS q
JOIN sys.dm_exec_query_stats qs
ON qs.plan_handle = cp.plan_handle
WHERE
cp.cacheobjtype = 'Compiled Plan' AND
p.query_plan.value('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";
max(//p:RelOp/@Parallel)', 'float') > 0
OPTION (MAXDOP 1)

Recherche de tous les plans qui utilisent un index:
inspiré de http://stackoverflow.com/questions/17572261/how-to-filter-xml-execution-plan-data-in-a-where-clause-using-tsql

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

DECLARE @IndexName nvarchar(100) = '[I_CDRRating_Status_SubscriberId_BillingPopulationId]';

WITH XMLNAMESPACES (default 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT OBJECT_NAME(qp.objectid, qp.dbid) as obj,
    st.text,
    cp.usecounts,
    cp.objtype,
    qp.query_plan.value('(//RelOp[IndexScan/Object/@Index = sql:variable("@IndexName")]/@PhysicalOp)[1]', 'varchar(50)') as usage,
    qp.query_plan
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) qp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st
where --cp.objtype = 'Proc' and
      qp.query_plan.exist('//RelOp[
                                  (@PhysicalOp = "Index Seek" or @PhysicalOp = "Index Scan") and
                                  IndexScan/Object/@Index = sql:variable("@IndexName")
                                  ]') = 1
ORDER BY cp.usecounts DESC
OPTION (MAXDOP 1);

Retrouver les index manquants dans les plans d’exécution en cache:
Un requête est disponible pour ce faire sur cette entrée de blog de Jason Strate.
http://www.jasonstrate.com/2010/12/can-you-dig-it-missing-indexes/

exporter la structure d’une base avec Powershell

Le code ci-dessous peut être sauvegardé dans un fichier .ps1, pour être utilisé comme script. Changez simplement l’adresse de votre serveur SQL à la ligne 3, et le chemin de sauvegarde des scripts à la ligne 17. Il va exporter toutes les structures de vos bases de données dans des sous-répertoires, dans un répertoire nommé selon le moment de l’exécution. Cela vous permet de conserver plusieurs versions de votre export, et d’utiliser un programme de diff (comme WinMerge) pour voir les différences.

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | out-null

$srv = New-Object "Microsoft.SqlServer.Management.Smo.Server" ".\SQL2008"
$so = New-Object "Microsoft.SqlServer.Management.Smo.ScriptingOptions";
#$so.ScriptDrops = $TRUE;
$so.IncludeIfNotExists = $TRUE;
$so.AppendToFile = $FALSE
$so.ToFileOnly = $TRUE
$so.AnsiFile = $TRUE
$so.ConvertUserDefinedDataTypesToBaseType = $TRUE
$so.DriAll = $TRUE
$so.Permissions = $TRUE
$so.Triggers = $TRUE
$so.PrimaryObject = $TRUE

#$path = "~/db_scripts/"
$path = "c:/temp/db_scripts/$((Get-Date f 'yyyyMMdd-HHmm'))/"
if (!(Test-Path -path $path)) { Mkdir $path }

foreach ($db in $srv.Databases) {
    if (!$db.IsSystemObject) {
        $localPath = $path+$db.Name+"/"
        if (!(Test-Path -path ($localPath))) { Mkdir ($localPath) }
       
        if (!(Test-Path -path ($localPath+"tables/"))) { Mkdir ($localPath+"tables/") }
        foreach ($tbl in $db.tables) {
            if (!$tbl.IsSystemObject) {
                $so.FileName = $localPath+"tables/"+$tbl.Schema+"."+$tbl.Name+".tbl.sql"
                #Write-Host "criture de $($tbl.Name) dans $($so.FileName)"
                $so.FileName
                $tbl.Script($so)
                #$tbl.Script($so) > [$tbl.Name].tbl.sql
            } # if (!$tbl.IsSystemObject)
        } # foreach $tbl
        if (!(Test-Path -path ($localPath+"procedures/"))) { Mkdir ($localPath+"procedures/") }
        foreach ($sp in $db.StoredProcedures) {
            if (!$sp.IsSystemObject) {
                $so.FileName = $localPath+"procedures/"+$sp.Schema+"."+$sp.Name+".sp.sql"
                $so.FileName
                $sp.Script($so)
            } # if (!$tbl.IsSystemObject)
        } # foreach $sp
        if (!(Test-Path -path ($localPath+"vues/"))) { Mkdir ($localPath+"vues/") }
        foreach ($vw in $db.Views) {
            if (!$vw.IsSystemObject) {
                $so.FileName = $localPath+"vues/"+$vw.Schema+"."+$vw.Name+".view.sql"
                $so.FileName
                $vw.Script($so)
            } # if (!$tbl.IsSystemObject)
        } # foreach $vw
    } # if ($db.IsSystemObject)
} # foreach $db

fonction UPDATE() dans un déclencheur

Attention à la fonction UPDATE() utilisée dans les déclencheures (triggers).
Ne pensez pas que cela vous indique que la valeur de la colonne a changé, mais simplement
que la colonne a été mentionnée dans l’instruction. Un exemple :

USE tempdb;
GO

CREATE TABLE dbo.Contact (Id INT, Name VARCHAR(50))
GO

INSERT INTO dbo.Contact
VALUES (1, 'Fillon'), (2, 'Copé');

CREATE TRIGGER atr_u_Contact
ON dbo.Contact
AFTER UPDATE
AS BEGIN
    IF UPDATE(Name)
        PRINT 'ok'
    ELSE
        PRINT 'non'
END

UPDATE dbo.Contact
SET Name = Name
WHERE Id = 2;
-- affiche oui

UPDATE dbo.Contact
SET Id = Id
WHERE Id = 2;
-- affiche non

Donc il vaut mieux tester s’il y a eu réellement une modification avant d’exécuter tout le reste. Par exemple, à la place de ceci :

CREATE TRIGGER atr_u_Contact
ON dbo.Contact
AFTER UPDATE
AS BEGIN
    IF UPDATE(Name)
        INSERT INTO ContactHistory (Id, Name)
        SELECT Id, Name
        FROM deleted;
END

ceci :

CREATE TRIGGER atr_u_Contact
ON dbo.Contact
AFTER UPDATE
AS BEGIN
    IF @@ROWCOUNT = 0 RETURN

    IF UPDATE(Name)
        INSERT INTO ContactHistory (Id, Name)
        SELECT d.Id, d.Name
        FROM deleted d
        JOIN inserted i ON d.Id = i.Id
        WHERE d.Name  i.Name;
END

Ici nous ajoutons aussi, en toute première ligne (c’est important), l’instruction

IF @@ROWCOUNT = 0 RETURN

pour tester si le déclencheur est appelé pour une bonne raison. En effet, une instruction comme celle-ci:

UPDATE dbo.Contact
SET Name = UPPER(Name)
WHERE 1 = 0;

n’affecterait aucune ligne, mais appellerait pourtant le déclencheur, qui lancerait des requêtes pour rien. Le test sur @@ROWCOUNT permet de sortir immédiatement du trigger si aucune ligne n’a été affectée.