XQuery jest językiem służącym do odpytywania dokumentów XML oraz wydobywania informacji z dokumentów XML. Jest o wiele bogatszy niż wyrażenia XPath który służy tylko do prostej nawigacji. Dla języka zapytań potrzeba silnika który przetwarza zapytania. SQL Serwer przetwarza XQuery w wyrażeniach T-SQL używając typu danych XML. Nie wszystkie funkcje XQuery są dostępne w SQL Serwerze. Przykładowo funkcje zdefiniowane przez użytkownika dla XQuery nie są wspierane ponieważ SQL Serwer posiada już funkcje T-SQL i CLR. Dodatkowo, T-SQL używa niestandardowego rozszerzenia do XQuery zwanego XML DML. Ponieważ typ danych XML to bardzo duży obiekt to może być on wąskim gardłem wydajnościowym jeżeli jedyną drogą do modyfikacji XML'a jest zastąpienie aktualnej wartości. T-SQL implementuje standard XQuery który jest tworzony przez World Wide Web Consortium (W3C). Ponadto używa rozszerzeń które pozwalają na modyfikację danych XML.

Podstawy XQuery

XQuery tak jak XML jest czuły na wielkość liter. XQuery zwraca sekwencje które mogą zawierać pojedyncze wartości lub złożone wartości jak węzły XML. Każdy węzeł, element, atrybut, tekst, instrukcja, komentarz lub dokument może być zawarty w sekwencji. Sekwencje można formatować aby otrzymać dobrze sformatowany XML. Poniższe przykłady pokazują różne sekwencje:

DECLARE @x AS XML;
SET @x=N'
<root>
<a>1<c>3</c><d>4</d></a>
<b>2</b>
</root>';
SELECT
@x.query('*') AS Complete_Sequence,
@x.query('data(*)') AS Complete_Data,
@x.query('data(root/a/c)') AS Element_c_Data;

Complete_Sequence Complete_Data Element_c_Data --------------------------------------------- ------------- -------------- <root><a>1<c>3</c><d>4</d></a><b>2</b></root> 1342 3 Pierwsze zapytanie używa możliwie najprostrzego wyrażenie które pobiera wszystko z instancji XML. Drugie zapytanie używa funkcji data() do wyodrębnienie wszystkich wartości atomowych z całego dokumentu. trzecie zapytanie używa funkcji data() do wyodrębnienia danych atomowych z elementu c. Każdy identyfikator w XQuery jest nazwą kwalifikowaną lub QName. QName zawiera nazwę lokalną i opcjonalnie prefiks przestrzeni nazw. W poprzednim przykładzie root, a, b, c i d były identyfikatorami Qname bez prefiksów. Poniżej przedstawione są zdefiniowane przez SQL Serwer standardowe przestrzenie nazw:

Można używać tych przestrzeni nazw bez ich definiowania. Własne typy danych definiuje się na początku zapytania XQuery w sekcji prolog którą oddziela się od zapytania średnikiem. Ponadto w T-SQL można deklarować przestrzenie nazw używane w XQuery z użyciem klauzuli WITH. Jeżeli dokument XML używa tylko jednej przestrzeni nazw to można ją zadeklarować jako domyślną w sekcji prolog. Komentarze w XQuery zapisuje się w nawiasie pomiędzy dwoma dwukropkami np: (:jakiś komentarz:). Nie wolno mieszać tego zapisu z komentarzami dokumentu XML. Poniższy kod pokazuje trzy metody deklaracji przestrzeni nazw i użycia komentarzy XQuery. Wyodrębniają one zamówienia dla pierwszego klienta z instancji XML.

