Dados ordenados inerentemente como se fosse um índice em cluster

8

Eu tenho a seguinte tabela com 7,5 milhões de registros:

CREATE TABLE [dbo].[TestTable](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [TestCol] [nvarchar](50) NOT NULL,
    [TestCol2] [nvarchar](50) NOT NULL,
    [TestCol3] [nvarchar](50) NOT NULL,
    [Anonymised] [tinyint] NOT NULL,
    [Date] [datetime] NOT NULL,
CONSTRAINT [PK_TestTable] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Percebo que quando há um índice não agrupado no campo de data:

CREATE NONCLUSTERED INDEX IX_TestTable_Date ON [dbo].[TestTable] ([Date])

e eu executo a seguinte consulta:

UPDATE TestTable 
SET TestCol='*GDPR*', TestCol2='*GDPR*', TestCol3='*GDPR*', Anonymised=1
WHERE [Date] <= '25 August 2016'

- os dados retornados pela operação de acesso ao índice são classificados para corresponder à ordem das chaves do PK / CX, reduzindo o desempenho.

Plano de consulta

Fiquei surpreso ao descobrir que a remoção do índice do campo de data realmente melhora o desempenho da consulta em cerca de 30% porque ela não executa mais a classificação:

Plano de consulta

Minha teoria, e isso pode ser óbvio para os mais experientes entre vocês, é que descobriu que a coluna da data está implicitamente ordenada exatamente da mesma forma que a chave primária / índice clusterizado.

Portanto, minha pergunta é: É possível aproveitar esse fato para melhorar o desempenho da minha consulta?

AproposArmadillo
fonte
11
Não examinei os planos, mas suspeitei que o desempenho (bem, duração, nenhum desses valores estimados de% de custo inútil) melhorou porque não era mais necessário atualizar o índice excluído, não por causa de uma operação de classificação.
Aaron Bertrand
@AaronBertrand Posso estar lendo isso incorretamente, por isso, corrija-me se estiver errado, mas parece haver uma operação de atualização de índice nos dois planos de consulta. Você está se referindo a outra coisa?
AproposArmadillo
11
Mais uma vez, eu disse, não olhei para os planos. Você disse que "remover o índice do campo de data melhora o desempenho da consulta" ... se você removeu o índice, ele não deve aparecer no plano, então talvez você tenha coletado o plano errado ou realmente não removido o índice que você pensou que fez. E mais uma vez, uma porcentagem estimada para um plano é um indicador, mas na verdade não reflete a verdadeira medição de desempenho de forma alguma. É uma estimativa calculada antes da consulta ser executada.
Aaron Bertrand
@ Aaron Bertrand, não precisou atualizar o índice, porque [Date] não estava entre os campos atualizados.
Denis Rubashkin
11
@ Shaffanhoon Você já tentou recriar o índice, [Date]mas em DESCordem? Apenas curioso, já que o predicado é <=. Além disso, se o índice ativado Date(por padrão, ACSordem) ajudar outras consultas, talvez você possa tentar adicionar uma dica de tabela ao UPDATE para forçá-lo a usar o PK? Ou, talvez, divida isso em duas partes: crie uma tabela temporária, preencha com [Id]base em [Date] <= '25 August 2016'e remova o WHEREda UPDATE e adicione FROM dbo.TestTable tt INNER JOIN #tmp ids ON ids.[Id] = tt.[Id]. É uma atualização, afinal, e precisa encontrar as linhas reais, índice ou não.
Solomon Rutzky

Respostas:

7

Simulei dados de teste que reproduzem principalmente o seu problema:

INSERT INTO [dbo].[TestTable] WITH (TABLOCK)
SELECT TOP (7000000) N'*NOT GDPR*', N'*NOT GDPR*', N'*NOT GDPR*', 0, DATEADD(DAY, q.RN  / 16965, '20160801')
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) q
ORDER BY q.RN
OPTION (MAXDOP 1);


DROP INDEX IF EXISTS [dbo].[TestTable].IX_TestTable_Date;
CREATE NONCLUSTERED INDEX IX_TestTable_Date ON [dbo].[TestTable] ([Date]);

Estatísticas para a consulta que usa o índice não clusterizado:

Tabela 'TestTable'. Contagem de varredura 1, leituras lógicas 1299838, leituras físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras físicas lob 0, leituras físicas lob 0, leituras antecipadas lob.

Tempos de execução do SQL Server: tempo de CPU = 984 ms, tempo decorrido = 988 ms.

Estatísticas para a consulta que usa o índice em cluster:

Tabela 'TestTable'. Contagem de varredura 1, leituras lógicas 72609, leituras físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras físicas lob 0, leituras físicas lob 0, leituras antecipadas lob.

Tempos de execução do SQL Server: tempo de CPU = 781 ms, tempo decorrido = 772 ms.

Chegando à sua pergunta:

