Come posso ottenere più rapidamente i totali delle righe recenti?


8

Attualmente sto progettando una tabella delle transazioni. Mi sono reso conto che il calcolo dei totali correnti per ogni riga sarà necessario e questo potrebbe essere lento nelle prestazioni. Quindi ho creato una tabella con 1 milione di righe a scopo di test.

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

E ho cercato di ottenere 10 righe recenti e i suoi totali in esecuzione, ma ci sono voluti circa 10 secondi.

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

Piano di esecuzione del 1 ° tentativo

Ho sospettato TOPper il motivo di prestazioni lente dal piano, quindi ho cambiato la query in questo modo e ci sono voluti circa 1 ~ 2 secondi. Ma penso che questo sia ancora lento per la produzione e mi chiedo se questo possa essere ulteriormente migliorato.

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

Piano di esecuzione del 2 ° tentativo

Le mie domande sono:

  • Perché la query dal primo tentativo è più lenta della seconda?
  • Come posso migliorare ulteriormente le prestazioni? Posso anche cambiare schemi.

Per essere chiari, entrambe le query restituiscono lo stesso risultato di seguito.

risultati


1
Di solito non uso le funzioni della finestra, ma ricordo di aver letto alcuni articoli utili su di esse. Dai un'occhiata a un'introduzione alle funzioni T-SQL Window , in particolare alla parte Window Aggregate Enhancements nel 2012 . Forse ti dà alcune risposte. ... e un altro articolo dello stesso eccellente autore Funzioni e prestazioni della finestra T-SQL
Denis Rubashkin,

Hai provato a mettere un indice value?
Jacob H,

Risposte:


5

Consiglio di provare con un po 'più di dati per avere un'idea migliore di ciò che sta succedendo e vedere come funzionano i diversi approcci. Ho caricato 16 milioni di righe in una tabella con la stessa struttura. Puoi trovare il codice per popolare la tabella in fondo a questa risposta.

Il seguente approccio richiede 19 secondi sulla mia macchina:

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

Piano reale qui . Passa la maggior parte del tempo a calcolare la somma e a fare l'ordinamento. Cosa preoccupante, il piano di query fa quasi tutto il lavoro per l'intero set di risultati e filtra fino alle 10 righe richieste alla fine. Il runtime di questa query viene ridimensionato con le dimensioni della tabella anziché con le dimensioni del set di risultati.

Questa opzione richiede 23 secondi sulla mia macchina:

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;

Piano reale qui . Questo approccio si ridimensiona sia con il numero di righe richieste sia con la dimensione della tabella. Quasi 160 milioni di righe vengono lette dalla tabella:

Ciao

Per ottenere risultati corretti è necessario sommare le righe per l'intera tabella. Idealmente, eseguire questa somma una sola volta. È possibile farlo se si cambia il modo in cui si affronta il problema. È possibile calcolare la somma per l'intera tabella, quindi sottrarre un totale parziale dalle righe nel set di risultati. Ciò ti consente di trovare la somma per l'ennesima riga. Un modo per farlo:

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;

Piano reale qui . La nuova query viene eseguita in 644 ms sul mio computer. La tabella viene scansionata una volta per ottenere il totale completo, quindi viene letta una riga aggiuntiva per ogni riga nel set di risultati. Non c'è ordinamento e quasi tutto il tempo viene impiegato per calcolare la somma nella parte parallela del piano:

piuttosto buono

Se desideri che questa query vada ancora più veloce, devi solo ottimizzare la parte che calcola la somma completa. La query precedente esegue una scansione dell'indice cluster. L'indice cluster include tutte le colonne ma è necessaria solo la [value]colonna. Un'opzione è quella di creare un indice non cluster su quella colonna. Un'altra opzione è quella di creare un indice columnstore non cluster su quella colonna. Entrambi miglioreranno le prestazioni. Se sei su Enterprise, un'ottima opzione è quella di creare una vista indicizzata come la seguente:

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

Questa vista restituisce una singola riga, quindi non occupa quasi spazio. Ci sarà una penalità quando si fa DML ma non dovrebbe essere molto diverso dalla manutenzione dell'indice. Con la vista indicizzata in esecuzione, la query ora richiede 0 ms:

inserisci qui la descrizione dell'immagine

