Tun wir etwas falsch oder ist es ein SQL Server-Fehler?
Es handelt sich um einen Fehler mit falschen Ergebnissen, den Sie über Ihren üblichen Support-Kanal melden sollten. Wenn Sie keine Supportvereinbarung haben, kann es hilfreich sein zu wissen, dass bezahlte Vorfälle normalerweise erstattet werden, wenn Microsoft das Verhalten als Fehler bestätigt.
Der Bug benötigt drei Zutaten:
- Verschachtelte Schleifen mit einem äußeren Verweis (Anwenden)
- Eine innere Lazy-Index-Spule, die nach der äußeren Referenz sucht
- Ein innerer Verkettungsoperator
Die Abfrage in der Frage erzeugt beispielsweise einen Plan wie den folgenden:
Es gibt viele Möglichkeiten, eines dieser Elemente zu entfernen, sodass der Fehler nicht mehr reproduziert wird.
Beispielsweise könnten Indizes oder Statistiken erstellt werden, die bedeuten, dass der Optimierer keine Lazy-Index-Spool verwendet. Sie können auch Hinweise verwenden, um einen Hash oder eine Zusammenführungsvereinigung zu erzwingen, anstatt Verkettung zu verwenden. Man könnte die Abfrage auch umschreiben, um dieselbe Semantik auszudrücken, was jedoch zu einer anderen Planform führt, bei der eines oder mehrere der erforderlichen Elemente fehlen.
Mehr Details
Ein Lazy Index Spool speichert die Ergebniszeilen der Innenseite träge in einer Arbeitstabelle zwischen, die durch äußere Referenzwerte (korrelierte Parameter) indiziert ist. Wenn eine Lazy-Index-Spool nach einer äußeren Referenz gefragt wird, die sie zuvor gesehen hat, ruft sie die zwischengespeicherte Ergebniszeile aus ihrer Arbeitstabelle ab (ein "Zurückspulen"). Wenn der Spool nach einem äußeren Referenzwert gefragt wird, den er zuvor noch nicht gesehen hat, führt er seinen Teilbaum mit dem aktuellen äußeren Referenzwert aus und speichert das Ergebnis zwischen (ein "Rebind"). Das Suchprädikat in der Lazy-Index-Spool gibt die Schlüssel für ihre Arbeitstabelle an.
Das Problem tritt in dieser speziellen Planform auf, wenn die Spule prüft, ob eine neue äußere Referenz dieselbe ist, die sie zuvor gesehen hat. Der Join mit verschachtelten Schleifen aktualisiert seine äußeren Referenzen korrekt und benachrichtigt Operatoren über seine inneren Eingaben über ihre PrepRecompute
Schnittstellenmethoden. Zu Beginn dieser Prüfung lesen die Operatoren der Innenseite die CParamBounds:FNeedToReload
Eigenschaft, um festzustellen, ob sich die äußere Referenz gegenüber dem letzten Mal geändert hat. Ein Beispiel für einen Stack-Trace ist unten dargestellt:
Wenn der oben gezeigte Unterbaum vorhanden ist, insbesondere wenn Verkettung verwendet wird, tritt bei den Bindungen ein Fehler auf (möglicherweise ein ByVal / ByRef / Copy-Problem), sodass CParamBounds:FNeedToReload
immer false zurückgegeben wird, unabhängig davon, ob sich die äußere Referenz tatsächlich geändert hat oder nicht.
Wenn derselbe Teilbaum vorhanden ist, aber eine Zusammenführungs- oder Hash-Union verwendet wird, wird diese wesentliche Eigenschaft bei jeder Iteration korrekt festgelegt, und die Lazy-Index-Spool wird bei Bedarf zurückgespult oder neu gebunden. Übrigens sind Distinct Sort und Stream Aggregate untadelig. Mein Verdacht ist, dass Merge und Hash Union eine Kopie des vorherigen Werts erstellen, während Concatenation einen Verweis verwendet. Ohne Zugriff auf den SQL Server-Quellcode ist dies leider kaum zu überprüfen.
Das Nettoergebnis ist, dass der Lazy Index Spool in der problematischen Planform immer denkt, er habe die aktuelle äußere Referenz bereits gesehen, spult zurück, indem er in seine Arbeitstabelle sucht, findet im Allgemeinen nichts, sodass für diese äußere Referenz keine Zeile zurückgegeben wird. Beim Durchlaufen der Ausführung in einem Debugger führt der Spool immer nur seine RewindHelper
Methode und niemals seine ReloadHelper
Methode aus (reload = rebind in diesem Kontext). Dies ist im Ausführungsplan ersichtlich, da alle Operatoren unter dem Spool 'Anzahl der Ausführungen = 1' haben.
Die Ausnahme ist natürlich, dass für die erste äußere Referenz die Lazy Index Spool angegeben wird. Dadurch wird immer der Teilbaum ausgeführt und eine Ergebniszeile in der Arbeitstabelle zwischengespeichert. Alle nachfolgenden Iterationen führen zu einem Zurückspulen, wodurch nur dann eine Zeile (die einzelne zwischengespeicherte Zeile) erzeugt wird, wenn die aktuelle Iteration denselben Wert für die äußere Referenz hat wie beim ersten Mal.
Daher gibt die Abfrage für alle Eingaben, die auf der Außenseite des Nested Loops Join festgelegt sind, so viele Zeilen zurück, wie Duplikate der ersten verarbeiteten Zeile vorhanden sind (plus natürlich eine für die erste Zeile selbst).
Demo
Tabellen- und Beispieldaten:
CREATE TABLE #T1
(
pk integer IDENTITY NOT NULL,
c1 integer NOT NULL,
CONSTRAINT PK_T1
PRIMARY KEY CLUSTERED (pk)
);
GO
INSERT #T1 (c1)
VALUES
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6);
Die folgende (triviale) Abfrage ergibt eine korrekte Anzahl von zwei für jede Zeile (insgesamt 18) unter Verwendung einer Zusammenführungsunion:
SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY
(
SELECT COUNT_BIG(*) AS c1
FROM
(
SELECT T1.c1
UNION
SELECT NULL
) AS U
) AS C;
Wenn wir jetzt einen Abfragehinweis hinzufügen, um eine Verkettung zu erzwingen:
SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY
(
SELECT COUNT_BIG(*) AS c1
FROM
(
SELECT T1.c1
UNION
SELECT NULL
) AS U
) AS C
OPTION (CONCAT UNION);
Der Ausführungsplan hat die problematische Form:
Und das Ergebnis ist jetzt falsch, nur drei Zeilen:
Obwohl dieses Verhalten nicht garantiert ist, hat die erste Zeile des Clustered Index Scan den c1
Wert 1. Es gibt zwei weitere Zeilen mit diesem Wert, sodass insgesamt drei Zeilen erstellt werden.
Kürzen Sie nun die Datentabelle und laden Sie sie mit weiteren Duplikaten der 'ersten' Zeile:
TRUNCATE TABLE #T1;
INSERT #T1 (c1)
VALUES
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6),
(1), (1), (1), (1), (1), (1);
Jetzt lautet der Verkettungsplan:
Und, wie angegeben, werden 8 Reihen produziert, alle c1 = 1
natürlich mit:
Ich stelle fest, dass Sie ein Connect-Element für diesen Fehler geöffnet haben, aber dies ist nicht der richtige Ort, um Probleme mit Auswirkungen auf die Produktion zu melden. In diesem Fall sollten Sie sich unbedingt an den Microsoft-Support wenden.
Dieser Fehler mit falschen Ergebnissen wurde irgendwann behoben. Ab 2012 ist für mich keine SQL Server-Version mehr verfügbar. Auf SQL Server 2008 R2 SP3-GDR Build 10.50.6560.0 (X64) wird ein Repro ausgeführt.