É possível aproveitar esse fato para melhorar o desempenho da minha consulta?

Sim. Você pode usar o índice não clusterizado que você já precisa para encontrar com eficiência o idvalor máximo que precisa ser atualizado. Se você salvar isso em uma variável e filtrar, obterá um plano de consulta para a atualização que faz a verificação do índice em cluster (sem a classificação) que para mais cedo e, portanto, faz menos IO. Aqui está uma implementação:

DECLARE @Id INT;

SELECT TOP (1) @Id = Id
FROM dbo.TestTable 
WHERE [Date] <= '25 August 2016'
ORDER BY [Date] DESC, Id DESC;

UPDATE TestTable 
SET TestCol='*GDPR*', TestCol2='*GDPR*', TestCol3='*GDPR*', Anonymised=1
WHERE [Id] < @Id AND [Date] <= '25 August 2016'
AND [Anonymised] <> 1 -- optional
OPTION (MAXDOP 1);

Execute estatísticas para a nova consulta:

Tabela 'TestTable'. Contagem de varreduras 1, leituras lógicas 3, leituras físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob.

Tabela 'TestTable'. Contagem de varredura 1, leituras lógicas 4776, leituras físicas 0, leituras de pré-leitura 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de lob 0.

Tempos de execução do SQL Server: tempo de CPU = 515 ms, tempo decorrido = 510 ms.

Bem como o plano de consulta:

ok plano de consulta

Com tudo isso dito, seu desejo de acelerar a consulta sugere que você planeja executar a consulta mais de uma vez. No momento, sua consulta tem um filtro aberto na datecoluna. É realmente necessário anonimizar as linhas mais de uma vez? Você pode evitar atualizar ou varrer linhas que já foram anonimizadas? Certamente deve ser mais rápido atualizar um intervalo de datas com datas dos dois lados. Você também pode adicionar a Anonymisedcoluna ao seu índice, mas esse índice precisará ser atualizado durante sua UPDATEconsulta. Em resumo, evite processar os mesmos dados repetidamente, se puder.

A consulta original que você possui com a classificação é mais lenta devido ao trabalho realizado no Clustered Index Updateoperador. A quantidade de tempo gasto no índice procura e a classificação é de apenas 407 ms. Você pode ver isso no plano real. O plano é executado no modo de linha, para que o tempo gasto na classificação seja o tempo desse operador junto com todos os operadores filhos:

insira a descrição da imagem aqui

Isso deixa o operador de classificação com cerca de 1600 ms de tempo. O SQL Server precisa ler páginas do índice clusterizado para executar a atualização. Você pode ver que o Clustered Index Updateoperador faz 1205921 leituras lógicas. Você pode ler mais sobre a classificação de otimizações para DML e a pré-busca otimizada nesta postagem de blog de Paul White .

O outro plano de consulta que você possui (sem a classificação) leva 683 ms para a verificação de índice em cluster e cerca de 550 ms para o Clustered Index Updateoperador. O operador de atualização não faz nenhum pedido de entrada / saída para esta consulta.

A resposta simples para o motivo pelo qual o plano com a classificação é mais lento é que o SQL Server faz leituras mais lógicas no índice de cluster para esse plano em comparação com o plano de verificação de índice em cluster. Mesmo se todos os dados necessários estiverem na memória, ainda haverá um custo adicional para fazer essas leituras lógicas. É muito mais difícil obter uma resposta melhor, pois, até onde sei, os planos não fornecerão mais detalhes. É possível usar o PerfView ou outra ferramenta baseada no rastreamento ETW para comparar pilhas de chamadas entre as consultas:

insira a descrição da imagem aqui

À esquerda, a consulta que faz a verificação do índice em cluster e, à direita, a consulta que faz a classificação. Marquei pilhas de chamadas em azul ou vermelho que aparecem apenas em uma consulta. Não é de surpreender que as diferentes pilhas de chamadas com um alto número de ciclos de CPU amostrados para a consulta de classificação pareçam ter a ver com as leituras lógicas necessárias para executar a atualização no índice em cluster. Além disso, existem diferenças no número de ciclos amostrados entre as consultas para a mesma operação. Por exemplo, a consulta com a classificação gasta 31 ciclos adquirindo travas, enquanto a consulta com a varredura gasta apenas 9 ciclos adquirindo travas.

Suspeito que o SQL Server esteja escolhendo o plano mais lento devido a uma limitação de custo do operador do plano de consulta. Talvez parte da diferença no tempo de execução seja devido ao hardware ou à sua edição do SQL Server. De qualquer forma, o SQL Server não é capaz de descobrir que a coluna da data está implicitamente ordenada exatamente da mesma forma que o índice clusterizado. Os dados são retornados da varredura de índice em cluster na ordem das chaves em cluster, portanto, não há necessidade de executar uma classificação na tentativa de otimizar a E / S ao fazer a atualização do índice em cluster.

Joe Obbish
fonte