Aquí está el resumen: estoy haciendo una consulta de selección. Cada columna de las cláusulas WHERE
y se ORDER BY
encuentra en un único índice no agrupado IX_MachineryId_DateRecorded
, como parte de la clave o como INCLUDE
columnas. Estoy seleccionando todas las columnas, de modo que resulte en una búsqueda de marcadores, pero solo estoy tomando TOP (1)
, por lo que seguramente el servidor puede decir que la búsqueda solo debe hacerse una vez, al final.
Lo más importante, cuando fuerzo la consulta a usar el índice IX_MachineryId_DateRecorded
, se ejecuta en menos de un segundo. Si dejo que el servidor decida qué índice usar, elige IX_MachineryId
, y toma hasta un minuto. Eso realmente me sugiere que hice bien el índice y que el servidor simplemente está tomando una mala decisión. ¿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]);
La tabla está dividida en rangos de mes (aunque todavía no entiendo lo que está pasando allí).
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)
La consulta que normalmente ejecutaría:
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
Plan de consulta: https://www.brentozar.com/pastetheplan/?id=r1c-RpxNx
Plan de consulta con índice forzado: https://www.brentozar.com/pastetheplan/?id=SywwTagVe
Los planes incluidos son los planes de ejecución reales, pero en la base de datos provisional (aproximadamente 1/100 del tamaño de la vida). Dudo en jugar con la base de datos en vivo porque solo comencé en esta empresa hace aproximadamente un mes.
Tengo la sensación de que se debe a la partición, y mi consulta generalmente abarca cada partición (por ejemplo, cuando quiero obtener la primera o la última OperationalSeconds
vez registrada para una máquina). Sin embargo, las consultas que he estado escribiendo a mano se ejecutan entre 10 y 100 veces más rápido de lo que EntityFramework ha generado, por lo que solo voy a hacer un procedimiento almacenado.
fuente
Respuestas:
Ese índice no está particionado, por lo que el optimizador reconoce que puede usarse para proporcionar el orden especificado en la consulta sin ordenar. Como índice no agrupado no exclusivo, también tiene las claves del índice agrupado como subclaves, por lo que el índice se puede utilizar para buscar
MachineryId
y elDateRecorded
rango:El índice no incluye
OperationalSeconds
, por lo que el plan debe buscar ese valor por fila en el índice agrupado (particionado) para probarOperationalSeconds > 0
:El optimizador estima que será necesario leer una fila del índice no agrupado y buscarla para satisfacerla
TOP (1)
. Este cálculo se basa en el objetivo de la fila (encontrar una fila rápidamente) y supone una distribución uniforme de valores.Del plan real, podemos ver que la estimación de 1 fila es inexacta. De hecho, se deben procesar 19.039 filas para descubrir que ninguna fila satisface las condiciones de consulta. Este es el peor de los casos para una optimización de objetivos de fila (1 fila estimada, todas las filas realmente necesarias):
Puede deshabilitar los objetivos de fila con la marca de seguimiento 4138 . Lo más probable es que SQL Server elija un plan diferente, posiblemente el que usted forzó. En cualquier caso, el índice
IX_MachineryId
podría hacerse más óptimo mediante la inclusiónOperationalSeconds
.Es bastante inusual tener índices no agrupados no alineados (índices particionados de una manera diferente de la tabla base, incluido ninguno).
Como de costumbre, el optimizador está seleccionando el plan más barato que considera.
El costo estimado del
IX_MachineryId
plan es de 0.01 unidades de costo, basado en el supuesto (incorrecto) objetivo de la fila de que una fila será probada y devuelta.El costo estimado del
IX_MachineryId_DateRecorded
plan es mucho más alto, con 0.27 unidades, principalmente porque espera leer 5.515 filas del índice, ordenarlas y devolver la que clasifica más bajo (porDateRecorded
):Este índice está particionado y no puede devolver filas en
DateRecorded
orden directamente (ver más adelante). Puede buscarMachineryId
y elDateRecorded
rango dentro de cada partición , pero se requiere una Clasificación:Si este índice no se particionara, no se requeriría una clasificación, y sería muy similar al otro índice (no particionado) con la columna adicional incluida. Un índice filtrado no particionado sería un poco más eficiente aún.
Debe actualizar la consulta de origen para que los tipos de datos de los parámetros
@From
y coincidan con la columna ( ). En este momento, SQL Server está calculando un rango dinámico debido a la falta de coincidencia de tipos en tiempo de ejecución (utilizando el operador Intervalo de combinación y su subárbol):@To
DateRecorded
datetime
Esta conversión evita que el optimizador razone correctamente sobre la relación entre las ID de partición ascendentes (que cubren un rango de
DateRecorded
valores en orden ascendente) y los predicados de desigualdad enDateRecorded
.La ID de partición es una clave inicial implícita para un índice particionado. Normalmente, el optimizador puede ver que ordenar por ID de partición (donde las ID ascendentes se asignan a valores disjuntos ascendentes de
DateRecorded
)DateRecorded
es lo mismo que ordenarDateRecorded
solo (dado queMachineryID
es constante). Esta cadena de razonamiento se rompe por la conversión de tipo.Manifestación
Una tabla e índice particionados simples:
Consulta con tipos coincidentes
Consulta con tipos no coincidentes
fuente
El índice parece bastante bueno para la consulta y no estoy seguro de por qué no lo elige el optimizador (¿estadísticas ?, ¿partición ?, ¿limitación azul ?, no tengo idea realmente).
Pero un índice filtrado sería aún mejor para la consulta específica, si
> 0
es un valor fijo y no cambia de una ejecución de consulta a otra:Hay dos diferencias entre el índice que tiene donde
OperationalSeconds
está la tercera columna y el índice filtrado:Primero, el índice filtrado es más pequeño, tanto en ancho (más estrecho) como en número de filas.
Esto hace que el índice filtrado sea más eficiente en general, ya que SQL Server necesita menos espacio para mantenerlo en la memoria.
Segundo, y esto es más sutil e importante para la consulta es que solo tiene filas que coinciden con el filtro utilizado en la consulta. Esto puede ser extremadamente importante, dependiendo de los valores de esta tercera columna.
Por ejemplo, un conjunto específico de parámetros para
MachineryId
yDateRecorded
puede producir 1000 filas. Si todas o casi todas estas filas coinciden con el(OperationalSeconds > 0)
filtro, ambos índices se comportarán bien. Pero si las filas que coinciden con el filtro son muy pocas (o solo la última o ninguna), el primer índice tendrá que pasar por muchas o todas esas 1000 filas hasta que encuentre una coincidencia. El índice filtrado, por otro lado, solo necesita buscar una fila coincidente (o devolver 0 filas) porque solo se almacenan las filas que coinciden con el filtro.fuente