100% CPU avec un mauvais plan d'exécution

8

J'ai un problème énorme avec des pics de processeur à 100% en raison d'un mauvais plan d'exécution utilisé par une requête spécifique. Je passe des semaines à résoudre par moi-même.

Ma base de données

Mon exemple de base de données contient 3 tableaux simplifiés.

[Enregistreur de données]

CREATE TABLE [model].[DataLogger](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [ProjectID] [bigint] NULL,
CONSTRAINT [PK_DataLogger] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

[Onduleur]

CREATE TABLE [model].[Inverter](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [SerialNumber] [nvarchar](50) NOT NULL,
 CONSTRAINT [PK_Inverter] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY],
 CONSTRAINT [UK_Inverter] UNIQUE NONCLUSTERED 
(
    [DataLoggerID] ASC,
    [SerialNumber] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [model].[Inverter] WITH CHECK
ADD CONSTRAINT [FK_Inverter_DataLogger]
FOREIGN KEY([DataLoggerID])
REFERENCES [model].[DataLogger] ([ID])

[InverterData]

CREATE TABLE [data].[InverterData](
    [InverterID] [bigint] NOT NULL,
    [Timestamp] [datetime] NOT NULL,
    [DayYield] [decimal](18, 2) NULL,
 CONSTRAINT [PK_InverterData] PRIMARY KEY CLUSTERED 
(
    [InverterID] ASC,
    [Timestamp] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF)
)

Statistiques et maintenance

Le [InverterData]tableau contient plusieurs millions de lignes (diffère dans plusieurs instances PaaS) partitionnées en jonques mensuelles.

Tous les indexeurs sont défragmentés et toutes les statistiques reconstruites / réorganisées selon les besoins sur un tour quotidien / hebdomadaire.

Ma requête

La requête est générée par Entity Framework et également simple. Mais je cours 1 000 fois par minute et les performances sont essentielles.

SELECT
[Extent1].[InverterID] AS [InverterID],
[Extent1].[DayYield] AS [DayYield]
FROM [data].[InverterDayData] AS [Extent1]
INNER JOIN [model].[Inverter] AS [Extent2] ON [Extent1].[InverterID] = [Extent2].[ID]
INNER JOIN [model].[DataLogger] AS [Extent3] ON [Extent2].[DataLoggerID] = [Extent3].[ID]
WHERE ([Extent3].[ProjectID] = @p__linq__0)
AND ([Extent1].[Date] = @p__linq__1) OPTION (MAXDOP 1)

L' MAXDOP 1astuce est pour un autre problème avec un plan parallèle parallèle.

Le "bon" plan

Au cours des 90% du temps, le plan utilisé est rapide comme l'éclair et ressemble à ceci:

plan rapide

Le problème

Au cours de la journée, le bon plan est devenu au hasard un plan mauvais et lent.

Le "mauvais" plan est utilisé pendant 10 à 60 minutes, puis changé pour le "bon" plan. Le "mauvais" plan pique le CPU jusqu'à 100% permanent.

Voici à quoi cela ressemble:

plan lent

Ce que j'essaie jusqu'à présent

Ma première pensée a été Hash Matchle mauvais garçon. J'ai donc modifié la requête avec un nouvel indice.

...Extent1].[Date] = @p__linq__1) OPTION (MAXDOP 1, LOOP JOIN)

Le LOOP JOINdevrait forcer à utiliser l' Nested Loopinstant de Hash Match.

Le résultat est que le plan à 90% ressemble à avant. Mais le plan est également devenu aléatoire.

