Разработка при помощи XQuery, лучшего языка программирования для разработчика баз данных

XQuery ускоряет разработку и упрощает обслуживание

Большинство программистов считает, что язык XQuery предназначен для нишевого рынка: язык запросов и преобразования данных для работы с XML-данными. В реляционных базах данных преобладающей методикой является использование SQL для не XML-данных и XQuery для XML-данных. В данной статье приводятся доводы в пользу того, что мощные программные конструкции, доступные в языке XQuery, делают его более привлекательным языком программирования, чем SQL, и что его выразительность и простота использования оправдывают растущее применение XML-данных при проектировании баз данных.

Кеннет Стивен, инженер-программист, IBM

Кеннет Стивен (Kenneth Stephen) – фотографияКеннет Стивен (Kenneth Stephen) является разработчиком с более чем 20-летним опытом проектирования и реализации приложений на различных платформах – от персональных компьютеров до мейнфреймов. Он также имеет большой опыт работы с XML-технологиями, включая XSLT и XQuery. В настоящее время Кеннет работает в подразделении IBM Software Services for WebSphere.



25.09.2012

Обзор

Язык SQL создавался для абстрагирования алгоритмов обработки, управления и преобразования данных, а также для того, чтобы не запутаться в деталях получения и сохранения данных при реализации этих алгоритмов. Эту задачу SQL выполняет очень успешно. Он позволяет создавать очень сложные OLTP-приложения, системы генерирования отчетов, хранилища данных и приложения для бизнес-аналитики. SQL и реляционная модель дизайна баз данных идут рука об руку, а мощность и гибкость SQL сделали реляционную модель доминирующим типом модели данных на протяжении последних десятилетий.

Часто используемые сокращения

  • OLTP: Online transaction processing (оперативная обработка транзакций)
  • RDBMS: Relational database management systems (системы управления реляционными базами данных)
  • SQL: Structured Query Language (язык структурированных запросов)

В этой истории успеха затерялась история иерархических баз данных. В этих базах данные были структурированы в графы, где типы данных могут иметь отношения родитель-потомок. Использовавшиеся с этими базами данных языки программирования были не так успешны, как SQL, в плане абстрагирования доступа к данным и персистентности, что привело к отказу от них.

XML-документы представляют иерархические отношения между логическими объектами. Для работы с XML-данными в одном или нескольких документах используется язык XQuery. XQuery позволяет абстрагироваться от персистентности и доступа к данным точно так же, как и SQL, и в то же время предоставляет лучшие возможности программирования в сравнении с SQL. Технология прошла полный цикл с точки зрения способа работы с данными на верхнем уровне. XQuery имеет несколько интересных аспектов, делающих этот язык более привлекательным для программистов. В данной статье мы рассмотрим его преимущества.

Работа с иерархическими данными

Сначала рассмотрим некоторые ситуации, в которых XQuery имеет очевидные преимущества над SQL. В RDBMS обычной операцией является поиск отношений родитель-потомок, представленных в двух отдельных таблицах базы данных. Например, заказ на покупку нескольких товаров может быть представлен в базе данных таблицей purchase_order и таблицей items (таблица purchase_order содержит номера заказов и номера клиентов, а таблица items содержит номера заказов и номера товаров.) Таблица items содержит также внешний ключ к таблице purchase_order. SQL очень хорошо обрабатывает отношения такого рода (см. рисунок 1 и листинг 1).

Рисунок 1. Дизайн таблиц родитель-потомок для заказа на покупку
Дизайн таблиц родитель-потомок для заказа на покупку
Листинг 1. SQL-выражение для поиска всех товаров в данном заказе
select item_no, item_desc 
from purchase_order po, items i 
where ord_no = 'A12345' 
and po.ord_no = i.ord_no

Слабости SQL проявляются, когда данные имеют цепочку взаимоотношений непредсказуемой длины. Например, менеджер может иметь много подчиненных, которые, в свою очередь, тоже могут иметь много подчиненных и т.д. В таком случае представлять данные в отдельных таблицах родитель-потомок непрактично. Во-первых, необходимо будет спроектировать таблицы для отношений максимальной длины, т.е. если цепочка управления имеет шесть уровней, необходимо создать шесть таблиц. Это ужасно непрактично. Еще хуже то, что для получения информации обо всех подчиненных уровня n менеджера уровня x необходимо выполнить соединение таблиц от уровня x до уровня n. Такой подход будет неэффективен с точки зрения использования ресурсов.

