É possível aumentar o desempenho da consulta em uma tabela estreita com milhões de linhas?

14

Eu tenho uma consulta que atualmente está levando uma média de 2500ms para ser concluída. Minha mesa é muito estreita, mas existem 44 milhões de linhas. Que opções eu tenho para melhorar o desempenho ou isso é tão bom quanto ele ganha?

A pergunta

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31'; 

A mesa

CREATE TABLE [dbo].[Heartbeats](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [DeviceID] [int] NOT NULL,
    [IsPUp] [bit] NOT NULL,
    [IsWebUp] [bit] NOT NULL,
    [IsPingUp] [bit] NOT NULL,
    [DateEntered] [datetime] NOT NULL,
 CONSTRAINT [PK_Heartbeats] 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]

O índice

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

Adicionar índices adicionais ajudaria? Se sim, como eles seriam? O desempenho atual é aceitável, porque a consulta é executada apenas ocasionalmente, mas estou me perguntando como um exercício de aprendizado, há algo que eu possa fazer para tornar isso mais rápido?

ATUALIZAR

Quando altero a consulta para usar uma dica do índice de força, a consulta é executada em 50ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats] WITH(INDEX(CommonQueryIndex))
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 

A adição de uma cláusula DeviceID corretamente seletiva também atinge o intervalo de 50ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' AND DeviceID = 4;

Se eu adicionar ORDER BY [DateEntered], [DeviceID]à consulta original, estou no intervalo de 50ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];

Todos eles usam o índice que eu esperava (CommonQueryIndex). Portanto, suponho que minha pergunta seja agora: existe uma maneira de forçar esse índice a ser usado em consultas como essa? Ou o tamanho da minha mesa está jogando muito fora do otimizador e devo apenas usar uma ORDER BYou uma dica?

Nate
fonte
Eu acho que você poderia adicionar um índice mais não agrupado em "DateEntered", que iria aumentar o desempenho a mais certa medida
Praveen
@ Praveen Seria basicamente o mesmo que o meu índice existente? Preciso fazer algo especial, pois haverá dois índices no mesmo campo?
Nate
@ Nate, como a tabela é chamada de pulsação e há 44 milhões de registros envolvidos, presumo que você tenha inserções pesadas nessa tabela? Com a indexação, você pode adicionar apenas um índice de cobertura para acelerar. Mas, como você mencionou, você só usa essa consulta ocasionalmente, eu não recomendaria isso se você fizer inserções pesadas. Basicamente, dobra sua carga de pastilha. Você está executando na edição corporativa?
Edward Dortland
Notei que você tem o deviceID no seu índice NC. É possível incluir isso na sua cláusula where? E isso reduziria o conjunto de resultados abaixo do limite? <35k registros (sem a cláusula 1000).
Edward Dortland
1
última pergunta, você está sempre inserindo em ordem de dateEntered? Ou eles podem estar com problemas, pois os dispositivos podem inserir assinaturas uns dos outros. Você pode tentar alterar o índice em cluster para a coluna DateEntered. Suas páginas de licença do índice em cluster agora são 445 páginas. Isso dobraria se você passasse de um int para um datetime. Mas, neste caso, isso pode não ser tão ruim.
Edward Dortland

Respostas:

13

Por que o otimizador não escolhe seu primeiro índice:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

É uma questão de seletividade da coluna [DateEntered].

Você nos disse que sua tabela tem 44 milhões de linhas. o tamanho da linha é:

4 bytes, para o ID, 4 bytes para o ID do dispositivo, 8 bytes para a data e 1 byte para as colunas de 4 bits. isso significa 17 bytes + 7 bytes de sobrecarga para (tags, bitmap nulo, deslocamento de col var e contagem de col) totaliza 24 bytes por linha.

Isso seria traduzido para 140k páginas. Para armazenar esses 44 milhões de linhas.

Agora, o otimizador pode fazer duas coisas:

  1. Ele pode verificar a tabela (verificação de índice em cluster)
  2. Ou poderia usar seu índice. Para cada linha do seu índice, seria necessário fazer uma pesquisa de marcador no índice clusterizado.

