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

39

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 une performance horrible (comme jamais pris la peine d'attendre la fin). Le plan de requête ressemble à ceci:

entrez la description de l'image ici

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

entrez la description de l'image ici

PK correcte et indexation ci-dessous.

Le fait que le TOP 1plan de requête modifié ne me surprenne pas, je suis un peu surpris que cela aggrave encore la situation.

Remarque: j'ai lu les résultats de cet article et j'ai compris le concept de Row Goaletc. Ce que je suis curieux de savoir, c'est comment modifier la requête afin qu'elle utilise le meilleur plan. Actuellement, je vide les données dans une table temporaire, puis en extrait la première ligne. Je me demande s'il existe une meilleure méthode.

Modifier Pour ceux qui liront cela après le fait, voici quelques informations supplémentaires.

  • Document_Queue - PK / CI est D_ID et contient environ 5 000 lignes.
  • Correspondence_Journal - PK / CI est FILE_NUMBER, CORRESPONDENCE_ID et compte environ 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)

Kenneth Fisher
la source
1
Avez-vous une contrainte de clé étrangère qui applique la DOCUMENT_IDrelation entre les deux tables (ou chaque enregistrement CORRESPONDENCE_JOURNALest-il associé à un enregistrement correspondant DOCUMENT_QUEUE)?
Daniel Hutmacher

Réponses:

28

Essayez de forcer un hash rejoindre *

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 serait meilleure avec le top 1 et que cela avait du sens, mais en réalité, cela ne fonctionnait pas ici. Juste une supposition, mais le coût estimé de ce spool était peut-être correct - il utilise TEMPDB - vous pouvez avoir une TEMPDB peu performante.


* Faites attention aux indications de jointure , car elles forcent l'ordre d'accès des tables de plan à correspondre à l' ordre écrit des tables dans la requête (comme si cela OPTION (FORCE ORDER)avait été spécifié). À partir du lien de la documentation:

Extrait de BOL

Cela peut ne produire aucun effet indésirable dans l'exemple, mais en général, cela pourrait très bien. FORCE ORDER(implicite ou explicite) est un indice très puissant qui va au-delà de l'exécution forcée de l'ordre; il empêche l'application d'un large éventail de techniques d'optimisation, y compris les agrégations partielles et la réorganisation.

Un indice de OPTION (HASH JOIN) requête peut être moins intrusif dans les cas appropriés, car cela n'implique pas FORCE ORDER. Cependant, il s'applique à toutes les jointures de la requête. D'autres solutions sont disponibles.

paparazzo
la source
1
On dirait que la bonne réponse et la seule différence entre ce plan et le plan plus simple était un tri supplémentaire à l’avant.
Kenneth Fisher
3
Pas sûr que j'aime cette réponse. Les allusions sont très invasives. Certaines modifications d'indexation simples doivent d'abord être essayées, par exemple l'index sur la colonne de date.
Usr le
@usr Il s'agit d'une simple jointure PK qui s'exécute en moins d'une seconde. Jolie valeur sûre ici.
paparazzo
4
En forçant une jointure de hachage, vous forcez une analyse de la grande table. Il y a de meilleures options.
Rob Farley
30

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

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 ce qui ROW_NUMBER()précède devrait être le même que si vous aviez un ORDER BY. Le plan de requête doit maintenant comporter un segment, un projet de séquence et enfin un opérateur de filtrage; le reste doit ressembler à votre bon plan.

Daniel Hutmacher
la source
3
En fait, bien qu’il fournisse l’opérateur principal (et un tas d’autres éléments (un projet de séquence, un segment et une sorte)), il fonctionnait toujours en dessous de la seconde. Je vais cependant donner la bonne réponse à @frisbee, car c'était la première et c'est plus simple. Bonne réponse cependant.
Kenneth Fisher
10
@KennethFisher, la réponse de frisbee est plus simple, mais dans la manière dont un marteau pilon enfonce un clou de finition plus simplement qu'un marteau à cadrage standard. Cela comporte aussi beaucoup de risques, surtout s’il est laissé en place à long terme. Je n'utiliserais pas de telles astuces, sauf dans les tests ou peut-être, PEUT-ÊTRE une exception marginale.
Steve Mangiameli le
@SteveMangiameli Dans ce cas particulier, il n'y a qu'un seul participant, un certain nombre de problèmes disparaissent. Je suis conscient des risques liés à l'utilisation d'un indicateur de jointure (ou indicateur de requête). Je pense simplement que cela est justifié dans ce cas.
Kenneth Fisher
5
@KennethFisher Imo, le principal risque des repères de requête est que, au fur et à mesure de la croissance ou de la modification de vos données, le plan de requête que vous appliquez peut devenir pire que ce que le système aurait trouvé par lui-même. Vous avez déjà vu comment une petite erreur dans le plan peut sérieusement affecter les performances. Utiliser un indice dans la production, c'est déclarer: "Je sais que ce plan sera toujours, toujours le meilleur, car je comprends très bien le planificateur et le comportement de mes données pendant la durée de vie de cette requête en production." Je n'ai jamais été aussi confiant à propos d'une requête.
jpmc26
29

