it-swarm-eu.dev

Proč se v tomto konkrétním případě používá proměnná tabulky více než dvakrát rychleji než tabulka #temp?

Díval jsem se na článek zde Dočasné tabulky vs. proměnné tabulky a jejich vliv na výkon serveru SQL a na serveru SQL Server 2008 bylo možné reprodukovat podobné výsledky, jaké jsou uvedeny v roce 2005.

Při provádění uložených procedur (definice níže) pouze s 10 řádky provede verze proměnné tabulky dočasnou verzi tabulky více než dvakrát.

Vymazal jsem mezipaměť procedur a obě uložené procedury jsem spustil 10 000krát a pak opakoval proces pro další 4 běhy. Výsledky níže (čas v ms na dávku)

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

Moje otázka zní: Jaký je důvod lepšího výkonu verze proměnné tabulky?

Provedl jsem nějaké vyšetřování. např. Při pohledu na čítače výkonu s

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

potvrzuje, že v obou případech jsou dočasné objekty ukládány do mezipaměti po prvním spuštění jak se očekávalo , spíše než aby byly vytvořeny znovu od začátku pro každou vyvolání.

Podobně sledujeme Auto Stats, SP:Recompile, SQL:StmtRecompileevents v Profiler (snímek obrazovky níže) ukazuje, že tyto události se vyskytují pouze jednou (při prvním vyvolání #temp tabulka uložená procedura) a dalších 9 999 spuštění nevyvolává žádnou z těchto událostí. (Verze proměnné tabulky neobdrží žádnou z těchto událostí)

Trace

O něco větší režie při prvním spuštění uložené procedury v žádném případě nezohledňuje velký celkový rozdíl, protože vymazání mezipaměti procedur a spuštění obou procedur trvá vždy jen několik ms, takže nevěřím ani statistikám ani příčiny mohou být recompiles.

Vytvoření požadovaných databázových objektů

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

Testovací skript

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

Výstup SET STATISTICS IO ON Pro oba vypadá podobně

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

Dává

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

A jak Aaron připomíná v komentářích, plán pro verzi proměnné tabulky je ve skutečnosti méně efektivní, zatímco zatímco oba mají plán vnořených smyček řízený hledáním indexu v dbo.NUM, #temp Verze tabulky provádí hledat do indexu na [#T].n = [dbo].[NUM].[n] s reziduálním predikátem [#T].[n]<=[@total], zatímco verze proměnné tabulky provádí vyhledávání indexu na @V.n <= [@total] se zbytkovým predikátem @V.[n]=[dbo].[NUM].[n], a tak zpracovává více řádků (což je důvod, proč tento plán funguje tak špatně pro větší počet řádků)

Použití Extended Events k prozkoumání typů čekání na konkrétní spid dává tyto výsledky pro 10 000 spuštění 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              |
+---------------------+------------+----------------+----------------+----------------+

a tyto výsledky pro 10 000 spuštění 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              |
+---------------------+------------+----------------+----------------+----------------+

Je tedy zřejmé, že počet čekání PAGELATCH_SH Je mnohem vyšší v případě tabulky #temp. Nevím o žádném způsobu přidání zdroje čekání do trasování rozšířených událostí, abych to prozkoumal, pokračoval jsem

WHILE 1=1
EXEC dbo.T2 10

Zatímco v jiném připojení dotazování 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'

Poté, co nechal tento běh asi 15 sekund, získal následující výsledky

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

Obě tyto blokované stránky patří do (různých) neslastovaných indexů v základní tabulce tempdb.sys.sysschobjs S názvem 'nc1' A 'nc2'.

Dotazování tempdb.sys.fn_dblog Během běhů naznačuje, že počet záznamů protokolu přidaných prvním provedením každé uložené procedury byl poněkud proměnný, ale pro následující spuštění byl počet přidaný každou iterací velmi konzistentní a předvídatelný. Jakmile jsou plány procedur uloženy do mezipaměti, počet položek protokolu je přibližně poloviční než počet potřebný pro verzi #temp.

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

Při podrobnějším pohledu na položky protokolu transakcí pro verzi tabulky #temp Tabulky SP) každé následné vyvolání uložené procedury vytvoří tři transakce a proměnnou tabulky pouze dvě.

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

Transakce INSERT/TVQUERY jsou identické s výjimkou názvu. Obsahuje záznamy protokolu pro každý z 10 řádků vložených do dočasné proměnné tabulky nebo tabulky plus položky LOP_BEGIN_XACT/LOP_COMMIT_XACT.

Transakce CREATE TABLE Se objeví pouze ve verzi #Temp A vypadá následovně.

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

Transakce FCheckAndCleanupCachedTempTable se objeví v obou, ale má 6 dalších položek ve verzi #temp. Jedná se o 6 řádků odkazujících na sys.sysschobjs A mají přesně stejný vzor jako výše.

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

Při pohledu na těchto 6 řádků v obou transakcích odpovídají stejným operacím. První LOP_MODIFY_ROW, LCX_CLUSTERED Je aktualizace sloupce modify_date Ve sys.objects. Zbývajících pět řádků se týká přejmenování objektu. Protože name je klíčový sloupec obou postižených NCI (nc1 A nc2), Provádí se to jako mazání/vkládání pro ty, pak se vrací zpět do seskupeného indexu a to také aktualizuje.

Zdá se, že pro verzi tabulky #temp, Když uložená procedura končí, část čištění prováděná transakcí FCheckAndCleanupCachedTempTable, má přejmenovat dočasnou tabulku z něčeho jako #T__________________________________________________________________________________________________________________00000000E316 Na jiné interní jméno, například #2F4A0079, a když je zadáno, transakce CREATE TABLE jej přejmenuje zpět. Toto překlopitelné jméno pro flopping lze vidět v jednom připojení provádějícím dbo.T2 Ve smyčce, zatímco v jiném

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

Příklad výsledků

Screenshot

Jedním možným vysvětlením pozorovaného rozdílu výkonu, jak se zmiňuje Alex, je, že je to tato další práce, která udržuje systémové tabulky v tempdb, která je zodpovědná.


Po spuštění obou procedur ve smyčce profiler Visual Studio Code odhalí následující

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

Verze proměnné tabulky tráví asi 60% času provedením příkazu insert a následným výběrem, zatímco dočasná tabulka je menší než polovina. To je v souladu s načasováním zobrazeným v OP a se závěrem výše, že rozdíl ve výkonu je až čas strávený prováděním pomocných prací, nikoli kvůli času strávenému samotným prováděním dotazu.

Nejdůležitější funkce přispívající k "chybějícím" 75% v dočasné verzi tabulky jsou

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

Ve funkcích vytvoření i uvolnění se zobrazí funkce CMEDProxyObject::SetName S inkluzivní vzorkovou hodnotou 19.6%. Z toho vyvodím, že 39,2% času v případě dočasného stolu je převzato přejmenováním popsaným výše.

A ty největší ve verzi proměnné tabulky, které přispívají k dalším 40%, jsou

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

Dočasný profil tabulky

enter image description here

Tabulka variabilní profil

enter image description here

31
Martin Smith

Disco Inferno

Protože se jedná o starší otázku, rozhodl jsem se problém znovu vyhledat v novějších verzích serveru SQL, abych zjistil, zda stále existuje stejný profil výkonu nebo zda se vlastnosti vůbec změnily.

Konkrétně se zdá, že přidání systémové paměťové tabulky pro SQL Server 2019 se vyplatí znovu otestovat.

Používám poněkud odlišný zkušební úvazek, protože jsem se v tomto problému setkal, když jsem pracoval na něčem jiném.

Testování, testování

Používám 2013 verze Stack Overflow , mám tento index a tyto dva postupy:

Index:

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

Tabulka teplot:

    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 

Proměnná tabulky:

    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 

Abychom předešli potenciálnímu ASYNC_NETWORK_IO čeká , používám procedury wrapperu.

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

Od roku 2014 a 2016 jsou v tomto okamžiku v podstatě RELICS, začínám s testováním s rokem 2017. Pro stručnost také skočím přímo k profilování kódu pomocí Perfview . Ve skutečném životě jsem se díval na čekání, západky, spinlocky, bláznivé stopy a další věci.

Profilování kódu je jediná věc, která odhalila něco zajímavého.

Časový rozdíl:

  • Tabulka teplot: 17891 ms
  • Proměnná tabulky: 5891 ms

Stále velmi jasný rozdíl, co? Ale co teď zasahuje SQL Server?

NUTS

Když se podíváme na první dvě zvýšení v různých vzorcích, uvidíme sqlmin a sqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket jsou dva největší pachatelé.

NUTS

Soudě podle jmen v hromadách hovorů se zdá, že čištění a interní přejmenování dočasných tabulek je největším časovým nasáváním volání dočasných tabulek oproti volání proměnné tabulky.

I když jsou proměnné tabulky interně podporovány dočasnými tabulkami, nezdá se, že by to byl problém.

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

Tabulka '# B98CE339'. Počet skenování 1

Při pohledu na hromady hovorů pro test proměnné tabulky se nezobrazí ani jeden z hlavních pachatelů:

NUTS

SQL Server 2019 (Vanilla)

Dobře, takže toto je stále problém v serveru SQL Server 2017, je něco jiného v roce 2019 mimo pole?

Za prvé, abych ukázal, že na mém rukávu není nic:

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

NUTS

Časový rozdíl:

  • Tabulka teplot: 15765 ms
  • Proměnná tabulky: 7250 ms

Oba postupy byly odlišné. Volání dočasné tabulky bylo o několik sekund rychlejší a volání proměnné tabulky bylo o 1,5 sekundy pomalejší. Zpomalení proměnné tabulky lze částečně vysvětlit pomocí kompilace odložené kompilace proměnné tabulky , nová volba optimalizátoru v roce 2019.

Při pohledu na rozdíl v Perfview se to trochu změnilo - sqlmin už tam není - ale sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket je.

NUTS

SQL Server 2019 (systémové tabulky Tempdb v paměti)

A co ta nová věc v tabulce paměti systému? Hm? Sup s tím?

Pojďme to zapnout!

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

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

Všimněte si, že to vyžaduje restart serveru SQL, aby se do něj zapojil, takže promiňte, když restartuji SQL v tento krásný pátek odpoledne.

Nyní věci vypadají jinak:

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

Časový rozdíl:

  • Tabulka teplot: 11638 ms
  • Proměnná tabulky: 7403 ms

Dočasné tabulky se o 4 sekundy zlepšily! To je něco.

Něco se mi líbí.

Tentokrát není rozdíl Perfview příliš zajímavý. Vedle sebe je zajímavé poznamenat si, jak blízko jsou časy napříč deskou:

NUTS

Jedním zajímavým bodem v rozdílu jsou volání k hkengine!, což se může zdát zřejmé, protože se nyní používají funkce hekaton-ish.

NUTS

Pokud jde o první dvě položky v rozdílu, nemůžu udělat hodně z ntoskrnl!?:

NUTS

Nebo sqltses!CSqlSortManager_80::GetSortKey, ale jsou tu, aby se Smrtr Ppl ™ podíval na:

NUTS

Všimněte si, že existuje nezdokumentovaná a rozhodně ne bezpečná výroba, proto ji prosím nepoužívejte příznak trasování při spuštění můžete použít k zahrnutí dalších systémových objektů dočasné tabulky (sysrowsets, sysallocunits a sysseobjvalues) do funkce v paměti, ale v tomto případě to nezaznamenalo znatelný rozdíl v době provádění.

Zátah

Dokonce i v novějších verzích serveru SQL jsou vysokofrekvenční volání do proměnných tabulky mnohem rychlejší než vysokofrekvenční volání do dočasných tabulek.

Přestože je lákavé obviňovat kompilace, rekompilace, automatické statistiky, západky, spinlocky, ukládání do mezipaměti nebo jiné problémy, problém je stále kolem správy čištění dočasných tabulek.

Jedná se o bližší volání v SQL Server 2019 s aktivovanými tabulkami v paměti, ale proměnné tabulky stále fungují lépe, když je frekvence volání vysoká.

Samozřejmě, že mudrc vapingu jednou přemýšlel: „použijte proměnné tabulky, když volba plánu není problém“.

10
Erik Darling