Optimiser la sélection sur sous-requête avec COALESCE (…)

8

J'ai une vue large que j'utilise depuis une application. Je pense que j'ai réduit mon problème de performances, mais je ne sais pas comment le résoudre. Une version simplifiée de la vue ressemble à ceci:

SELECT ISNULL(SEId + '-' + PEId, '0-0') AS Id,
   *,
   DATEADD(minute, Duration, EventTime) AS EventEndTime
FROM (
    SELECT se.SEId, pe.PEId,
        COALESCE(pe.StaffName, se.StaffName) AS StaffName, -- << Problem!
        COALESCE(pe.EventTime, se.EventTime) AS EventTime,
        COALESCE(pe.EventType, se.EventType) AS EventType,
        COALESCE(pe.Duration, se.Duration) AS Duration,
        COALESCE(pe.Data, se.Data) AS Data,
        COALESCE(pe.Field, se.Field) AS Field,
        pe.ThisThing, se.OtherThing
    FROM PE pe FULL OUTER JOIN SE se 
      ON pe.StaffName = se.StaffName
     AND pe.Duration = se.Duration
     AND pe.EventTime = se.EventTime
    WHERE NOT(pe.ThisThing = 1 AND se.OtherThing = 0)
) Z

Cela ne justifie probablement pas toute la raison de la structure de la requête, mais peut-être vous donne une idée - cette vue rejoint deux tables très mal conçues sur lesquelles je n'ai pas de contrôle et essaie de synthétiser certaines informations.

Donc, puisque c'est une vue utilisée depuis l'application, en essayant d'optimiser je l'enveloppe dans un autre SELECT, comme ceci:

SELECT * FROM (
    -- … above code …
) Q
WHERE StaffName = 'SMITH, JOHN Q'

car l'application recherche des membres du personnel spécifiques dans le résultat.

Le problème semble être la COALESCE(pe.StaffName, se.StaffName) AS StaffNamesection et que je sélectionne dans la vue StaffName. Si je change cela en pe.StaffName AS StaffNameou se.StaffName AS StaffName, les problèmes de performances disparaissent (mais voir la mise à jour 2 ci-dessous) . Mais cela ne fonctionnera pas car un côté ou l'autre du FULL OUTER JOINpeut être manquant, donc l'un ou l'autre champ peut être NULL.

Puis-je refactoriser ceci en remplaçant le COALESCE(…)par quelque chose d'autre, qui sera réécrit dans la sous-requête?

Autres notes:

  • J'ai déjà ajouté quelques index pour résoudre les problèmes de performances avec le reste de la requête - sans COALESCEcela, c'est très rapide.
  • À ma grande surprise, le fait de regarder le plan d'exécution ne déclenche aucun indicateur, même lorsque la sous-requête et la WHEREdéclaration d' habillage sont incluses. Mon coût total de sous-requête dans l'analyseur est 0.0065736. Hmph. L'exécution prend quatre secondes.
  • Changer l'application pour interroger différemment (par exemple retourner pe.StaffName AS PEStaffName, se.StaffName AS SEStaffNameet faire WHERE PEStaffName = 'X' OR SEStaffName = 'X') pourrait fonctionner, mais en dernier recours - j'espère vraiment pouvoir optimiser la vue sans avoir à recourir à toucher l'application.
  • Une procédure stockée aurait probablement plus de sens pour cela, mais l'application est construite avec Entity Framework, et je n'ai pas pu comprendre comment la faire fonctionner correctement avec un SP qui renvoie un type de table (un autre sujet entièrement).

Index

Les index que j'ai ajoutés jusqu'à présent ressemblent à ceci:

CREATE NONCLUSTERED INDEX [IX_PE_EventTime]
ON [dbo].[PE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[ThisThing])

CREATE NONCLUSTERED INDEX [IX_SE_EventTime]
ON [dbo].[SE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[OtherThing])

Mise à jour

