Pourquoi utiliser une variable de table plus de deux fois plus vite qu'une table #temp dans ce cas particulier?

37

Je regardais l'article ici Tables temporaires par rapport aux variables de table et leur impact sur les performances de SQL Server et sur SQL Server 2008 a été en mesure de reproduire des résultats similaires à ceux indiqués ici pour 2005.

Lors de l'exécution des procédures stockées (définitions ci-dessous) avec seulement 10 lignes, la version de variable de table exécute la version de table temporaire plus de deux fois.

J'ai effacé le cache de procédure et exécuté les deux procédures stockées 10 000 fois, puis répété le processus pendant 4 autres exécutions. Résultats ci-dessous (temps en ms par lot)

T2_Time     V2_Time
----------- -----------
8578        2718      
6641        2781    
6469        2813   
6766        2797
6156        2719

Ma question est la suivante: quelle est la raison de la meilleure performance de la version de la variable de table?

J'ai fait des recherches. Par exemple, regarder les compteurs de performance avec

SELECT cntr_value
from sys.dm_os_performance_counters
where counter_name = 'Temp Tables Creation Rate';

confirme que, dans les deux cas, les objets temporaires sont mis en cache après la première exécution, comme prévu, plutôt que créés à nouveau pour chaque invocation.

De même , le suivi Auto Stats, SP:Recompile, les SQL:StmtRecompileévénements dans Profiler (capture d' écran ci - dessous) montre que ces événements se produisent qu'une seule fois (la première invocation de la #temptable de procédure stockée) et les autres 9.999 exécutions ne soulèvent aucun de ces événements. (La version de la variable de table ne reçoit aucun de ces événements)

Trace

La surcharge légèrement plus importante de la première exécution de la procédure stockée ne peut en aucune manière expliquer la grande différence globale, toutefois, comme il ne faut que quelques ms pour effacer le cache de procédure et exécuter les deux procédures une fois, je ne crois pas que les statistiques ou Les recompilations peuvent en être la cause.

Créer les objets de base de données requis

CREATE DATABASE TESTDB_18Feb2012;

GO

USE TESTDB_18Feb2012;

CREATE TABLE NUM 
  ( 
     n INT PRIMARY KEY, 
     s VARCHAR(128) 
  ); 

WITH NUMS(N) 
     AS (SELECT TOP 1000000 ROW_NUMBER() OVER (ORDER BY $/0) 
         FROM   master..spt_values v1, 
                master..spt_values v2) 
INSERT INTO NUM 
SELECT N, 
       'Value: ' + CONVERT(VARCHAR, N) 
FROM   NUMS 

GO

CREATE PROCEDURE [dbo].[T2] @total INT 
AS 
  CREATE TABLE #T 
    ( 
       n INT PRIMARY KEY, 
       s VARCHAR(128) 
    ) 

  INSERT INTO #T 
  SELECT n, 
         s 
  FROM   NUM 
  WHERE  n%100 > 0 
         AND n <= @total 

  DECLARE @res VARCHAR(128) 

  SELECT @res = MAX(s) 
  FROM   NUM 
  WHERE  n <= @total 
         AND NOT EXISTS(SELECT * 
                        FROM   #T 
                        WHERE  #T.n = NUM.n) 
GO

CREATE PROCEDURE [dbo].[V2] @total INT 
AS 
  DECLARE @V TABLE ( 
    n INT PRIMARY KEY, 
    s VARCHAR(128)) 

  INSERT INTO @V 
  SELECT n, 
         s 
  FROM   NUM 
  WHERE  n%100 > 0 
         AND n <= @total 

  DECLARE @res VARCHAR(128) 

  SELECT @res = MAX(s) 
  FROM   NUM 
  WHERE  n <= @total 
         AND NOT EXISTS(SELECT * 
                        FROM   @V V 
                        WHERE  V.n = NUM.n) 


GO

Script de test

SET NOCOUNT ON;

DECLARE @T1 DATETIME2,
        @T2 DATETIME2,
        @T3 DATETIME2,  
        @Counter INT = 0

SET @T1 = SYSDATETIME()

WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.T2 10
SET @Counter += 1
END

SET @T2 = SYSDATETIME()
SET @Counter = 0

WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.V2 10
SET @Counter += 1
END

SET @T3 = SYSDATETIME()

SELECT DATEDIFF(MILLISECOND,@T1,@T2) AS T2_Time,
       DATEDIFF(MILLISECOND,@T2,@T3) AS V2_Time
Martin Smith
la source
La trace du profileur indique que les statistiques ne sont créées sur la #temptable qu'une fois, bien qu'elles aient été effacées et à nouveau remplies 9 999 fois par la suite.
Martin Smith

Réponses:

31

La sortie de SET STATISTICS IO ONpour les deux ressemble

SET STATISTICS IO ON;
PRINT 'V2'
EXEC dbo.V2 10
PRINT 'T2'
EXEC dbo.T2 10

Donne

V2
Table '#58B62A60'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3

Table '#58B62A60'. Scan count 10, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3

T2
Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3

Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3

Et comme Aaron le souligne dans les commentaires, le plan de la version de la variable de table est en réalité moins efficace, car les deux ont un plan de boucles imbriquées piloté par un index. La recherche de dbo.NUMla #tempversion de la table effectue une recherche dans l'index [#T].n = [dbo].[NUM].[n]avec le prédicat résiduel, [#T].[n]<=[@total]tandis que version effectue une recherche d'index @V.n <= [@total]avec le prédicat résiduel @V.[n]=[dbo].[NUM].[n]et traite ainsi plus de lignes (c'est pourquoi ce plan fonctionne si mal pour un plus grand nombre de lignes)

L'utilisation d' événements étendus pour examiner les types d'attente du spid spécifique donne ces résultats pour 10 000 exécutions deEXEC dbo.T2 10

+---------------------+------------+----------------+----------------+----------------+
|                     |            |     Total      | Total Resource |  Total Signal  |
| Wait Type           | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| SOS_SCHEDULER_YIELD | 16         | 19             | 19             | 0              |
| PAGELATCH_SH        | 39998      | 14             | 0              | 14             |
| PAGELATCH_EX        | 1          | 0              | 0              | 0              |
+---------------------+------------+----------------+----------------+----------------+

et ces résultats pour 10 000 exécutions de EXEC dbo.V2 10

+---------------------+------------+----------------+----------------+----------------+
|                     |            |     Total      | Total Resource |  Total Signal  |
| Wait Type           | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| PAGELATCH_EX        | 2          | 0              | 0              | 0              |
| PAGELATCH_SH        | 1          | 0              | 0              | 0              |
| SOS_SCHEDULER_YIELD | 676        | 0              | 0              | 0              |
+---------------------+------------+----------------+----------------+----------------+

Il est donc clair que le nombre d’ PAGELATCH_SHattentes est beaucoup plus élevé dans le #tempcas de la table. Je ne suis au courant d'aucun moyen d'ajouter la ressource d'attente à la trace des événements étendus. Pour approfondir cette question, j'ai lancé

WHILE 1=1
EXEC dbo.T2 10

Dans une autre connexion, interrogation sys.dm_os_waiting_tasks

CREATE TABLE #T(resource_description NVARCHAR(2048))

WHILE 1=1
INSERT INTO #T
SELECT resource_description
FROM sys.dm_os_waiting_tasks
WHERE session_id=<spid_of_other_session> and wait_type='PAGELATCH_SH'

Après avoir fonctionné pendant environ 15 secondes, il avait obtenu les résultats suivants

+-------+----------------------+
| Count | resource_description |
+-------+----------------------+
|  1098 | 2:1:150              |
|  1689 | 2:1:146              |
+-------+----------------------+

Ces deux pages verrouillées appartiennent à (différents) index non clusterisés sur la tempdb.sys.sysschobjstable de base nommée 'nc1'and 'nc2'.

L'interrogation au tempdb.sys.fn_dblogcours des exécutions indique que le nombre d'enregistrements de journal ajoutés lors de la première exécution de chaque procédure stockée était quelque peu variable, mais que pour les exécutions suivantes, le nombre ajouté à chaque itération était très cohérent et prévisible. Une fois que les plans de procédure sont mis en cache, le nombre d'entrées de journal est environ la moitié de celui requis pour la #tempversion.

+-----------------+----------------+------------+
|                 | Table Variable | Temp Table |
+-----------------+----------------+------------+
| First Run       |            126 | 72 or 136  |
| Subsequent Runs |             17 | 32         |
+-----------------+----------------+------------+

En examinant plus en détail les entrées du journal des transactions pour la #tempversion de table du SP, chaque appel ultérieur de la procédure stockée crée trois transactions et la variable de table un seulement deux.

+---------------------------------+----+---------------------------------+----+
|           #Temp Table                |         @Table Variable              |
+---------------------------------+----+---------------------------------+----+
| CREATE TABLE                    |  9 |                                 |    |
| INSERT                          | 12 | TVQuery                         | 12 |
| FCheckAndCleanupCachedTempTable | 11 | FCheckAndCleanupCachedTempTable |  5 |
+---------------------------------+----+---------------------------------+----+

Les INSERT/ TVQUERYtransactions sont identiques à l'exception du nom. Celui-ci contient les enregistrements de journal pour chacune des 10 lignes insérées dans la table temporaire ou la variable de table plus les entrées LOP_BEGIN_XACT/ LOP_COMMIT_XACT.

La CREATE TABLEtransaction apparaît uniquement dans la #Tempversion et se présente comme suit.

+-----------------+-------------------+---------------------+
|    Operation    |      Context      |    AllocUnitName    |
+-----------------+-------------------+---------------------+
| LOP_BEGIN_XACT  | LCX_NULL          |                     |
| LOP_SHRINK_NOOP | LCX_NULL          |                     |
| LOP_MODIFY_ROW  | LCX_CLUSTERED     | sys.sysschobjs.clst |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1  |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF    | sys.sysschobjs.nc1  |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2  |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF    | sys.sysschobjs.nc2  |
| LOP_MODIFY_ROW  | LCX_CLUSTERED     | sys.sysschobjs.clst |
| LOP_COMMIT_XACT | LCX_NULL          |                     |
+-----------------+-------------------+---------------------+

La FCheckAndCleanupCachedTempTabletransaction apparaît dans les deux, mais comporte 6 entrées supplémentaires dans la #tempversion. Ce sont les 6 rangées en référence sys.sysschobjset ils ont exactement le même motif que ci-dessus.

+-----------------+-------------------+----------------------------------------------+
|    Operation    |      Context      |                AllocUnitName                 |
+-----------------+-------------------+----------------------------------------------+
| LOP_BEGIN_XACT  | LCX_NULL          |                                              |
| LOP_DELETE_ROWS | LCX_NONSYS_SPLIT  | dbo.#7240F239.PK__#T________3BD0199374293AAB |
| LOP_HOBT_DELTA  | LCX_NULL          |                                              |
| LOP_HOBT_DELTA  | LCX_NULL          |                                              |
| LOP_MODIFY_ROW  | LCX_CLUSTERED     | sys.sysschobjs.clst                          |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1                           |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF    | sys.sysschobjs.nc1                           |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2                           |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF    | sys.sysschobjs.nc2                           |
| LOP_MODIFY_ROW  | LCX_CLUSTERED     | sys.sysschobjs.clst                          |
| LOP_COMMIT_XACT | LCX_NULL          |                                              |
+-----------------+-------------------+----------------------------------------------+

En regardant ces 6 lignes dans les deux transactions, elles correspondent aux mêmes opérations. Le premier LOP_MODIFY_ROW, LCX_CLUSTEREDest une mise à jour de la modify_datecolonne dans sys.objects. Les cinq lignes restantes concernent toutes le renommage d'objets. Parce que namec’est une colonne clé des deux NCI concernés ( nc1et nc2), cette opération est effectuée en tant que suppression / insertion pour ceux-ci, puis elle retourne à l’index clusterisé et est également mise à jour.

Il semble que pour la #tempversion de la table, lorsque la procédure stockée se termine, une partie du nettoyage effectué par la FCheckAndCleanupCachedTempTabletransaction consiste à renommer la table temporaire #T__________________________________________________________________________________________________________________00000000E316en un nom similaire à celui d'un nom interne différent, par exemple #2F4A0079lorsque la CREATE TABLEtransaction est entrée, la renomme. Ce nom est visible dans une connexion s’exécutant dbo.T2dans une boucle alors que dans une autre

WHILE 1=1
SELECT name, object_id, create_date, modify_date
FROM tempdb.sys.objects 
WHERE name LIKE '#%'

Exemple de résultats

Capture d'écran

Ainsi, une explication potentielle du différentiel de performance observé évoquée par Alex est que c'est ce travail supplémentaire de maintenance des tables système tempdbqui est responsable.


L'exécution des deux procédures en boucle par le profileur de code Visual Studio révèle les éléments suivants:

+-------------------------------+--------------------+-------+-----------+
|           Function            |    Explanation     | Temp  | Table Var |
+-------------------------------+--------------------+-------+-----------+
| CXStmtDML::XretExecute        | Insert ... Select  | 16.93 | 37.31     |
| CXStmtQuery::ErsqExecuteQuery | Select Max         | 8.77  | 23.19     |
+-------------------------------+--------------------+-------+-----------+
| Total                         |                    | 25.7  | 60.5      |
+-------------------------------+--------------------+-------+-----------+

La version de la variable de table consacre environ 60% du temps à l'exécution de l'instruction d'insertion et à la sélection ultérieure, tandis que la table temporaire correspond à moins de la moitié de celle-ci. Cela correspond aux délais indiqués dans le PO et à la conclusion ci-dessus, à savoir que la différence de performances est due au temps consacré à l'exécution de travaux auxiliaires et non au temps consacré à l'exécution de la requête elle-même.

Les fonctions les plus importantes contribuant aux 75% "manquants" dans la version à table temporaire sont

+------------------------------------+-------------------+
|              Function              | Inclusive Samples |
+------------------------------------+-------------------+
| CXStmtCreateTableDDL::XretExecute  | 26.26%            |
| CXStmtDDL::FinishNormalImp         | 4.17%             |
| TmpObject::Release                 | 27.77%            |
+------------------------------------+-------------------+
| Total                              | 58.20%            |
+------------------------------------+-------------------+

Sous les fonctions create et release, la fonction CMEDProxyObject::SetNameest affichée avec une valeur d'échantillon inclusif de 19.6%. J'en déduis que 39,2% du temps de la table temporaire est utilisé avec le changement de nom décrit plus haut.

Et les plus gros dans la version de la variable de table contribuant à l'autre 40% sont

+-----------------------------------+-------------------+
|             Function              | Inclusive Samples |
+-----------------------------------+-------------------+
| CTableCreate::LCreate             | 7.41%             |
| TmpObject::Release                | 12.87%            |
+-----------------------------------+-------------------+
| Total                             | 20.28%            |
+-----------------------------------+-------------------+

Profil de table temporaire

entrez la description de l'image ici

Table Variable Profile

entrez la description de l'image ici

Martin Smith
la source
10

Disco Inferno

Comme il s’agit d’une question plus ancienne, j’ai décidé de revenir sur la question des versions plus récentes de SQL Server pour voir si le même profil de performances existait toujours ou si les caractéristiques avaient changé.

Plus précisément, l'ajout de tables système en mémoire pour SQL Server 2019 semble être une occasion intéressante de procéder à un nouveau test.

J'utilise un harnais de test légèrement différent, car j'ai rencontré ce problème alors que je travaillais sur autre chose.

Test, test

En utilisant la version 2013 de Stack Overflow , j'ai cet index et ces deux procédures:

Indice:

CREATE INDEX ix_whatever 
    ON dbo.Posts(OwnerUserId) INCLUDE(Score);
GO

Table de temp:

    CREATE OR ALTER PROCEDURE dbo.TempTableTest(@Id INT)
    AS
    BEGIN
    SET NOCOUNT ON;

        CREATE TABLE #t(i INT NOT NULL);
        DECLARE @i INT;

        INSERT #t ( i )
        SELECT p.Score
        FROM dbo.Posts AS p
        WHERE p.OwnerUserId = @Id;

        SELECT @i = AVG(t.i)
        FROM #t AS t;

    END;
    GO 

Variable de table:

    CREATE OR ALTER PROCEDURE dbo.TableVariableTest(@Id INT)
    AS
    BEGIN
    SET NOCOUNT ON;

        DECLARE @t TABLE (i INT NOT NULL);
        DECLARE @i INT;

        INSERT @t ( i )
        SELECT p.Score
        FROM dbo.Posts AS p
        WHERE p.OwnerUserId = @Id;

        SELECT @i = AVG(t.i)
        FROM @t AS t;

    END;
    GO 

Pour éviter toute attente potentielle ASYNC_NETWORK_IO , j'utilise des procédures d'encapsulation.

CREATE PROCEDURE #TT AS
SET NOCOUNT ON;
    DECLARE @i INT = 1;
    DECLARE @StartDate DATETIME2(7) = SYSDATETIME();

    WHILE @i <= 50000
        BEGIN
            EXEC dbo.TempTableTest @Id = @i;
            SET @i += 1;
        END;
    SELECT DATEDIFF(MILLISECOND, @StartDate, SYSDATETIME()) AS [ElapsedTimeMilliseconds];
GO

CREATE PROCEDURE #TV AS
SET NOCOUNT ON;
    DECLARE @i INT = 1;
    DECLARE @StartDate DATETIME2(7) = SYSDATETIME();

    WHILE @i <= 50000
        BEGIN
            EXEC dbo.TableVariableTest @Id = @i;
            SET @i += 1;
        END;
    SELECT DATEDIFF(MILLISECOND, @StartDate, SYSDATETIME()) AS [ElapsedTimeMilliseconds];
GO

SQL Server 2017

Depuis 2014 et 2016 sont essentiellement des RELICS à ce stade, je commence mes tests avec 2017. En outre, par souci de concision, je vais tout de suite passer au profilage du code avec Perfview . Dans la vie réelle, j'ai examiné les attentes, les verrous, les verrous pivotants, les indicateurs de traces fous, etc.

Le profilage du code est la seule chose qui ait révélé quelque chose d'intéressant.

Différence de temps:

  • Table temporaire: 17891 ms
  • Variable de table: 5891 ms

Encore une différence très nette, hein? Mais qu'est-ce que SQL Server frappe maintenant?

DES NOISETTES

En regardant les deux plus fortes augmentations d'échantillons différenciés, nous voyons sqlminet sqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucketsommes les deux plus gros délinquants.

DES NOISETTES

À en juger par les noms dans les piles d’appel, le nettoyage et le renommage interne des tables temporaires semblent être le temps le plus long dans l’appel de la table temporaire par rapport à celui de la variable table.

Même si les variables de table sont sauvegardées en interne par des tables temporaires, cela ne semble pas poser de problème.

SET STATISTICS IO ON;
DECLARE @t TABLE(id INT);
SELECT * FROM @t AS t;

Tableau '# B98CE339'. Nombre de numérisations 1

En regardant à travers les piles d'appels pour le test de variable de table, on ne voit aucun des principaux délinquants:

DES NOISETTES

SQL Server 2019 (Vanilla)

Bon, c'est donc toujours un problème dans SQL Server 2017, y a-t-il autre chose de différent en 2019?

Tout d'abord, pour montrer qu'il n'y a rien dans ma manche:

SELECT c.name,
       c.value_in_use,
       c.description
FROM sys.configurations AS c
WHERE c.name = 'tempdb metadata memory-optimized';

DES NOISETTES

Différence de temps:

  • Table temporaire: 15765 ms
  • Variable de table: 7250 ms

Les deux procédures étaient différentes. L'appel de table temporaire a été quelques secondes plus rapide et l'appel de variable de table a été environ 1,5 seconde plus lent. La lenteur de la variable de table peut s’expliquer en partie par la compilation différée de la variable de table , un nouveau choix de l’optimiseur en 2019.

En regardant le diff dans Perfview, cela a un peu changé - sqlmin n’est plus là - mais le sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucketreste.

DES NOISETTES

SQL Server 2019 (tables système Tempdb en mémoire)

Qu'en est-il de cette nouvelle chose dans la table système en mémoire? Hm? Sup avec ça?

Allumons-le!

EXEC sys.sp_configure @configname = 'advanced', 
                      @configvalue = 1  
RECONFIGURE;

EXEC sys.sp_configure @configname = 'tempdb metadata memory-optimized', 
                      @configvalue = 1 
RECONFIGURE;

Notez que cela nécessite un redémarrage de SQL Server, alors excusez-moi pendant le redémarrage de SQL en ce beau vendredi après-midi.

Maintenant, les choses sont différentes:

SELECT c.name,
       c.value_in_use,
       c.description
FROM sys.configurations AS c
WHERE c.name = 'tempdb metadata memory-optimized';

SELECT *, 
       OBJECT_NAME(object_id) AS object_name, 
       @@VERSION AS sql_server_version
FROM tempdb.sys.memory_optimized_tables_internal_attributes;

DES NOISETTES

Différence de temps:

  • Table temporaire: 11638 ms
  • Variable de table: 7403 ms

Les tables temporaires ont fait environ 4 secondes de mieux! C'est quelque chose.

J'aime quelque chose

Cette fois, le diff Perfview n’est pas très intéressant. Côte à côte, il est intéressant de noter à quel point les temps sont proches l'un de l'autre:

DES NOISETTES

Les appels à hkengine!, qui peuvent sembler évidents depuis que les fonctionnalités hekaton-ish sont maintenant utilisées, constituent un point intéressant du diff .

DES NOISETTES

En ce qui concerne les deux premiers articles du diff, je ne peux pas en dire beaucoup ntoskrnl!?:

DES NOISETTES

Ou sqltses!CSqlSortManager_80::GetSortKey, mais ils sont là pour que Smrtr Ppl ™ regarde:

DES NOISETTES

Notez qu’il existe un document non documenté et qu’il n’est absolument pas sûr pour la production. Par conséquent, n’utilisez pas cet indicateur de trace de démarrage que vous pouvez utiliser pour inclure des objets système de table temporaire supplémentaires (sysrowsets, sysallocunits et sysseobjvalues) inclus dans la fonction en mémoire. n'a pas fait une différence notable dans les temps d'exécution dans ce cas.

Roundup

Même dans les versions les plus récentes de SQL Server, les appels haute fréquence aux variables de table sont beaucoup plus rapides que les appels haute fréquence aux tables temporaires.

Bien qu'il soit tentant de blâmer les compilations, les recompilations, les statistiques automatiques, les verrous, les verrous spinblocks, la mise en cache ou d'autres problèmes, le problème reste clairement lié à la gestion du nettoyage de la table temporaire.

C'est un appel plus étroit dans SQL Server 2019 avec les tables système en mémoire activées, mais les variables de table fonctionnent toujours mieux lorsque la fréquence des appels est élevée.

Bien sûr, comme un sage de vapotage avait une fois déclaré: "utilisez des variables de tableau lorsque le choix du plan n'est pas un problème".

Erik Darling
la source
Sympa - désolé d’avoir manqué le fait que vous ayez ajouté une réponse à cette question jusqu’à ce que vous suiviez simplement le lien dans votre article de blog «débogage»
Martin Smith Le