Agora, em um determinado momento, fica mais caro fazer todas essas pesquisas únicas no índice de cluster para cada entrada de índice encontrada no seu índice não em cluster. O limite para isso geralmente é a contagem total de pesquisas deve exceder 25% a 33% da contagem total de páginas da tabela.

Portanto, neste caso: 140k / 25% = 35000 linhas 140k / 33% = 46666 linhas.

(@RBarryYoung, 35k é 0,08% do total de linhas e 46666 é 0,10%, então acho que é aí que estava a confusão)

Portanto, se a sua cláusula where resultar em algo entre 35000 e 46666 linhas (isso está abaixo da cláusula superior!) É muito provável que o seu não clusterizado não seja usado e a varredura do índice clusterizado.

As únicas duas maneiras de mudar isso são:

  1. Torne sua cláusula where mais seletiva. (se possível)
  2. Solte o * e selecione apenas algumas colunas para poder usar um índice de cobertura.

Agora, certifique-se de criar um índice de cobertura mesmo quando usar um select *. Qualquer um que apenas crie uma sobrecarga enorme para suas inserções / atualizações / exclusões. Teríamos que saber mais sobre sua carga de trabalho (leitura versus gravação) para garantir que essa seja a melhor solução.

A mudança de datetime para smalldatetime é uma redução de 16% no tamanho no índice em cluster e uma redução de 24% no tamanho no índice não em cluster.

Edward Dortland
fonte
o limite de varredura é normalmente muito menor que esse (10% ou até mais baixo), no entanto, como o intervalo é de um único dia, há mais de um ano, ele não deve atingir esse limite. E uma Varredura de Índice em Cluster não é um dado, pois um índice de cobertura foi adicionado. Como esse índice torna a cláusula WHERE capaz de SARG, ela deve ser preferida.
precisa saber é o seguinte
@RBarryYoung Eu estava tentando explicar por que o índice não clusterizado no [EnteredDate], [DeviceID] não estava sendo usado em primeiro lugar. Em relação ao limiar, acho que ambos concordamos, estou apenas falando da perspectiva da página. Vou alterar minha resposta para deixar mais claro.
Edward Dortland
Alterei a resposta para deixar mais claro o que eu estava respondendo. Não sei explicar por que o índice de cobertura sugerido pelo @RBarryYoung não é usado. Eu testei em um milhão de linhas aqui e o otimizador usando o índice de cobertura.
Edward Dortland
Obrigado por uma resposta muito abrangente, faz muito sentido. Com relação à carga de trabalho, a tabela possui de 150 a 300 inserções por período de 5 minutos e algumas leituras por dia para fins de relatório.
Nate
A sobrecarga do índice de cobertura não é realmente significativa, uma vez que é uma tabela estreita e a "cobertura" é apenas uma adição ao índice preexistente que já incluía a maior parte da linha.
usar o seguinte código
8

Existe um motivo específico para o seu PK estar em cluster? Muitas pessoas fazem isso porque o padrão é dessa maneira, ou acham que as PKs devem ser agrupadas. Não é assim. Os índices agrupados geralmente são melhores para consultas de intervalo (como esta) ou na chave estrangeira de uma tabela filha.

Um efeito de um índice de cluster é que ele agrupa todos os dados porque os dados são armazenados nos nós das folhas da árvore do cluster b. Portanto, supondo que você não esteja solicitando um intervalo 'muito amplo', o otimizador saberá exatamente qual parte da árvore b contém os dados e não precisará encontrar um identificador de linha e, em seguida, pular para onde os dados é (como acontece ao lidar com um índice NC). O que é "muito amplo" de um intervalo? Um exemplo ridículo seria solicitar 11 meses de dados de uma tabela que possui apenas um ano de registros. Obter um dia de dados não deve ser um problema, supondo que suas estatísticas estejam atualizadas. No entanto, o otimizador pode ter problemas se você estiver procurando os dados de ontem e não atualizar as estatísticas por três dias.

Como você está executando uma consulta "SELECT *", o mecanismo precisará retornar todas as colunas da tabela (mesmo se alguém adicionar uma nova que seu aplicativo não precisa naquele momento), para um índice de cobertura ou um índice com colunas incluídas não ajudará muito, se for o caso. (Se você estiver incluindo todas as colunas da tabela em um índice, estará fazendo algo errado.) O otimizador provavelmente ignorará esses índices NC.

