Unvorhersehbare SQL Server-Auswahlergebnisse (DBMS-Fehler?)

37

Im Folgenden finden Sie ein einfaches Beispiel, das seltsame Ergebnisse liefert, die nicht vorhersehbar sind und die wir in unserem Team nicht erklären können. Tun wir etwas falsch oder ist es ein SQL Server-Fehler?

Nach einigen Nachforschungen haben wir den Suchbereich in der Unterabfrage auf union-Klausel reduziert , die einen Datensatz aus der Tabelle "men" auswählt

Es funktioniert wie erwartet in SQL Server 2000 (gibt 12 Zeilen zurück), aber in 2008 und 2012 gibt es nur eine Zeile zurück.

create table dual (dummy int)

insert into dual values (0)

create table men (
man_id int,
wife_id int )

-- there are 12 men, 6 married 
insert into men values (1, 1)
insert into men values (2, 2)
insert into men values (3, null)
insert into men values (4, null)
insert into men values (5, null)
insert into men values (6, 3)
insert into men values (7, 5)
insert into men values (8, 7)
insert into men values (9, null)
insert into men values (10, null)
insert into men values (11, null)
insert into men values (12, 9)

Dies gibt nur eine Zeile zurück: 1 1 2

select 
man_id,
wife_id,
(select count( * ) from 
    (select dummy from dual
     union select men.wife_id  ) family_members
) as family_size
from men
--where wife_id = 2 -- uncomment me and try again

Die letzte Zeile auskommentieren und es gibt: 2 2 2

Es gibt viele merkwürdige Verhaltensweisen:

  • Nach einer Reihe von Abwürfen, Erstellen, Abschneiden und Einfügen in "Männer" -Tabellen funktioniert dies manchmal (es werden 12 Zeilen zurückgegeben).
  • Wenn Sie "union select men.wife_id" in "union all select men.wife_id" oder "union select isnull (men.wife_id, null)" (!!!) ändern, werden (wie erwartet) 12 Zeilen zurückgegeben.
  • Das seltsame Verhalten scheint nichts mit dem Datentyp der Spalte "wife_id" zu tun zu haben. Wir haben es auf Entwicklungssystemen mit viel größeren Datenmengen beobachtet.
  • "where wife_id> 0" gibt 6 Zeilen zurück
  • bei solchen aussagen beobachten wir auch ein merkwürdiges verhalten der ansichten. SELECT * gibt eine Teilmenge von Zeilen zurück, SELECT TOP 1000 gibt alle zurück
Ryszard Bocian
quelle

Antworten:

35

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:

  1. Verschachtelte Schleifen mit einem äußeren Verweis (Anwenden)
  2. Eine innere Lazy-Index-Spule, die nach der äußeren Referenz sucht
  3. Ein innerer Verkettungsoperator

Die Abfrage in der Frage erzeugt beispielsweise einen Plan wie den folgenden:

Kommentierter Plan

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 PrepRecomputeSchnittstellenmethoden. Zu Beginn dieser Prüfung lesen die Operatoren der Innenseite die CParamBounds:FNeedToReloadEigenschaft, 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:

CParamBounds: FNeedToReload

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:FNeedToReloadimmer 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 RewindHelperMethode und niemals seine ReloadHelperMethode aus (reload = rebind in diesem Kontext). Dies ist im Ausführungsplan ersichtlich, da alle Operatoren unter dem Spool 'Anzahl der Ausführungen = 1' haben.

RewindHelper

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;

Union Plan zusammenführen

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:

Verkettungsplan

Und das Ergebnis ist jetzt falsch, nur drei Zeilen:

Dreireihiges Ergebnis

Obwohl dieses Verhalten nicht garantiert ist, hat die erste Zeile des Clustered Index Scan den c1Wert 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:

8-zeiliger Verkettungsplan

Und, wie angegeben, werden 8 Reihen produziert, alle c1 = 1natürlich mit:

8 Zeilen Ergebnis

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.

Paul White sagt GoFundMonica
quelle
-3

Warum verwenden Sie eine Unterabfrage ohne from-Anweisung? Ich denke, das kann den Unterschied in 2005 und 2008 Servern verursachen. Vielleicht könntest du explizit mitmachen?

select 
m1.man_id,
m1.wife_id,
(select count( * ) from 
    (select dummy from dual
     union
     select m2.wife_id
     from men m2
     where m2.man_id = m1.man_id) family_members
) as family_size
from men m1

quelle
3
Ja, das funktioniert, aber meine Version sollte auch funktionieren. Das obige abstrakte Beispiel ist eine stark vereinfachte Version unserer Produktionsabfrage, die weitaus sinnvoller ist.