Pourquoi mon index n'est-il pas utilisé dans un SELECT TOP?

15

Voici le bilan: je fais une requête de sélection. Chaque colonne des clauses WHEREet se ORDER BYtrouve dans un index non cluster unique IX_MachineryId_DateRecorded, soit en tant que partie de la clé, soit en tant que INCLUDEcolonnes. Je sélectionne toutes les colonnes, ce qui entraînera une recherche de signet, mais je ne prends TOP (1), donc le serveur peut sûrement dire que la recherche ne doit être effectuée qu'une seule fois, à la fin.

Plus important encore, lorsque je force la requête à utiliser l'index IX_MachineryId_DateRecorded, elle s'exécute en moins d'une seconde. Si je laisse le serveur décider quel index utiliser, il choisit IX_MachineryIdet cela prend jusqu'à une minute. Cela me suggère vraiment que j'ai bien fait l'index, et le serveur prend juste une mauvaise décision. Pourquoi?

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

Le tableau est divisé en plages de mois (bien que je ne comprenne toujours pas vraiment ce qui se passe là-bas).

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 requête que j'exécuterais normalement:

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 requête: https://www.brentozar.com/pastetheplan/?id=r1c-RpxNx

Plan de requête avec index forcé: https://www.brentozar.com/pastetheplan/?id=SywwTagVe

Les plans inclus sont les plans d'exécution réels, mais sur la base de données de staging (environ 1 / 100e de la taille du live). J'hésite à jouer avec la base de données en direct car je n'ai commencé dans cette entreprise qu'il y a environ un mois.

J'ai l'impression que c'est à cause du partitionnement, et ma requête couvre généralement chaque partition (par exemple lorsque je veux obtenir la première ou la dernière OperationalSecondsjamais enregistrée pour une machine). Cependant, les requêtes que j'ai écrites à la main fonctionnent toutes 10 à 100 fois plus rapidement que ce que EntityFramework a généré, donc je vais simplement créer une procédure stockée.

Andrew Williamson
la source
1
Salut @AndrewWilliamson, Ce pourrait être un problème de statistiques. Si vous voyez le plan réel à partir du plan non forcé, le nombre estimé de lignes est de 1,22 et le réel est de 19039. Cela entraîne à son tour la recherche de clé que vous verrez plus tard dans le plan. avez-vous essayé de mettre à jour les statistiques? Sinon, essayez d'effectuer une analyse complète sur la base de données intermédiaire.
jesijesi

Réponses:

21

Si je laisse le serveur décider quel index utiliser, il choisit IX_MachineryIdet cela prend jusqu'à une minute.

Cet index n'est pas partitionné, de sorte que l'optimiseur reconnaît qu'il peut être utilisé pour fournir l'ordre spécifié dans la requête sans tri. En tant qu'index non cluster non unique, il possède également les clés de l'index cluster comme sous-clés, de sorte que l'index peut être utilisé pour rechercher MachineryIdet la DateRecordedplage:

Recherche d'index

L'index ne comprend pas OperationalSeconds, donc le plan doit rechercher cette valeur par ligne dans l'index clusterisé (partitionné) afin de tester OperationalSeconds > 0:

Chercher

L'optimiseur estime qu'une ligne devra être lue à partir de l'index non cluster et recherchée pour satisfaire le TOP (1). Ce calcul est basé sur l'objectif de ligne (trouver rapidement une ligne) et suppose une distribution uniforme des valeurs.

D'après le plan réel, nous pouvons voir que l'estimation d'une ligne est inexacte. En fait, 19 039 lignes doivent être traitées pour découvrir qu'aucune ligne ne satisfait aux conditions de la requête. C'est le pire des cas pour une optimisation d'objectif de ligne (1 ligne estimée, toutes les lignes réellement nécessaires):

Réel / estimation

Vous pouvez désactiver les objectifs de ligne avec l' indicateur de trace 4138 . Cela entraînerait très probablement que SQL Server choisisse un autre plan, peut-être celui que vous avez forcé. Dans tous les cas, l'indice IX_MachineryIdpourrait être rendu plus optimal en incluant OperationalSeconds.

Il est assez inhabituel d'avoir des index non-cluster non alignés (index partitionnés d'une manière différente de la table de base, y compris pas du tout).

Cela me suggère vraiment que j'ai bien fait l'index, et le serveur prend juste une mauvaise décision. Pourquoi?

