Variables de type table, statistiques et drapeau de trace 2453

M’étant arrêté d’écrire pendant un certain temps, mais certainement pas de lire, me voilà de retour au clavier pour partager sur mon sujet préféré : l’optimiseur de requêtes, et plus particulièrement l’estimation de cardinalités. En butinant des billets de blog ici et là à propos de SQL Server, je trouvais un billet détaillant le comportement des requêtes spécifiant des variables de type TABLE.

Qu’est-ce que l’estimation de cardinalités ? Pour faire très court, c’est l’ensemble des règles mathématiques qui permettent à SQL Server d’avoir une idée assez précise du nombre de lignes qu’il aura à traiter lors de l’exécution d’une requête. C’est avec ce calcul-là qu’il sélectionne les algorithmes de jointure, de regroupement, l’ordre et la façon d’accéder aux tables, et bien d’autres choses encore. Bien sûr, ces choix varient suivant le volume de données à traiter. On le comprend donc, c’est un sujet très important, puisqu’il impacte directement les performances de l’exécution de nos chères (parfois en IO et temps CPU !) requêtes.

Alors vous allez me dire, crevant d’impatience : « bon d’accord, mais il le sort de son chapeau magique ce calcul ? » : au risque de vous décevoir, non. Dès lors qu’on soumet une requête qui filtre une table par une colonne, ou l’utilise dans une jointure, un regroupement, alors par défaut, le moteur crée automatiquement des objets de statistique. C’est à dire qu’il va échantillonner les données des colonnes des tables participant à la requête pour évaluer la distribution des données dans les colonnes (et index) de ces tables. Fort de ces informations, il peut alors réaliser le fameux calcul. Bref, ce sont des mathématiques :)

L’auteur du billet en question partageait sur le fait que par défaut, SQL Server estime qu’il n’y a qu’une seule ligne dans une variable de type TABLE. Ceci s’explique par le fait que SQL Server ne maintient pas d’objet de statistique sur les variables de type TABLE (pour les curieux, il le fait néanmoins sur les tables temporaires). Quand on sait l’usage qui est fait des variables de type TABLE dans les applications, il est évident qu’il arrive rarement que ces tables ne soient en charge que d’une seule ligne; de là des performances qui ne sont pas toujours en adéquation avec le volume de données à traiter.

Voyons le comportement par défaut du moteur à l’aide de la base de données AdventureWorks2012 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
SET NOCOUNT ON
GO

USE AdventureWorks2012
GO

DECLARE @BusinessEntityId TABLE
(
        BusinessEntityID INT NOT NULL
);
 
INSERT INTO @BusinessEntityId
(
        BusinessEntityID
)
SELECT  BusinessEntityID
FROM    Person.Person;
 
SET STATISTICS IO, TIME, XML ON
 
SELECT          COUNT(*)
FROM            @BusinessEntityId b
INNER JOIN      Person.Person p
                        ON b.BusinessEntityID = p.BusinessEntityID;

SET STATISTICS IO, TIME, XML OFF

L’option de session SET STATISTICS nous permet de collecter des métriques IO, temps CPU et durée en millisecondes de la requête. Son option XML expose le plan réel de requête en plus du résultat de la requête. La sortie de ce lot est, tronquée des lectures « physiques » (i.e. sur disque et pas en RAM), et des lectures LOB, puisqu’il n’y en a pas :

Table ‘Person’. Scan count 0, logical reads 59916
Table ‘#BEF1AB90′. Scan count 1, logical reads 33

SQL Server Execution Times:
CPU time = 47 ms, elapsed time = 56 ms.

Si l’on étudie le plan de requête, voici ce que l’on trouve pour notre variable de type TABLE :

Pour voir sur quelle base le moteur a réalisé ses calculs, il suffit de survoler l’opérateur Table Scan avec le curseur de la souris :

Le moteur a donc estimé qu’il n’y qu’une seule ligne dans la variable de type TABLE, puis il a exécuté le plan de cette requête. D’une certaine manière, il nous avoue s’être un peu trompé, puisqu’il nous indique qu’en réalité, il a lu 19972 lignes.

