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:StmtRecompile
eventos en Profiler (imagen abajo) muestra que estos eventos ocurren solamente una vez (en la primera invocación del #temp
procedimiento 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)
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
fuente
#temp
tabla una vez a pesar de que se borran y se vuelven a llenar otras 9.999 veces después de eso.Respuestas:
La salida de
SET STATISTICS IO ON
para ambos se ve similarDa
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.NUM
la#temp
versió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 de
EXEC dbo.T2 10
y estos resultados para 10,000 ejecuciones de
EXEC dbo.V2 10
Por lo tanto, está claro que el número de
PAGELATCH_SH
esperas es mucho mayor en el#temp
caso 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íMientras que en otra conexión sondeo
sys.dm_os_waiting_tasks
Después de dejarlo funcionando durante unos 15 segundos, había obtenido los siguientes resultados
Ambas páginas que se enganchan pertenecen a índices (diferentes) no agrupados en la
tempdb.sys.sysschobjs
tabla base denominada'nc1'
y'nc2'
.La consulta
tempdb.sys.fn_dblog
durante 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#temp
versión.Mirando las entradas del registro de transacciones con más detalle para la
#temp
versión de tabla del SP, cada invocación posterior del procedimiento almacenado crea tres transacciones y la variable de tabla uno solo dos.Las transacciones
INSERT
/TVQUERY
son 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 entradasLOP_BEGIN_XACT
/LOP_COMMIT_XACT
.La
CREATE TABLE
transacción solo aparece en la#Temp
versión y tiene el siguiente aspecto.La
FCheckAndCleanupCachedTempTable
transacción aparece en ambos pero tiene 6 entradas adicionales en la#temp
versión. Estas son las 6 filas a las que se refierensys.sysschobjs
y tienen exactamente el mismo patrón que el anterior.Mirando estas 6 filas en ambas transacciones, corresponden a las mismas operaciones. El primero
LOP_MODIFY_ROW, LCX_CLUSTERED
es una actualización de lamodify_date
columna ensys.objects
. Las cinco filas restantes están relacionadas con el cambio de nombre de los objetos. Debido a quename
es una columna clave de ambos NCI afectados (nc1
ync2
) 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
#temp
versión de la tabla, cuando el procedimiento almacenado finaliza parte de la limpieza realizada por laFCheckAndCleanupCachedTempTable
transacción, es cambiar el nombre de la tabla temporal de algo así#T__________________________________________________________________________________________________________________00000000E316
a un nombre interno diferente, como#2F4A0079
y cuando se ingresa, laCREATE TABLE
transacción cambia el nombre. Este nombre de flip flop puede verse en una conexión ejecutándosedbo.T2
en un bucle mientras que en otraResultados de ejemplo
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
tempdb
responsable.Al ejecutar ambos procedimientos en un bucle, el generador de perfiles de código de Visual Studio revela lo siguiente
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
Tanto en las funciones de creación como de liberación, la función
CMEDProxyObject::SetName
se muestra con un valor de muestra inclusivo de19.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
Perfil de tabla temporal
Perfil variable de tabla
fuente
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:
Tabla de temperatura:
Variable de tabla:
Para evitar posibles esperas ASYNC_NETWORK_IO , estoy usando procedimientos de envoltura.
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:
Sigue siendo una diferencia muy clara, ¿eh? Pero, ¿qué está golpeando SQL Server ahora?
Mirando los dos aumentos superiores en las muestras difusas, vemos
sqlmin
ysqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket
somos los dos mayores delincuentes.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.
Mirar a través de las pilas de llamadas para la prueba de variable de tabla no muestra ninguno de los principales delincuentes:
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:
Diferencia horaria:
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
.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!
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:
Diferencia horaria:
¡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:
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.En cuanto a los dos primeros elementos en la diferencia, no puedo hacer mucho de
ntoskrnl!?
:O
sqltses!CSqlSortManager_80::GetSortKey
, pero están aquí para que Smrtr Ppl ™ los vea: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".
fuente