Статьи

Курсоры в хранимых процедурах MySQL

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

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

Что такое КУРСОР?

Курсор не может использоваться сам по себе в MySQL. Это важный компонент в хранимых процедурах. Я был бы склонен рассматривать курсор как «указатель» в C / C ++ или итератор в выражении PHP foreach .

С помощью курсоров мы можем просматривать набор данных и манипулировать каждой записью для выполнения определенных задач. Когда такую ​​операцию над записью можно также выполнить на уровне PHP, она сохраняет суммы передачи данных, поскольку мы можем просто вернуть обработанный агрегационный / статистический результат обратно на уровень PHP (таким образом устраняя процесс манипуляции selectforeach — на клиенте боковая сторона).

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

Официальная документация по курсорам находится здесь . Он содержит только четыре команды, которые связаны с объявлением курсора, открытием, закрытием и извлечением. Как упоминалось выше, мы также коснемся некоторых других операторов хранимых процедур. Давайте начнем.

Вопрос реального мира

На моем личном веб-сайте есть страница с результатами моей любимой команды NBA: LA Lakers. Структура таблицы за ней проста:

Рис 1. Структура таблицы соответствия Лейкерс

Я обновляю эту таблицу с 2008 года. Некоторые из последних записей, показывающих сезон Lakers 2013-14, показаны ниже:

Рис. 2. Данные таблицы статусов Лейкерс (частично) за сезон 2013-2014 гг.

(Я использую MySQL Workbench в качестве инструмента с графическим интерфейсом для управления базами данных MySQL. Вы можете использовать свой любимый инструмент.)

Ну, я должен признать, что Лейкерс не очень хорошо играют в эти дни. 6 последовательных потерь до 15 января. Я получаю эти «6 последовательных проигрышей», подсчитывая вручную от последнего сыгранного матча до winlose (в сторону более ранних игр) и вижу, как долго может появиться winlose «L» (означающая проигрыш) в колонке winlose . Это, безусловно, выполнимо, но если требование в более крупной таблице усложняется, это занимает больше времени и более подвержено ошибкам.

Можем ли мы сделать это с помощью одного оператора SQL? Я не эксперт по SQL, и мне не удалось понять, как достичь желаемого результата («6 последовательных потерь») из одного оператора SQL. Вклад гуру будет высоко оценен — ​​оставьте это в комментариях ниже.

Можем ли мы сделать это на PHP? Да, конечно. Мы можем извлечь игровые данные (в частности, столбец winlose ) за текущий сезон и выполнить winlose записей, чтобы рассчитать текущую самую длинную серию выигрыш / проигрыш. Но чтобы сделать это, нам нужно будет собрать все данные за этот год, и большая часть данных будет потрачена впустую (поскольку вряд ли у команды будет серия побед / поражений в более чем 20 играх в 82 играх). регулярный сезон). Тем не менее, мы не знаем, сколько записей нужно извлечь в PHP, чтобы определить полосу, поэтому такая трата является обязательной. И наконец, если текущая полоса выигрыша / проигрыша — это единственное, что мы хотим узнать из этой таблицы, зачем извлекать все необработанные данные?

Можем ли мы сделать это другими способами? Да, это возможно. Например, мы можем создать избыточную таблицу, специально предназначенную для хранения текущей серии выигрышей / проигрышей. Каждая вставка записи будет обновлять эту таблицу тоже. Но это слишком громоздко и чревато ошибками.

Итак, что является лучшим способом для достижения этого результата?

Использование курсора в хранимой процедуре

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

Давайте создадим первый SP в MySQL Workbench следующим образом:

 DELIMITER $$ CREATE DEFINER = `root` @ `localhost`  PROCEDURE `streak` ( in  cur_year int ,   out  longeststreak int ,   out  status char ( 1 )) 
 BEGIN declare current_win char ( 1 ); declare current_streak int ; declare current_status char   ( 1 ); declare cur cursor for   select  winlose from  lakers where  year = cur_year and  winlose <> ''  order by  id desc ; 

     set  current_streak = 0 ; open cur ; fetch cur into  current_win ; 
     set  current_streak =  current_streak + 1 ; start_loop :  loop fetch cur into  current_status ; 
             if  current_status <>  current_win then leave start_loop ; 
             else 
                 set  current_streak = current_streak + 1 ; 
             end   if ; 

     end  loop ; close cur ; 

     select  current_streak into  longeststreak ; 
     select  current_win into   `status` ; 
 END 