Então o que fazer?

Minha sugestão seria descartar o índice NC, alterar a PK clusterizada para não clusterizada e criar um índice clusterizado em [DateEntered]. Mais simples é melhor, até que se prove o contrário.

darin strait
fonte
Supondo que as linhas sejam inseridas em ordem crescente, essa é a resposta mais simples - mas a inserção em ordem não linear causará fragmentação.
precisa saber é o seguinte
Adicionar dados a qualquer estrutura de árvore b fará com que perca o equilíbrio. Mesmo se você estiver adicionando linhas na ordem do cluster, os índices perderão o equilíbrio. A re-indexação de tabelas remove a fragmentação, e qualquer DBA informa que as tabelas precisam ser re-indexadas após a adição de dados "suficientes" a uma tabela. (A definição de "suficiente" pode ser debatida ou "quando" pode ser uma discussão.) Não vejo nada na pergunta que diga que a re-indexação não pode ser feita por algum motivo.
DARIN estreito
4

Desde que você tenha esse "*" lá, a única coisa que eu poderia imaginar que faria muita diferença seria alterar sua definição de índice para isso:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)INCLUDE (ID, IsWebUp, IsPingUp, IsPUp)
 WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

Como observei nos comentários, ele deve usar esse índice, mas, se não, você pode persuadi-lo com uma ORDER BY ou uma dica de índice.

RBarryYoung
fonte
Eu apenas tentei isso e ainda estou no mesmo local, 2500ms aguardam resposta do servidor e 10ms de tempo de processo do cliente.
Nate
Poste o plano de consulta.
precisa saber é o seguinte
Parece que está usando o Índice de Cluster. (Custo SELECT: 0% <- Custo máximo: 20% <- Varredura de índice em cluster PK_Heartbeats Cost: 80%)
Nate
Sim, isso não está certo, algumas coisas acabam com as estatísticas / otimizador. Adicione uma dica para forçá-lo a usar o novo índice.
precisa saber é o seguinte
@ Max Vernon: Talvez, mas isso deveria ter sido sinalizado no plano de consulta.
precisa saber é o seguinte
3

Eu consideraria isso um pouco diferente.

  • Sim, eu sei que é um tópico antigo, mas estou intrigado.

Eu despejaria a coluna datetime - alteraria para int. Tenha uma tabela de pesquisa ou faça uma conversão para a sua data.

Despejar o índice em cluster - deixe-o como um heap e crie um índice não em cluster na nova coluna INT que representa a data. ou seja, hoje seria 20121015. Essa ordem é importante. Dependendo da frequência com que você carrega a tabela, observe a criação desse índice na ordem DESC. O custo de manutenção será maior e você desejará introduzir um fator de preenchimento ou particionamento. O particionamento também ajudaria a diminuir o tempo de execução.

Por fim, se você pode usar o SQL 2012, tente usar SEQUENCE - ele superará a identidade () para inserções.

Jeremy Lowell
fonte
Solução interessante. Embora não seja óbvio da minha pergunta, a parte do tempo do DateTime é muito importante. Geralmente, eu consulta com base na data, para revisar horários específicos durante esse período. Como você ajustaria esta solução para dar conta disso?
Nate
Nesse caso, mantenha a coluna datetime, adicione a coluna int para date (já que seu intervalo é baseado no elemento date e não no elemento time). Você também pode considerar o uso do tipo de dados TIME e, em seguida, dividir efetivamente o horário da data. Dessa maneira, sua área de cobertura de dados é menor e você ainda possui o elemento Time da coluna.
amigos estão
1
Não sei por que perdi isso anteriormente, mas use a compactação de linha no índice clusterizado e no não clusterizado também. Acabei de fazer um teste rápido com sua tabela e eis o que encontrei: Criei um conjunto de dados (5,8 milhões de linhas) na tabela definida acima. Compactei (linha) o índice clusterizado e não clusterizado. as leituras lógicas, com base na sua consulta exata, diminuíram de 2.074 para 1.433. É uma diminuição significativa e estou confiante de que só isso poderia ajudá-lo - e é um risco muito baixo.
Jeremy Lowell