FLWOR-выражения
FLWOR расшифровывается как for let where order by return. К сожалению, SQL Server не поддерживает ключевых слов LET и ORDER BY.
Бессмысленно говорить об отличиях в реализации SQL Server от текущего черновика стандарта, потому что фактически у них нет ничего общего, кроме трех ключевых слов – for, where и return.
FLWOR-выражения, являются, пожалуй самым мощным средством обработки последовательностей в XQuery. Синтаксис выражения в SQL Server примерно следующий (примерно, потому что в документации по SQL Server нигде не указан точный синтаксис FLWOR-выражения):
("for" "$" VariableName "in" Expression) + ("where" WhereExpression)? "return" ReturnExpression |
Где:
- VariableName – имя переменной.
- Expression – выражение XQuery, возвращающее последовательность.
Смысл выражения FLWOR: в цикле каждый элемент последовательности, возвращаемой Expression, связывается с переменной, чье имя задается в VariableName. Далее, в каждой итерации выполняется выражение WhereExpression, и если его результат равен true – выполняется выражение ReturnExpression. Важно понимать, что ReturnExpression выполняется для каждогоэлемента последовательности, т.е. n раз, где n – размер последовательности. Если выражение WhereExpression вернуло false, выполняется следующая итерация. Общее количество итераций равно размеру последовательности Expression.
Названия переменных в XQuery всегда начинаются со знака доллара. Объявлять переменные можно только в FLWOR-выражениях, кванторных выражениях (см. ниже), в прологе запроса и при объявлении параметров функций. SQL Server не поддерживает переменные в прологе запроса и функции, так что объявить переменную можно только в FLWOR-выражениях и кванторных выражениях.
В самом FLWOR-выражении переменную можно объявлять в разделе for и в разделе let. Так как раздел let не поддерживается в SQL Server, переменные можно объявлять только в разделе for. Примеры объявления переменных в настоящем FLWOR-выражении находятся в конце этого раздела.
FLWOR-выражение в SQL Server нельзя применять для связывания нескольких XML-документов, так как отсутствует функция doc, которая возвращает узел документа по заданному URI. Возможно, в финальной версии появится подобная функция или функция расширения, которая будет принимать значение первичного ключа для данной таблицы и имя XML-поля. Фактически же, текущая версия вообще не поддерживает узел документа.
Пожалуй, FLWOR-выражение в SQL Server можно применять только для фильтрации элементов последовательности и трансформации структуры XML-фрагмента. В следующем примере демонстрируется и то, и другое: сначала выполняется фильтрация, а затем конструирование последовательности узлов элемента b.
declare @xml xmlset @xml = '<a>10</a>, <a>2</a>, <a>3</a>'select @xml::query(' for $a in /a where $a < 10 return <b>{$a/text()}</b>') |
Результат:
<b>2</b><b>3</b> |
Выражения FLWOR могут быть вложенными. Например:
declare @xml xmlset @xml = '<a>10</a>, <a>2</a>, <b>4</b>, <a>3</a>, <b>6</b>'select @xml::query('{-- для каждого элемента а --}for $a in /a {-- для каждого элемента b --} for $b in /b {-- этот раздел return относится к внутреннему циклу for Поэтому исполняется 3 * 2 раз --} return <r> <expression>{$a/text()} * {$b/text()}</expression> <equals>{$a*$b}</equals> </r>') |
Здесь во внешнем цикле производится перебор элементов а, а во внутреннем – элементов b, и все элементы a умножаются на все элементы b. Результат:
<r><expression>10 * 4</expression><equals>40</equals></r><r><expression>10 * 6</expression><equals>60</equals></r><r><expression>2 * 4</expression><equals>8</equals></r><r><expression>2 * 6</expression><equals>12</equals></r><r><expression>3 * 4</expression><equals>12</equals></r><r><expression>3 * 6</expression><equals>18</equals></r> |
Вследствие того, что SQL Server очень неважно соответствует стандарту, у вас может сложиться обманчивое впечатление, что выражения FLWOR не так круты, как их рекламируют. Ниже я попробую продемонстрировать возможности FLWOR из последнего черновика стандарта.
В качестве исходных данных будем использовать небольшую базу данных по форумам RSDN из статьи [6], которая, допустим, находится в файле forums.xml:
<?xml version="1.0" encoding="windows-1251" ?><rsdn> <forums date="09.01.03"> <forum name="WinAPI" totalposts="16688" description="Системное программирование"> <moderators/> <top-poster>Alex Fedotov</top-poster> </forum> <forum name="COM" totalposts="10116" description="Компонентные технологии"> <moderators/> <top-poster>Vi2</top-poster> </forum> <forum name="Delphi" totalposts="5001" description="Delphi и Builder"> <moderators> <moderator name="Sinclair"/> <moderator name="Hacker_Delphi"/> </moderators> <top-poster>Sinclair</top-poster> </forum> <forum name="DB" totalposts="6606" description="Базы данных"> <moderators> <moderator name="_MarlboroMan_"/> </moderators> <top-poster>Merle</top-poster> </forum> </forums></rsdn> |
И базу данных пользователей, которая находится в файле users.xml:
<?xml version="1.0" encoding="windows-1251"?><rsdn date="20/04/2004"> <users> <user nickname="Alexey Shirshov" name="Ширшов Алексей Николаевич" posts="4327" articles="16" rate="5779" total-rating="27"> <location from="Уфа" workin="Москва"/> </user> <user nickname="Alex Fedotov" name="Alex Fedotov" posts="2617" articles="11" rate="6885" total-rating="31"> <location from="" workin="San Francisco"/> </user> <user nickname="Vi2" name="Шарахов Виктор" posts="3094" articles="1" rate="5922" total-rating="26"> <location from="Ижевск" workin="Ижевск"/> </user> <user nickname="Sinclair" name="Антон Злыгостев" posts="4325" articles="5" rate="11136" total-rating="39"> <location from="Новосибирск" workin="Новосибирск"/> </user> <user nickname="Hacker_Delphi" name="Michael Polyudov" posts="2740" articles="2" rate="1880" total-rating="12"> <location from="Новосибирск" workin="Новосибирск"/> </user> <user nickname="_MarlboroMan_" name="Полюдов Дмитрий Петрович" posts="2104" articles="1" rate="2798" total-rating="13"> <location from="Новосибирск" workin="Новосибирск"/> </user> <user nickname="Merle" name="Ivan D. Bodiaguine" posts="2049" articles="5" rate="4974" total-rating="23"> <location from="Москва" workin="Москва"/> </user> </users></rsdn> |
Выберем всех пользователей, которые являются модераторами форумов:
(: для каждого модератора :)for $moders in doc("forums.xml")/rsdn/forums/forum/moderators/moderator/@name (: загружаем его профиль по имени :)let $user := doc("users.xml")/rsdn/users/user[@nickname = $moders] (: и копируем его в результирующую последовательность :)return $user |
Теперь выберем всех пользователей, которые являются модераторами с количеством баллов больше среднего:
(: выбираем всех пользователей :)let $user := doc("users.xml")/rsdn/users/user (: считаем средний бал для всех пользователей :)let $avg-rate := avg($user/@rate) (: в цикле для каждого модератора :)for $moders in doc("forums.xml")/rsdn/forums/forum/moderators/moderator/@name (: находим соответствующий ему профиль :)let $moder-user := $user[@nickname = $moders] (: и фильтруем по баллам :)where $moder-user/@rate > $avg-rate (: копируем модератора в результирующую последовательность :)return $moder-user |
Следующий запрос возвращает модифицированный элемент top-poster из файла forums.xml:
(: для каждого топ-постера :)for $top-poster in doc("forums.xml")/rsdn/forums/forum/top-poster (: получаем его профиль :)let $user := doc("users.xml")/rsdn/users/user[@nickname = $top-poster]return (: атрибут nick - имя пользователя :)<top-poster nick="{$top-poster}"> <name> { (: копируем атрибут name с использованием конструктора xs:string (имя пользователя в миру) :) xs:string($user/@name) }</name> { (: копируем элемент location из профиля пользователя :) $user/location } <rating> { (: копируем значение атрибута total-rating :) $user/@total-rating }</rating></top-poster> |
Результат:
<?xml version="1.0" encoding="UTF-8"?><top-poster nick="Alex Fedotov"> <name>Alex Fedotov</name> <location from="" workin="San Francisco"/> <rating total-rating="31"/></top-poster><top-poster nick="Vi2"> <name>Шарахов Виктор</name> <location from="Ижевск" workin="Ижевск"/> <rating total-rating="26"/></top-poster><top-poster nick="Sinclair"> <name>Антон Злыгостев</name> <location from="Новосибирск" workin="Новосибирск"/> <rating total-rating="39"/></top-poster><top-poster nick="Merle"> <name>Ivan D. Bodiaguine</name> <location from="Москва" workin="Москва"/> <rating total-rating="23"/></top-poster> |
И последний запрос: выберем сумму балов пользователей, проживающих в одном городе:
(: для каждого города :)for $cities in distinct-values(doc("users.xml")/rsdn/users/user/location/@workin) (: найдем пользователей, в нем проживающих :)let $users-in-the-same-city := doc("users.xml")/rsdn/users/user[location/@workin = $cities] (: отсортируем выходную последовательность в порядке, обратном сумме балов пользователей данного города :)order by sum($users-in-the-same-city/@rate) descending (: сконструируем результат :)return <city name="{$cities}" total-rate="{sum($users-in-the-same-city/@rate)}" /> |
Вот результат этого запроса:
<?xml version="1.0" encoding="UTF-8"?><city name="Новосибирск" total-rate="15814"/><city name="Москва" total-rate="10753"/><city name="San Francisco" total-rate="6885"/><city name="Ижевск" total-rate="5922"/> |
Как видите, XQuery мало в чем уступает SQL, и даже, я уверен, значительно превосходит его по гибкости. Единственной проблемой остается производительность подобных запросов, но об этом поговорим в конце статьи.