Actualmente estoy diseñando una tabla de transacciones. Me di cuenta de que será necesario calcular los totales acumulados para cada fila y esto podría tener un rendimiento lento. Así que creé una tabla con 1 millón de filas para fines de prueba.
CREATE TABLE [dbo].[Table_1](
[seq] [int] IDENTITY(1,1) NOT NULL,
[value] [bigint] NOT NULL,
CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED
(
[seq] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
Y traté de obtener 10 filas recientes y su total acumulado, pero me llevó unos 10 segundos.
--1st attempt
SELECT TOP 10 seq
,value
,sum(value) OVER (ORDER BY seq) total
FROM Table_1
ORDER BY seq DESC
--(10 rows affected)
--Table 'Worktable'. Scan count 1000001, logical reads 8461526, physical reads 2, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Table_1'. Scan count 1, logical reads 2608, physical reads 516, read-ahead reads 2617, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
-- CPU time = 8483 ms, elapsed time = 9786 ms.
Sospeché TOP
por el lento rendimiento del plan, por lo que cambié la consulta de esta manera y me llevó aproximadamente 1 ~ 2 segundos. Pero creo que esto sigue siendo lento para la producción y me pregunto si esto se puede mejorar aún más.
--2nd attempt
SELECT *
,(
SELECT SUM(value)
FROM Table_1
WHERE seq <= t.seq
) total
FROM (
SELECT TOP 10 seq
,value
FROM Table_1
ORDER BY seq DESC
) t
ORDER BY seq DESC
--(10 rows affected)
--Table 'Table_1'. Scan count 11, logical reads 26083, physical reads 1, read-ahead reads 443, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
-- CPU time = 1422 ms, elapsed time = 1621 ms.
Mis preguntas son:
- ¿Por qué la consulta del primer intento es más lenta que la segunda?
- ¿Cómo puedo mejorar aún más el rendimiento? También puedo cambiar los esquemas.
Para ser claros, ambas consultas devuelven el mismo resultado que a continuación.
sql-server
database-design
t-sql
query-performance
execution-plan
usuario2652379
fuente
fuente
value
?Respuestas:
Recomiendo probar con un poco más de datos para tener una mejor idea de lo que está sucediendo y ver cómo funcionan los diferentes enfoques. Cargué 16 millones de filas en una tabla con la misma estructura. Puede encontrar el código para llenar la tabla al final de esta respuesta.
El siguiente enfoque toma 19 segundos en mi máquina:
Plan actual aquí . La mayor parte del tiempo se gasta calculando la suma y haciendo la clasificación. Lo preocupante es que el plan de consulta hace casi todo el trabajo para todo el conjunto de resultados y filtra las 10 filas que solicitó al final. El tiempo de ejecución de esta consulta se escala con el tamaño de la tabla en lugar de con el tamaño del conjunto de resultados.
Esta opción tarda 23 segundos en mi máquina:
Plan actual aquí . Este enfoque se escala con el número de filas solicitadas y el tamaño de la tabla. Se leen casi 160 millones de filas de la tabla:
Para obtener resultados correctos, debe sumar filas para toda la tabla. Lo ideal sería realizar esta suma solo una vez. Es posible hacer esto si cambia la forma en que aborda el problema. Puede calcular la suma de toda la tabla y luego restar un total acumulado de las filas en el conjunto de resultados. Eso le permite encontrar la suma de la enésima fila. Una forma de hacer esto:
Plan actual aquí . La nueva consulta se ejecuta en 644 ms en mi máquina. La tabla se escanea una vez para obtener el total completo y luego se lee una fila adicional para cada fila del conjunto de resultados. No hay clasificación y se dedica casi todo el tiempo a calcular la suma en la parte paralela del plan:
Si desea que esta consulta sea aún más rápida, solo necesita optimizar la parte que calcula la suma completa. La consulta anterior realiza un escaneo de índice agrupado. El índice agrupado incluye todas las columnas, pero solo necesita la
[value]
columna. Una opción es crear un índice no agrupado en esa columna. Otra opción es crear un índice de almacén de columnas no agrupado en esa columna. Ambos mejorarán el rendimiento. Si está en Enterprise, una excelente opción es crear una vista indexada como la siguiente:Esta vista devuelve una sola fila, por lo que casi no ocupa espacio. Habrá una penalización al hacer DML pero no debería ser muy diferente al mantenimiento del índice. Con la vista indexada en juego, la consulta ahora tarda 0 ms:
Plan actual aquí . La mejor parte de este enfoque es que el tiempo de ejecución no cambia según el tamaño de la tabla. Lo único que importa es cuántas filas se devuelven. Por ejemplo, si obtiene las primeras 10000 filas, la consulta ahora tarda 18 ms en ejecutarse.
Código para llenar la tabla:
fuente
Diferencia en los dos primeros enfoques
El primer plan pasa aproximadamente 7 de los 10 segundos en el operador Window Spool, por lo que esta es la razón principal por la que es tan lento. Realiza muchas E / S en tempdb para crear esto. Mis estadísticas de E / S y tiempo se ven así:
El segundo plan es capaz de evitar el carrete y, por lo tanto, la mesa de trabajo por completo. Simplemente toma las 10 filas superiores del índice agrupado, y luego se unen los bucles anidados a la agregación (suma) que sale de un escaneo de índice agrupado separado. El lado interno todavía termina leyendo toda la tabla, pero la tabla es muy densa, por lo que esto es razonablemente eficiente con un millón de filas.
Mejorando el desempeño
Almacén de columnas
Si realmente desea el enfoque de "informes en línea", el almacén de columnas es probablemente su mejor opción.
Entonces esta consulta es ridículamente rápida:
Aquí están las estadísticas de mi máquina:
Es probable que no lo superes (a menos que seas realmente inteligente , agradable, Joe). Columnstore es muy bueno para escanear y agregar grandes cantidades de datos.
Usar en
ROW
lugar de laRANGE
opción de función de ventanaPuede obtener un rendimiento muy similar a su segunda consulta con este enfoque, que se mencionó en otra respuesta y que utilicé en el ejemplo de almacén de columnas anterior ( plan de ejecución ):
Resulta en menos lecturas que su segundo enfoque, y no hay actividad tempdb frente a su primer enfoque porque el spool de la ventana ocurre en la memoria :
Desafortunadamente, el tiempo de ejecución es casi lo mismo que su segundo enfoque.
Solución basada en esquemas: totales acumulados asíncronos
Como está abierto a otras ideas, podría considerar actualizar el "total acumulado" de forma asincrónica. Puede tomar periódicamente los resultados de una de estas consultas y cargarla en una tabla de "totales". Entonces harías algo como esto:
Cárguelo todos los días / hora / lo que sea (esto tomó aproximadamente 2 segundos en mi máquina con filas de 1 mm, y podría optimizarse):
Entonces su consulta de informes es muy eficiente:
Aquí están las estadísticas de lectura:
Solución basada en esquemas: totales en fila con restricciones
Una solución realmente interesante para esto se cubre en detalle en esta respuesta a la pregunta: Cómo escribir un esquema bancario simple: ¿Cómo debo mantener mis saldos sincronizados con su historial de transacciones?
El enfoque básico sería rastrear el total acumulado actual en fila junto con el total acumulado anterior y el número de secuencia. Luego puede usar restricciones para validar que los totales acumulados son siempre correctos y actualizados.
Gracias a Paul White por proporcionar una implementación de muestra para el esquema en estas preguntas y respuestas:
fuente
Cuando se trata de un pequeño subconjunto de filas devueltas, la unión triangular es una buena opción. Sin embargo, cuando utiliza funciones de ventana tiene más opciones que pueden aumentar su rendimiento. La opción predeterminada para la opción de ventana es RANGE, pero la opción óptima es ROWS. Tenga en cuenta que la diferencia no está solo en el rendimiento, sino también en los resultados cuando se trata de vínculos.
El siguiente código es un poco más rápido que los que presentó.
fuente
ROWS
. Lo intenté pero no puedo decir que sea más rápido que mi segunda consulta. El resultado fueCPU time = 1438 ms, elapsed time = 1537 ms.