Jürgen Auer
Legendäres Mitglied
Beim Microsoft-SqlServer 2005, auf dem server-daten basiert, gibt es eine tückische Geschichte mit UNION in Unterabfragen.
Aufgabenstellung: Eine Abfrage soll bsp. zu einem Nutzer genau eine Zeile zurückliefern. Wenn bsp. kein Nutzer angemeldet ist, soll 0 zurückgegeben werden. Notwendig ist dies bsp., falls im Rahmen einer Update-Anweisung der neue Wert nach einem komplizierten Verfahren ermittelt werden muß und zu jeder Zeile deterministisch genau ein neuer Wert benötigt wird.
Übliches Verfahren:
CODE Select Top 1 A.TabellenId, 1 As pos
From Tabelle As A
Where A._Owner = @USER_ID
Union
Select 0, 2
Order By pos, A.TabellenId
Wenn in der Tabelle eine Zeile drin ist, die @USER_ID als Owner gehört, dann wird die erste ID ausgegeben. Gibt es keinen Datensatz, der @USER_ID gehört, wird eine 0 geliefert. Analog kann nach 'Union' eine komplexe Abfrage notiert sein. Aufgrund der Where-Bedingungen ist sichergestellt, daß entweder nur die obere oder nur die untere Abfrage Zeilen liefert.
Nun gibt es Fälle, bei denen jede der Einzelabfragen mehrere Zeilen zurückliefern, bei denen weitere Verknüpfungen gewünscht sind usw. Auch für die oben erwähnte UPDATE-Aufgabe muß diese Abfrage als Unterabfrage notiert werden, so daß sie im JOIN-Abschnitt als Tabelle verfügbar ist. Also den obigen Code in eine Unterabfrage, mit Top 1 eine Zeile ausliefern. Zwei Alternativen:
CODE Select Top 1 B.TabellenId
From
(Select Top 1 A.TabellenId, 1 As pos
From Tabelle As A
Where A._Owner = @USER_ID
Union
Select 0, 2
Order By pos, A.TabellenId) As B
oder
CODE Select Top 1 B.TabellenId
From
(Select A.TabellenId, 1 As pos
From Tabelle As A
Where A._Owner = @USER_ID
Union
Select 0, 2) As B
Order By B.pos, B.TabellenId
Einer der beiden Codes führt unter bestimmten Umständen (bsp. mehrere Unionabschnitte, dort weitere verknüpfte Tabellen) zu beliebig falschen Ergebnissen.
Welcher? Warum ist das so? Wo steckt der Denkfehler?
PS: Das Problem wurde in einem Codeteil sichtbar, der nach dem Parsen einer vom Benutzer definierbaren Abfrage (mit Unterabfragen und Union) und nach deren Abbilden auf einen binären Baum versuchte, zu jedem Baumknoten die Schachtelungsebene zu bestimmen. Dieser - rückblickend fehlerhafte - Code läuft seit zwei Jahren und wurde schon für hunderte von Abfragen genutzt, ohne daß der Fehler sichtbar wurde (das knallt dann sofort). Das nichtdeterministische Verhalten der obigen - falschen - Variante wird erst sichtbar, wenn Abfragen mit mehreren tausend Zeichen verarbeitet werden oder wenn eine Abfrage 'sich wiederholende Unterabfragen' enthält.
Aufgabenstellung: Eine Abfrage soll bsp. zu einem Nutzer genau eine Zeile zurückliefern. Wenn bsp. kein Nutzer angemeldet ist, soll 0 zurückgegeben werden. Notwendig ist dies bsp., falls im Rahmen einer Update-Anweisung der neue Wert nach einem komplizierten Verfahren ermittelt werden muß und zu jeder Zeile deterministisch genau ein neuer Wert benötigt wird.
Übliches Verfahren:
CODE Select Top 1 A.TabellenId, 1 As pos
From Tabelle As A
Where A._Owner = @USER_ID
Union
Select 0, 2
Order By pos, A.TabellenId
Wenn in der Tabelle eine Zeile drin ist, die @USER_ID als Owner gehört, dann wird die erste ID ausgegeben. Gibt es keinen Datensatz, der @USER_ID gehört, wird eine 0 geliefert. Analog kann nach 'Union' eine komplexe Abfrage notiert sein. Aufgrund der Where-Bedingungen ist sichergestellt, daß entweder nur die obere oder nur die untere Abfrage Zeilen liefert.
Nun gibt es Fälle, bei denen jede der Einzelabfragen mehrere Zeilen zurückliefern, bei denen weitere Verknüpfungen gewünscht sind usw. Auch für die oben erwähnte UPDATE-Aufgabe muß diese Abfrage als Unterabfrage notiert werden, so daß sie im JOIN-Abschnitt als Tabelle verfügbar ist. Also den obigen Code in eine Unterabfrage, mit Top 1 eine Zeile ausliefern. Zwei Alternativen:
CODE Select Top 1 B.TabellenId
From
(Select Top 1 A.TabellenId, 1 As pos
From Tabelle As A
Where A._Owner = @USER_ID
Union
Select 0, 2
Order By pos, A.TabellenId) As B
oder
CODE Select Top 1 B.TabellenId
From
(Select A.TabellenId, 1 As pos
From Tabelle As A
Where A._Owner = @USER_ID
Union
Select 0, 2) As B
Order By B.pos, B.TabellenId
Einer der beiden Codes führt unter bestimmten Umständen (bsp. mehrere Unionabschnitte, dort weitere verknüpfte Tabellen) zu beliebig falschen Ergebnissen.
Welcher? Warum ist das so? Wo steckt der Denkfehler?
PS: Das Problem wurde in einem Codeteil sichtbar, der nach dem Parsen einer vom Benutzer definierbaren Abfrage (mit Unterabfragen und Union) und nach deren Abbilden auf einen binären Baum versuchte, zu jedem Baumknoten die Schachtelungsebene zu bestimmen. Dieser - rückblickend fehlerhafte - Code läuft seit zwei Jahren und wurde schon für hunderte von Abfragen genutzt, ohne daß der Fehler sichtbar wurde (das knallt dann sofort). Das nichtdeterministische Verhalten der obigen - falschen - Variante wird erst sichtbar, wenn Abfragen mit mehreren tausend Zeichen verarbeitet werden oder wenn eine Abfrage 'sich wiederholende Unterabfragen' enthält.