Comme d'habitude, l'optimiseur sélectionne le plan le moins cher qu'il considère.

Le coût estimé du IX_MachineryIdplan est de 0,01 unité de coût, sur la base de l'hypothèse (incorrecte) d'objectif de ligne qu'une ligne sera testée et renvoyée.

Le coût estimé du IX_MachineryId_DateRecordedplan est beaucoup plus élevé, à 0,27 unité, principalement parce qu'il s'attend à lire 5 515 lignes de l'index, à les trier et à renvoyer celle qui trie le plus bas (par DateRecorded):

Top N Sort

Cet index est partitionné et ne peut pas retourner DateRecordeddirectement les lignes dans l' ordre (voir plus loin). Il peut rechercher MachineryIdet la DateRecordedplage dans chaque partition , mais un tri est requis:

Recherche partitionnée

Si cet index n'était pas partitionné, un tri ne serait pas requis et il serait très similaire à l'autre index (non partitionné) avec la colonne supplémentaire incluse. Un indice filtré non partitionné serait encore un peu plus efficace.


Vous devez mettre à jour la requête source afin que les types de données des paramètres @Fromet correspondent à la colonne ( ). À l'heure actuelle, SQL Server calcule une plage dynamique en raison de la non-correspondance de type lors de l'exécution (à l'aide de l'opérateur Merge Interval et de son sous-arbre):@ToDateRecordeddatetime

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

Cette conversion empêche l'optimiseur de raisonner correctement sur la relation entre les ID de partition ascendants (couvrant une plage de DateRecordedvaleurs dans l'ordre croissant) et les prédicats d'inégalité DateRecorded.

L'ID de partition est une clé principale implicite pour un index partitionné. Normalement, l'optimiseur peut voir que la commande par ID de partition (où les ID ascendants correspondent aux valeurs croissantes et disjointes de DateRecorded) DateRecordedest alors la même que la commande par DateRecordedseule (étant donné qu'elle MachineryIDest constante). Cette chaîne de raisonnement est rompue par la conversion de type.

Démo

Une table et un index partitionnés 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');

Requête avec types correspondants

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

Ne cherchez pas

Requête avec des types incompatibles

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

Intervalle de fusion et tri

Paul White 9
la source
5

L'index semble assez bon pour la requête et je ne sais pas pourquoi il n'est pas choisi par l'optimiseur (statistiques? Le partitionnement? Limitation azur?, Aucune idée vraiment.)

Mais un index filtré serait encore mieux pour la requête spécifique, si le > 0est une valeur fixe et ne change pas d'une exécution de requête à une autre:

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

Il existe deux différences entre l'index dont vous disposez, où se OperationalSecondstrouve la 3e colonne et l'index filtré:

  • Tout d'abord, l'indice filtré est plus petit, à la fois en largeur (plus étroit) et en nombre de lignes.
    Cela rend l'index filtré plus efficace en général, car SQL Server a besoin de moins d'espace pour le conserver en mémoire.

  • Deuxièmement, ce qui est plus subtil et important pour la requête, c'est qu'il n'a que des lignes qui correspondent au filtre utilisé dans la requête. Cela peut être extrêmement important, selon les valeurs de cette 3e colonne.
    Par exemple, un ensemble spécifique de paramètres pour MachineryIdet DateRecordedpeut produire 1 000 lignes. Si toutes ou presque toutes ces lignes correspondent au (OperationalSeconds > 0)filtre, les deux index se comporteront bien. Mais si les lignes correspondant au filtre sont très peu nombreuses (ou juste la dernière ou aucune), le premier index devra parcourir beaucoup ou toutes ces 1000 lignes jusqu'à ce qu'il trouve une correspondance. En revanche, l'index filtré n'a besoin que d'une seule recherche pour trouver une ligne correspondante (ou pour retourner 0 lignes) car seules les lignes correspondant au filtre sont stockées.

ypercubeᵀᴹ
la source
1
L'ajout de l'index a-t-il rendu la requête plus efficace?
ypercubeᵀᴹ
Pas à la base de données intermédiaire (elle a vraiment besoin de plus de données pour être testée correctement), je ne l'ai pas encore essayé en direct, les nouveaux index mettent plus d'une heure à s'appuyer sur celui-ci. J'hésite également à faire quoi que ce soit à notre base de données en direct, car elle fonctionne déjà lentement. Nous avons besoin d'un meilleur système pour cloner notre vie en scène.
Andrew Williamson