XQuery-Singleton oder warum ein einzelnes Element nicht unbedingt als solches erkannt wird

Bei der Arbeit mit dem XML-Datentyp ab SQL Server 2005 kann es zu einer Fehlermeldung kommen, die - zumindest auf den ersten Blick - ein wenig unverständlich erscheint.

Msg 2389, Level 16, State 1, Line #
XQuery [value()]: ‘value()’ erfordert ein Singleton (oder eine leere Sequenz). Ein Operand vom ‘xdt:untypedAtomic *’-Typ wurde gefunden.

bzw.

Msg 2389, Level 16, Status 1, Line #
XQuery [value()]: ‘value()’ requires a singleton (or empty sequence), found operand of type ‘xdt:untypedAtomic *’

Dabei ist nicht die Aussage der Fehlermeldung verwirrend, sondern vielmehr die Tatsache, dass alles korrekt zu sein scheint. Dazu folgendes Beispiel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DECLARE @test xml
SET @test = '
<test>
<name>Wolfgang Kluge</name>
<links>
<link>http://klugesoftware.de/</link>
<link>http://gehirnwindung.de/</link>
<link>https://coveraje.github.io/</link>
<link>http://vbwelt.de/</link>
</links>
</test>'
SELECT @test.value('/test/links/link[2]/text()', 'varchar(200)')

Die Meldung weißt ja bereits darauf hin. Die value()-Funktion erfordert einen einzelnen Wert oder eine leere Sequenz - also eine Abfrage, die keinen oder genau einen Wert zurückgibt. Eben das macht die Abfrage aber eigentlich bereits - sie gibt einen einzelnen Wert zurück. Deswegen scheint die Fehlermeldung nicht wirklich viel Sinn machen.

Nun, möglicherweise ist ja die text()-Funktion der Auslöser. Kurz entfernt und getestet,

1
SELECT @test.value('/test/links/link[2]', 'varchar(200)')

wird aber immer noch der gleiche Fehler ausgegeben… Erfolgreich ist man erst, wenn man die gesamte Abfrage in Klammern setzt und von dieser Menge wiederum ein einziges Element auswählt. Folgende Abfragen funktionieren korrekt:

1
2
SELECT @test.value('(/test/links/link)[2]', 'varchar(200)')
SELECT @test.value('(/test/links/link[2])[1]', 'varchar(200)')

Schöner (weil meiner Meinung nach auch einfacher lesbar) ist hierbei die erste Zeile. Innerhalb der Klammer werden 3 <link>-Nodes selektiert und außerhalb die zweite aus dieser Menge ausgewählt. In der zweite Zeile bleibt bereits innerhalb der Klammer nur noch eine <link>-Node übrig, wovon dann aber trotzdem noch die erste ausgewählt werden muss…

Das Gleiche kann man auch mit Text-Nodes betreiben, solange kein XML Schema angegeben wird, in dem die <link>-Node als reines Textelement definiert ist (ansonsten kommt es zu einem anderen Fehler, weil es schlicht unnötig wäre).

1
2
SELECT @test.value('(/test/links/link/text())[2]', 'varchar(200)')
SELECT @test.value('(/test/links/link[2]/text())[1]', 'varchar(200)')

Die runden Klammern definieren hier noch Mengen von Text-Nodes, von denen (wie oben) eine einzige ausgewählt werden muss.

Wozu aber die Klammern in XPath

Es gibt natürlich auch einen Grund dafür. Angenommen, es existieren in diesem Beispiel mehrere <links>-Nodes, so kann es auch mehrere zweite <link>-Elemente im Dokument geben - und somit ist nicht sichergestellt, dass die Abfrage /test/links/link[2] einen einzelnen Wert zurückgibt. Leider ist das sogar auch dann noch so, wenn man per XML SCHEMA COLLECTION definiert, dass es nur eine einzige <links>-Node geben darf.

Auch schade ist, dass die “Erkennung”, ob nun eine Node eindeutig ausgewählt wurde, nicht alle Möglichkeiten von XPath abdeckt. Das Predikat innerhalb des letzten Knotentests muss ein Zahlwert sein (und nicht ein Vergleich, der true zurückgibt). Daher funktioniert zwar [last()], das ebenso eindeutige [position()=2] wird dagegen aber nicht als eindeutig erkannt.

Da damit jedoch sichergestellt werden kann, dass nur ein Element ausgewählt ist, funktioniert es auch, in jeder Elementhierarchie ein (mit Zahl-Predikaten) genau spezifiziertes Element auszuwählen. Das gilt allerdings auch für die text()-Funktion, weswegen man auch dort noch einen Knotentest braucht. Die Zeilen

1
2
SELECT @test.value('/test[1]/links[1]/link[2]', 'varchar(200)')
SELECT @test.value('/test[1]/links[1]/link[2]/text()[1]', 'varchar(200)')

sind somit auch gültig (ich finde es nur nicht besonders schön). Bei meinen Tests (mit erheblich mehr <link>-Nodes) war die Performancereihenfolge zudem mehr als eindeutig:

Nr.AbfrageAufwandFaktor
1(/test/links/link/text())[2]7,49%1,00
2(/test/links/link)[2]8,82%1,18
3/test[1]/links[1]/link[2]14,02%1,87
4/test[1]/links[1]/link[2]/text()[1]14,74%1,97
5(/test/links/link[2])[1]17,96%2,40
6(/test/links/link[2]/text())[1]36,98%4,94

Anhand der dazugehörigen Grafik lassen sich die Unterschiede noch leichter erkennen

Dass die letzten beiden Möglichkeiten nicht so schnell sind wie die ersten beiden, kann man noch mit den doppelten Knotentests erklären. Für mich ist (und bleibt vorerst) verwunderllich, warum die letzte Abfrage so unglaublich schlecht abschneidet. Grob scheint es aber so zu sein, dass die Abfrage länger zur Ausführung braucht, je mehr Knotentests in der Abfrage enthalten sind.

Fazit

Um ein echtes XQuery-Singleton zu bekommen, muss die eigentliche Abfrage in runde Klammern gesetzt und mit eckigen Klammern ein einzelner Datensatz aus dieser Menge gewählt werden: (xpath)[1].
Dabei sollte man der Performance zu Liebe versuchen, so viel Knotentests [] als irgend möglich aus der Abfrage zu entfernen.