Por que meu índice não está sendo usado em um SELECT TOP?

15

Aqui está o detalhamento: estou fazendo uma consulta de seleção. Cada coluna nas cláusulas WHEREe ORDER BYestá em um único índice não agrupado em cluster IX_MachineryId_DateRecorded, como parte da chave ou como INCLUDEcolunas. Estou selecionando todas as colunas, para que resultem em uma pesquisa de favoritos, mas estou apenas fazendo TOP (1), portanto, com certeza, o servidor pode dizer que a pesquisa precisa ser feita apenas uma vez, no final.

Mais importante, quando forço a consulta a usar o índice IX_MachineryId_DateRecorded, ela é executada em menos de um segundo. Se eu deixar o servidor decidir qual índice usar, ele seleciona IX_MachineryIde leva até um minuto. Isso realmente me sugere que eu corrigi o índice e o servidor está apenas tomando uma má decisão. Por quê?

CREATE TABLE [dbo].[MachineryReading] (
    [Id]                 INT              IDENTITY (1, 1) NOT NULL,
    [Location]           [sys].[geometry] NULL,
    [Latitude]           FLOAT (53)       NOT NULL,
    [Longitude]          FLOAT (53)       NOT NULL,
    [Altitude]           FLOAT (53)       NULL,
    [Odometer]           INT              NULL,
    [Speed]              FLOAT (53)       NULL,
    [BatteryLevel]       INT              NULL,
    [PinFlags]           BIGINT           NOT NULL,
    [DateRecorded]       DATETIME         NOT NULL,
    [DateReceived]       DATETIME         NOT NULL,
    [Satellites]         INT              NOT NULL,
    [HDOP]               FLOAT (53)       NOT NULL,
    [MachineryId]        INT              NOT NULL,
    [TrackerId]          INT              NOT NULL,
    [ReportType]         NVARCHAR (1)     NULL,
    [FixStatus]          INT              DEFAULT ((0)) NOT NULL,
    [AlarmStatus]        INT              DEFAULT ((0)) NOT NULL,
    [OperationalSeconds] INT              DEFAULT ((0)) NOT NULL,
    CONSTRAINT [PK_dbo.MachineryReading] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Machinery_MachineryId] FOREIGN KEY ([MachineryId]) REFERENCES [dbo].[Machinery] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Tracker_TrackerId] FOREIGN KEY ([TrackerId]) REFERENCES [dbo].[Tracker] ([Id]) ON DELETE CASCADE
);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId]
    ON [dbo].[MachineryReading]([MachineryId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_TrackerId]
    ON [dbo].[MachineryReading]([TrackerId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId_DateRecorded]
    ON [dbo].[MachineryReading]([MachineryId] ASC, [DateRecorded] ASC)
    INCLUDE([OperationalSeconds], [FixStatus]);

A tabela é particionada em intervalos de meses (embora eu ainda não entenda o que está acontecendo lá).

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-01-01T00:00:00.000') 

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-02-01T00:00:00.000') 
...

CREATE UNIQUE CLUSTERED INDEX [PK_dbo.MachineryReadingPs] ON MachineryReading(DateRecorded, Id) ON PartitionSchemeMonthRange(DateRecorded)

A consulta que eu normalmente executaria:

SELECT TOP (1) [Id], [Location], [Latitude], [Longitude], [Altitude], [Odometer], [ReportType], [FixStatus], [AlarmStatus], [Speed], [BatteryLevel], [PinFlags], [DateRecorded], [DateReceived], [Satellites], [HDOP], [OperationalSeconds], [MachineryId], [TrackerId]
    FROM [dbo].[MachineryReading]
    --WITH(INDEX(IX_MachineryId_DateRecorded)) --This makes all the difference
    WHERE ([MachineryId] = @p__linq__0) AND ([DateRecorded] >= @p__linq__1) AND ([DateRecorded] < @p__linq__2) AND ([OperationalSeconds] > 0)
    ORDER BY [DateRecorded] ASC

Plano de consulta: https://www.brentozar.com/pastetheplan/?id=r1c-RpxNx

Plano de consulta com índice forçado: https://www.brentozar.com/pastetheplan/?id=SywwTagVe

Os planos incluídos são os planos de execução reais, mas no banco de dados temporário (aproximadamente 1/100 do tamanho da vida útil). Eu hesito em mexer com o banco de dados ativo, porque eu só comecei nesta empresa há cerca de um mês.

Eu sinto que é por causa do particionamento, e minha consulta normalmente abrange todas as partições (por exemplo, quando eu quero obter a primeira ou a última OperationalSecondsjá registrada para uma máquina). No entanto, todas as consultas que escrevi à mão estão sendo executadas 10 a 100 vezes mais rápido do que o EntityFramework gerou, portanto, vou fazer um procedimento armazenado.

Andrew Williamson
fonte
1
Oi @AndrewWilliamson, pode ser um problema de estatísticas. Se você visualizar o plano real a partir do plano não forçado, o número estimado de linhas é 1,22 e o atual é 19039. Isso, por sua vez, leva à pesquisa de chaves que você vê posteriormente no plano. você tentou atualizar as estatísticas? Caso contrário, tente fazer uma varredura completa no banco de dados temporário.
jesijesi

Respostas:

21

Se eu deixar o servidor decidir qual índice usar, ele seleciona IX_MachineryIde leva até um minuto.

Esse índice não está particionado; portanto, o otimizador reconhece que pode ser usado para fornecer a ordem especificada na consulta sem classificação. Como um índice não clusterizado não exclusivo, ele também possui as chaves do índice clusterizado como subchaves, para que o índice possa ser usado para procurar MachineryIde o DateRecordedintervalo:

Busca de índice

O índice não inclui OperationalSeconds , portanto, o plano deve procurar esse valor por linha no índice em cluster (particionado) para testar OperationalSeconds > 0:

Olho para cima

O otimizador estima que uma linha precisará ser lida no índice não clusterizado e procurada para satisfazer o TOP (1). Esse cálculo é baseado no objetivo da linha (encontre uma linha rapidamente) e assume uma distribuição uniforme de valores.

No plano real, podemos ver que a estimativa de 1 linha é imprecisa. De fato, 19.039 linhas precisam ser processadas para descobrir que nenhuma linha atende às condições da consulta. Esse é o pior caso para uma otimização de meta de linha (1 linha estimada, todas as linhas realmente necessárias):

Real / estimativa

Você pode desativar as metas de linha com o sinalizador de rastreamento 4138 . Isso provavelmente resultaria no SQL Server escolhendo um plano diferente, possivelmente o que você forçou. De qualquer forma, o índiceIX_MachineryId pode ser otimizado ao incluir OperationalSeconds.

É bastante incomum ter índices não clusterizados não alinhados (índices particionados de uma maneira diferente da tabela base, inclusive nenhuma).

Isso realmente me sugere que eu corrigi o índice e o servidor está apenas tomando uma má decisão. Por quê?

Como de costume, o otimizador está selecionando o plano mais barato que considera.

O custo estimado da IX_MachineryId plano é de 0,01 unidades de custo, com base na suposição de meta de linha (incorreta) de que uma linha será testada e retornada.

O custo estimado do IX_MachineryId_DateRecordedplano é muito mais alto, em 0,27 unidades, principalmente porque ele espera ler 5.515 linhas do índice, classificá-las e retornar a que classifica mais baixa (em DateRecorded):

Top N Sort

Esse índice é particionado e não pode retornar linhas em DateRecordedordem diretamente (veja mais adiante). Ele pode procurar MachineryIde o DateRecordedintervalo dentro de cada partição , mas uma Classificação é necessária:

Procura Particionada

Se esse índice não fosse particionado, uma classificação não seria necessária e seria muito semelhante ao outro índice (não particionado) com a coluna extra incluída. Um índice filtrado não particionado ainda seria um pouco mais eficiente.


Você deve atualizar a consulta de origem para que os tipos de dados dos parâmetros @Frome correspondam à coluna ( ). No momento, o SQL Server está computando um intervalo dinâmico devido à incompatibilidade de tipos no tempo de execução (usando o operador Merge Interval e sua subárvore):@ToDateRecordeddatetime

<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@From],NULL,(22))">
<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@To],NULL,(22))">

