Je regardais l'article ici Tables temporaires vs variables de table et leur effet sur les performances de SQL Server et sur SQL Server 2008 a pu reproduire des résultats similaires à ceux qui y sont présentés pour 2005.
Lors de l'exécution des procédures stockées (définitions ci-dessous) avec seulement 10 lignes, la version de la variable de table out exécute la version de table temporaire de plus de deux fois.
J'ai effacé le cache de procédures et exécuté les deux procédures stockées 10 000 fois, puis j'ai répété le processus pour 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: Quelle est la raison des meilleures performances de la version variable de table?
J'ai fait une enquête. par exemple. En regardant 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év plutôt que créés à partir de zéro pour chaque appel.
De même, traçant le Auto Stats
, SP:Recompile
, SQL:StmtRecompile
events in Profiler (capture d'écran ci-dessous) montre que ces événements ne se produisent qu'une seule fois (lors de la première invocation de #temp
table stocked procedure) et les 9 999 autres exécutions ne déclenchent aucun de ces événements. (La version de la variable de table ne reçoit aucun de ces événements)
La surcharge légèrement supérieure de la première exécution de la procédure stockée ne peut en aucun cas expliquer la grande différence globale, car il ne faut que quelques ms pour vider le cache de la procédure et exécuter les deux procédures une fois, donc je ne crois ni aux statistiques ni 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
La sortie de SET STATISTICS IO ON
pour les deux semble similaire
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 fait remarquer dans les commentaires, le plan de la version variable de table est en fait moins efficace, car les deux ont un plan de boucles imbriquées piloté par une recherche d'index sur dbo.NUM
le #temp
la version de table effectue une recherche dans l'index sur [#T].n = [dbo].[NUM].[n]
avec prédicat résiduel [#T].[n]<=[@total]
alors que la version de la variable de table effectue une recherche d'index sur @V.n <= [@total]
avec prédicat résiduel @V.[n]=[dbo].[NUM].[n]
et traite donc plus de lignes (c'est pourquoi ce plan fonctionne si mal pour un plus grand nombre de lignes)
L'utilisation de Événements étendus pour examiner les types d'attente pour le spid spécifique donne ces résultats pour 10 000 exécutions de EXEC 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 de PAGELATCH_SH
les attentes sont beaucoup plus élevées dans les #temp
étui de table. Je ne connais aucun moyen d'ajouter la ressource d'attente à la trace des événements étendus.
WHILE 1=1
EXEC dbo.T2 10
Dans une autre connexion, l'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 laissé ce fonctionnement pendant environ 15 secondes, il avait recueilli les résultats suivants
+-------+----------------------+
| Count | resource_description |
+-------+----------------------+
| 1098 | 2:1:150 |
| 1689 | 2:1:146 |
+-------+----------------------+
Ces deux pages verrouillées appartiennent à des index (différents) non groupés sur le tempdb.sys.sysschobjs
table de base nommée 'nc1'
et 'nc2'
.
Requête tempdb.sys.fn_dblog
pendant les exécutions indique que le nombre d'enregistrements de journal ajoutés par la première exécution de chaque procédure stockée était quelque peu variable, mais pour les exécutions suivantes, le nombre ajouté par chaque itération était très cohérent et prévisible. Une fois les plans de procédure mis en cache, le nombre d'entrées de journal est environ la moitié de celui nécessaire pour le #temp
version.
+-----------------+----------------+------------+
| | Table Variable | Temp Table |
+-----------------+----------------+------------+
| First Run | 126 | 72 or 136 |
| Subsequent Runs | 17 | 32 |
+-----------------+----------------+------------+
Examen des entrées du journal des transactions plus en détail pour le #temp
version de table de SP chaque appel ultérieur de la procédure stockée crée trois transactions et la variable de table une seulement deux.
+---------------------------------+----+---------------------------------+----+
| #Temp Table | @Table Variable |
+---------------------------------+----+---------------------------------+----+
| CREATE TABLE | 9 | | |
| INSERT | 12 | TVQuery | 12 |
| FCheckAndCleanupCachedTempTable | 11 | FCheckAndCleanupCachedTempTable | 5 |
+---------------------------------+----+---------------------------------+----+
Les transactions INSERT
/TVQUERY
sont identiques à l'exception du nom. Il contient les enregistrements de journal pour chacune des 10 lignes insérées dans la table temporaire ou la variable de table, plus le LOP_BEGIN_XACT
/LOP_COMMIT_XACT
entrées.
Le CREATE TABLE
la transaction n'apparaît que dans le #Temp
version et ressemble à ceci.
+-----------------+-------------------+---------------------+
| 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 transaction FCheckAndCleanupCachedTempTable
apparaît dans les deux mais comporte 6 entrées supplémentaires dans le #temp
version. Ce sont les 6 lignes faisant référence à sys.sysschobjs
et ils ont exactement le même schéma 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. La première LOP_MODIFY_ROW, LCX_CLUSTERED
est une mise à jour de modify_date
colonne dans sys.objects
. Les cinq lignes restantes concernent toutes le changement de nom des objets. Parce que name
est une colonne clé des deux NCI affectés (nc1
et nc2
) ceci est effectué comme une suppression/insertion pour ceux-ci, puis il revient à l'index clusterisé et le met à jour également.
Il semble que pour le #temp
version table lorsque la procédure stockée termine une partie du nettoyage effectué par la transaction FCheckAndCleanupCachedTempTable
consiste à renommer la table temporaire à partir de quelque chose comme #T__________________________________________________________________________________________________________________00000000E316
avec un nom interne différent tel que #2F4A0079
et lorsqu'il est saisi, le CREATE TABLE
transaction le renomme. Ce nom de bascule est visible par une connexion exécutant dbo.T2
dans une boucle tandis que dans une autre
WHILE 1=1
SELECT name, object_id, create_date, modify_date
FROM tempdb.sys.objects
WHERE name LIKE '#%'
Exemples de résultats
Ainsi, une explication potentielle du différentiel de performances observé comme fait allusion à Alex est que c'est ce travail supplémentaire de maintenance des tables système dans tempdb
qui est responsable.
L'exécution des deux procédures en boucle, 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 passe environ 60% du temps à exécuter l'instruction d'insertion et la sélection suivante, tandis que la table temporaire est inférieure à la moitié. Cela est conforme aux délais indiqués dans l'OP et à la conclusion ci-dessus selon laquelle la différence de performances est due au temps passé à effectuer des travaux auxiliaires et non au temps passé dans 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 de création et de libération, la fonction CMEDProxyObject::SetName
est affiché avec une valeur d'échantillon inclusive de 19.6%
. D'où je déduis que 39,2% du temps dans le cas de table temporaire est repris avec le changement de nom décrit précédemment.
Et les plus grands de la version variable du tableau contribuant aux 40% restants sont
+-----------------------------------+-------------------+
| Function | Inclusive Samples |
+-----------------------------------+-------------------+
| CTableCreate::LCreate | 7.41% |
| TmpObject::Release | 12.87% |
+-----------------------------------+-------------------+
| Total | 20.28% |
+-----------------------------------+-------------------+
Comme il s'agit d'une question plus ancienne, j'ai décidé de réexaminer le problème sur les nouvelles versions de SQL Server pour voir si le même profil de performances existe toujours ou si les caractéristiques ont changé.
Plus précisément, l'ajout de tables système en mémoire pour SQL Server 2019 semble une occasion intéressante de tester à nouveau.
J'utilise un harnais de test légèrement différent, car j'ai rencontré ce problème en travaillant sur autre chose.
En utilisant 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
Tableau des températures:
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 tout potentiel ASYNC_NETWORK_IO attend , j'utilise des procédures wrapper.
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
Puisque 2014 et 2016 sont essentiellement des RELICS à ce stade, je commence mes tests avec 2017. Aussi, pour être bref, je passe directement au profilage du code avec Perfview . Dans la vraie vie, j'ai regardé les attentes, les verrous, les verrous tournants, les drapeaux de trace fous et d'autres choses.
Le profilage du code est la seule chose qui a révélé quelque chose d'intéressant.
Décalage horaire:
Encore une différence très nette, hein? Mais qu'est-ce que SQL Server frappe maintenant?
En regardant les deux premières augmentations des échantillons diffed, nous voyons sqlmin
et sqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket
sont les deux plus grands contrevenants.
À en juger par les noms dans les piles d'appels, le nettoyage et le renommage interne des tables temporaires semble être le plus gros temps d'aspiration dans l'appel de table temporaire par rapport à l'appel de variable de table.
Même si les variables de table sont soutenues en interne par des tables temporaires, cela ne semble pas être un problème.
SET STATISTICS IO ON;
DECLARE @t TABLE(id INT);
SELECT * FROM @t AS t;
Tableau '# B98CE339'. Nombre de numérisations 1
L'examen des piles d'appels pour le test de variable de table ne montre aucun des principaux délinquants:
Bon, donc c'est toujours un problème dans SQL Server 2017, est-ce que quelque chose de différent en 2019 est prêt à l'emploi?
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';
Décalage horaire:
Les deux procédures étaient différentes. L'appel de table temporaire était plus rapide de quelques secondes et l'appel de variable de table était environ 1,5 seconde plus lent. Le ralentissement de la variable de table peut être partiellement expliqué par compilation différée de variable de table , un nouveau choix d'optimiseur en 2019.
En regardant le diff dans Perfview, il a un peu changé - sqlmin n'est plus là - mais sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket
est.
Qu'en est-il de cette nouvelle table de système de mémoire? Hm? Souper 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 pour démarrer, alors pardonnez-moi pendant que je redémarre SQL ce beau vendredi après-midi.
Maintenant, les choses semblent 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;
Décalage horaire:
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:
Un point intéressant dans le diff sont les appels à hkengine!
, ce qui peut sembler évident puisque les fonctionnalités hekaton-ish sont maintenant utilisées.
En ce qui concerne les deux premiers éléments du diff, je ne peux pas faire beaucoup de ntoskrnl!?
:
Ou sqltses!CSqlSortManager_80::GetSortKey
, mais ils sont là pour que Smrtr Ppl ™ examine:
Notez qu'il existe un document non documenté et certainement pas sûr pour la production, veuillez donc ne pas l'utiliser indicateur de trace de démarrage vous pouvez utiliser pour avoir des objets système de table temporaire supplémentaires (sysrowsets, sysallocunits et sysseobjvalues) inclus dans le fonction en mémoire, mais cela n'a pas fait de différence notable dans les temps d'exécution dans ce cas.
Même dans les versions 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 tournants, la mise en cache ou d'autres problèmes, le problème concerne clairement la gestion du nettoyage de la table temporaire.
C'est un appel plus proche 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 le pensait un sage vapoteur: "utilisez les variables de table lorsque le choix du plan n'est pas un problème".