it-swarm.it

Perché in questo caso specifico l'utilizzo di una variabile di tabella è due volte più veloce di una tabella #temp?

Stavo guardando l'articolo qui tabelle temporanee contro variabili di tabella e il loro effetto sulle prestazioni di SQL Server e su SQL Server 2008 è stato in grado di riprodurre risultati simili a quelli mostrati per il 2005.

Quando si eseguono le stored procedure (definizioni di seguito) con solo 10 righe, la versione della variabile della tabella in uscita esegue la versione della tabella temporanea per più di due volte.

Ho cancellato la cache delle procedure ed eseguito entrambe le procedure memorizzate 10.000 volte, quindi ho ripetuto il processo per altre 4 corse. Risultati di seguito (tempo in ms per batch)

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

La mia domanda è: Qual è il motivo per la migliore prestazione della versione variabile della tabella?

Ho fatto qualche indagine. per esempio. Guardando i contatori delle prestazioni con

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

conferma che in entrambi i casi gli oggetti temporanei vengono memorizzati nella cache dopo la prima esecuzione come previsto anziché essere nuovamente creati da zero per ogni chiamata.

Allo stesso modo, tracciando il Auto Stats, SP:Recompile, SQL:StmtRecompileevents in Profiler (screenshot sotto) mostra che questi eventi si verificano una sola volta (al primo richiamo di #temp tabella stored procedure) e le altre 9.999 esecuzioni non generano nessuno di questi eventi. (La versione della variabile della tabella non ottiene nessuno di questi eventi)

Trace

L'overhead leggermente maggiore della prima esecuzione della procedura memorizzata non può in alcun modo tenere conto della grande differenza complessiva, tuttavia, poiché bastano ancora pochi ms per svuotare la cache delle procedure ed eseguire entrambe le procedure una volta, quindi non credo che le statistiche o la ricompilazione può essere la causa.

Crea oggetti database richiesti

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

Test script

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
37
Martin Smith

L'output di SET STATISTICS IO ON Per entrambi sembra simile

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

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

E come sottolinea Aaron nei commenti, il piano per la versione variabile della tabella è in realtà meno efficiente poiché mentre entrambi hanno un piano di cicli nidificati guidato da una ricerca indice su dbo.NUM La versione della tabella #temp Esegue un cerca nell'indice su [#T].n = [dbo].[NUM].[n] con predicato residuo [#T].[n]<=[@total] mentre la versione della variabile della tabella esegue una ricerca indice su @V.n <= [@total] con predicato residuo @V.[n]=[dbo].[NUM].[n] e quindi elabora più righe (motivo per cui questo piano funziona così male per un numero maggiore di righe)

L'uso di Eventi estesi per esaminare i tipi di attesa per lo spid specifico fornisce questi risultati per 10.000 esecuzioni di 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              |
+---------------------+------------+----------------+----------------+----------------+

e questi risultati per 10.000 esecuzioni di 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              |
+---------------------+------------+----------------+----------------+----------------+

Quindi è chiaro che il numero di attese PAGELATCH_SH È molto più alto nel caso della tabella #temp. Non sono a conoscenza di alcun modo per aggiungere la risorsa wait alla traccia degli eventi estesi, quindi per approfondire ulteriormente ho eseguito

WHILE 1=1
EXEC dbo.T2 10

Mentre in un'altra connessione polling 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'

Dopo aver lasciato la corsa per circa 15 secondi, aveva raccolto i seguenti risultati

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

Entrambe queste pagine bloccate appartengono a (diversi) indici non cluster sulla tabella di base tempdb.sys.sysschobjs Denominata 'nc1' E 'nc2'.

L'interrogazione tempdb.sys.fn_dblog Durante le esecuzioni indica che il numero di record di registro aggiunti dalla prima esecuzione di ciascuna procedura memorizzata era in qualche modo variabile ma per le esecuzioni successive il numero aggiunto da ciascuna iterazione era molto coerente e prevedibile. Una volta memorizzati nella cache i piani delle procedure, il numero di voci del registro è circa la metà di quelle necessarie per la versione #temp.

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

Osservando le voci del registro delle transazioni in modo più dettagliato per la versione della tabella #temp Della SP ogni successiva chiamata della procedura memorizzata crea tre transazioni e la variabile della tabella solo una due.

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

Le transazioni INSERT/TVQUERY sono identiche ad eccezione del nome. Questo contiene i record di registro per ciascuna delle 10 righe inserite nella tabella temporanea o variabile della tabella più le voci LOP_BEGIN_XACT/LOP_COMMIT_XACT.

La transazione CREATE TABLE Appare solo nella versione #Temp E ha il seguente aspetto.

+-----------------+-------------------+---------------------+
|    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 transazione FCheckAndCleanupCachedTempTable appare in entrambi ma ha 6 voci aggiuntive nella versione #temp. Queste sono le 6 righe che si riferiscono a sys.sysschobjs E hanno esattamente lo stesso modello di cui sopra.

+-----------------+-------------------+----------------------------------------------+
|    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          |                                              |
+-----------------+-------------------+----------------------------------------------+

Guardando queste 6 righe in entrambe le transazioni corrispondono alle stesse operazioni. Il primo LOP_MODIFY_ROW, LCX_CLUSTERED È un aggiornamento della colonna modify_date In sys.objects. Le restanti cinque righe riguardano tutte la ridenominazione degli oggetti. Poiché name è una colonna chiave di entrambi gli NCI interessati (nc1 E nc2) Questo viene eseguito come eliminazione/inserimento per quelli, quindi torna all'indice cluster e aggiorna anche quello.

Sembra che per la versione della tabella #temp Quando la procedura memorizzata termina parte del cleanup effettuato dalla transazione FCheckAndCleanupCachedTempTable consiste nel rinominare la tabella temporanea da qualcosa come #T__________________________________________________________________________________________________________________00000000E316 In un nome interno diverso come #2F4A0079 e quando viene immessa la transazione CREATE TABLE lo rinomina. Questo nome di infradito può essere visto da una connessione eseguendo dbo.T2 In un ciclo mentre in un'altra

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

Risultati di esempio

Screenshot

Quindi una potenziale spiegazione per il differenziale prestazionale osservato come accennato da Alex è che è questo lavoro aggiuntivo a mantenere le tabelle di sistema in tempdb che è responsabile.


Eseguendo entrambe le procedure in un ciclo il profiler di Visual Studio Code rivela quanto segue

+-------------------------------+--------------------+-------+-----------+
|           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 versione della variabile della tabella impiega circa il 60% del tempo in cui viene eseguita l'istruzione insert e la selezione successiva, mentre la tabella temporanea è inferiore alla metà. Ciò è in linea con i tempi indicati nel PO e con la conclusione sopra che la differenza di prestazioni è dovuta al tempo impiegato nell'esecuzione di lavori accessori non dovuto al tempo impiegato nell'esecuzione della query stessa.

Le funzioni più importanti che contribuiscono al "mancante" 75% nella versione della tabella temporanea sono

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

In entrambe le funzioni di creazione e rilascio viene mostrata la funzione CMEDProxyObject::SetName Con un valore di esempio inclusivo di 19.6%. Da cui deduco che il 39,2% delle volte nel caso della tabella temporanea è occupato con la ridenominazione descritta in precedenza.

E quelli più grandi nella versione variabile della tabella che contribuiscono all'altro 40% lo sono

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

Profilo della tabella temporanea

enter image description here

Profilo variabile tabella

enter image description here

31
Martin Smith

Disco Inferno

Poiché questa è una domanda più vecchia, ho deciso di rivisitare il problema sulle versioni più recenti di SQL Server per vedere se esiste ancora lo stesso profilo delle prestazioni o se le caratteristiche sono cambiate del tutto.

In particolare, l'aggiunta di tabelle di sistema in memoria per SQL Server 2019 sembra un'occasione utile per ripetere il test.

Sto usando un cablaggio di prova leggermente diverso, dal momento che ho riscontrato questo problema mentre lavoravo su qualcos'altro.

Test, test

Utilizzando versione 2013 di Stack Overflow , ho questo indice e queste due procedure:

Indice:

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

Tabella temporanea:

    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 

Variabile di tabella:

    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 

Per prevenire qualsiasi potenziale ASYNC_NETWORK_IO attende , sto usando le procedure 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

SQL Server 2017

Dal 2014 e 2016 sono fondamentalmente RELICS a questo punto, sto iniziando i miei test con il 2017. Inoltre, per brevità, sto saltando a destra per profilare il codice con Perfview . Nella vita reale, ho guardato ad attese, chiavistelli, spinlock, pazze bandiere traccia e altre cose.

La profilazione del codice è l'unica cosa che ha rivelato qualcosa di interessante.

Differenza oraria:

  • Tabella delle temp: 17891 ms
  • Variabile di tabella: 5891 ms

C'è ancora una differenza molto chiara, eh? Ma cosa sta colpendo SQL Server ora?

NUTS

Guardando i primi due aumenti nei campioni diffusi, vediamo sqlmin e sqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket Sono i due più grandi trasgressori.

NUTS

A giudicare dai nomi nelle pile di chiamate, ripulire e rinominare internamente le tabelle temporanee sembra essere il tempo più lungo che risucchia nella chiamata della tabella temporanea rispetto alla chiamata della variabile della tabella.

Anche se le variabili di tabella sono supportate internamente da tabelle temporanee, questo non sembra essere un problema.

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

Tabella '# B98CE339'. Conteggio scansioni 1

Esaminare gli stack di chiamate per il test delle variabili della tabella non mostra affatto nessuno dei principali autori del reato:

NUTS

SQL Server 2019 (Vanilla)

Bene, quindi questo è ancora un problema in SQL Server 2017, c'è qualcosa di diverso nel 2019 pronto all'uso?

Innanzitutto, per mostrare che non c'è niente nella manica:

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

NUTS

Differenza oraria:

  • Tabella delle temp: 15765 ms
  • Variabile di tabella: 7250 ms

Entrambe le procedure erano diverse. La chiamata della tabella temporanea era un paio di secondi più veloce e la chiamata della variabile della tabella era più lenta di circa 1,5 secondi. Il rallentamento della variabile di tabella può essere parzialmente spiegato da compilazione differita di variabili di tabella , una nuova scelta di ottimizzatore nel 2019.

Guardando il diff in Perfview, è cambiato un po '- sqlmin non è più lì - ma lo è sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket.

NUTS

SQL Server 2019 (tabelle di sistema Tempdb in memoria)

Che dire di questa novità nella tabella dei sistemi di memoria? Hm? Sup con quello?

Accendiamolo!

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

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

Nota che questo richiede un riavvio di SQL Server per iniziare, quindi scusami mentre riavvio SQL in questo delizioso venerdì pomeriggio.

Ora le cose sembrano diverse:

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;

NUTS

Differenza oraria:

  • Tabella delle temp: 11638 ms
  • Variabile di tabella: 7403 ms

Le tabelle temporanee hanno fatto circa 4 secondi meglio! È qualcosa.

Mi piace qualcosa.

Questa volta, la differenza Perfview non è molto interessante. Fianco a fianco, è interessante notare quanto i tempi siano vicini su tutta la linea:

NUTS

Un punto interessante nel diff sono le chiamate a hkengine!, Che possono sembrare ovvie poiché le funzionalità di hekaton sono ora in uso.

NUTS

Per quanto riguarda i primi due elementi nel diff, non posso fare molto di ntoskrnl!?:

NUTS

O sqltses!CSqlSortManager_80::GetSortKey, Ma sono qui per essere visti da Smrtr Ppl ™:

NUTS

Si noti che esiste un documento non documentato e sicuramente non sicuro per la produzione, quindi non utilizzarlo flag di traccia di avvio è possibile utilizzare per includere oggetti di sistema di tabelle temporanee aggiuntive (sysrowset, sysallocunits e sysseobjvalues) nel in memoria, ma in questo caso non ha fatto alcuna differenza nei tempi di esecuzione.

Arrotondare

Anche nelle versioni più recenti di SQL Server, le chiamate ad alta frequenza alle variabili di tabella sono molto più veloci delle chiamate ad alta frequenza alle tabelle temporanee.

Anche se è allettante dare la colpa a compilazioni, ricompilazioni, statistiche automatiche, latch, spinlock, cache o altri problemi, il problema è chiaramente ancora legato alla gestione della pulizia della tabella temporanea.

È una chiamata più vicina in SQL Server 2019 con le tabelle di sistema in memoria abilitate, ma le variabili di tabella continuano a funzionare meglio quando la frequenza delle chiamate è elevata.

Certo, come rifletteva una volta un saggio di svapo: "usa le variabili di tabella quando la scelta del piano non è un problema".

10
Erik Darling