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.
Suspeitei TOP
pelo 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.
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.
sql-server
database-design
t-sql
query-performance
execution-plan
user2652379
fonte
fonte
value
?Respostas:
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:
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:
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:
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:
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:
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: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:
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:
fonte
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:
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.
Melhorando a performance
Columnstore
Se você realmente deseja a abordagem "relatórios on-line", o columnstore provavelmente é sua melhor opção.
Então, essa 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 - legal, Joe). O columnstore é muito bom em digitalizar 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 esta abordagem, mencionada em outra resposta e que usei no exemplo columnstore acima ( plano de execução ):
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 :
Infelizmente, o tempo de execução é praticamente o mesmo que sua segunda abordagem.
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:
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):
Em seguida, 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 é 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:
fonte
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.
fonte
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.