web-dev-qa-db-fra.com

Pourquoi l'ajout d'un TOP 1 dégrade-t-il considérablement les performances?

J'ai une requête assez simple

SELECT TOP 1 dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Cela me donne des performances horribles (comme jamais pris la peine d'attendre la fin). Le plan de requête ressemble à ceci:

enter image description here

Cependant, si je supprime le TOP 1 Je reçois un plan qui ressemble à ceci et il s'exécute en 1-2 secondes:

enter image description here

PK et indexation corrects ci-dessous.

Le fait que le TOP 1 le plan de requête modifié ne me surprend pas, je suis juste un peu surpris que cela le rende bien pire.

Remarque: j'ai lu les résultats de cette post et comprendre le concept d'un Row Goal etc. Ce qui m'intéresse, c'est comment je peux changer la requête pour qu'elle utilise le meilleur plan. Actuellement, je vide les données dans une table temporaire, puis j'en tire la première ligne. Je me demande s'il y a une meilleure méthode.

Modifier Pour les personnes qui lisent ceci après coup, voici quelques informations supplémentaires.

  • Document_Queue - PK/CI est D_ID et il a environ 5k lignes.
  • Correspondence_Journal - PK/CI est FILE_NUMBER, CORRESPONDENCE_ID et il a ~ 1,4 mil de lignes.

Quand j'ai commencé, il n'y avait pas d'autres index. Je me suis retrouvé avec un sur Correspondence_Journal (Document_Id, File_Number)

39
Kenneth Fisher

Essayez de forcer une hash join *

SELECT TOP 1 
       dc.DOCUMENT_ID,
       dc.COPIES,
       dc.REQUESTOR,
       dc.D_ID,
       cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
INNER HASH JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
       AND dc.QUEUE_DATE <= GETDATE()
       AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

L'optimiseur pensait probablement qu'une boucle allait être meilleure avec le top 1 et ce genre de sens, mais en réalité cela n'a pas fonctionné ici. Juste une supposition ici mais peut-être que le coût estimé de cette bobine était éteint - il utilise TEMPDB - vous pouvez avoir un TEMPDB peu performant.


* Soyez prudent avec conseils de jointure , car ils forcent l'ordre d'accès aux tables du plan à correspondre à l'ordre écrit des tables dans la requête (comme si OPTION (FORCE ORDER) avait été spécifié). Depuis le lien de documentation:

BOL extract

Cela peut ne pas produire d'effets indésirables dans l'exemple, mais en général, c'est très bien possible. FORCE ORDER (Implicite ou explicite) est un indice très très puissant qui va au-delà de l'application de l'ordre; il empêche l'application d'un large éventail de techniques d'optimisation, notamment les agrégations partielles et les réorganisations.

Une indication de OPTION (HASH JOIN) peut être moins intrusive dans les cas appropriés, car cela n'implique pas FORCE ORDER. Elle s'applique cependant à toutes les jointures de la requête. D'autres solutions sont disponibles.

28
paparazzo

Puisque vous obtenez le bon plan avec le ORDER BY, Peut-être pourriez-vous simplement lancer votre propre opérateur TOP?

SELECT DOCUMENT_ID, COPIES, REQUESTOR, D_ID, FILE_NUMBER
FROM (
    SELECT dc.DOCUMENT_ID,
           dc.COPIES,
           dc.REQUESTOR,
           dc.D_ID,
           cj.FILE_NUMBER,
           ROW_NUMBER() OVER (ORDER BY cj.FILE_NUMBER) AS _rownum
    FROM DOCUMENT_QUEUE dc
    INNER JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
    WHERE dc.QUEUE_DATE <= GETDATE()
      AND dc.PRINT_LOCATION = 2
) AS sub
WHERE _rownum=1;

Dans mon esprit, le plan de requête pour la ROW_NUMBER() ci-dessus devrait être le même que si vous aviez un ORDER BY. Le plan de requête devrait maintenant avoir un segment, un projet de séquence et enfin un opérateur de filtre, le reste devrait ressembler à votre bon plan.

30
Daniel Hutmacher

Modifier: +1 fonctionne dans cette situation car il s'avère que FILE_NUMBER Est une version de chaîne à zéro complétée d'un entier. Une meilleure solution ici pour les chaînes consiste à ajouter '' ( la chaîne vide), car l'ajout d'une valeur peut affecter l'ordre, ou que les nombres ajoutent quelque chose qui est une constante mais qui contient une fonction non déterministe, telle que sign(Rand()+1). L'idée de "casser le tri" est toujours valable ici, c'est juste que ma méthode n'était pas idéale.

+1

Non, je ne veux pas dire que je suis d'accord avec quoi que ce soit, je veux dire que comme solution. Si vous modifiez votre requête en ORDER BY cj.FILE_NUMBER + 1, Le TOP 1 Se comportera différemment.

Vous voyez, avec l'objectif de petite ligne en place pour une requête ordonnée, le système va essayer de consommer les données dans l'ordre, pour éviter d'avoir un opérateur de tri. Cela évitera également de créer une table de hachage, en supposant qu'il ne doit probablement pas faire trop de travail pour trouver cette première ligne. Dans votre cas, c'est faux - de l'épaisseur de ces flèches, il semble qu'il doive consommer beaucoup de données pour trouver une seule correspondance.

L'épaisseur de ces flèches suggère que votre table DOCUMENT_QUEUE (DQ) est beaucoup plus petite que votre table CORRESPONDENCE_JOURNAL (CJ). Et que le meilleur plan serait en fait de vérifier les lignes DQ jusqu'à ce qu'une ligne CJ soit trouvée. En effet, c'est ce que ferait l'Optimiseur de Requête (QO) s'il n'avait pas ce ORDER BY Embêtant, qui est bien supporté par un indice de couverture sur CJ.

Donc, si vous supprimez complètement le ORDER BY, Je m'attends à ce que vous obteniez un plan impliquant une boucle imbriquée, itérant sur les lignes dans DQ, cherchant dans CJ pour vous assurer que la ligne existe. Et avec TOP 1, Cela s'arrêterait après qu'une seule ligne ait été tirée.

Mais si vous avez réellement besoin de la première ligne dans l'ordre FILE_NUMBER, Alors vous pourriez inciter le système à ignorer cet index qui semble (incorrectement) être si utile, en faisant ORDER BY CJ.FILE_NUMBER+1 - ce que nous savons gardera le même ordre qu'auparavant, mais surtout le QO ne le fait pas. Le QO se concentrera sur l'obtention de l'ensemble, afin qu'un opérateur Top N Sort puisse être satisfait. Cette méthode doit produire un plan qui contient un opérateur de calcul scalaire pour déterminer la valeur de la commande et un opérateur Top N Sort pour obtenir la première ligne. Mais à droite de ceux-ci, vous devriez voir une belle boucle imbriquée, faisant beaucoup de recherches sur CJ. Et de meilleures performances que de parcourir une grande table de lignes qui ne correspondent à rien dans DQ.

Le Hash Match n'est pas nécessairement horrible, mais si l'ensemble de lignes que vous retournez de DQ est beaucoup plus petit que CJ (comme je m'y attendrais), alors le Hash Match va scanner beaucoup plus de CJ qu'il n'en a besoin.

Remarque: j'ai utilisé +1 au lieu de +0, car l'optimiseur de requête est susceptible de reconnaître que +0 ne change rien. Bien sûr, la même chose pourrait s'appliquer au +1, sinon maintenant, puis à un moment donné dans le futur.

29
Rob Farley

J'ai lu les résultats de ce post et je comprends le concept d'un objectif de ligne, etc. Ce qui m'intéresse, c'est comment je peux changer la requête pour qu'elle utilise le meilleur plan.

L'ajout de OPTION (QUERYTRACEON 4138) désactive l'effet des objectifs de ligne pour cette requête uniquement, sans être trop normatif sur le plan final, et sera probablement le moyen le plus simple/le plus direct.

Si l'ajout de ce conseil vous donne une erreur d'autorisation (requise pour DBCC TRACEON), vous pouvez l'appliquer à l'aide d'un guide de plan:

tilisation de QUERYTRACEON dans les guides de plan par spaghettidba

... ou utilisez simplement une procédure stockée:

De quelles autorisations QUERYTRACEON a-t-elle besoin? par Kendra Little

7
Martin Smith

Les versions plus récentes de SQL Server offrent des options différentes (et sans doute meilleures) pour traiter les requêtes qui obtiennent des performances sous-optimales lorsque l'optimiseur est en mesure d'appliquer des optimisations d'objectif de ligne. SQL Server 2016 SP1 a introduit le DISABLE_OPTIMIZER_ROWGOAL USE HINT qui a le même effet que l'indicateur de trace 4138. Si vous n'êtes pas sur cette version, vous pouvez également envisager d'utiliser le OPTIMIZE FOR indice de requête pour obtenir un plan de requête conçu pour renvoyer toutes les lignes au lieu de 1. La requête ci-dessous renverra les mêmes résultats que celui de la question, mais elle ne sera pas créée dans le but d'obtenir une seule ligne.

DECLARE @top INT = 1;

SELECT TOP (@top) dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
OPTION (OPTIMIZE FOR (@top = 987654321));
3
Joe Obbish

Puisque vous effectuez une TOP(1), je recommande de faire la ORDER BY déterministe pour commencer. À tout le moins, cela garantira la prévisibilité fonctionnelle des résultats (toujours utile pour les tests de régression). Il semble que vous deviez ajouter DC.D_ID et CJ.CORRESPONDENCE_ID pour ça.

Lorsque je regarde des plans de requête, je trouve parfois instructif de simplifier la requête: il est possible de sélectionner à l'avance toutes les lignes DC pertinentes dans une table temporaire, pour éliminer les problèmes d'estimation de cardinalité sur QUEUE_DATE et PRINT_LOCATION. Cela devrait être rapide compte tenu du faible nombre de lignes. Vous pouvez ensuite ajouter des index à cette table temporaire si nécessaire sans modifier la table permanente.

2
Simon Birch