DECLARE @x AS XML;
SET @x='
<CustomersOrders xmlns:co="TK461-CustomersOrders">
<co:Customer co:custid="1" co:companyname="Customer NRZBB">
<co:Order co:orderid="10692" co:orderdate="2007-10-03T00:00:00" />
<co:Order co:orderid="10702" co:orderdate="2007-10-13T00:00:00" />
<co:Order co:orderid="10952" co:orderdate="2008-03-16T00:00:00" />
</co:Customer>
<co:Customer co:custid="2" co:companyname="Customer MLTDN">
<co:Order co:orderid="10308" co:orderdate="2006-09-18T00:00:00" />
<co:Order co:orderid="10926" co:orderdate="2008-03-04T00:00:00" />
</co:Customer>
</CustomersOrders>';
-- Namespace in prolog of XQuery
SELECT @x.query('
(: explicit namespace :)
declare namespace co="TK461-CustomersOrders";
//co:Customer[1]/*') AS [Explicit namespace];
-- Default namespace for all elements in prolog of XQuery
SELECT @x.query('
(: default namespace :)
declare default element namespace "TK461-CustomersOrders";
//Customer[1]/*') AS [Default element namespace];
-- Namespace defined in WITH clause of T-SQL SELECT
WITH XMLNAMESPACES('TK461-CustomersOrders' AS co)
SELECT @x.query('
(: namespace declared in T-SQL :)
//co:Customer[1]/*') AS [Namespace in WITH clause];

Explicit namespace -------------------------------------------------------------------------------- <co:Order xmlns:co="TK461-CustomersOrders" co:orderid="10692" co:orderd Default element namespace -------------------------------------------------------------------------------- <Order xmlns="TK461-CustomersOrders" xmlns:p1="TK461-Customers Namespace in WITH clause -------------------------------------------------------------------------------- <co:Order xmlns:co="TK461-CustomersOrders" co:orderid="10692" co:orderd

Typy danych XQuery

XQuery używa około 50 zdefiniowanych typów danych. Dodatkowo implementacja pochodząca z SQL Serwera posiada przestrzeń nazw z typami SQL Serwera. typy danych XQuery dzielą się na typy węzłowe i atomowe. Typy węzłowe zawierają atrybuty, komentarze, elementy, przestrzenie nazw, tekst, instrukcje i węzły dokumentów. Najważniejsze typy atomowe to  xs:boolean, xs:string, xs:QName, xs:date, xs:time, xs:datetime, xs:float, xs:double, xs:decimal i xs:integer. 

Funkcje XQuery

Istnieją dziesiątki funkcji w XQuery. Są one podzielone na wiele kategorii. Oto najważniejsze z nich które są wspierane przez SQL Serwer:

  • Numeryczne: ceiling(), floor(), round()
  • Tekstowe: concat(), contains(), substring(), string-length(), lower-case(), uper-case()
  • Binarne: not(), true(), false()
  • Węzłowe: local-name(), namespace-uri()
  • Agregujące: count(), min(), max(), avg(), sum()
  • Dostępu do danych: data(), string()
  • Rozszerzenia SQL Serwera: sql:column(), sql:variable()

Nazwy funkcji i kategorii wskazują na ich przeznaczenie. Pełną listę funkcji z opisami można znaleźć na stronie http://msdn.microsoft.com/en-us/library/ms189254.aspx. Poniżej przykładowe zapytanie które używa funkcji count() i max() aby otrzymać informacje o zamówieniach dla każdego klienta:

DECLARE @x AS XML;
SET @x='
<CustomersOrders>
<Customer custid="1" companyname="Customer NRZBB">
<Order orderid="10692" orderdate="2007-10-03T00:00:00" />
<Order orderid="10702" orderdate="2007-10-13T00:00:00" />
<Order orderid="10952" orderdate="2008-03-16T00:00:00" />
</Customer>
<Customer custid="2" companyname="Customer MLTDN">
<Order orderid="10308" orderdate="2006-09-18T00:00:00" />
<Order orderid="10926" orderdate="2008-03-04T00:00:00" />
</Customer>
</CustomersOrders>';
SELECT @x.query('
for $i in //Customer
return
<OrdersInfo>
{ $i/@companyname }
<NumberOfOrders>
{ count($i/Order) }
</NumberOfOrders>
<LastOrder>
{ max($i/Order/@orderid) }
</LastOrder>
</OrdersInfo>
');

Wynik:

<OrdersInfo companyname="Customer NRZBB">
<NumberOfOrders>3</NumberOfOrders>
<LastOrder>10952</LastOrder>
</OrdersInfo>
<OrdersInfo companyname="Customer MLTDN">
<NumberOfOrders>2</NumberOfOrders>
<LastOrder>10926</LastOrder>
</OrdersInfo>

Jak widać XQuery jest bardziej skomplikowane niż wcześniejsze przykłady. Zapytania XQuery używają iteracji zwanych wyrażeniami XQueryFLWOR oraz formatowania zwracanego wyniku XML.

Nawigacja

Używając XQuery istnieje wiele sposobów aby przeglądać dokument XML. Najprostrzym sposobem jest użycie wyrażeń XPath. Można używać ścieżki absolutnej lub relatywnej względem aktualnego węzła. XQuery zajmuje się aktualną pozycją w dokumencie. Ścieżka składa się z jednego lub więcej kroków lokalizacji oddzielonych od siebie znakami / lub //. Jeżeli zaczyna się od / to nazywamy ją ścieżką bezwzględną gdyż całą ścieżkę podaje się względem węzła głównego. Ścieżka względna zaczyna się od bieżącego węzła.   Kompletna ścieżka może być zapisywana w takiej formie:

Node-name/child::element-name[@attribute-name = value]

Kroki są oddzielone ukośnikami dlatego ścieżka powyżej ma dwa kroki. Drugi krok skonstruowany jest z części opisujących detale. Krok może zawierać:

  • Axis - wskazuje kierunek przeszukiwania. W przykładzie osią jest child:: wskazujące węzeł child dla poprzedniego kroku.
  • Node test - badanie węzła, wyznacza kryteria dla wybieranych węzłów. W przykładzie jest nim element-name.
  • Predicate - Predykat zawężający wyszukiwanie. W przykładzie jest nim wyrażenie [@attribute-name = value].

Ważne jest, że w przykładowym predykacie jest odwołanie do atrybutu osi attribute:: . Znak małpy "@" jest skrótem dla attribute::. Można to rozumieć tak, że po dokumencie XML można się poruszać w górę i w dół (w hierarchii), w aktualnym węźle i w prawo (na aktualnym poziomie). Poniżej przedstawiona jest lista osi w SQL Serwer:

  • child:: -zwraca dzieci aktualnego węzła. Jest do domyślny axis.
  • descendant:: - pobiera wszystkich potomków aktualnego węzła.
  • self:: - pobiera aktualny węzeł.
  • descendant-or-self:: - aktualny węzeł i wszyscy potomkowie. Skrót "//".
  • attribute:: - pobiera wyznaczony atrybut aktualnego węzła. Skrót "@".
  • parent:: - pobiera rodzica aktualnego węzła. Skrót: "..".

Jako test węzłów można użyć nazw węzłów lub * do wybrania wszystkich elementów. Węzły mogą być testowane wg ich rodzaju za pomocą funkcji:

  • node() - dowolne węzły.
  • element() - dowolne elementy.
  • text() - węzły tekstowe.
  • processing-instruction() - instrukcje przetwarzania.
  • comment() - komentarze.
  • attribute() - atrybuty.

Podstawowe predykaty to numeryczne i logiczne. Numeryczne wyznaczają węzły wg pozycji. Definiuje się je w nawiasie kwadratowym [] np. /x/y[1] oznacza pierwszy element y który jest dzieckiem elementu x. Predykaty logiczne wybierają węzły które spełniają wyrażenie predykatu. Należy zwrócić uwagę na to, jak działają operatory porównania. Działają one jednocześnie na wartościach atomowych i sekwencjach. Np:

DECLARE @x AS XML = N'';
SELECT @x.query('(1, 2, 3) = (2, 4)'); -- true
SELECT @x.query('(5, 6) < (2, 4)'); -- false
SELECT @x.query('(1, 2, 3) = 1'); -- true
SELECT @x.query('(1, 2, 3) != 1'); -- true

Pierwsze wyrażenie zwróci TRUE ponieważ numer 2 jest w obu sekwencjach. Drugie wyrażenie zwróci FALSE ponieważ żadna wartość atomowa z pierwszej sekwencji nie jest mniejsza niż którakolwiek wartość w drugiej sekwencji. Aby porównywać singletony należy używać operatorów porównania wartości:General Value Description

  • = eq
  • != ne
  • < lt
  • <= le
  • > gt
  • >= ge
DECLARE @x AS XML = N'';
SELECT @x.query('(5) lt (2)'); -- false
SELECT @x.query('(1) eq 1'); -- true
SELECT @x.query('(1) ne 1'); -- false
GO
DECLARE @x AS XML = N'';
SELECT @x.query('(2, 2) eq (2, 2)'); -- error
GO

XQuery pozwala również na użycie wyrażenia if w postaci:

if (<wyrażenie1>)
then
<wyrażenie2>
else
<wyrażenie3>

Wyrażenie to nie jest wyrażeniem mającym na celu zmianę przepływu zapytania QXuery tylko wyrażeniem pełniącym funkcję wyrażenia logicznego np.

DECLARE @x AS XML = N'
<Employee empid="2">
<FirstName>fname</FirstName>
<LastName>lname</LastName>
</Employee>
';
DECLARE @v AS NVARCHAR(20) = N'FirstName';
SELECT @x.query('
if (sql:variable("@v")="FirstName") then
/Employee/FirstName
else
/Employee/LastName
') AS FirstOrLastName;
GO

Wyrażenia FLWOR

Prawdziwą potęgą XQuery są wyrażenia FLWOR - jest to skrót od FOR, LET, WHERE, ORDER BY i RETURN. Wyrażenia te są tak naprawdę pętlą FOREACH. Używa się ich do iteracji sekwencji zwróconych przez wyrażenie XPath.

  • FOR - w tej klauzuli tworzy się zmienną iteracyjną operującą na sekwencji wejściowej.
  • LET - w tej klauzuli przypisujemy wartości do zmiennych dla iteracji. Te wyrażenia mogą zwracać zarówno sekwencje węzłów jak i wartości atomowe.
  • WHERE - klauzula ta służy do filtrowania iteracji.
  • ORDER BY - klauzula która wyznacza kolejność przetwarzania sekwencji wejściowej. Kolejność kontrolowana jest na podstawie wartości atomowych.
  • RETURN - jest to klauzula która jest przeliczana w każdej iteracji i zwracana do klienta. W tej klauzuli formatuje się zwracany XML.

Przykład użycia klauzuli FLWOR:

DECLARE @x AS XML;
SET @x = N'
<CustomersOrders>
<Customer custid="1">
<!-- Comment 111 -->
<companyname>Customer NRZBB</companyname>
<Order orderid="10692">
<orderdate>2007-10-03T00:00:00</orderdate>
</Order>
<Order orderid="10702">
<orderdate>2007-10-13T00:00:00</orderdate>
</Order>
<Order orderid="10952">
<orderdate>2008-03-16T00:00:00</orderdate>
</Order>
</Customer>
<Customer custid="2">
<!-- Comment 222 -->
<companyname>Customer MLTDN</companyname>
<Order orderid="10308">
<orderdate>2006-09-18T00:00:00</orderdate>
</Order>
<Order orderid="10952">
<orderdate>2008-03-04T00:00:00</orderdate>
</Order>
</Customer>
</CustomersOrders>';
SELECT @x.query('for $i in CustomersOrders/Customer/Order
let $j := $i/orderdate
where $i/@orderid < 10900
order by ($j)[1]
return
<Order-orderid-element>
<orderid>{data($i/@orderid)}</orderid>
{$j}
</Order-orderid-element>')
AS [Filtered, sorted and reformatted orders with let clause];

Wynik:

<Order-orderid-element>
<orderid>10308</orderid>
<orderdate>2006-09-18T00:00:00</orderdate>
</Order-orderid-element>
<Order-orderid-element>
<orderid>10692</orderid>
<orderdate>2007-10-03T00:00:00</orderdate>
</Order-orderid-element>
<Order-orderid-element>
<orderid>10702</orderid>
<orderdate>2007-10-13T00:00:00</orderdate>
</Order-orderid-element>

Zapytanie iteruje na wszystkich węzłach zamówień używając zmiennej iteracyjnej $i w klauzuli FOR, która zwraca te węzły. Nazwa zmiennej musi zaczynać się od znaku $. Klauzula WHERE ogranicza węzły do tych które posiadają identyfikator zamówienia mniejszy niż 10900. Wyrażenie w klauzuli ORDER BY musi zwracać wartość typu kompatybilnego z operatorem gt XQuery. W tym przypadku zwraca wartość atomową. Zapytanie jest wg daty zamówienia. Nawet jeżeli istnieje tylko jeden element daty dla zamówienia to XQuery tego nie wie i traktuje go jak sekwencję. Używając nawiasu kwadratowego z wartością numeryczną wskazujemy na element sekwencji, w tym wypadku na pierwszy element. Klauzula RETURN formatuje zwracany XML. Tworzy węzeł <orderid> i wstawia do niego wartość identyfikatora zamówienia z pomocą funkcji data(). Zwraca również element daty zamówienia i otacza oba elementem <Order-orderid-element>. Ważnym elementem są nawiasy {} otaczające wyrażenie z elementami identyfikatora i daty zamówienia. XQuery wylicza wyrażenia w takich nawiasach, wszystkie pozostałe wyrażenia traktuje jako literały. Klauzula LET przypisuje do zmiennej $j wyrażenie $i/orderdate. XQuery wstawia wyrażenie ze zmiennej zadeklarowanej w LET ilekroć ta zostanie użyta.

Ćwiczenia

I. Użycie wyrażeń XPath do nawigacji w XQuery.

  1. Utwórz instancję dokumentu XML która jest poniżej:
    DECLARE @x AS XML;
    SET @x = N'
    
    
    
    Customer NRZBB
    
    2007-10-03T00:00:00
    
    
    2007-10-13T00:00:00
    
    
    2008-03-16T00:00:00
    
    
    
    
    Customer MLTDN
    
    2006-09-18T00:00:00
    
    
    2008-03-04T00:00:00
    
    
    ';
    
  2. Napisz zapytanie które pobierze klientów z węzłami potomnymi. Wybierz tylko główne węzły.
    SELECT @x.query('CustomersOrders/Customer/*')
    AS [1. Principal nodes];
    
    Wynik:
    Customer NRZBB
    
    2007-10-03T00:00:00
    
    
    2007-10-13T00:00:00
    
    
    2008-03-16T00:00:00
    
    Customer MLTDN
    
    2006-09-18T00:00:00
    
    
    2008-03-04T00:00:00
    
    
  3. Napisz zapytanie które pobierze klientów ze wszystkimi węzłami potomnymi (nie tylko z głównymi).
    SELECT @x.query('CustomersOrders/Customer/node()')
    AS [2. All nodes];
    
    Wynik:
    
    Customer NRZBB
    
    2007-10-03T00:00:00
    
    
    2007-10-13T00:00:00
    
    
    2008-03-16T00:00:00
    
    
    Customer MLTDN
    
    2006-09-18T00:00:00
    
    
    2008-03-04T00:00:00
    
    
  4. Napisz zapytanie które zwróci tylko komentarze
    SELECT @x.query('CustomersOrders/Customer/comment()')
    AS [3. Comment nodes];
    
    Wynik:
    
    
    

II. Użycie wyrażeń XPath z predykatami.

  1. Utwórz instancję dokumentu XML która jest poniżej:
    DECLARE @x AS XML;
    SET @x = N'
    
    
    
    Customer NRZBB
    
    2007-10-03T00:00:00
    
    
    2007-10-13T00:00:00
    
    
    2008-03-16T00:00:00
    
    
    
    
    Customer MLTDN
    
    2006-09-18T00:00:00
    
    
    2008-03-04T00:00:00
    
    
    ';
    
  2. Napisz zapytanie zwracające zamówienia dla klienta z numerem 2.
    SELECT @x.query('//Customer[@custid=2]/Order')
    AS [4. Customer 2 orders];
    
    Wynik:
    2006-09-18T00:00:00
    
    
    2008-03-04T00:00:00
    
    
  3. Napisz zapytanie zwracające wszystkie zamówienia z numerem 10952.
    SELECT @x.query('//Order[@orderid=10952]')
    AS [5. Orders with orderid=10952];
    
    Wynik:
    2008-03-16T00:00:00
    
    
    2008-03-04T00:00:00
    
    
  4. Napisz zapytanie zwracające drugiego klienta który posiada co najmniej jedno zamówienie.
    SELECT @x.query('(/CustomersOrders/Customer/Order/parent::Customer)[2]')
    AS [6. 2nd Customer with at least one Order];
    
    Wynik:
    
    Customer MLTDN
    
    2006-09-18T00:00:00
    
    
    2008-03-04T00:00:00
    
    
    

Podsumowanie

  1. Możesz używać XQuery wewnątrz zapytań T-SQL do zapytań operujących na dokumentach XML
  2. XQuery posiada własne typy danych i funkcje
  3. Do nawigacji po dokumencie XML służą wyrażenia XPath
  4. Wyrażenia FLWOR pozwalają na tworzenie zaawansowanych zapytań na dokumentach XML