Optimierungsproblem mit benutzerdefinierter Funktion

26

Ich habe ein Problem zu verstehen, warum SQL Server für jeden Wert in der Tabelle eine benutzerdefinierte Funktion aufruft, obwohl nur eine Zeile abgerufen werden sollte. Das eigentliche SQL ist viel komplexer, aber ich konnte das Problem auf folgendes reduzieren:

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Bei dieser Abfrage ruft SQL Server die GetGroupCode-Funktion für jeden einzelnen Wert in der PRODUCT-Tabelle auf, obwohl die geschätzte und tatsächliche Anzahl der von ORDERLINE zurückgegebenen Zeilen 1 beträgt (dies ist der Primärschlüssel):

Abfrageplan

Gleicher Plan im Plan-Explorer mit den Zeilenzahlen:

Planen Sie den Explorer Tabellen:

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

Der für den Scan verwendete Index lautet:

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

Die Funktion ist eigentlich etwas komplexer, aber dasselbe passiert mit einer Dummy-Funktion mit mehreren Anweisungen wie der folgenden:

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

Ich konnte die Leistung "korrigieren", indem ich SQL Server zwang, das Top-1-Produkt abzurufen, obwohl 1 das Maximum ist, das jemals gefunden werden kann:

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Dann ändert sich auch die Grundrissform so, wie ich es ursprünglich erwartet hatte:

Abfrageplan mit top

Ich denke auch, dass der Index PRODUCT_FACTORY kleiner als der Clustered-Index PRODUCT_PK ist, aber selbst wenn die Abfrage zur Verwendung von PRODUCT_PK gezwungen wird, bleibt der Plan mit 6655 Aufrufen der Funktion derselbe.

Wenn ich ORDERHDR komplett weglasse, beginnt der Plan mit einer verschachtelten Schleife zwischen ORDERLINE und PRODUCT und die Funktion wird nur einmal aufgerufen.

Ich würde gerne verstehen, was der Grund dafür sein könnte, da alle Vorgänge mit Primärschlüsseln ausgeführt werden und wie dies behoben werden kann, wenn dies in einer komplexeren Abfrage geschieht, die nicht so einfach zu lösen ist.

Bearbeiten: Tabellenanweisungen erstellen:

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)
James Z
quelle

Antworten:

30

Es gibt drei technische Hauptgründe, warum Sie den Plan erhalten, den Sie tun:

  1. Das Kalkulationsframework des Optimierers bietet keine echte Unterstützung für Nicht-Inline-Funktionen. Es wird kein Versuch unternommen, in der Funktionsdefinition nachzuschlagen, wie teuer sie sein könnte. Es werden lediglich sehr geringe Fixkosten zugewiesen, und es wird geschätzt, dass die Funktion bei jedem Aufruf eine Zeile Ausgabe erzeugt. Beide Modellannahmen sind sehr oft völlig unsicher. Die Situation hat sich 2014 durch die Aktivierung des neuen Kardinalitätsschätzers geringfügig verbessert, da die feste 1-Zeilen-Schätzung durch eine feste 100-Zeilen-Schätzung ersetzt wurde. Es gibt jedoch noch keine Unterstützung für die Kostenberechnung für Inhalte von Nicht-Inline-Funktionen.
  2. SQL Server reduziert Verknüpfungen zunächst und wendet sie auf eine einzelne interne n-fache logische Verknüpfung an. Dies hilft dem Optimierer bei der späteren Begründung von Join-Aufträgen. Die Erweiterung des Single-N-Ary-Joins zu Kandidaten-Join-Aufträgen erfolgt später und basiert weitgehend auf Heuristiken. Beispielsweise stehen innere Verknüpfungen vor äußeren Verknüpfungen, kleine Tabellen und selektive Verknüpfungen vor großen Tabellen und weniger selektiven Verknüpfungen usw.
  3. Wenn SQL Server eine kostenbasierte Optimierung durchführt, wird der Aufwand in optionale Phasen aufgeteilt, um die Wahrscheinlichkeit zu minimieren, dass zu lange für die Optimierung kostengünstiger Abfragen aufgewendet wird. Es gibt drei Hauptphasen: Suche 0, Suche 1 und Suche 2. Jede Phase hat Eingabebedingungen, und spätere Phasen ermöglichen mehr Optimierungsuntersuchungen als frühere. Ihre Anfrage qualifiziert sich zufällig für die am wenigsten geeignete Suchphase, Phase 0. Dort wird ein so niedriger Kostenplan gefunden, dass spätere Phasen nicht eingegeben werden.