Hmm… J'ai essayé de simuler le changement frappé ci-dessus, et cela n'a pas aidé. C'est-à-dire, avant ) Zci-dessus, j'ai ajouté AND (pe.StaffName = 'SMITH, JOHN Q' OR se.StaffName = 'SMITH, JOHN Q'), mais les performances sont les mêmes. Maintenant, je ne sais vraiment pas par où commencer.

Update 2

Le commentaire de @ypercube sur la nécessité de la jointure complète m'a fait réaliser que ma requête synthétisée laissait de côté un composant probablement important. Alors que, oui, j'ai besoin de la jointure complète, le test que j'ai fait ci-dessus en supprimant COALESCEet en testant un seul côté de la jointure pour une valeur non nulle rendrait l'autre côté de la jointure complète non pertinent , et l'optimiseur utilisait probablement cela fait pour accélérer la requête. De plus, j'ai mis à jour l'exemple pour montrer qu'il StaffNames'agit en fait d'une des clés de jointure - ce qui a probablement une incidence significative sur la question. Je penche également maintenant vers sa suggestion selon laquelle la rupture en une union à trois au lieu d'une jointure complète peut être la réponse, et simplifiera l'abondance de COALESCEs que je fais de toute façon. L'essayer maintenant.

S'pht'Kr
la source
Quels index avez-vous ajoutés? Incluez-vous le StaffName dans l'index?
Mark Sinkinson
@MarkSinkinson J'ai un index non clusterisé sur chaque table KeyField, à la fois indexe INCLUDEle StaffNamechamp et plusieurs autres champs. Je peux poster les définitions d'index dans la question. J'y travaille sur un serveur de test afin que je puisse ajouter tous les index que vous pensez être utiles à essayer!
S'pht'Kr
1
Vous avez la WHERE pe.ThisThing = 1 AND se.OtherThing = 0condition qui annule la FULL OUTERjointure et rend la requête équivalente à une jointure interne. Êtes-vous sûr d'avoir besoin d'une adhésion COMPLÈTE?
ypercubeᵀᴹ
@ypercube Je suis désolé, c'était un mauvais air-codage de ma part, le point est plus que j'ai des conditions sur les deux tables, mais oui, je tiens compte des valeurs nulles de chaque côté dans la vraie requête. Je fusionne les deux tables et cherche des correspondances, mais j'ai besoin des données disponibles de l'une ou l'autre table quand il n'y a pas d'enregistrement correspondant à gauche ou à droite - donc oui, j'ai besoin de la jointure complète.
S'pht'Kr
1
Une pensée: c'est un long shot mais vous pouvez essayer de diviser la requête interne en trois parties ( INNER JOIN, LEFT JOINavec WHERE IS NULLvérification, RIGHT JOIN with IS NULL) puis UNION ALLles trois parties. De cette façon, il ne sera pas nécessaire d'utiliser COALESCE()et cela pourrait (pourrait juste) aider l'optimiseur à comprendre la réécriture.
ypercubeᵀᴹ

Réponses:

4

C'était plutôt long mais puisque l'OP dit que cela a fonctionné, je l'ajoute comme réponse (n'hésitez pas à le corriger si vous trouvez quelque chose de mal).

Essayez de diviser la requête interne en trois parties ( INNER JOIN, LEFT JOINavec WHERE IS NULLvérification, RIGHT JOINavec IS NULLvérification), puis en UNION ALLtrois parties. Cela présente les avantages suivants:

  • L'optimiseur a moins d'options de transformation disponibles pour les FULLjointures que pour (les plus courantes) INNERet les LEFTjointures.

  • La Ztable dérivée peut être supprimée (vous pouvez le faire quand même) de la définition de la vue.

  • Le NOT(pe.ThisThing = 1 AND se.OtherThing = 0)sera nécessaire uniquement sur la INNERpartie de jointure.

  • Amélioration mineure, l'utilisation COALESCE()sera minime voire nulle (je suppose que se.SEIdet pe.PEIdne sont pas annulables. Si plusieurs colonnes ne sont pas annulables, vous pourrez supprimer plus d' COALESCE()appels.)
    Plus important encore, l'optimiseur peut repousser toutes les conditions dans vos requêtes qui impliquent ces colonnes (maintenant cela COALESCE()ne bloque pas le push.)

  • Tout ce qui précède donnera à l'optimiseur plus d'options pour transformer / réécrire toute requête qui utilise la vue afin qu'il puisse trouver un plan d'exécution qui indexe sur les tables sous-jacentes peut être utilisé.