Общепринятым решением является одна таблица, строки которой имеют друг с другом отношение родитель-потомок (см. рисунок 2). Столбцы с идентификаторами сотрудников, идентификаторами менеджеров и именами сотрудников представляют отношения. В нашем примере Jack Brown руководит двумя сотрудниками – John Silver и Ron McDonald. John Silver руководит Jon Carino. К сожалению, подобные запросы XML-данных трудно составлять и обслуживать. В листинге 2 приведен код SQL-выражения для поиска сотрудников, прямо или опосредованно подчиненных менеджеру.

Рисунок 2. Дизайн таблицы данных о сотрудниках с взаимосвязанными строками
Image showing the single table design for employee data
Листинг 2. SQL-выражение для поиска сотрудников, прямо или опосредованно подчиняющихся менеджеру
with reporting_to(emp_id, emp_name) as 
( 
     select emp_id, emp_name 
     from employees 
     where emp_name = 'John Silver' 

     union all 

     select direct_reports.emp_id, direct_reports.emp_name 
     from employees direct_reports, reporting_to 
     where reporting_to.emp_id = direct_reports.mgr_id 
) 
select emp_id, emp_name 
from reporting_to

Как видите, этот запрос использует рекурсию и очень труден для понимания. Подобная проблема намного лучше решается с использованием XML-данных и XQuery. В этом случае вся организация представляется в едином XML-документе в одной строке таблицы. В листингах 3 и 4 представлена конечная реализация, и вы можете сами решить, какая программная реализация проще в разработке и обслуживании.

Листинг 3. XML-представление данных о сотрудниках
<?xml version="1.0"?> 
<org> 
     <employee id="0001"> 
          <name>Jack Brown</name> 
          <employee id="0002"> 
               <name>John Silver</name> 
               <employee id="0004"> 
                         <name>John Silver</name> 
               </employee> 
          </employee> 
          <employee id="0003"> 
               <name>Ron McDonald</name> 
          </employee> 
     </employee>
</org>
Листинг 4. Поиск сотрудников с использованием XML-данных
select emp.emp_id, emp.emp_name 
from employees, xmltable( 
     '$ORG/org//employee[name = "John Silver"]/descendant-or-self::employee' 
     columns 
          emp_id char(4) path '@id', 
          emp_name varchar(254) path 'name/text()' 
) emp

Отметим также, что иерархические данные более распространены, чем кажется. Например, в предыдущем примере заказы на покупку не могут содержать другие заказы, поэтому на первый взгляд этот пример не иерархический. Заказ на покупку может быть невыполним по причине отсутствия или недостаточного количества 2 из 20 заказанных товаров. Поэтому выполняется заказ имеющихся в наличии товаров и генерируется новый заказ для отсутствующих товаров. В подобных ситуациях желательно, чтобы новый заказ содержал ссылку на старый. Таким образом образуется цепочка заказов, ссылочные отношения которых очень напоминают отношения в рассмотренном выше примере с сотрудниками.

Работа с неиерархическими данными

Неиерархические данные представляют собой более крупные массивы данных, моделируемые в реляционной среде. Важно, чтобы любой язык программирования, использующийся для доступа к базе данных или для записи в базу данных, мог хорошо их обрабатывать. Приведенные ниже примеры показывают, что XQuery хорошо обрабатывает такие данные, а с точки зрения программиста предоставляет более совершенное решение по сравнению с SQL.

Учебный пример 1. Поиск и использование ближайшего соответствия

Данный пример описывает ситуацию, когда программные задания организованы в соревнования ("события"). Каждому такому событию его создатель или менеджер присваивает определенное количество "баллов". Предоставляется таблица пересчета ценовых точек, в которой каждой из точек назначается целочисленная метка под названием "уровень". Задача состоит в том, чтобы найти ценовую точку, максимально соответствующую значению "балла" события, и сделать это значение "ценой" уровня (сколько получит победитель соревнования). Если наилучшее соответствие наблюдается для двух уровней, для определения применяемой ценовой точки выбирается более высокий уровень.

В листингах 5 и 6 приведен стандартный дизайн базы данных и пример данных.

