Optimieren Sie die Auswahl bei Unterabfrage mit COALESCE (…)

8

Ich habe eine große Ansicht, die ich innerhalb einer Anwendung verwende. Ich glaube, ich habe mein Leistungsproblem eingegrenzt, bin mir aber nicht sicher, wie ich es beheben soll. Eine vereinfachte Version der Ansicht sieht folgendermaßen aus:

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

Das rechtfertigt wahrscheinlich nicht den ganzen Grund für die Abfragestruktur, gibt Ihnen aber vielleicht eine Idee - diese Ansicht verbindet zwei sehr schlecht gestaltete Tabellen, über die ich keine Kontrolle habe, und versucht, einige Informationen daraus zu synthetisieren.

Da dies eine Ansicht ist, die von der Anwendung verwendet wird, verpacke ich sie beim Optimieren in ein anderes SELECT wie folgt:

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

weil die Anwendung im Ergebnis nach bestimmten Mitarbeitern sucht.

Das Problem scheint der COALESCE(pe.StaffName, se.StaffName) AS StaffNameAbschnitt zu sein, den ich aus der Ansicht auf auswähle StaffName. Wenn ich das auf pe.StaffName AS StaffNameoder ändere, se.StaffName AS StaffNameverschwinden die Leistungsprobleme (siehe jedoch Update 2 unten) . Aber das geht nicht, weil die eine oder andere Seite derFULL OUTER JOIN fehlen könnte und das eine oder andere Feld möglicherweise NULL ist.

Kann ich das ersetzen, indem ich das ersetze? COALESCE(…) durch etwas anderes , das in die Unterabfrage umgeschrieben wird?

Weitere Hinweise:

  • Ich habe bereits einige Indizes hinzugefügt, um Leistungsprobleme mit dem Rest der Abfrage zu beheben - ohne die COALESCE diese ist es sehr schnell.
  • Zu meiner Überraschung werden beim Betrachten des Ausführungsplans keine Flags ausgelöst, selbst wenn die umschließende Unterabfrage und WHEREAnweisung eingeschlossen sind. Meine gesamten Unterabfragekosten im Analysator betragen0.0065736 . Hmph. Die Ausführung dauert vier Sekunden.
  • Das Ändern der Anwendung auf eine andere Abfrage (z. B. Zurückgeben pe.StaffName AS PEStaffName, se.StaffName AS SEStaffNameund Ausführen WHERE PEStaffName = 'X' OR SEStaffName = 'X') funktioniert möglicherweise, aber als letzter Ausweg hoffe ich wirklich, dass ich die Ansicht optimieren kann, ohne die Anwendung berühren zu müssen.
  • Eine gespeicherte Prozedur wäre wahrscheinlich sinnvoller, aber die Anwendung wurde mit Entity Framework erstellt, und ich konnte nicht herausfinden, wie sie mit einem SP, der einen Tabellentyp zurückgibt, gut funktioniert (ein ganz anderes Thema).

Indizes

Die Indizes, die ich bisher hinzugefügt habe, sehen ungefähr so ​​aus:

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])

Aktualisieren

Hmm ... Ich habe versucht, die betroffene Veränderung oben zu simulieren, und es hat nicht geholfen. Dh vorher habe ) Zich hinzugefügtAND (pe.StaffName = 'SMITH, JOHN Q' OR se.StaffName = 'SMITH, JOHN Q') , aber die Leistung ist die gleiche. Jetzt weiß ich wirklich nicht, wo ich anfangen soll.

Update 2

Durch den Kommentar von @ypercube zur Notwendigkeit des vollständigen Joins wurde mir klar, dass meine synthetisierte Abfrage eine wahrscheinlich wichtige Komponente ausgelassen hat. Während, ja, ich den vollständigen Join benötige, würde der Test, den ich oben durchgeführt habe, indem ich den gelöscht COALESCEund nur eine Seite des Joins auf einen Wert ungleich Null getestet habe , die andere Seite des vollständigen Joins irrelevant machen , und der Optimierer hat dies wahrscheinlich verwendet Tatsache, um die Abfrage zu beschleunigen. Außerdem habe ich das Beispiel aktualisiert, um zu zeigen, dass StaffNamees sich tatsächlich um einen der Join-Schlüssel handelt - was wahrscheinlich einen erheblichen Einfluss auf die Frage hat. Ich neige jetzt auch zu seinem Vorschlag, dass es die Antwort sein könnte, dies in eine Drei-Wege-Union zu zerlegen, anstatt sich vollständig anzuschließen, und die Fülle von COALESCEs, die ich sowieso mache, zu vereinfachen . Ich versuche es jetzt.

