¿Por qué está usando una variable de tabla más del doble de rápido que una tabla #temp en este caso específico?

37

Estaba mirando el artículo aquí Las tablas temporales frente a las variables de tabla y su efecto en el rendimiento de SQL Server y en SQL Server 2008 pudieron reproducir resultados similares a los mostrados allí para 2005.

Al ejecutar los procedimientos almacenados (definiciones a continuación) con solo 10 filas, la versión variable de la tabla supera la versión de la tabla temporal más de dos veces.

Limpié el caché de procedimientos y ejecuté ambos procedimientos almacenados 10,000 veces y luego repetí el proceso para otras 4 ejecuciones. Resultados a continuación (tiempo en ms por lote)

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

Mi pregunta es: ¿Cuál es la razón del mejor rendimiento de la versión de tabla variable?

He investigado un poco. Por ejemplo, mirar los contadores de rendimiento con

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

confirma que en ambos casos los objetos temporales se almacenan en caché después de la primera ejecución como se esperaba en lugar de crearse desde cero nuevamente para cada invocación.

Del mismo modo trazando el Auto Stats, SP:Recompile, SQL:StmtRecompileeventos en Profiler (imagen abajo) muestra que estos eventos ocurren solamente una vez (en la primera invocación del #tempprocedimiento almacenado mesa) y los otros 9.999 ejecuciones no plantear cualquiera de estos eventos. (La versión de la variable de tabla no obtiene ninguno de estos eventos)

Rastro

Sin embargo, la sobrecarga levemente mayor de la primera ejecución del procedimiento almacenado de ninguna manera puede explicar la gran diferencia general, ya que solo lleva unos pocos ms borrar el caché del procedimiento y ejecutar ambos procedimientos una vez, por lo que no creo que las estadísticas o Las recompilaciones pueden ser la causa.

Crear objetos de base de datos necesarios

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 prueba

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
fuente
La traza del generador de perfiles indica que las estadísticas solo se crean en la #temptabla una vez a pesar de que se borran y se vuelven a llenar otras 9.999 veces después de eso.
Martin Smith

Respuestas:

31

La salida de SET STATISTICS IO ONpara ambos se ve similar

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

Da

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

Y como Aaron señala en los comentarios, el plan para la versión de la variable de tabla es en realidad menos eficiente, ya que ambos tienen un plan de bucles anidados impulsado por una búsqueda de índice en dbo.NUMla #tempversión de tabla que realiza una búsqueda en el índice [#T].n = [dbo].[NUM].[n]con predicado residual [#T].[n]<=[@total]mientras que la variable de tabla La versión realiza una búsqueda de índice @V.n <= [@total]con predicado residual @V.[n]=[dbo].[NUM].[n]y, por lo tanto, procesa más filas (por lo que este plan funciona tan mal para un mayor número de filas)

El uso de eventos extendidos para ver los tipos de espera para el spid específico proporciona estos resultados para 10,000 ejecuciones 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              |
+---------------------+------------+----------------+----------------+----------------+

y estos resultados para 10,000 ejecuciones 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              |
+---------------------+------------+----------------+----------------+----------------+

Por lo tanto, está claro que el número de PAGELATCH_SHesperas es mucho mayor en el #tempcaso de la tabla. No conozco ninguna forma de agregar el recurso de espera a la traza de eventos extendidos, así que para investigar esto más a fondo, corrí

WHILE 1=1
EXEC dbo.T2 10

Mientras que en otra conexión sondeo 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'

Después de dejarlo funcionando durante unos 15 segundos, había obtenido los siguientes resultados

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

Ambas páginas que se enganchan pertenecen a índices (diferentes) no agrupados en la tempdb.sys.sysschobjstabla base denominada 'nc1'y 'nc2'.

La consulta tempdb.sys.fn_dblogdurante las ejecuciones indica que el número de registros de anotaciones agregados por la primera ejecución de cada procedimiento almacenado fue algo variable, pero para las ejecuciones posteriores el número agregado por cada iteración fue muy consistente y predecible. Una vez que se almacenan en caché los planes de procedimiento, el número de entradas de registro es aproximadamente la mitad de las necesarias para la #tempversión.

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

Mirando las entradas del registro de transacciones con más detalle para la #tempversión de tabla del SP, cada invocación posterior del procedimiento almacenado crea tres transacciones y la variable de tabla uno solo dos.

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

Las transacciones INSERT/ TVQUERYson idénticas excepto por el nombre. Contiene los registros de cada una de las 10 filas insertadas en la tabla temporal o variable de tabla más las entradas LOP_BEGIN_XACT/ LOP_COMMIT_XACT.

La CREATE TABLEtransacción solo aparece en la #Tempversión y tiene el siguiente aspecto.

+-----------------+-------------------+---------------------+
|    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 FCheckAndCleanupCachedTempTabletransacción aparece en ambos pero tiene 6 entradas adicionales en la #tempversión. Estas son las 6 filas a las que se refieren sys.sysschobjsy tienen exactamente el mismo patrón que el anterior.

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

Mirando estas 6 filas en ambas transacciones, corresponden a las mismas operaciones. El primero LOP_MODIFY_ROW, LCX_CLUSTEREDes una actualización de la modify_datecolumna en sys.objects. Las cinco filas restantes están relacionadas con el cambio de nombre de los objetos. Debido a que namees una columna clave de ambos NCI afectados ( nc1y nc2) esto se lleva a cabo como una eliminación / inserción para aquellos, luego vuelve al índice agrupado y lo actualiza también.

Parece que para la #tempversión de la tabla, cuando el procedimiento almacenado finaliza parte de la limpieza realizada por la FCheckAndCleanupCachedTempTabletransacción, es cambiar el nombre de la tabla temporal de algo así #T__________________________________________________________________________________________________________________00000000E316a un nombre interno diferente, como #2F4A0079y cuando se ingresa, la CREATE TABLEtransacción cambia el nombre. Este nombre de flip flop puede verse en una conexión ejecutándose dbo.T2en un bucle mientras que en otra

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

Resultados de ejemplo

Captura de pantalla

Entonces, una posible explicación para el diferencial de rendimiento observado al que Alex aludió es que es este trabajo adicional el que mantiene las tablas del sistema como tempdbresponsable.


Al ejecutar ambos procedimientos en un bucle, el generador de perfiles de código de Visual Studio revela lo siguiente

+-------------------------------+--------------------+-------+-----------+
|           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 versión variable de la tabla pasa aproximadamente el 60% del tiempo realizando la instrucción de inserción y la selección posterior, mientras que la tabla temporal es menos de la mitad. Esto está en línea con los tiempos que se muestran en el OP y con la conclusión anterior de que la diferencia en el rendimiento se debe al tiempo dedicado a realizar trabajos auxiliares, no al tiempo dedicado a la ejecución de la consulta.

Las funciones más importantes que contribuyen al 75% "faltante" en la versión de tabla temporal son

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

Tanto en las funciones de creación como de liberación, la función CMEDProxyObject::SetNamese muestra con un valor de muestra inclusivo de 19.6%. De lo cual deduzco que el 39,2% del tiempo en el caso de la tabla temporal se ocupa del cambio de nombre descrito anteriormente.

Y los más grandes en la versión variable de la tabla que contribuyen al otro 40% son

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

Perfil de tabla temporal

ingrese la descripción de la imagen aquí

Perfil variable de tabla

ingrese la descripción de la imagen aquí

Martin Smith
fuente
10

Disco Inferno

Dado que esta es una pregunta anterior, decidí volver a visitar el problema en las versiones más recientes de SQL Server para ver si el mismo perfil de rendimiento todavía existe o si las características han cambiado.

Específicamente, la adición de tablas del sistema en memoria para SQL Server 2019 parece una ocasión que vale la pena volver a probar.

Estoy usando un arnés de prueba ligeramente diferente, ya que me encontré con este problema mientras trabajaba en otra cosa.

Prueba, prueba

Usando la versión 2013 de Stack Overflow , tengo este índice y estos dos procedimientos:

Índice:

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

Tabla de temperatura:

    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 tabla:

    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 

Para evitar posibles esperas ASYNC_NETWORK_IO , estoy usando procedimientos de envoltura.

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

Dado que 2014 y 2016 son básicamente RELICS en este punto, estoy comenzando mis pruebas con 2017. Además, por brevedad, estoy saltando a perfilar el código con Perfview . En la vida real, miraba esperas, pestillos, cerraduras giratorias, banderas de rastreo locas y otras cosas.

Perfilar el código es lo único que reveló algo de interés.

Diferencia horaria:

  • Tabla de temperatura: 17891 ms
  • Variable de tabla: 5891 ms

Sigue siendo una diferencia muy clara, ¿eh? Pero, ¿qué está golpeando SQL Server ahora?

NUECES

Mirando los dos aumentos superiores en las muestras difusas, vemos sqlminy sqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucketsomos los dos mayores delincuentes.

NUECES

A juzgar por los nombres en las pilas de llamadas, limpiar y renombrar internamente las tablas temporales parece ser el mayor tiempo en la llamada de la tabla temporal frente a la llamada de la variable de tabla.

Aunque las variables de tabla están respaldadas internamente por tablas temporales, esto no parece ser un problema.

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

Tabla '# B98CE339'. Escaneo recuento 1

Mirar a través de las pilas de llamadas para la prueba de variable de tabla no muestra ninguno de los principales delincuentes:

NUECES

SQL Server 2019 (Vanilla)

Bien, entonces esto sigue siendo un problema en SQL Server 2017, ¿hay algo diferente en 2019 fuera de la caja?

Primero, para mostrar que no hay nada bajo la manga:

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

NUECES

Diferencia horaria:

  • Tabla temporal: 15765 ms
  • Variable de tabla: 7250 ms

Ambos procedimientos fueron diferentes. La llamada a la tabla temporal fue un par de segundos más rápida, y la llamada a la tabla variable fue aproximadamente 1.5 segundos más lenta. La ralentización de la variable de la tabla puede explicarse parcialmente por la compilación diferida de la variable de la tabla , una nueva opción de optimizador en 2019.

Mirando la diferencia en Perfview, ha cambiado un poco, sqlmin ya no está allí, pero sí sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket.

NUECES

SQL Server 2019 (tablas del sistema Tempdb en memoria)

¿Qué pasa con esta nueva cosa en la tabla del sistema de memoria? Hm? ¿Qué tal eso?

¡Vamos a encenderlo!

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

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

Tenga en cuenta que esto requiere un reinicio de SQL Server para iniciarse, así que discúlpeme mientras reinicio SQL este encantador viernes por la tarde.

Ahora las cosas se ven diferentes:

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;

NUECES

Diferencia horaria:

  • Tabla temporal: 11638 ms
  • Variable de tabla: 7403 ms

¡Las tablas temporales funcionaron unos 4 segundos mejor! Eso es algo.

Me gusta algo

Esta vez, la diferencia de Perfview no es muy interesante. Lado a lado, es interesante observar cuán cercanos son los tiempos en todos los ámbitos:

NUECES

Un punto interesante en el diff son las llamadas a hkengine!, que pueden parecer obvias ya que las características hekaton-ish están ahora en uso.

NUECES

En cuanto a los dos primeros elementos en la diferencia, no puedo hacer mucho de ntoskrnl!?:

NUECES

O sqltses!CSqlSortManager_80::GetSortKey, pero están aquí para que Smrtr Ppl ™ los vea:

NUECES

Tenga en cuenta que hay un indocumentado y definitivamente no es seguro para la producción, así que no lo use como indicador de seguimiento de inicio que puede usar para tener objetos adicionales del sistema de tabla temporal (sysrowsets, sysallocunits y sysseobjvalues) incluidos en la función en memoria, pero no hizo una diferencia notable en los tiempos de ejecución en este caso.

Redondeo

Incluso en las versiones más recientes del servidor SQL, las llamadas de alta frecuencia a las variables de tabla son mucho más rápidas que las llamadas de alta frecuencia a las tablas temporales.

Aunque es tentador culpar a las compilaciones, recompilaciones, estadísticas automáticas, pestillos, spinlocks, almacenamiento en caché u otros problemas, el problema claramente sigue siendo la gestión de la limpieza de la tabla temporal.

Es una llamada más cercana en SQL Server 2019 con las tablas del sistema en memoria habilitadas, pero las variables de la tabla aún funcionan mejor cuando la frecuencia de las llamadas es alta.

Por supuesto, como reflexionó un sabio vaping: "use variables de tabla cuando la elección del plan no sea un problema".

Erik Darling
fuente
Agradable - lo siento, me perdí de que hayas agregado una respuesta a esto hasta solo seguir el enlace en tu publicación de blog de "depuración"
Martin Smith