Mais, à la lecture du résumé l’article 2952444, on voit que ce comportement a été corrigé à l’aide d’un correctif dès SQL Server 2012 SP2 et le CU3 de SQL Server 2014, ce qui fait qu’il est dans les RTM des versions suivantes de SQL Server, à ce jour SQL Server 2016 et 2017 (bientôt !) :

When you populate a table variable with many rows and then join it with other tables, the query optimizer may choose an inefficient query plan, which may lead to slow query performance.

Voyons donc ce qu’il en est : nous rejouons le même lot de requête, mais avec le drapeau de trace activé cette fois :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- Activation du drapeau de trace 2453 pour toutes
-- les bases de données que l'instance héberge
DBCC TRACEON (2453, -1)
GO

SET NOCOUNT ON
GO

USE AdventureWorks2012
GO

DECLARE @BusinessEntityId TABLE
(
        BusinessEntityID INT NOT NULL
);
 
INSERT INTO @BusinessEntityId
(
        BusinessEntityID
)
SELECT  BusinessEntityID
FROM    Person.Person;
 
SET STATISTICS IO, TIME, XML ON
 
SELECT          COUNT(*)
FROM            @BusinessEntityId b
INNER JOIN      Person.Person p
                        ON b.BusinessEntityID = p.BusinessEntityID;

SET STATISTICS IO, TIME, XML OFF

La sortie est :

Table ‘Workfile’. Scan count 7, logical reads 64, physical reads 0, read-ahead reads 64
Table ‘Worktable’. Scan count 0, logical reads 0
Table ‘Person’. Scan count 1, logical reads 67
Table ‘#A96F2843′. Scan count 1, logical reads 33

SQL Server Execution Times:
CPU time = 31 ms, elapsed time = 39 ms.

Le gain de temps pour une requête aussi simple n’est pas négligeable, mais c’est surtout le gain en IOs qui est très intéressant ! Voyons ce que nous dit le plan de requête :

L’ordre d’accès aux tables est le même, mais on voit bien que la façon de les traiter a changé : les jointures et agrégats sont exécutés par hachage. On note aussi les avertissements (petit point d’exclamation noir dans un triangle jaune) sur ces deux opérateurs : ils ont du accéder à TempDB pour exécuter la jointure et l’agrégat, d’où les Workfile et Worktable dans la sortie de SET STATISTICS : ce sont des structures créées à la volée dans TempDB, souvent à défaut d’index pouvant supporter la requête efficacement.

Voyons ce que nous indique l’opérateur Table Scan :

Ici on voit bien que l’estimation du nombre de lignes s’est faite correctement, et c’est donc bien cela qui a conduit le moteur à changer les algorithmes de jointure et de calcul de l’agrégat.

Pour ma part, la grande majorité des instances SQL Server que j’administre actuellement sont de version 2014, et j’ai activé ce drapeau de trace sur toutes celles-ci. En effet, l’application qui repose sur cette base de données fait souvent appel à des Tabled-Valued Parameters, qui sont des variables de type table que l’on peut passer en paramètre à une procédure stockée ou un appel à la procédure stockée système sp_executesql. Les performances générales de l’application s’en sont immédiatement ressenti positivement.

Je n’irai pas jusqu’à écrire qu’il faudrait que ce drapeau de trace soit activé sur toute les instances de production, car :

  1. toutes les applications et leurs charges de travail sont différentes
  2. il est toujours primordial de tester et de mesurer avant d’effectuer un quelconque changement de configuration sur une instance de production

Je vous engage vivement donc à tester pour en observer l’effet :

  • Pour savoir quels sont les drapeaux de trace actifs, il suffit d’exécuter DBCC TRACESTATUS seul, ou si l’on veut être plus spécifique : DBCC TRACESTATUS (2453, -1)
  • Pour désactiver un drapeau de trace, il faut exécuter : DBCC TRACEOFF ({traceFlag}, -1), soit dans le cadre de ce billet : DBCC TRACEOFF (2453, -1)

N’hésitez pas à me laisser un petit commentaire !

ElSüket.

Laisser un commentaire