¿Cómo puedo obtener totales de filas recientes más rápido?

8

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.

Plan de ejecución del primer intento

Sospeché TOPpor 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.

Segundo intento de plan de ejecución

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.

resultados

usuario2652379
fuente
1
Por lo general, no uso funciones de ventana, pero recuerdo que leí algunos artículos útiles sobre ellas. Eche un vistazo a una Introducción a las funciones de ventana de T-SQL , especialmente en la parte Mejoras de agregado de ventana en 2012 . Quizás te dé algunas respuestas. ... y un artículo más del mismo autor excelente Funciones y rendimiento de la ventana T-SQL
Denis Rubashkin
¿Has intentado poner un índice value?
Jacob H

Respuestas:

5

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:

SELECT TOP (10) seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM dbo.[Table_1_BIG]
ORDER BY seq DESC;

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:

SELECT *
    ,(
        SELECT SUM(value)
        FROM dbo.[Table_1_BIG]
        WHERE seq <= t.seq
        ) total
FROM (
    SELECT TOP (10) seq
        ,value
    FROM dbo.[Table_1_BIG]
    ORDER BY seq DESC
    ) t
ORDER BY seq DESC;

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:

Hola

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:

SELECT TOP (10) seq
,value
, [value]
    - SUM([value]) OVER (ORDER BY seq DESC ROWS UNBOUNDED PRECEDING)
    + (SELECT SUM([value]) FROM dbo.[Table_1_BIG]) AS total
FROM dbo.[Table_1_BIG]
ORDER BY seq DESC;

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:

bastante bueno

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:

CREATE OR ALTER VIEW dbo.Table_1_BIG__SUM
WITH SCHEMABINDING
AS
SELECT SUM([value]) SUM_VALUE
, COUNT_BIG(*) FOR_U
FROM dbo.[Table_1_BIG];

GO

CREATE UNIQUE CLUSTERED INDEX CI ON dbo.Table_1_BIG__SUM (SUM_VALUE);

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:

ingrese la descripción de la imagen aquí

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:

DROP TABLE IF EXISTS dbo.[Table_1_BIG];

CREATE TABLE dbo.[Table_1_BIG] (
    [seq] [int] NOT NULL,
    [value] [bigint] NOT NULL
);

DROP TABLE IF EXISTS #t;
CREATE TABLE #t (ID BIGINT);

INSERT INTO #t WITH (TABLOCK)
SELECT TOP (4000) -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

INSERT INTO dbo.[Table_1_BIG] WITH (TABLOCK)
SELECT t1.ID * 4000 + t2.ID, 8 * t2.ID + t1.ID
FROM (SELECT TOP (4000) ID FROM #t) t1
CROSS JOIN #t t2;

ALTER TABLE dbo.[Table_1_BIG]
ADD CONSTRAINT [PK_Table_1] PRIMARY KEY ([seq]);
Joe Obbish
fuente
4

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

Table 'Worktable'. Scan count 1000001, logical reads 8461526
Table 'Table_1'. Scan count 1, logical reads 2609
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 8641 ms,  elapsed time = 8537 ms.

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.

Table 'Table_1'. Scan count 11, logical reads 26093
 SQL Server Execution Times:
   CPU time = 1563 ms,  elapsed time = 1671 ms.

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.

ALTER TABLE [dbo].[Table_1] DROP CONSTRAINT [PK_Table_1];

CREATE CLUSTERED COLUMNSTORE INDEX [PK_Table_1] ON dbo.Table_1;

Entonces esta consulta es ridículamente rápida:

SELECT TOP 10
    seq, 
    value, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)
FROM dbo.Table_1
ORDER BY seq DESC;

Aquí están las estadísticas de mi máquina:

Table 'Table_1'. Scan count 4, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 3319
Table 'Table_1'. Segment reads 1, segment skipped 0.
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 375 ms,  elapsed time = 205 ms.

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 ROWlugar de la RANGEopción de función de ventana

Puede 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 ):

SELECT TOP 10
    seq, 
    value, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)
FROM dbo.Table_1
ORDER BY seq DESC;

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 :

... RANGE usa un carrete en disco, mientras que ROWS usa un carrete en memoria

Desafortunadamente, el tiempo de ejecución es casi lo mismo que su segundo enfoque.

Table 'Worktable'. Scan count 0, logical reads 0
Table 'Table_1'. Scan count 1, logical reads 2609
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 1984 ms,  elapsed time = 1474 ms.

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:

CREATE TABLE [dbo].[Table_1_Totals]
(
    [seq] [int] NOT NULL,
    [running_total] [bigint] NOT NULL,
    CONSTRAINT [PK_Table_1_Totals] PRIMARY KEY CLUSTERED ([seq])
);

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

INSERT INTO dbo.Table_1_Totals
SELECT
    seq, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) as total
FROM dbo.Table_1 t
WHERE NOT EXISTS (
            SELECT NULL 
            FROM dbo.Table_1_Totals t2
            WHERE t.seq = t2.seq)
ORDER BY seq DESC;

Entonces su consulta de informes es muy eficiente:

SELECT TOP 10
    t.seq, 
    t.value, 
    t2.running_total
FROM dbo.Table_1 t
    INNER JOIN dbo.Table_1_Totals t2
        ON t.seq = t2.seq
ORDER BY seq DESC;

Aquí están las estadísticas de lectura:

Table 'Table_1'. Scan count 0, logical reads 35
Table 'Table_1_Totals'. Scan count 1, logical reads 3

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:

CREATE TABLE dbo.Table_1
(
    seq integer IDENTITY(1,1) NOT NULL,
    val bigint NOT NULL,
    total bigint NOT NULL,

    prev_seq integer NULL,
    prev_total bigint NULL,

    CONSTRAINT [PK_Table_1] 
        PRIMARY KEY CLUSTERED (seq ASC),

    CONSTRAINT [UQ dbo.Table_1 seq, total]
        UNIQUE (seq, total),

    CONSTRAINT [UQ dbo.Table_1 prev_seq]
        UNIQUE (prev_seq),

    CONSTRAINT [FK dbo.Table_1 previous seq and total]
        FOREIGN KEY (prev_seq, prev_total) 
        REFERENCES dbo.Table_1 (seq, total),

    CONSTRAINT [CK dbo.Table_1 total = prev_total + val]
        CHECK (total = ISNULL(prev_total, 0) + val),

    CONSTRAINT [CK dbo.Table_1 denormalized columns all null or all not null]
        CHECK 
        (
            (prev_seq IS NOT NULL AND prev_total IS NOT NULL)
            OR
            (prev_seq IS NULL AND prev_total IS NULL)
        )
);
Josh Darnell
fuente
2

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ó.

SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM Table_1
ORDER BY seq DESC
Luis Cazares
fuente
Gracias por decir que usted 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.
user2652379
Pero esto es solo en esta opción. Su segunda consulta no escala bien. Intenta devolver más filas y la diferencia se vuelve bastante evidente.
Luis Cazares
Tal vez fuera de t-sql? Puedo cambiar el esquema
user2652379