В этом SP у нас есть один входной параметр и два выходных параметра. Это определяет подпись SP.

В теле SP мы также объявили несколько локальных переменных для хранения статуса серии (победа или поражение, current_win ), текущей серии и текущего статуса победы / поражения для определенного матча.

 declare cur cursor for   select  winlose from  lakers where  year = cur_year and  winlose <> ''  order by  id desc ; 

Вышеуказанная строка является объявлением курсора. Мы объявили курсор с именем cur и привязка набора данных к этому курсору является статусом выигрыша / проигрыша для тех сыгранных матчей (таким образом, его столбец winlose имеет значение «W» или «L» вместо ничего) в конкретный год, упорядоченный по id ( последние сыгранные игры будут иметь самый высокий ID) по убыванию.

Хотя это и не отображается явно, мы можем представить, что этот набор данных будет содержать серию букв «L» и «W». На основании данных, показанных на рисунке 2 выше, оно должно быть: «LLLLLLWLL…» (6 Ls, 1 W и т. Д.).

Чтобы рассчитать серию побед / поражений, мы начнем с самых последних (и первых в наборе данных) данных о совпадениях. Когда курсор открыт, он всегда начинается с первой записи в соответствующем наборе данных.

После получения первых данных курсор переместится на следующую запись. Таким образом, курсор ведет себя очень похоже на очередь, пересекая набор данных способом FIFO (первым пришел — первым вышел). Это именно то, что мы хотели.

После получения текущего статуса выигрыша / проигрыша и установки номера полосы мы продолжаем перебирать (проходить) оставшуюся часть набора данных. При каждой итерации цикла курсор будет «указывать» на следующую запись, пока мы не разорвем цикл или пока все записи не будут использованы.

Если следующий статус выигрыша / проигрыша совпадает с текущим статусом выигрыша / проигрыша, это означает, что полоса продолжается, и мы увеличиваем число полос на 1 и продолжаем движение; в противном случае это означает, что полоса прекращается, и мы можем выйти из цикла раньше.

Наконец, мы закрываем курсор и освобождаем ресурсы. Затем мы возвращаем желаемый результат.

Далее мы можем улучшить контроль доступа SP, как описано в моей предыдущей статье .

Чтобы проверить вывод этого SP, мы напишем короткий PHP-скрипт:

 <? php $dbms =   'mysql' ; $host =   'localhost' ; $db =   'sitepoint' ; $user =   'root' ; $pass =   'your_pass_here' ; $dsn =   "$dbms:host=$host;dbname=$db" ; $cn = new  PDO ( $dsn ,  $user ,  $pass ); $cn -> exec ( 'call streak(2013, @longeststreak, @status)' ); $res = $cn -> query ( 'select @longeststreak, @status' )-> fetchAll (); var_dump ( $res );   //Dump the output here to get a raw view of the output $win = $res [ 0 ][ '@status' ]= 'L' ? 'Loss' : 'Win' ; $streak = $res [ 0 ][ '@longeststreak' ]; echo "Lakers is now $streak consecutive $win.\n" ; 

Это выведет что-то вроде следующего рисунка:

(Этот вывод основан на матче Лейкерс до 15 января 2014 года.)

Вернуть набор данных из хранимой процедуры

Было проведено несколько дискуссий о том, как вернуть набор данных из SP, который строит набор данных из результатов нескольких повторных вызовов к другому SP.

Пользователь может захотеть узнать больше от нашего ранее созданного SP, который возвращает выигрыш / проигрыш только в течение одного года; таким образом, мы можем получить таблицу, показывающую серии побед / поражений за все годы в такой форме:

ГОД Выиграть потерять жилка
2013 L 6
2012 L 4
2011 L 2

