La création d’une trace profiler est un passage quasi obligatoire lorsqu’il s’agit d’auditer les performances et les ressources monopolisées des requêtes, lots de requêtes ou des procédures stockées qui s’exécutent sur le serveur de bases de données lors d’un audit. Bien qu’il existe les DMV depuis la version 2005, celles-ci ne peuvent être réellement utilisées qu’après une période significative de fonctionnement du serveur. Par conséquent il est plus intéressant de les utiliser dans un contexte de production que dans celui d’un audit ponctuel.
De plus, il peut exister plusieurs exécutions d’une même requête ou procédure dans une trace profiler mais avec des paramètres différents. Une question peut alors se poser : comment connaître les durées et consommations globales des différents modèles de requêtes (indépendamment de la valeur des paramètres utilisés) et pouvoir ainsi mettre l’accent sur l’optimisation de certains modèles de requêtes ?
Lors de mon dernier audit, j’ai également dû répondre à la problématique suivante : le serveur de bases de données comportait plusieurs instances SQL Server. Dans un tel cas, comment connaitre le ratio entre la consommation d’une requête exécutée sur une instance et celle de l’ensemble des instances présentes sur ce même serveur ? Cela peut être utile pour cibler les requêtes ou procédures les plus consommatrices à l’échelle du serveur et pour lesquelles il est utile de revoir la conception.
Le script suivant permet de répondre aux deux problématiques décrites ci-dessus.
Ce script est divisé en plusieurs parties :
- Création d’une fonction SQL qui permettra de concevoir les modèles de requêtes en fonction des requêtes exécutés dans la trace profiler.
- Création d’une table temporaire de travail pour effectuer les analyses nécessaires
- Import des données du ou des fichiers de trace profiler
- Analyse des différentes consommations de requêtes
Script :
/****************************************************************
* @Auteur = BARBARIN DAVID (Mikedavem)Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â *
* @Description =Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â *
* Analyse des fichiers de trace                               *
* Eléments du script :                                        *
* – Fonction dbo.SQLSig : Génération des patterns de requêtes *
* – #Workload : Table de travail pour les analyses de requêtes *
****************************************************************/USE tempdb;
GO/******************************************************************
* Fonction de génération des modèles de requêtes ** Paramètres d’entrée : *
* – @p1 : Requête ou procédure *
******************************************************************/
IF EXISTS (SELECT * FROM sys.sql_modules
WHERE object_id = OBJECT_ID(‘dbo.SQLSig’))
DROP FUNCTION dbo.SQLSig;
GO
– Création de la fonction de traitement des patterns
CREATE FUNCTION dbo.SQLSig(@p1 NVARCHAR(MAX))
RETURNS NVARCHAR(MAX)
WITH SCHEMABINDING
AS
– Fonction réalisée par la Team SQL Server Customer Advisory
– Auteur = Stuart Ozer
BEGIN
DECLARE @pos INT;
DECLARE @mode CHAR(10);
DECLARE @maxlength INT;
DECLARE @p2 NVARCHAR(MAX);
DECLARE @p2i NVARCHAR(MAX);
DECLARE @currchar CHAR(1);
DECLARE @nextchar CHAR(1);
DECLARE @p2len INT;
SET @maxlength = LEN(RTRIM(SUBSTRING(@p1, 1, 4000)));
SET @pos = 1;
SET @p2i = »;SET @p2 = »;
SET @p2len = 0;
SET @currchar = »;
SET @nextchar = »;
SET @mode = ‘command';
– Modification David BARBARIN (pour prendre en compte sp_executesql)
IF (PATINDEX(‘%sp_executesql%’, @p1)) > 0BEGIN
SET @pos = PATINDEX(‘% »,@%’,@p1) + 2;
SET @p2i = SUBSTRING(@p1, 1, PATINDEX(‘% »,@%’,@p1) + 1);
SET @p2 = SUBSTRING(@p1, PATINDEX(‘% »,@%’,@p1) + 2, LEN(@p1));
END
WHILE (@pos <= @maxlength)
BEGINSET @currchar = SUBSTRING(@p1, @pos, 1);
SET @nextchar = SUBSTRING(@p1, @pos + 1, 1);
IF @mode = ‘command’
BEGIN
SET @p2 = LEFT(@p2, @p2len) + @currchar;
SET @p2len = @p2len + 1;
IF @currchar IN (‘,’,'(‘,’ ‘,’=’,'<‘,’>’,’!’)
AND @nextchar BETWEEN ‘0’ AND ‘9’
BEGIN
SET @mode = ‘number';
SET @p2 = LEFT(@p2, @p2len) + ‘#';
SET @p2len = @p2len + 1;
END
IF @currchar = » »
BEGIN
SET @mode = ‘literal';
SET @p2 = LEFT(@p2, @p2len) + ‘# »';
SET @p2len = @p2len + 2;
END
END
ELSE IF @mode = ‘number’ AND @nextchar IN (‘,’,’)’,’ ‘,’=’,'<‘,’>’,’!’)
SET @mode = ‘command';
ELSE IF @mode = ‘literal’ AND @currchar = » »
SET @mode = ‘command’
SET @pos = @pos + 1;
ENDRETURN @p2i + @p2;
END;
GO
/******************************************************************
* Création de la table de travail pour les analyses             *
******************************************************************/
IF EXISTS(SELECT * FROM tempdb.sys.objects
         WHERE object_id = OBJECT_ID(‘tempdb.dbo.#workload’))
   DROP TABLE #workload;CREATE TABLE #workload
(
ServerName VARCHAR(50),
TextData VARCHAR(MAX),
DatabaseName VARCHAR(100),
ApplicationName VARCHAR(100),
Duration INT,
Reads INT,
Writes INT,
CPU INT,
Template AS dbo.SQLSig(TextData) PERSISTED,
CheckSum_Template AS CHECKSUM(dbo.SQLSig(TextData)) PERSISTED
);
GO– Création index cluster sur la table de travail
CREATE CLUSTERED INDEX idx_clust_workload
ON #workload
(
CheckSum_Template
);
GO/******************************************************************
* Import des données des fichiers de trace                      *
* Paramètres d’entrée :                                         *
* – @path_trc : Chemin du fichier de trace à importer           *
******************************************************************/
DECLARE @path_trc VARCHAR(100);
SET @path_trc = ‘path_fichier_trace';INSERT INTO #workload (ServerName, TextData, DatabaseName, ApplicationName, Duration, Reads, Writes, CPU)
SELECT ServerName, TextData, DatabaseName, ApplicationName, Duration, Reads, Writes, CPU
FROM fn_trace_gettable(@path_trc, NULL);
GO/******************************************************************
* Requête d’analyse par pattern des données du fichier de trace *
* Paramètres de tri : @classement                               *
* = reads   = Classement IO lectures                           *
* = writes  = Classement IO écritures                          *
* = CPUÂ Â Â Â Â = Classement par consommation CPUÂ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â *
* = Duration = Classement par durée d’exécution                 *
******************************************************************/DECLARE @classement VARCHAR(10)
SET @classement = ‘duration'; — Type de classement : duration, cpu, reads or writesWITH total_workload_by_server_checksum_template
AS
(
   SELECT
       CheckSum_Template,
       ServerName,
       COUNT(*) AS nb_executions,
       SUM(CAST(duration AS BIGINT)) AS total_duration_by_template_server,
       SUM(CAST(CPU AS BIGINT)) AS total_cpu_by_template_server,
       SUM(CAST(Reads AS BIGINT)) AS total_reads_by_template_server,
       SUM(CAST(Writes AS BIGINT)) AS total_writes_by_template_server
   FROM #workload
   GROUP BY ServerName, CheckSum_Template
),
total_workload_total_servers
AS
(
    SELECT
       W.*,
       W.total_duration_by_template_server * 100.0 / S.duration_total_server AS percent_duration_template_server,
       W.total_cpu_by_template_server * 100.0 / S.total_cpu_total_server AS percent_cpu_template_server,
       W.total_reads_by_template_server * 100.0 / S.total_reads_server AS percent_reads_template_server,
       W.total_writes_by_template_server * 100.0 / S.total_writes_server AS percent_writes_template_server,
       W.total_duration_by_template_server * 100.0 / T.duration_total AS percent_duration_total,
       W.total_cpu_by_template_server * 100.0 / T.cpu_total AS percent_cpu_total,
       W.total_reads_by_template_server * 100.0 / T.reads_total AS percent_reads_total,
       W.total_writes_by_template_server * 100.0 / T.writes_total AS percent_writes_total
    FROM total_workload_by_server_checksum_template AS W
    INNER JOIN
    (
       SELECT
           ServerName,
           SUM(CAST(duration AS BIGINT)) AS duration_total_server,
           SUM(CAST(CPU AS BIGINT)) AS total_cpu_total_server,
           SUM(CAST(Reads AS BIGINT)) AS total_reads_server,
           SUM(CAST(Writes AS BIGINT)) AS total_writes_server
       FROM #workload
       GROUP BY ServerName
    ) AS S
    ON S.ServerName = W.ServerName
    CROSS JOIN
    (
       SELECT
           SUM(CAST(duration AS BIGINT)) AS duration_total,
           SUM(CAST(CPU AS BIGINT)) AS cpu_total,
           SUM(CAST(Reads AS BIGINT)) AS reads_total,
           SUM(CAST(Writes AS BIGINT)) AS writes_total
       FROM #workload
    ) AS T
),
rank_template_workload_by_type
AS
(
SELECT
   *,
   RANK() OVER(ORDER BY percent_duration_total DESC) AS rank_duration,
   RANK() OVER(ORDER BY percent_cpu_total DESC) AS rank_cpu,
   RANK() OVER(ORDER BY percent_reads_total DESC) AS rank_reads,
   RANK() OVER(ORDER BY percent_writes_total DESC) AS rank_writes,
   RANK() OVER(ORDER BY percent_duration_template_server DESC) AS rank_duration_server,
   RANK() OVER(ORDER BY percent_cpu_template_server DESC) AS rank_cpu_server,
   RANK() OVER(ORDER BY percent_reads_template_server DESC) AS rank_reads_server,
   RANK() OVER(ORDER BY percent_writes_template_server DESC) AS rank_writes_server
FROM total_workload_total_servers
)
SELECT
W.ServerName,
W.Template,
R.nb_executions,
R.percent_duration_total,
R.percent_duration_template_server,
R.percent_cpu_total,
R.percent_cpu_template_server,
R.percent_reads_total,
R.percent_reads_template_server,
R.percent_writes_total,
R.percent_writes_template_server,
R.total_duration_by_template_server AS total_Duration,
R.total_cpu_by_template_server AS total_CPU,
R.total_reads_by_template_server AS total_Reads,
R.total_writes_by_template_server AS total_Writes,
W.TextData,
W.Duration,
W.CPU,
W.Reads,
W.Writes
FROM #workload AS W
INNER JOIN rank_template_workload_by_type AS R
ON W.CheckSum_Template = R.CheckSum_Template
AND W.ServerName = R.ServerName
–Récupération des requêtes les plus consommatrices (>= 5%)
WHERE (@classement = ‘duration’ AND R.percent_duration_total >= 5)
OR (@classement = ‘cpu’ AND R.percent_cpu_total >=5)
 OR (@classement = ‘reads’ AND R.percent_reads_total >= 5)
  OR (@classement = ‘writes’ AND R.percent_writes_total >= 5)
ORDER BY CASE @classement
         WHEN ‘duration’ THEN R.rank_duration
         WHEN ‘cpu’ THEN R.rank_cpu
         WHEN ‘reads’ THEN R.rank_reads
         WHEN ‘writes’ THEN R.rank_writes
        END,
        CASE @classement
         WHEN ‘duration’ THEN R.rank_duration_server
         WHEN ‘cpu’ THEN R.rank_cpu_server
         WHEN ‘reads’ THEN R.rank_reads_server
         WHEN ‘writes’ THEN R.rank_writes_server
        END,
        CASE @classement
         WHEN ‘duration’ THEN W.Duration
         WHEN ‘cpu’ THEN W.CPU
         WHEN ‘reads’ THEN W.Reads
         WHEN ‘writes’ THEN W.writes
        END DESC;
Remarque :
La fonction de traitement des requêtes pour générer les modèles associés peut poser un problème si le fichier de trace à important est trop important. Par habitude, je préfère importer plusieurs fichiers de trace à volumétrie moyenne qu’un seul gros fichier à la fois.
Bon audit de trace !
David BARBARIN (Mikedavem)
Elève ingénieur CNAM Lyon
MVP SQL Server