In Anbetracht der geringen Kardinalitätsschätzung, die der UDF zugewiesen wurde, positionieren die Heuristiken für n-fache Join-Erweiterungen diese leider früher in der Struktur, als Sie es wünschen.

Die Abfrage eignet sich auch für die Suche 0-Optimierung, da mindestens drei Joins vorhanden sind (einschließlich trifft zu). Der endgültige physische Plan, den Sie mit dem seltsam aussehenden Scan erhalten, basiert auf dieser heuristisch abgeleiteten Verknüpfungsreihenfolge. Die Kosten sind so niedrig, dass der Optimierer den Plan als "gut genug" ansieht. Die niedrige Kostenschätzung und Kardinalität für die UDF tragen zu diesem frühen Abschluss bei.

Die Suche 0 (auch als Transaktionsverarbeitungsphase bezeichnet) zielt auf OLTP-Abfragen mit geringer Kardinalität ab, wobei die endgültigen Pläne normalerweise Joins mit verschachtelten Schleifen enthalten. Noch wichtiger ist, dass Search 0 nur eine relativ kleine Teilmenge der Erkundungsfähigkeiten des Optimierers ausführt. Diese Untermenge umfasst nicht das Ziehen und Anwenden des Abfragebaums über einen Join (Regel PullApplyOverJoin). Dies ist genau das, was im Testfall erforderlich ist, um die UDF-Anwendung über den Joins neu zu positionieren und als letzte in der Abfolge der Operationen (sozusagen) zu erscheinen.

Es gibt auch ein Problem, bei dem der Optimierer zwischen einem Join mit naiven verschachtelten Schleifen (Join-Prädikat für den Join selbst) und einem Join mit korreliertem Index (Apply) entscheiden kann, bei dem das korrelierte Prädikat mithilfe einer Indexsuche auf die Innenseite des Joins angewendet wird. Letzteres ist normalerweise die gewünschte Planform, aber der Optimierer kann beide untersuchen. Mit falschen Kosten- und Kardinalitätsschätzungen kann der nicht zutreffende NL-Join wie in den eingereichten Plänen ausgewählt werden (Erläuterung des Scans).

Es gibt also mehrere Gründe für die Interaktion, bei denen mehrere allgemeine Optimierungsfunktionen zum Einsatz kommen, die normalerweise gut funktionieren, um in kurzer Zeit gute Pläne zu finden, ohne übermäßige Ressourcen zu verbrauchen. Es reicht aus, einen der Gründe zu vermeiden, um die 'erwartete' Planform für die Beispielabfrage zu erstellen, auch bei leeren Tabellen:

Planen Sie leere Tabellen mit deaktivierter Suche 0

Es gibt keine unterstützte Möglichkeit, die Auswahl von Search 0-Plänen, die vorzeitige Beendigung des Optimierers oder die Verbesserung der Kosten für UDFs zu vermeiden (abgesehen von den begrenzten Verbesserungen im SQL Server 2014 CE-Modell hierfür). So bleiben Dinge wie Plananleitungen, manuelle Umschreibungen von Abfragen (einschließlich der TOP (1)Idee oder der Verwendung temporärer Zwischentabellen) und die Vermeidung von „Black Boxes“ mit schlechten Kosten (aus Sicht der Qualitätssicherung) wie Nicht-Inline-Funktionen.

Umschreiben CROSS APPLYals OUTER APPLYauch Arbeit, wie es zur Zeit einige der frühen Mitmach kollabiert Arbeit verhindert, aber man muss vorsichtig sein , um die ursprüngliche Abfrage Semantik zu erhalten (zB keine Ablehnung NULL-extended Zeilen , die ohne die Optimierer eingeführt, könnten zurück zu einem Kollabieren Kreuz anwenden). Sie müssen sich jedoch bewusst sein, dass dieses Verhalten nicht unbedingt stabil bleibt. Daher müssen Sie jedes Mal, wenn Sie den SQL Server patchen oder aktualisieren, derartige beobachtete Verhalten erneut testen.

