Atualmente estou projetando uma tabela de transações. Percebi que será necessário calcular os totais em execução para cada linha e isso pode ser lento no desempenho. 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.
Suspeitei TOP
pelo motivo do desempenho lento do plano, então alterei a consulta assim e demorou 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.
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 esquemas.
Apenas para esclarecer, ambas as consultas retornam o mesmo resultado abaixo.
Diferença nas duas primeiras abordagens
O primeiro plano gasta cerca de 7 dos 10 segundos no operador Window Spool, então esta é a principal razão pela qual é tão lento. Ele está executando muita E/S no tempdb para criar isso. Minhas estatísticas de E/S e tempo são assim:
O segundo plano é capaz de evitar o carretel e, portanto, a mesa de trabalho inteiramente. Ele simplesmente pega as 10 primeiras linhas do índice clusterizado e, em seguida, faz uma junção de loops aninhados à agregação (soma) que sai de uma varredura de índice clusterizada separada. O lado interno ainda acaba lendo a tabela inteira, mas a tabela é muito densa, então isso é razoavelmente eficiente com um milhão de linhas.
Melhorando a performance
Armazenamento de colunas
Se você realmente deseja a abordagem de "relatórios on-line", o columnstore provavelmente é sua melhor opção.
Então esta consulta é ridiculamente rápida:
Aqui estão as estatísticas da minha máquina:
Você provavelmente não vai superar isso (a menos que você seja realmente inteligente - bom, Joe). O Columnstore é muito bom em escanear e agregar grandes quantidades de dados.
Usando
ROW
aRANGE
opção de função de janelaVocê pode obter um desempenho muito semelhante à sua segunda consulta com essa abordagem, que foi mencionada em outra resposta e que usei no exemplo columnstore acima ( plano de execução ):
Isso resulta em menos leituras do que sua segunda abordagem e nenhuma atividade tempdb versus sua primeira abordagem porque o spool da janela ocorre na memória :
Infelizmente, o tempo de execução é quase o mesmo que sua segunda abordagem.
Solução baseada em esquema: totais em execução assíncronos
Como você está aberto a outras ideias, considere atualizar o "total em execução" de forma assíncrona. Você pode obter periodicamente os resultados de uma dessas consultas e carregá-los em uma tabela de "totais". Então você faria algo assim:
Carregue-o todos os dias / horas / o que for (isso levou cerca de 2 segundos na minha máquina com linhas de 1 mm e pode ser otimizado):
Então sua consulta de relatórios é muito eficiente:
Aqui estão as estatísticas de leitura:
Solução baseada em esquema: totais em linha com restrições
Uma solução realmente interessante para isso é 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 de execução atual em linha junto com o total de execução 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.
Crédito a Paul White por fornecer uma implementação de amostra para o esquema nesta sessão de perguntas e respostas:
Eu recomendo testar com um pouco mais de dados para ter uma ideia melhor do que está acontecendo e ver como as diferentes abordagens funcionam. 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:
Plano real aqui . A maior parte do tempo é gasto calculando a soma e fazendo a classificação. Preocupantemente, o plano de consulta faz quase todo o trabalho para todo o conjunto de resultados e filtra as 10 linhas que você solicitou no final. O tempo de execução dessa consulta é dimensionado com o tamanho da tabela em vez de com o tamanho do conjunto de resultados.
Esta opção leva 23 segundos na minha máquina:
Plano real aqui . Essa abordagem é dimensionada com o número de linhas solicitadas e o tamanho da tabela. Quase 160 milhões de linhas são lidas da tabela:
Para obter resultados corretos, você deve somar as linhas de toda a tabela. Idealmente, você realizaria essa soma apenas uma vez. É possível fazer isso se você mudar a maneira como aborda o problema. Você pode calcular a soma de toda a tabela e subtrair um total das linhas no conjunto de resultados. Isso permite que você encontre a soma da enésima linha. Uma maneira de fazer isso:
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, em seguida, 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:
Se você quiser 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 clusterizado. 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 visualização indexada como a seguinte:Essa exibição retorna uma única linha, portanto, quase não ocupa espaço. Haverá uma penalidade ao fazer DML, mas não deve ser muito diferente da manutenção do índice. Com a visualização indexada em jogo, a consulta agora leva 0 ms:
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ê obtiver as primeiras 10.000 linhas, a consulta agora leva 18 ms para ser executada.
Código para preencher a tabela:
Ao lidar com um subconjunto tão pequeno de linhas retornadas, a junção triangular é uma boa opção. No entanto, ao usar funções de janela, você tem mais opções que podem aumentar seu desempenho. A opção padrão para a opção de janela é RANGE, mas a opção ideal é ROWS. Esteja ciente de que a diferença não está apenas no desempenho, mas nos resultados também quando há empates.
O código a seguir é um pouco mais rápido do que os que você apresentou.