Листинг 5. Дизайн таблиц базы данных, не использующий XML-данные
[db2pe@lc4eb4168274532 code]$ db2 describe table pricing 
                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
EVENT_ID                        SYSIBM    INTEGER                      4     0 No 
POINTS                          SYSIBM    INTEGER                      4     0 Yes 
PRICE                           SYSIBM    INTEGER                      4     0 Yes 

     3 record(s) selected. 

[db2pe@lc4eb4168274532 code]$ db2 describe table pricing_tier 
                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
LEVEL                           SYSIBM    INTEGER                      4     0 No 
PAYMENT                         SYSIBM    INTEGER                      4     0 Yes 

     2 record(s) selected.
Листинг 6. Пример данных из таблиц
[db2pe@lc4eb4168274532 code]$ db2 "select * from pricing fetch first 5 rows only" 

EVENT_ID    POINTS      PRICE 
---------- ---------- ----------- 
      10472         640           0 
      10471         220           0 
      10470         190           0 
      10469         180           0 
      10466         780           0 

     5 record(s) selected. 

[db2pe@lc4eb4168274532 code]$ db2 "select * from pricing_tier" 

LEVEL       PAYMENT 
----------- ----------- 
          0          60 
          1         120 
          2         240 
          3         360 
          4         480 
          5         600 
          6         720 
          7         840 

     8 record(s) selected.

В листинге 7 приведен необходимый SQL-запрос на обновление.

Листинг 7. Запрос на обновление таблицы цен (pricing) с использованием стандартного SQL
update pricing o 
    set (price) = ( 
        select payment 
        from ( 
            select pricing_info.event_id event_id , max(level) matched_level 
            -- "min_values" – это наименьшая разнице между текущей ценой и всеми 
            -- предопределенными ценовыми точками для каждого события. 
            -- "pricing_info" – это индекс всех разниц ценовых точек, 
            -- сгруппированных по уровню цены. Соединение двух этих таблиц 
            -- по event_id и разность ценовых точек должны дать уровень ценовой
            -- точки, который мы ищем для каждого события. 
            from ( 
                select event_id, min(absdiff) 
                from ( 
                    -- Для каждого event_id вычислить абсолютное значение разности 
                    -- между существующей ценой и ценовыми точками 
                    select event_id, abs(points - payment) absdiff, level 
                    from pricing, pricing_tier 
                ) 
                group by event_id 
            ) as min_values(event_id, closest_match), ( 
                -- Для каждого event_id вычислить абсолютное значение разности 
                -- существующей ценой и ценовыми точками 
                select event_id, abs(points - payment) absdiff, level 
                from pricing, pricing_tier 
            ) as pricing_info(event_id, absdiff, level)
            where min_values.event_id = pricing_info.event_id 
            and closest_match = absdiff 
            group by pricing_info.event_id 
        )x , pricing_tier y 
        where x.matched_level = y.level 
        and x.event_id = o.event_id 
    )

Теперь рассмотрим реализацию с использованием XML-данных. Таблицу pricing_tier и сведения о событиях можно представить XML-документами, приведенными в листингах 8 и 9.

Листинг 8. Информация о цене в виде XML-документа
<pricing> 
     <pricingtier level="0"><price>60</price></pricingtier> 
     <pricingtier level="1"><price>120</price></pricingtier> 
     <pricingtier level="2"><price>240</price></pricingtier> 
     <pricingtier level="3"><price>360</price></pricingtier> 
     <pricingtier level="4"><price>480</price></pricingtier> 
     <pricingtier level="5"><price>600</price></pricingtier> 
     <pricingtier level="6"><price>720</price></pricingtier> 
     <pricingtier level="7"><price>840</price></pricingtier> 
</pricing>
Листинг 9. Сведения о событии в виде XML-документа
<event id="9083" eventstate="Cancelled: Client request"> 
     <title>UCD research 5</title> 
     <points>170</points> 
</event>

Дизайн таблицы приведен в листинге 10.

Листинг 10. Дизайн таблиц базы данных при использовании XML-данных
[db2pe@lc4eb4168274532 code]$ db2 describe table events 
                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
EVENT_ID                        SYSIBM    INTEGER                      4     0 No 
EVENT                           SYSIBM    XML                          0     0 No 

     2 record(s) selected. 