Essa conversão impede que o otimizador raciocine corretamente sobre o relacionamento entre os IDs de partição ascendente (cobrindo um intervalo de DateRecordedvalores em ordem crescente) e os predicados de desigualdade emDateRecorded .

O ID da partição é uma chave inicial implícita para um índice particionado. Normalmente, o otimizador pode ver que a ordenação por ID da partição (onde os IDs ascendentes são mapeados para valores ascendentes e disjuntos de DateRecorded) DateRecordedé o mesmo que ordenar DateRecordedsozinha (dado queMachineryID é constante). Essa cadeia de raciocínio é interrompida pela conversão de tipo.

Demo

Uma tabela e índice particionados simples:

CREATE PARTITION FUNCTION PF (datetime)
AS RANGE LEFT FOR VALUES ('20160101', '20160201', '20160301');

CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY]);

CREATE TABLE dbo.T (c1 integer NOT NULL, c2 datetime NOT NULL) ON PS (c2);

CREATE INDEX i ON dbo.T (c1, c2) ON PS (c2);

INSERT dbo.T (c1, c2) 
VALUES (1, '20160101'), (1, '20160201'), (1, '20160301');

Consulta com tipos correspondentes

-- Types match (datetime)
DECLARE 
    @From datetime = '20010101',
    @To datetime = '20090101';

