¿Por qué mi índice no se usa en un SELECT TOP?


15

Aquí está el resumen: estoy haciendo una consulta de selección. Cada columna de las cláusulas WHEREy se ORDER BYencuentra en un único índice no agrupado IX_MachineryId_DateRecorded, como parte de la clave o como INCLUDEcolumnas. 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 OperationalSecondsvez 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.


1
Hola @AndrewWilliamson, podría ser un problema de estadísticas. Si ve el plan real del plan no forzado, el número estimado de filas es 1.22 y el real es 19039. Esto a su vez conduce a la búsqueda de claves que verá más adelante en el plan. ¿Has intentado actualizar las estadísticas? Si no, intente con el escaneo completo en la base de datos provisional.
jesijesi

Respuestas:


21

Si dejo que el servidor decida qué índice usar, elige IX_MachineryId, y toma hasta un minuto.

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 MachineryIdy el DateRecordedrango:

Búsqueda de índice

El índice no incluye OperationalSeconds, por lo que el plan debe buscar ese valor por fila en el índice agrupado (particionado) para probar OperationalSeconds > 0:

Buscar

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):

Actual / estimado

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_MachineryIdpodría hacerse más óptimo mediante la inclusión OperationalSeconds.

Es bastante inusual tener índices no agrupados no alineados (índices particionados de una manera diferente de la tabla base, incluido ninguno).

Eso realmente me sugiere que hice bien el índice y que el servidor simplemente está tomando una mala decisión. ¿Por qué?

Como de costumbre, el optimizador está seleccionando el plan más barato que considera.

El costo estimado del IX_MachineryIdplan 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_DateRecordedplan 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 (por DateRecorded):

Top N Ordenar

Este índice está particionado y no puede devolver filas en DateRecordedorden directamente (ver más adelante). Puede buscar MachineryIdy el DateRecordedrango dentro de cada partición , pero se requiere una Clasificación:

Búsqueda Particionada

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 @Fromy 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):@ToDateRecordeddatetime

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

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 DateRecordedvalores en orden ascendente) y los predicados de desigualdad en DateRecorded.

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) DateRecordedes lo mismo que ordenar DateRecordedsolo (dado que MachineryIDes constante). Esta cadena de razonamiento se rompe por la conversión de tipo.

Manifestación

Una tabla 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 con tipos coincidentes

-- 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;

No busques ningún tipo

Consulta con tipos no coincidentes

-- 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;

Intervalo de fusión y ordenar


5

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 > 0es un valor fijo y no cambia de una ejecución de consulta a otra:

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

Hay dos diferencias entre el índice que tiene donde OperationalSecondsestá 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 MachineryIdy DateRecordedpuede 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.


1
¿Agregar el índice ha hecho que la consulta sea más eficiente?
ypercubeᵀᴹ

No a la base de datos provisional (realmente necesita más datos para probarla correctamente), todavía no la he probado en vivo, los nuevos índices tardan más de una hora en crearse. También dudo mucho en hacer algo a nuestra base de datos en vivo, ya que ya está funcionando lentamente. Necesitamos un mejor sistema para clonar nuestra vida en escena.
Andrew Williamson
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.