Piano reale qui . La parte migliore di questo approccio è che il runtime non viene modificato dalle dimensioni della tabella. L'unica cosa che conta è quante righe vengono restituite. Ad esempio, se si ottengono le prime 10000 righe, l'esecuzione della query ora richiede 18 ms.

Codice per popolare la tabella:

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]);

4

Differenza nei primi due approcci

Il primo piano impiega circa 7 dei 10 secondi nell'operatore Window Spool, quindi questo è il motivo principale per cui è così lento. Sta eseguendo molti I / O in tempdb per creare questo. Le mie statistiche I / O e il tempo si presentano così:

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.

Il secondo piano è in grado di evitare la bobina e quindi il piano di lavoro interamente. Prende semplicemente le prime 10 righe dall'indice cluster e quindi i loop nidificati si uniscono all'aggregazione (somma) che esce da una scansione dell'indice cluster separata. Il lato interno finisce ancora per leggere l'intero tavolo, ma il tavolo è molto denso, quindi questo è ragionevolmente efficiente con un milione di righe.

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

Miglioramento delle prestazioni

columnstore

Se vuoi davvero l'approccio del "reporting online", il columnstore è probabilmente l'opzione migliore.

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

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

Quindi questa query è incredibilmente veloce:

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

Ecco le statistiche dalla mia macchina:

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.

Probabilmente non lo batterai (a meno che tu non sia davvero intelligente - bello, Joe). Columnstore è incredibilmente bravo a scansionare e aggregare grandi quantità di dati.

Utilizzo dell'opzione funzione ROWanziché RANGEfinestra

Puoi ottenere prestazioni molto simili alla tua seconda query con questo approccio, che è stato menzionato in un'altra risposta e che ho usato nell'esempio columnstore sopra ( piano di esecuzione ):

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

Risulta in meno letture rispetto al secondo approccio e nessuna attività tempdb rispetto al primo approccio perché lo spool della finestra si verifica in memoria :

... RANGE utilizza uno spool su disco, mentre ROWS utilizza uno spool in memoria

Sfortunatamente, il runtime è quasi lo stesso del tuo secondo approccio.

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.

Soluzione basata su schema: totali in esecuzione asincroni

Dato che sei aperto ad altre idee, potresti considerare di aggiornare il "totale parziale" in modo asincrono. È possibile prendere periodicamente i risultati di una di queste query e caricarli in una tabella "totali". Quindi faresti qualcosa del genere:

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

Caricalo ogni giorno / ora / qualunque cosa (ci sono voluti circa 2 secondi sulla mia macchina con file di 1 mm e potrebbe essere ottimizzato):

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;

Quindi la tua query sui rapporti è molto efficiente:

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;

Ecco le statistiche di lettura:

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

Soluzione basata sullo schema: totali in riga con vincoli

Una soluzione davvero interessante a questo è trattata in dettaglio in questa risposta alla domanda: Scrivere un semplice schema bancario: come devo mantenere i miei saldi in sincronia con la loro cronologia delle transazioni?

L'approccio di base sarebbe quello di tenere traccia del totale corrente corrente in fila insieme al totale corrente corrente e al numero progressivo. Quindi è possibile utilizzare i vincoli per convalidare i totali in esecuzione sono sempre corretti e aggiornati.

Ringraziamo Paul White per aver fornito un'implementazione di esempio per lo schema in queste domande e risposte:

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

2

Quando si ha a che fare con un sottoinsieme così piccolo di righe restituite, l'unione triangolare è una buona opzione. Tuttavia, quando si utilizzano le funzioni della finestra, sono disponibili più opzioni che possono aumentarne le prestazioni. L'opzione predefinita per l'opzione finestra è RANGE, ma l'opzione ottimale è ROWS. Essere consapevoli del fatto che la differenza non è solo nelle prestazioni, ma anche nei risultati quando sono coinvolti legami.

Il seguente codice è leggermente più veloce di quelli che hai presentato.

SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM Table_1
ORDER BY seq DESC

Grazie per avermelo detto ROWS. L'ho provato ma non posso dire che sia più veloce della mia seconda query. Il risultato è statoCPU time = 1438 ms, elapsed time = 1537 ms.
user2652379

Ma questo è solo su questa opzione. La tua seconda query non si adatta bene. Prova a restituire più righe e la differenza diventa piuttosto evidente.
Luis Cazares,

Forse al di fuori di t-sql? Posso cambiare schema.
user2652379
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.