Edit: +1 fonctionne dans cette situation car il s'avère qu'il FILE_NUMBERs'agit d'une version 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 de constant mais qui contient une fonction non déterministe, telle que sign(rand()+1). L'idée de "briser le genre" 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 cela comme une solution. Si vous modifiez votre requête en ORDER BY cj.FILE_NUMBER + 1alors le TOP 1se comportera différemment.

Vous voyez, avec l'objectif de petite ligne en place pour une requête ordonnée, le système essaiera de consommer les données dans l'ordre, pour éviter d'avoir un opérateur de tri. Cela évitera également de construire une table de hachage, sachant qu'il n'aura probablement pas à travailler trop pour trouver la première ligne. Dans votre cas, cela est faux: vu 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 DOCUMENT_QUEUEtable (DQ) est beaucoup plus petite que votre CORRESPONDENCE_JOURNALtable (CJ). Et que le meilleur plan serait en réalité de vérifier toutes les lignes DQ jusqu'à ce qu'une ligne CJ soit trouvée. En fait, c’est ce que l’optimiseur de requêtes (QO) ferait s’il ne contenait pas cette embêtade ORDER BY, c’est bien soutenu par un index couvrant sur CJ.

Donc, si vous les supprimiez ORDER BYcomplètement, je suppose que vous obtiendrez un plan comportant une boucle imbriquée, qui parcourt les rangées de DQ, recherchant dans CJ pour s’assurer que la rangée existe. Et avec TOP 1, cela s'arrêterait après qu'une seule rangée ait été tirée.

Mais si vous avez réellement besoin de la première ligne dans l' FILE_NUMBERordre, vous pourriez alors tromper le système en ignorant cet index qui semble (à tort) être si utile, en agissant ORDER BY CJ.FILE_NUMBER+1- et nous savons qu'il gardera le même ordre qu'auparavant, mais surtout le QO ne le fait pas L’assurance qualité s’attachera à obtenir l’ensemble complet de manière à pouvoir satisfaire un opérateur de tri par ordre croissant. Cette méthode doit produire un plan contenant un opérateur Compute Scalar pour calculer la valeur pour 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 correspond à rien dans DQ.

Le hachage n'est pas forcément horrible, mais si l'ensemble des lignes que vous retournez de DQ est bien plus petit que CJ (comme je le pensais), alors le hachage analysera beaucoup plus de CJ. que ce dont il a besoin.

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

Rob Farley
la source
7

J'ai lu les résultats de cet article et j'ai compris le concept d'objectif de rangée, etc. Ce qui m'intrigue, c'est comment modifier la requête afin qu'elle utilise le meilleur plan

L'ajout 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 constituera probablement le moyen le plus simple / le plus direct.

Si l'ajout de cette astuce vous donne une erreur d'autorisations (obligatoire pour DBCC TRACEON), vous pouvez l'appliquer à l'aide d'un repère de plan:

Utilisation QUERYTRACEONde guides de plan par spaghettidba

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

Quelles sont les autorisations QUERYTRACEONnécessaires? par Kendra Little

Martin Smith
la source
3

Les nouvelles versions de SQL Server offrent des options différentes (et sans doute meilleures) pour traiter les requêtes dont les performances sont 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 HINTqui a le même effet que l'indicateur de suivi 4138. Si vous n'êtes pas sur cette version, vous pouvez également envisager d'utiliser l' OPTIMIZE FORindicateur 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 il ne sera pas créé dans le but d'obtenir seulement 1 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));
Joe Obbish
la source
2

Depuis que vous faites un TOP(1), je recommande de faire le ORDER BYdéterministe pour un début. Au minimum, cela garantira des résultats prévisibles sur le plan fonctionnel (toujours utiles pour les tests de régression). Il semble que vous ayez besoin d'ajouter DC.D_IDet CJ.CORRESPONDENCE_IDpour cela.

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 continues pertinentes dans une table temporaire, afin d’éliminer les problèmes d’estimation de cardinalité sur QUEUE_DATEet PRINT_LOCATION. Cela devrait être rapide étant donné le faible nombre de lignes. Vous pouvez ensuite ajouter des index à cette table temporaire si nécessaire sans modifier la table permanente.

Simon Birch
la source