web-dev-qa-db-fra.com

Le plan d'exécution des requêtes est horrible jusqu'à la mise à jour des statistiques

J'espère que vous pouvez m'aider ici. Notre application interroge un tableau de messages toutes les 3 secondes à la recherche de notifications à envoyer. Cela fonctionne bien sur tous nos clients (DB à locataire unique) sauf un. Ils n'auront aucune activité pendant 23 heures par jour, puis chargeront des milliers de messages à la fois (3000+). Dans d'autres cas, ce volume n'est rien et nous pouvons facilement le gérer, sauf que dans ce cas, la requête SQL ci-dessous prend environ 30 secondes pour s'exécuter et s'aggrave à mesure que la file d'attente sauvegarde lors d'une mise à jour, nécessite un verrou exclusif et donc bloquer toutes les autres requêtes et donc les problèmes provoquent toutes sortes de ravages. Tout cela est dû à un mauvais plan de requête.

Nous avons une réindexation quotidienne qui fonctionne tous les matins à 5h du matin (réorganiser <30%, reconstruire> 30%, ignorer <5%) ainsi que des statistiques de mises à jour. Ils proviennent tous deux de la solution de maintenance Ola Hallengren. Nous sommes également sur SQL Server 2016 et sommes entièrement à jour (13.0.5492.2)

Je n'ai pas les 2 plans à portée de main, mais en gros le mauvais plan va et fait une analyse complète de la table MessagesSent (3,5 m de lignes).

Ma théorie est que parce que la requête ne renvoie rien toute la journée, certaines parties ne sont pas exécutées et donc la mauvaise requête est la requête la plus efficace pour SQL.

Cela continuera après avoir vidé le plan de la requête car il génère simplement le même plan, mais lorsque je METTRE À JOUR LES STATISTIQUES sur la table MessagesSent, le bon plan est créé et tout est sain, avec la requête s'exécutant dans environ 10-30 ms.

Est-ce que quelqu'un sait comment je peux affiner cela pour toujours utiliser le meilleur plan même si aucune donnée n'existe pour que la requête revienne? En tant que correctif, nous avons ajouté une option de recompilation à l'application, mais je ne pense pas que ce soit la solution idéale pour une requête qui s'exécute toutes les 3 secondes.

Voici la requête:

WITH TopMessage
    AS
    (
        SELECT TOP 1 ID, BatchID FROM MessagesSent 
        JOIN Units ON Unit = idUnit 
        WHERE   MessageDate <= GETDATE() 
          AND         Active = 'True' 
          AND         Status = 'Queued' 
          AND NOT(DialString = 'null') 
          AND           Unit = ('29') 
          AND System in ('SystemName', 'Q1', '') 
        ORDER BY 
            CASE 
                WHEN QPriority IS NULL 
                    THEN 
                        CASE 
                            WHEN DefaultPriority IS NULL 
                                THEN 999999 
                                ELSE DefaultPriority 
                        END
                 ELSE QPriority 
            END ASC,
            Retries ASC, 
            MessageDate ASC, 
            ID ASC
    ),
    BatchCalls
    AS
    (
        SELECT * FROM MessagesSent 
        WHERE (
                 (LEN(BatchID) > 0 
                  AND BatchID = (SELECT TOP 1 BatchID FROM TopMessage)
                 ) 
        OR ID = (SELECT TOP 1 ID FROM TopMessage)
        )
        AND Status = 'Queued' AND Active = 'True'
    )

    UPDATE BatchCalls
    SET LastUpdated = @dtNow
    OUTPUT INSERTED.*
    WHERE Status = 'Queued'

Merci beaucoup de votre temps et de votre visite.

4
WadeH

Vous pouvez utiliser un manuel guide de plan pour appliquer le plan souhaité.

Alternativement, vous pouvez utiliser Query Store pour appliquer les guides de plan via l'interface graphique.

En outre, vous pouvez exécuter un travail de mise à jour des statistiques après le gros chargement de messages. J'ai écrit un article sur mon blog montrant un moyen facile de le faire .

9
Max Vernon

Étant donné que vous avez un filtrage commun entre ces requêtes, vous pourrez peut-être accélérer les choses et réduire la quantité de verrouillage/blocage en cours, en ajoutant un index filtré sur les colonnes "Actif" et "État".

Il est difficile de dire quelles colonnes appartiennent à quelles tables sans le schéma, mais l'index ressemblerait à ceci:

CREATE NONCLUSTERED INDEX IX_BatchID_Filtered
ON dbo.MessagesSent (BatchID, Active, Status)
WHERE Active = 'True' AND Status = 'Queued';

Remarque: il pourrait y avoir une meilleure colonne de début que BatchID, et vous voudrez peut-être inclure d'autres colonnes de la table (comme DialString, System, QPriority, etc.) - Je ne savais simplement pas quelles colonnes appartenaient à quelles tables, et quels sont leurs types de données, etc.

Cet indice ne comprendrait que le sous-ensemble (espérons-le petit) de lignes qui répondent à ces critères et fournirait des performances plus prévisibles face à des statistiques quelque peu obsolètes.

6
Josh Darnell