[db2pe@lc4eb4168274532 code]$ db2 describe table pricing 
                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
PRICING                         SYSIBM    XML                          0     0 Yes 

     1 record(s) selected.

Этот дизайн предполагает, что информация о цене вставляется в документ как соседний элемент элемента "points". В листинге 11 приведен соответствующий запрос на обновление.

Листинг 11. Запрос на обновление, использующий XML-данные и XQuery
update events o 
   set (event) = ( 
      select xmlquery(' 
         (: 
          : Сначала создается последовательность элементов "pair", отображающих
          : номер ценового уровня на абсолютное значение разности между баллами 
          : события и ценовой точкой. Полученная последовательность сортируется 
          : по разности и выбирается наименьшее значение. В результате будет
          : выбрано ближайшее соответствие по цене. Поскольку мы разбиваем
          : уровни по ближайшему значению путем выбора более высокого уровня,
	  : мы добавляем второй ключ сортировки (level) в нисходящем порядке.
          :) 
         let $closestMatch := 
            ( 
               for $pair in ( for $p in $PRICING/pricing/pricingtier 
                  let $lp := xs:int($EVENT/event/points) 
                  let $absdiff := abs($lp - xs:int($p/price)) 
                  return 
                     <pair> 
                        <level>{$p/@level}</level> 
                        <diff>{$absdiff}</diff> 
                     </pair> 
               ) 
               order by xs:int($pair/diff/text()), 
                  xs:int($pair/level/text()) descending 
               return $pair 
            )[1] 
         return 
            transform 
               copy $e := $EVENT 
            modify 
               do insert 
               <pricing>{ 
                  $PRICING/pricing/pricingtier[@level = $closestMatch/level/@level]/ 
                     price/text() 
               }</pricing> after $e/event/points 
            return $e 
         ') 
         from events a, (select pricing from pricing fetch first row only) b 
         where a.event_id = o.event_id 
      )

Отметим главное: стиль программирования очень похож на процедурное программирование – выполняются промежуточные вычисления, и их результаты присваиваются переменным, которые затем используются повторно. Есть даже циклы, присущие процедурному программированию. SQL-выражение, приведенное в листинге 7, идентично по функциям и методологии, но труднее для восприятия, поскольку стиль программирования далек от обычного процедурного. Стиль XQuery довольно прост в создании и обслуживании, что повышает производительность программиста.

Учебный пример 2. Поиск связанных событий в темпоральных данных

Темпоральные данные представляют состояния логического объекта в виде одного или нескольких аспектов, меняющихся во времени. Существует много способов фиксации меняющегося состояния. Для простоты в последующих примерах мы сконцентрируемся на изменении одной переменной. Рассмотрим операции с данными, являющиеся типичными для глубинного анализа данных: на верхнем уровне мы пытаемся идентифицировать набор объектов, изменение состояние которых происходит по определенным характеристикам. Такой набор может содержать, например, клиентов, покупающих моющее средство сразу после мыла. В нашем случае мы рассмотрим расширение предыдущего примера. Найдем сотрудников, которые более одного раза повышались по службе за период с 2000 по 2009 год (см. листинг 12).

Листинг 12. Структура таблицы, содержащей историю повышения сотрудников по службе
[db2pe@lc4eb4168274532 code]$ db2 describe table pay_history 

                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
EMP_ID                          SYSIBM    CHARACTER                    4     0 No 
START_TIME                      SYSIBM    TIMESTAMP                   10     6 No 
END_TIME                        SYSIBM    TIMESTAMP                   10     6 Yes 
JOB_LEVEL                       SYSIBM    INTEGER                      4     0 No 

     4 record(s) selected.

В листинге 13 приведено соответствующее SQL-выражение.

Листинг 13. Выражение select для идентификации сотрудников, повышавшихся по службе более одного раза за указанный период
select emp_id, count(emp_id) 
from pay_history 
where start_time > '2000-01-01-00.00.00.000000' 
and end_time < '2010-01-01-00.00.00.000000' 
group by emp_id 
having count(emp_id) > 1

В листинге 14 приведен дизайн таблицы с использованием XML-данных.

Листинг 14. Структура таблицы, содержащей историю повышения сотрудников в виде XML-данных
[db2pe@lc4eb4168274532 code]$ db2 describe table pay_history 
                                Data type                     Column 
Column name                     schema    Data type name      Length     Scale Nulls 
------------------------------- --------- ------------------- ---------- ----- ------ 
EMP_ID                          SYSIBM    CHARACTER                    4     0 No 
HISTORY                         SYSIBM    XML                          0     0 No 

     2 record(s) selected.

В листинге 15 приведены XML-данные, представляющие историю продвижения по службе.

Листинг 15. Структура таблицы, содержащей историю повышения сотрудников в виде XML-данных
<employee id="0001"> 
     <pay startDate="2001-11-23T00:00:00.000000Z" endDate="2002-10-07T00:00:00.000000Z" 
          level="4">70500</pay> 
     <pay startDate="2002-10-07T00:00:00.000000Z" endDate="2005-06-18T00:00:00.000000Z" 
          level="5">81500</pay> 
     <pay startDate="2005-06-18T00:00:00.000000Z" endDate="2007-06-01T00:00:00.000000Z" 
          level="6">96700</pay> 
     <pay startDate="2007-06-01T00:00:00.000000Z" level="7">120000</pay> 
</employee>

В листинге 16 приведено XQuery-выражение select, эквивалентное выражению select из листинга 13.

Листинг 16. Структура таблицы, содержащей историю повышения сотрудников в виде XML-данных
select emp_id 
from pay_history 
where xmlexists(' 
     let $numPromotions := count( 
               $HISTORY/employee/pay[@startDate gt "2000-01-01T00:00:00.000000Z" 
                    and @endDate lt "2010-01-01T00:00:00.000000Z"] 
          ) 
     return 
          if($numPromotions gt 1)then 
          ( 
               true() 
          )else() 
')

Кажется, что XQuery ничего не улучшил. Рассмотрим дополнительное требование. Сотрудники могут понижаться или повышаться в должности. Каждая запись в истории продвижения может представлять как понижение, так и повышение по службе. Если сконцентрироваться только на повышениях и исключить все понижения, XQuery-выражение изменится (см. листинг 17).

Листинг 17. XQuery-выражение только для повышений по службе
select emp_id 
from pay_history 
where xmlexists(' 
   let $i := 0 
   (: 
    : Нужно учитывать тот факт, что DB2 pureXML-реализация XQuery
    : не поддерживает оси preceding-sibling. Поэтому выполните итерирование 
    : "по элементам "pay" данного сотрудника и добавьте позицию элемента.
    : Затем используйте эту позицию для определения предыдущего элемента "pay". 
    :) 
   let $pairs := ( 
      for $jobChanges in $HISTORY/employee/pay[@startDate gt 
         "2000-01-01T00:00:00.000000Z" and @endDate lt "2010-01-01T00:00:00.000000Z"] 
      let $i := $i + 1 
      return 
         <pair><position>{$i}</position>{$jobChanges}</pair> 
   ) 
   let $numPromotions := count( 
      for $p in $pairs 
      let $currentPos := xs:int($p/position) 
      return 
         (: If this is the first "pay" element, its not a demotion :) 
         if($currentPos eq 1)then 
         ( 
            $p/pay 
         )else( 
            if($pairs[$currentPos - 1]/data(@level) lt $p/data(@level))then 
            ( 
               $p/pay 
            )else() 
         ) 
   ) 
   return 
      if($numPromotions gt 1)then 
      ( 
         true() 
      )else() 
')

Эквивалентное SQL-выражение, использующее не XML-данные, требует реализации рефлексивного соединения (self-join). И даже после этого его будет довольно трудно построить. Чем-то помочь может добавление столбца "номер строки истории продвижения по службе" в таблицу pay_history и последующее выполнение self-join по этому столбцу. Но это мы оставим для самостоятельной работы.

Заключение

Рассмотренные в данной статье идеи – это не перечисление возможностей XQuery. Приведенные примеры демонстрируют мощные возможности этого языка, позволяющие быстро разрабатывать приложения, которые легче поддерживать. По мнению автора, это веская причина для более широкого использования XML-данных в базах данных.

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=XML, Open source
ArticleID=837393
ArticleTitle=Разработка при помощи XQuery, лучшего языка программирования для разработчика баз данных
publish-date=09252012