(Что ж, более полезным результатом может быть возвращение самой длинной серии побед и серии потерь в конкретном сезоне. Это требование может быть легко расширено по сравнению с предыдущим SP, поэтому я оставлю его для реализации заинтересованным сторонам. Для целей этой статьи , мы будем придерживаться текущей серии побед / поражений.)

MySQL SP может возвращать только скалярные результаты (целое число, строку и т. Д.), Если только результат не возвращается оператором select ... from ... (и он становится набором данных). Проблема в том, что данные табличной формы, которые мы хотим видеть, не существуют в нашей текущей структуре БД и созданы из другого SP.

Чтобы справиться с этим, нам нужна помощь временной таблицы или, если ситуация позволяет и требует, избыточной таблицы. Давайте посмотрим, как мы можем достичь нашей цели с помощью временной таблицы.

Сначала мы создадим второй SP, как показано ниже:

 DELIMITER $$ CREATE DEFINER = `root` @ `%`  PROCEDURE `yearly_streak` () 
 begin declare cur_year ,  max_year ,  min_year int ; 

     select  max ( year ),  min ( year )   from  lakers into  max_year ,  min_year ; DROP TEMPORARY TABLE IF EXISTS yearly_streak ; CREATE TEMPORARY TABLE yearly_streak ( season int ,  streak int ,  win char ( 1 )); 

     set  cur_year = max_year ; year_loop :  loop if  cur_year < min_year then leave year_loop ; 
         end   if ; call streak ( cur_year ,   @l ,   @s ); insert into  yearly_streak values ( cur_year ,   @l ,   @s ); 

         set  cur_year = cur_year - 1 ; 
     end  loop ; 

     select   *   from  yearly_streak ; DROP TEMPORARY TABLE IF EXISTS yearly_streak ; 

 END 

Несколько ключевых моментов, на которые следует обратить внимание:

  1. Мы определяем максимальный год и минимальный год, выбирая из таблицы lakers ;
  2. Мы создали временную таблицу для хранения выходных данных со структурой, запрошенной выходными данными ( season , streak , win );
  3. В цикле мы сначала выполняем ранее созданный SP с необходимыми параметрами ( call streak(cur_year, @l, @s); ), затем берем возвращенные данные и вставляем в временную таблицу ( insert into yearly_streak values (cur_year, @l, @s); ).
  4. Наконец, мы выбираем из временной таблицы и возвращаем набор данных, затем выполняем некоторую очистку ( DROP TEMPORARY TABLE IF EXISTS yearly_streak; ).

Чтобы получить результаты, мы создаем еще один короткий скрипт PHP, как показано ниже:

 <? php ...   // Here goes the db connection parameters $cn = new  PDO ( $dsn ,  $user ,  $pass ); $res = $cn -> query ( 'call yearly_streak' )-> fetchAll (); 

 foreach   ( $res as  $r ) 
 { echo sprintf ( "In year %d, the longest W/L streaks is %d %s\n" ,  $r [ 'season' ],  $r [ 'streak' ],  $r [ 'win' ]); 
 } 

И дисплей будет выглядеть так:

Обратите внимание, что вышесказанное немного отличается от вызова нашего первого SP.

Первый SP не возвращает набор данных, а только два параметра. В этом случае мы используем PDO exec затем query вывод; в то время как во втором SP мы вернули набор данных из SP, поэтому мы напрямую используем query PDO для вызова этого SP.

Вуаля! Мы сделали это!

Вывод

В этой статье мы углубились в хранимые процедуры MySQL и рассмотрели функциональность курсора. Мы продемонстрировали, как извлекать скалярные данные по выходным параметрам (определенным как out var_name vartype в объявлении SP), а также извлекать вычисленный набор данных через временную таблицу. Во время этого процесса также появилось несколько операторов, которые иначе используются в хранимых процедурах.

Официальную документацию по синтаксису хранимой процедуры и различным операторам можно найти на сайте MySQL. Чтобы создать хранимую процедуру, пожалуйста, обратитесь к этим документам и чтобы понять утверждения, пожалуйста, смотрите здесь .

Не стесняйтесь комментировать и дайте нам знать ваши мысли!