Le "mauvais" plan ressemble maintenant à ceci (l'ordre des boucles de table a changé):

plan aussi lent

Le CPU jette également un œil à 100% lors du "nouveau mauvais" plan.

Solution?

Il me vient à l'esprit de forcer le «bon» plan. Mais je ne sais pas si c'est une bonne idée.

À l'intérieur du plan se trouve un index recommandé qui inclut toutes les colonnes. Mais cela doublera le tableau complet et ralentira les inserts qui sont très fréquents.

Aidez-moi, s'il vous plaît!


Mise à jour 1 - liée au commentaire @James

Voici les deux plans (certains champs supplémentaires affichés dans le plan, car ils proviennent de la table réelle):

Bon plan

Mauvais plan 1 (Hash Match)

Mauvais plan 2 (boucle imbriquée)

Mise à jour 2 - liée à @David Fowler répondeur

Le mauvais plan intervient sur une valeur de paramètre aléatoire. Donc normalement je @p__linq__1 ='2016-11-26 00:00:00.0000000' @p__linq__0 =20825le jour du trou et que le mauvais plan arrive sur la même valeur.

Je connais le problème de reniflement des paramètres des procédures stockées et comment les éviter dans SP. Avez-vous une astuce pour moi comment éviter ce problème pour ma requête?

La création de l'index recommandé inclura toutes les colonnes. Cela doublera le tableau complet et ralentira les inserts, qui sont très fréquents. Cela ne «semble» pas juste de construire un index qui clone simplement la table. Je veux aussi doubler la taille des données de cette grande table.

Mise à jour 3 - liée au commentaire de @David Fowler

Cela n'a pas fonctionné non plus et je pense que non. Pour une meilleure compréhension, je vais vous expliquer comment la requête est appelée.

Supposons que j'ai 3 entités dans le [DataLogger]tableau. Au cours de la journée, j'appelle encore et encore les 3 mêmes requêtes dans un aller-retour:

Requête de base: ...WHERE ([Extent3].[ProjectID] = @p__linq__0) AND ([Extent1].[Date] = @p__linq__1)

Paramètre:

  1. @p__linq__0 = 1; @p__linq__1 = '2018-01-05 00:00:00.0000000'
  2. @p__linq__0 = 2; @p__linq__1 = '2018-01-05 00:00:00.0000000'
  3. @p__linq__0 = 3; @p__linq__1 = '2018-01-05 00:00:00.0000000'

Le paramètre @p__linq__1est toujours la même date. Mais il choisit le mauvais plan au hasard sur une requête qui s'exécute toutes les fois avec un bon plan avant. Avec le même paramètre!

Mise à jour 4 - liée au commentaire @Nic

L'entretien fonctionne tous les soirs et ressemble à ceci.

Indice

Si un indice est fragmenté à plus de 5%, il est réorganisé ...

ALTER INDEX [{index}] ON [{table}] REORGANIZE

Si un indice est fragmenté à plus de 30%, il est reconstruit ...

ALTER INDEX [{index}] ON [{table}] REBUILD WITH (ONLINE=ON, MAXDOP=1)

Si l'index est partitionné, il sera protégé contre la fragmentation et modifié par partition ...

ALTER INDEX [{index}] ON [{table}] REBUILD PARTITION = {partitionNr} WITH (ONLINE=ON, MAXDOP=1)

Statistiques

Toutes les statistiques seront mises à jour si elles modification_countersont supérieures à 0 ...

UPDATE STATISTICS [{schema}].[{object}] ([{stats}]) WITH FULLSCAN

ou sur partitionné ..

UPDATE STATISTICS [{schema}].[{object}] ([{stats}]) WITH RESAMPLE ON PARTITIONS({partitionNr})

La maintenance inclut toutes les statistiques, également celle générée automatiquement.

Exemple

Steffen Mangold
la source
Vous ne le mentionnez pas, mais avez-vous récemment mis à jour les statistiques sur l'un de ces tableaux?
Nic
Thx @Nic J'ai ajouté une mise à jour sur la question. Veuillez consulter les informations détaillées ici.
Steffen Mangold

Réponses:

3

Regardez les plans, il y a quelques différences entre le bon et le mauvais. La première chose à noter est que le bon plan effectue une recherche sur InverterDayData alors que les deux mauvais plans effectuent un scan. Pourquoi cela, si vous vérifiez les lignes estimées, vous verrez que le bon plan attend 1 ligne alors que les mauvais plans attendent 6661 et environ 7000 lignes.

Jetez maintenant un œil aux valeurs des paramètres compilés,

Bon plan @ p__linq__1 = '2016-11-26 00: 00: 00.0000000' @ p__linq__0 = 20825

Mauvais plans @ p__linq__1 = '2018-01-03 00: 00: 00.0000000' @ p__linq__0 = 20686

il me semble donc que c'est un problème de reniflage de paramètre, quelles valeurs de paramètre transmettez-vous à cette requête lorsqu'elle fonctionne mal?

Il y a une recommandation d'index dans les mauvais plans sur InverterDayData qui semble raisonnable, j'essaierais de l'exécuter et de voir si cela vous aide. Il peut permettre à SQL d'effectuer une analyse sur la table.

David Fowler
la source
merci pour votre réponse! J'ai modifié la question pour vous (à la fin).
Steffen Mangold
Je ne savais pas que l'index suggéré couvrait toute la table, ce n'est pas une bonne idée. Vous pouvez simplement essayer un index sur la colonne date.
David Fowler
l'utilisation de l'indice OPTIMIZE FOR forcera le plan à compiler avec les bonnes valeurs, OPTION (OPTIMIZE FOR (@ p__linq__1 = '2016-11-26 00: 00: 00.0000000', @ p__linq__0 20825)) mais soyez prudent avec celui-ci et testez pour vous assurer que cela ne vous causera pas de maux de tête inattendus
David Fowler
hey @david J'ai posté une mise à jour.
Steffen Mangold
Ok, c'était un problème de reniflement de paramètres! En raison du nombre de lignes différent à l'intérieur de la table partitionnée, l'index SEEK ou SCAN est choisi. Je l'ai corrigé avec un ...OPTION (OPTIMIZE FOR UNKNOWN)indice.
Steffen Mangold