Voici le bilan: je fais une requête de sélection. Chaque colonne des clauses WHERE
et se ORDER BY
trouve dans un index non cluster unique IX_MachineryId_DateRecorded
, soit en tant que partie de la clé, soit en tant que INCLUDE
colonnes. 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_MachineryId
et 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 OperationalSeconds
jamais 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.
la source
Réponses:
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
MachineryId
et laDateRecorded
plage:L'index ne comprend pas
OperationalSeconds
, donc le plan doit rechercher cette valeur par ligne dans l'index clusterisé (partitionné) afin de testerOperationalSeconds > 0
: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):
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_MachineryId
pourrait être rendu plus optimal en incluantOperationalSeconds
.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).
Comme d'habitude, l'optimiseur sélectionne le plan le moins cher qu'il considère.
Le coût estimé du
IX_MachineryId
plan 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_DateRecorded
plan 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 (parDateRecorded
):Cet index est partitionné et ne peut pas retourner
DateRecorded
directement les lignes dans l' ordre (voir plus loin). Il peut rechercherMachineryId
et laDateRecorded
plage dans chaque partition , mais un tri est requis: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
@From
et 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):@To
DateRecorded
datetime
Cette conversion empêche l'optimiseur de raisonner correctement sur la relation entre les ID de partition ascendants (couvrant une plage de
DateRecorded
valeurs 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
)DateRecorded
est alors la même que la commande parDateRecorded
seule (étant donné qu'elleMachineryID
est constante). Cette chaîne de raisonnement est rompue par la conversion de type.Démo
Une table et un index partitionnés simples:
Requête avec types correspondants
Requête avec des types incompatibles
la source
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
> 0
est une valeur fixe et ne change pas d'une exécution de requête à une autre:Il existe deux différences entre l'index dont vous disposez, où se
OperationalSeconds
trouve 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
MachineryId
etDateRecorded
peut 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.la source