Como posso executar totais de linhas recentes mais rapidamente?


8

Atualmente, estou projetando uma tabela de transações. Percebi que o cálculo dos totais em execução para cada linha será necessário e isso pode ter um desempenho lento. Então, criei uma tabela com 1 milhão de linhas para fins de teste.

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 tentei obter 10 linhas recentes e seus totais em execução, mas demorou cerca de 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.

1ª tentativa de plano de execução

Suspeitei TOPpelo motivo de desempenho lento do plano, então mudei a consulta assim e levou cerca de 1 a 2 segundos. Mas acho que isso ainda é lento para a produção e me pergunto se isso pode ser melhorado ainda mais.

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

Plano de execução da segunda tentativa

Minhas perguntas são:

  • Por que a consulta da 1ª tentativa é mais lenta que a 2ª?
  • Como posso melhorar ainda mais o desempenho? Eu também posso mudar de esquema.

Só para ficar claro, as duas consultas retornam o mesmo resultado abaixo.

resultados


11
Normalmente não uso funções de janela, mas lembro que li alguns artigos úteis sobre elas. Dê uma olhada em uma Introdução às funções da janela T-SQL , especialmente na parte Melhoramentos agregados da janela em 2012 . Talvez lhe dê algumas respostas. ... e mais um artigo do mesmo autor excelente funções da janela T-SQL e Desempenho
Denis Rubashkin

Você já tentou colocar um índice value?
Jacob H

Respostas:


5

Eu recomendo testar com um pouco mais de dados para ter uma idéia melhor do que está acontecendo e para ver o desempenho de diferentes abordagens. Carreguei 16 milhões de linhas em uma tabela com a mesma estrutura. Você pode encontrar o código para preencher a tabela na parte inferior desta resposta.

A abordagem a seguir leva 19 segundos na minha 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;

Plano real aqui . A maior parte do tempo é gasta calculando a soma e fazendo a classificação. O preocupante é que o plano de consulta faz quase todo o trabalho para todo o conjunto de resultados e filtra as 10 linhas solicitadas no final. O tempo de execução desta consulta é escalonado com o tamanho da tabela, e não com o tamanho do conjunto de resultados.

Esta opção leva 23 segundos na minha 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;

Plano real aqui . Essa abordagem é escalada com o número de linhas solicitadas e o tamanho da tabela. Quase 160 milhões de linhas são lidas da tabela:

Olá

Para obter resultados corretos, você deve somar linhas para a tabela inteira. Idealmente, você executaria esse somatório apenas uma vez. É possível fazer isso se você mudar a maneira como aborda o problema. Você pode calcular a soma da tabela inteira e subtrair um total corrente das linhas no conjunto de resultados. Isso permite que você encontre a soma da enésima linha. Uma maneira de fazer isso:

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;

Plano real aqui . A nova consulta é executada em 644 ms na minha máquina. A tabela é varrida uma vez para obter o total completo, e uma linha adicional é lida para cada linha no conjunto de resultados. Não há classificação e quase todo o tempo é gasto calculando a soma na parte paralela do plano:

muito bom

Se você deseja que essa consulta seja ainda mais rápida, basta otimizar a parte que calcula a soma completa. A consulta acima faz uma verificação de índice em cluster. O índice clusterizado inclui todas as colunas, mas você só precisa da [value]coluna. Uma opção é criar um índice não clusterizado nessa coluna. Outra opção é criar um índice columnstore não clusterizado nessa coluna. Ambos irão melhorar o desempenho. Se você estiver no Enterprise, uma ótima opção é criar uma exibição indexada como a seguinte:

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

Essa exibição retorna uma única linha, portanto, ocupa quase nenhum espaço. Haverá uma penalidade ao fazer o DML, mas não deve ser muito diferente da manutenção do índice. Com a exibição indexada em reprodução, a consulta agora leva 0 ms:

insira a descrição da imagem aqui