Insgesamt hängt die richtige Lösung für Sie von einer Vielzahl von Faktoren ab, die wir für Sie nicht beurteilen können. Ich möchte Sie jedoch ermutigen, Lösungen in Betracht zu ziehen, die garantiert auch in Zukunft funktionieren und die nach Möglichkeit mit dem Optimierer (und nicht dagegen) zusammenarbeiten.

Paul White sagt GoFundMonica
quelle
24

Es sieht so aus, als ob dies eine kostenbasierte Entscheidung des Optimierers ist, aber eine ziemlich schlechte.

Wenn Sie PRODUCT 50000 Zeilen hinzufügen, ist der Scan nach Ansicht des Optimierers zu aufwendig, und Sie erhalten einen Plan mit drei Suchvorgängen und einem Aufruf der UDF.

Den Plan bekomme ich für 6655 Zeilen in PRODUCT

Bildbeschreibung hier eingeben

Mit 50000 Zeilen in PRODUCT erhalte ich stattdessen diesen Plan.

Bildbeschreibung hier eingeben

Ich denke, die Kosten für das Anrufen der UDF sind stark unterschätzt.

Eine Problemumgehung, die in diesem Fall problemlos funktioniert, besteht darin, die Abfrage so zu ändern, dass für die UDF Outer Apply verwendet wird. Ich bekomme den guten Plan, egal wie viele Zeilen es in der Tabelle PRODUCT gibt.

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

Bildbeschreibung hier eingeben

Die beste Lösung in Ihrem Fall besteht wahrscheinlich darin, die benötigten Werte in eine temporäre Tabelle zu übernehmen und dann die temporäre Tabelle mit einem Kreuz abzufragen, das auf die UDF angewendet wird. Auf diese Weise können Sie sicher sein, dass die UDF nicht mehr als nötig ausgeführt wird.

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

Anstatt die temporäre Tabelle beizubehalten, können Sie top()SQL Server in einer abgeleiteten Tabelle zwingen, das Ergebnis der Verknüpfungen auszuwerten, bevor die UDF aufgerufen wird. Verwenden Sie einfach eine sehr hohe Zahl im oberen Bereich, damit SQL Server die Zeilen für diesen Teil der Abfrage zählen muss, bevor die UDF verwendet werden kann.

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

Bildbeschreibung hier eingeben

Ich würde gerne verstehen, was der Grund dafür sein könnte, da alle Vorgänge mit Primärschlüsseln ausgeführt werden und wie dies behoben werden kann, wenn dies in einer komplexeren Abfrage geschieht, die nicht so einfach zu lösen ist.

Das kann ich wirklich nicht beantworten, aber ich dachte, ich sollte teilen, was ich sowieso weiß. Ich weiß nicht, warum ein Scan der PRODUCT-Tabelle überhaupt in Betracht gezogen wird. Es kann Fälle geben, in denen dies das Beste ist, und es gibt Dinge darüber, wie die Optimierer UDFs behandeln, von denen ich nichts weiß.

Eine zusätzliche Beobachtung war, dass Ihre Abfrage in SQL Server 2014 mit dem neuen Kardinalitätsschätzer einen guten Plan erhält. Dies liegt daran, dass die geschätzte Anzahl der Zeilen für jeden Aufruf der UDF 100 statt 1 beträgt, wie dies in SQL Server 2012 und früher der Fall ist. Es wird jedoch immer noch die gleiche kostenbasierte Entscheidung zwischen der Scanversion und der Suchversion des Plans getroffen. Mit weniger als 500 (in meinem Fall 497) Zeilen in PRODUCT erhalten Sie die Scanversion des Plans auch in SQL Server 2014.

Mikael Eriksson
quelle
2
Erinnert mich irgendwie an Adam Machanics Session bei SQL Bits: sqlbits.com/Sessions/Event14/…
James Z