-- Seek with no sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

Não procure nenhum tipo

Consulta com tipos incompatíveis

-- Mismatched types (datetime2 vs datetime)
DECLARE 
    @From datetime2 = '20010101',
    @To datetime2 = '20090101';

-- Merge Interval and Sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

Mesclar intervalo e classificação

Paul White 9
fonte
5

O índice parece bastante bom para a consulta e não sei por que não foi escolhido pelo otimizador (estatísticas? O particionamento? Limitação do azul ?, nenhuma ideia realmente).

Mas um índice filtrado seria ainda melhor para a consulta específica, se esse > 0for um valor fixo e não mudar de uma execução de consulta para outra:

CREATE NONCLUSTERED INDEX IX_MachineryId_DateRecorded_filtered
    ON dbo.MachineryReading
        (MachineryId, DateRecorded) 
    WHERE (OperationalSeconds > 0) ;

Existem duas diferenças entre o índice que você possui, onde OperationalSecondsestá a terceira coluna e o índice filtrado:

  • Primeiro, o índice filtrado é menor, tanto em largura (mais estreito) quanto em número de linhas.
    Isso torna o índice filtrado mais eficiente em geral, pois o SQL Server precisa de menos espaço para mantê-lo na memória.

  • Segundo, e isso é mais sutil e importante para a consulta: ela possui apenas linhas que correspondem ao filtro usado na consulta. Isso pode ser extremamente importante, dependendo dos valores desta terceira coluna.
    Por exemplo, um conjunto específico de parâmetros para MachineryIde DateRecordedpode gerar 1000 linhas. Se todas ou quase todas essas linhas corresponderem ao (OperationalSeconds > 0)filtro, os dois índices se comportarão bem. Mas se as linhas correspondentes ao filtro forem muito poucas (ou apenas a última ou nenhuma), o primeiro índice precisará passar por muitas ou todas essas 1000 linhas até encontrar uma correspondência. O índice filtrado, por outro lado, precisa apenas de uma busca para encontrar uma linha correspondente (ou retornar 0 linhas) porque apenas as linhas correspondentes ao filtro são armazenadas.

ypercubeᵀᴹ
fonte
1
A adição do índice tornou a consulta mais eficiente?
ypercubeᵀᴹ
Não no banco de dados de teste (ele realmente precisa de mais dados para testar adequadamente), ainda não o testei ao vivo, novos índices levam mais de uma hora para serem desenvolvidos com base nesse. Também estou bastante hesitante em fazer qualquer coisa em nosso banco de dados ativo, pois ele já está sendo executado lentamente. Precisamos de um sistema melhor para clonar nossa vida na montagem.
Andrew Williamson