En tout, la vue peut s'écrire:

SELECT 
    se.SEId + '-' + pe.PEId AS Id,
    se.SEId, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    COALESCE(pe.EventType, se.EventType) AS EventType,
    pe.Duration,
    COALESCE(pe.Data, se.Data) AS Data,
    COALESCE(pe.Field, se.Field) AS Field,
    pe.ThisThing, se.OtherThing,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe INNER JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1 AND se.OtherThing = 0) 

UNION ALL

SELECT 
    '0-0',
    NULL, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    pe.EventType,
    pe.Duration,
    pe.Data,
    pe.Field,
    pe.ThisThing, NULL,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe LEFT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1)
  AND se.StaffName IS NULL

UNION ALL

SELECT 
    '0-0',
    se.SEId, NULL,
    se.StaffName, 
    se.EventTime,
    se.EventType,
    se.Duration,
    se.Data,
    se.Field,
    NULL, se.OtherThing, 
    DATEADD(minute, se.Duration, se.EventTime) AS EventEndTime
FROM PE pe RIGHT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (se.OtherThing = 0)
  AND pe.StaffName IS NULL ;
ypercubeᵀᴹ
la source
0

Mon intuition serait que cela ne devrait pas être un problème car au moment où COALESCE(pe.StaffName, se.StaffName) AS StaffNametoutes les lignes des deux sources devraient déjà avoir été extraites et mises en correspondance, l'appel de fonction est une simple comparaison en mémoire à null et -choisir. De toute évidence, ce n'est pas le cas, alors peut-être que quelque chose dans l'une des sources (s'il s'agit de vues ou de tables dérivées en ligne) ou les tables de base (c'est-à-dire le manque d'index) fait penser au planificateur de requêtes qu'il doit analyser ces colonnes séparément.

Sans plus de détails sur la requête exacte que vous exécutez, les structures de support et les plans de requête produits, tout ce que nous proposons est une conjecture.

Pour essayer de forcer la comparaison à être effectuée après tout le reste, vous pouvez simplement sélectionner les deux valeurs dans la table dérivée ( pe.StaffName AS pe.StaffName, se.StaffName AS seStaffName) puis faire le choix dans la requête externe ( COALESCE(peStaffName, seStaffName) AS StaffName), ou vous pouvez même pousser les données de la requête interne dans une table temporaire effectue ensuite la requête externe en la sélectionnant (mais cela nécessiterait une procédure stockée, et en fonction du nombre de lignes, ce vidage vers tempdb pourrait être coûteux et donc problématique en soi).

David Spillett
la source
Merci David, je me suis trompé du côté de la paranoïa quant à ce que je devrais divulguer à ce sujet, même en ce qui concerne la structure (pe => PatientEvent, donc…) mais je sais que cela rend les choses plus difficiles. Je pense qu'il s'agit en fait de la jointure basée sur des index, puis de faire une "comparaison en mémoire simple" pour filtrer ... mais la table dérivée non filtrée Zrevient actuellement avec environ 1,5 m de lignes. Ce que je veux, c'est réécrire ce prédicat dans la requête pour Zqu'il utilise les index ... mais maintenant je suis aussi confus parce que quand je mets manuellement le prédicat là-bas, il n'utilise toujours pas d'index ... alors maintenant Je ne suis pas sûr.
S'pht'Kr