Plano real aqui . A melhor parte dessa abordagem é que o tempo de execução não é alterado pelo tamanho da tabela. A única coisa que importa é quantas linhas são retornadas. Por exemplo, se você receber as primeiras 10000 linhas, a consulta agora leva 18 ms para ser executada.

Código para preencher a tabela:

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

Diferença nas duas primeiras abordagens

O primeiro plano gasta cerca de 7 dos 10 segundos no operador de spool de janelas, portanto, esse é o principal motivo pelo qual é tão lento. Ele está executando muitas E / S no tempdb para criar isso. Minhas estatísticas de E / S e tempo são assim:

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.

O segundo plano é capaz de evitar o carretel e, portanto, a mesa de trabalho inteiramente. Ele simplesmente captura as 10 principais linhas do índice em cluster e, em seguida, um loop aninhado se junta à agregação (soma) resultante de uma verificação de índice em cluster separada. O lado interno ainda acaba lendo a tabela inteira, mas a tabela é muito densa, portanto é razoavelmente eficiente com um milhão de linhas.

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

Melhorando a performance

Columnstore

Se você realmente deseja a abordagem "relatórios on-line", o columnstore provavelmente é sua melhor opção.

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

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

Então, essa consulta é ridiculamente rápida:

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

Aqui estão as estatísticas da minha 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.

Você provavelmente não vai superar isso (a menos que você seja realmente inteligente - legal, Joe). O columnstore é muito bom em digitalizar e agregar grandes quantidades de dados.

Usando ROWa RANGEopção de função de janela

Você pode obter um desempenho muito semelhante à sua segunda consulta com esta abordagem, mencionada em outra resposta e que usei no exemplo columnstore acima ( plano de execução ):

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

Isso resulta em menos leituras que sua segunda abordagem e nenhuma atividade tempdb em relação à sua primeira abordagem porque o spool da janela ocorre na memória :

... O RANGE usa um carretel no disco, enquanto o ROWS usa um carretel na memória

Infelizmente, o tempo de execução é praticamente o mesmo que sua segunda abordagem.

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.

Solução baseada em esquema: totais assíncronos em execução

Como você está aberto a outras idéias, considere atualizar o "total total" de forma assíncrona. Periodicamente, você pode obter os resultados de uma dessas consultas e carregá-lo em uma tabela "totais". Então, você faria algo assim:

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

Carregue-o todos os dias / hora / o que for (isso levou cerca de 2 segundos na minha máquina com linhas de 1 mm e pode ser otimizado):

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;

Em seguida, sua consulta de relatórios é muito 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;

Aqui estão as estatísticas de leitura:

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

Solução baseada em esquema: totais em linha com restrições

Uma solução realmente interessante é abordada em detalhes nesta resposta à pergunta: Escrevendo um esquema bancário simples: Como devo manter meus saldos sincronizados com o histórico de transações?

A abordagem básica seria rastrear o total atual de corrida em linha, juntamente com o total anterior e o número de sequência. Em seguida, você pode usar restrições para validar que os totais em execução estejam sempre corretos e atualizados.

Agradecemos a Paul White por fornecer uma implementação de amostra para o esquema nestas Perguntas e Respostas:

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

Ao lidar com um pequeno subconjunto de linhas retornado, a junção triangular é uma boa opção. No entanto, ao usar as funções da janela, você tem mais opções que podem aumentar seu desempenho. A opção padrão para a janela é RANGE, mas a opção ideal é ROWS. Esteja ciente de que a diferença não está apenas no desempenho, mas também nos resultados quando há vínculos.

O código a seguir é um pouco mais rápido que o que você apresentou.

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

Obrigado por contar ROWS. Eu tentei, mas não posso dizer que é mais rápido que a minha segunda consulta. O resultado foiCPU time = 1438 ms, elapsed time = 1537 ms.
user2652379 03/04/19

Mas isso é apenas nesta opção. Sua segunda consulta não é bem dimensionada. Tente retornar mais linhas e a diferença se torna bastante evidente.
Luis Cazares

Talvez fora do t-sql? Eu posso mudar o esquema.
precisa saber é o seguinte
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.