Aqui está o detalhamento: estou fazendo uma consulta de seleção. Cada coluna nas cláusulas WHERE
e ORDER BY
está em um único índice não agrupado em cluster IX_MachineryId_DateRecorded
, como parte da chave ou como INCLUDE
colunas. 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_MachineryId
e 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 OperationalSeconds
já 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.
fonte
Respostas:
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
MachineryId
e oDateRecorded
intervalo:O índice não inclui
OperationalSeconds
, portanto, o plano deve procurar esse valor por linha no índice em cluster (particionado) para testarOperationalSeconds > 0
: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):
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 índice
IX_MachineryId
pode ser otimizado ao incluirOperationalSeconds
.É bastante incomum ter índices não clusterizados não alinhados (índices particionados de uma maneira diferente da tabela base, inclusive nenhuma).
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_DateRecorded
plano é 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 (emDateRecorded
):Esse índice é particionado e não pode retornar linhas em
DateRecorded
ordem diretamente (veja mais adiante). Ele pode procurarMachineryId
e oDateRecorded
intervalo dentro de cada partição , mas uma Classificação é necessária: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
@From
e 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):@To
DateRecorded
datetime
Essa conversão impede que o otimizador raciocine corretamente sobre o relacionamento entre os IDs de partição ascendente (cobrindo um intervalo de
DateRecorded
valores 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 ordenarDateRecorded
sozinha (dado queMachineryID
é constante). Essa cadeia de raciocínio é interrompida pela conversão de tipo.Demo
Uma tabela e índice particionados simples:
Consulta com tipos correspondentes
Consulta com tipos incompatíveis
fonte
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
> 0
for um valor fixo e não mudar de uma execução de consulta para outra:Existem duas diferenças entre o índice que você possui, onde
OperationalSeconds
está 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
MachineryId
eDateRecorded
pode 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.fonte