S'pht'Kr
quelle
Welche Indizes haben Sie hinzugefügt? Nehmen Sie den StaffName in den Index auf?
Mark Sinkinson
@ MarkSinkinson Ich habe einen nicht gruppierten Index für jede Tabelle KeyField, beide indiziert INCLUDEdas StaffNameFeld und mehrere andere Felder. Ich kann die Indexdefinitionen in der Frage posten. Ich arbeite auf einem Testserver daran, damit ich alle Indizes hinzufügen kann, die Sie für hilfreich halten, um es zu versuchen!
S'pht'Kr
1
Sie haben die WHERE pe.ThisThing = 1 AND se.OtherThing = 0Bedingung, dass der FULL OUTERJoin abgebrochen wird und die Abfrage einem inneren Join entspricht. Sind Sie sicher, dass Sie einen vollständigen Join benötigen?
Ypercubeᵀᴹ
@ypercube Es tut mir leid, das war eine schlechte Luftkodierung meinerseits. Punkt ist mehr, dass ich Bedingungen für beide Tabellen habe, aber ja, ich berücksichtige Nullen auf beiden Seiten in der realen Abfrage. Ich füge die beiden Tabellen zusammen und suche nach Übereinstimmungen, aber ich benötige die verfügbaren Daten aus beiden Tabellen, wenn links oder rechts kein übereinstimmender Datensatz vorhanden ist. Ja, ich benötige den vollständigen Join.
S'pht'Kr
1
Ein Gedanke: Es ist ein gewagtes Spiel ist , aber Sie können versuchen , die interne Abfrage in drei Teile zu brechen ( INNER JOIN, LEFT JOINmit WHERE IS NULLScheck, RIGHT mit IS NULL JOIN) und dann UNION ALLdie drei Teile. Auf diese Weise ist keine Verwendung erforderlich, COALESCE()und es kann (möglicherweise) dem Optimierer helfen, das Umschreiben herauszufinden.
Ypercubeᵀᴹ

Antworten:

4

Dies war ziemlich langwierig, aber da das OP sagt, dass es funktioniert hat, füge ich es als Antwort hinzu (Sie können es gerne korrigieren, wenn Sie etwas falsch finden).

Versuchen Sie, die interne Abfrage in drei Teile zu brechen ( INNER JOIN, LEFT JOINmit WHERE IS NULLScheck, RIGHT JOINmit IS NULLScheck) und dann UNION ALLden drei Teilen. Dies hat folgende Vorteile:

  • Das Optimierungsprogramm verfügt über weniger Transformationsoptionen für FULLJoins als für (die häufigeren) INNERund LEFTJoins.

  • Die Zabgeleitete Tabelle kann aus der Ansichtsdefinition entfernt werden (Sie können dies trotzdem tun).

  • Das NOT(pe.ThisThing = 1 AND se.OtherThing = 0)wird nur für den INNERJoin-Teil benötigt.

  • Geringfügige Verbesserung, die Verwendung COALESCE()ist minimal, wenn überhaupt (ich habe angenommen, dass se.SEIdund pe.PEIdnicht nullbar. Wenn mehr Spalten nicht nullbar sind, können Sie mehr COALESCE()Aufrufe entfernen .)
    Wichtiger ist, dass der Optimierer möglicherweise alle Bedingungen in herunterdrückt Ihre Abfragen, die diese Spalten betreffen (jetzt COALESCE()wird der Push nicht blockiert.)

  • All dies bietet dem Optimierer mehr Optionen zum Transformieren / Umschreiben von Abfragen, die die Ansicht verwenden, sodass möglicherweise ein Ausführungsplan gefunden wird, der Indizes für die zugrunde liegenden Tabellen verwendet.

Insgesamt kann die Ansicht wie folgt geschrieben werden:

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ᵀᴹ
quelle
0

Meine Intuition wäre, dass dies kein Problem sein sollte, da zu dem Zeitpunkt, an dem dies COALESCE(pe.StaffName, se.StaffName) AS StaffNamegeschieht, alle Zeilen aus den beiden Quellen bereits eingezogen und abgeglichen werden sollten, sodass der Funktionsaufruf ein einfacher speicherinterner Vergleich mit Null und ist -pick. Offensichtlich ist dies nicht der Fall. Vielleicht lässt etwas in einer der Quellen (wenn es sich um Ansichten oder inline abgeleitete Tabellen handelt) oder in den Basistabellen (dh fehlende Indizes) den Abfrageplaner glauben, dass diese Spalten separat gescannt werden müssen.

Ohne weitere Einzelheiten zu der genauen Abfrage, die Sie ausführen, den unterstützenden Strukturen und den erstellten Abfrageplänen ist alles, was wir vorschlagen, eine Vermutung.

Um zu versuchen, den Vergleich zu erzwingen, können Sie versuchen, einfach beide Werte in der deribierten Tabelle ( pe.StaffName AS pe.StaffName, se.StaffName AS seStaffName) auszuwählen und dann die Auswahl in der äußeren Abfrage ( COALESCE(peStaffName, seStaffName) AS StaffName) durchzuführen , oder Sie können sogar die Daten aus der inneren Abfrage in verschieben Eine temporäre Tabelle führt dann die äußere Abfrage durch Auswahl aus (dies würde jedoch eine gespeicherte Prozedur erfordern, und abhängig von der Anzahl der Zeilen könnte dieser Dump-to-Tempdb teuer und daher für sich genommen problematisch sein).

David Spillett
quelle
Danke David, ich habe mich auf der Seite der Paranoia geirrt, wie viel ich darüber offenlegen sollte, selbst was die Struktur betrifft (pe => PatientEvent, also ...), aber ich weiß, dass das es schwieriger macht. Ich denke, es wird tatsächlich der Join basierend auf Indizes durchgeführt und dann ein "einfacher In-Memory-Vergleich" durchgeführt, um zu filtern ... aber die ungefilterte abgeleitete Tabelle Zkommt derzeit mit ~ 1,5 m Zeilen zurück. Ich möchte, dass dieses Prädikat in die Abfrage umgeschrieben wird, Zdamit es die Indizes verwendet. Aber jetzt bin ich auch verwirrt, denn wenn ich das Prädikat manuell dort ablege, wird immer noch kein Index verwendet Ich bin mir nicht sicher.
S'pht'Kr