最近は非同期処理に興味があります。

Reactor パターンというのは、非同期処理の実装パターンのひとつで、例えば HTTP 問い合わせなんかで「読み込みが完了するのを監視しつつその他のタスクを進め、読み込みが完了したタイミングでその結果を処理する」みたいなヤツです。
例えば Node.js では Reactor パターンを使ったプログラミングが比較的簡単に行えます。

PHP でも同じことをやりたい、というときには React というフレームワークがあって、少し前に話題になりました。

PHPでもリアルタイムWeb。node.php「React」

現状 React ではファイル I/O やソケット通信を非同期に行えるようになっております。
が、MySQL で非同期処理を行う方法は今の所提供されていません。

PHP でも mysqli_poll を使えば非同期に MySQL にクエリを投げられるということを知りました。
以下の記事が詳しいです。

PHPの非同期クエリで並行処理をやってみる

ただマニュアルのままの使い方だとさすがに辛いだろうと思ったので、コールバックを使って書きやすくするためのライブラリを書いてみました。

yuya-takeyama/async_mysql

これを使うと、例えばこんな感じに書けるようになります。

このように、Node.js っぽいインターフェイスで非同期問い合わせができます。

ローカルで試したい場合は以下のようにしてみてください。

localhost に root ユーザがパスワード無しでいることを前提にしているので、そうでない場合は以下のようにしてみてください。

$conn = $loop->connect(‘HOST’, ‘USER’, ‘PASSWORD’);

この例では数秒 SLEEP() するだけのクエリをいくつか投げており、直列に実行した場合は SLEEP() の合計時間が全体の実行時間となってしまうところを、並列に実行することで、ほぼ同時に全てのクエリが発行され、一番長いクエリの分だけの実行時間で済みます。

ただし、実行するクエリそれぞれ別々にコネクションが確率されてしまうため、実際にちゃんと使うにはキューの仕組みが必要だと思われます。
例えば最大で 16 並列までしかクエリを投げない、みたいなことができないと、KEN_ALL.csv の全件 INSERT を試みるだけで too many connections になってしまうことでしょう。 (まだ試してないですが)

ただ、これの開発を継続して行うつもりはなくて、あくまでも実装の例として作っています。
最終的には React に組み込めたらいいなー、なんて思っているんですが、React がイベントループに使用している stream_select() は MySQL では使えないため、どうしたもんかというところです。
(libevent を使った実装もあるけど、そもそも libevent に対する理解が足りていないのでどうすべきかイメージが湧いてない)

とりあえずは Ruby の EventMachine や Perl の AnyEvent あたりのコードを読んでみようと思います。

ところで話は変わるのですが、React 上で子プロセスの実行を非同期に行うものを書いていて、React の中の人たちにレビューをいただきつつ開発を進めています。

Pull Request #61: WIP: ChildProcess by yuya-takeyama · react-php/react

今は API が固まってきたので、ユニットテストを書いているところです。
(こういう自分の中でチャレンジングなものを作るときは API がコロコロ変わって、テストファーストが全然うまくいかないので、とりあえずガーッと作ってその後にテストで固めて行く、というやり方で作ることが多いです)

これについてはマージされたら改めて紹介しようと思います。

, , ,

以下のような、メールアドレス等の情報を保持したテーブルがあるとします。

mysql> SELECT * FROM users;
+----+---------+------------------+
| id | name    | mail             |
+----+---------+------------------+
|  1 | foo     | foo@yuyat.jp     |
|  2 | bar     | bar@yuyat.jp     |
|  3 | baz     | baz@example.com  |
|  4 | hoge    | hoge@example.org |
|  5 | moge    | moge@example.com |
|  6 | foobar  | foobar@yuyat.jp  |
|  7 | test    | test@example.net |
|  8 | example | example@yuyat.jp |
|  9 | mage    | mage@example.org |
| 10 | huga    | huga@example.com |
| 11 | piyo    | piyo@yuyat.jp    |
| 12 | hige    | hige@yuyat.jp    |
+----+---------+------------------+
12 rows in set (0.00 sec)

ここで、以下のような SQL を用意します。

SELECT
  SUBSTRING_INDEX(mail, '@', -1) AS domain
FROM
  users

すると、このような結果が得られます。

mysql> SELECT SUBSTRING_INDEX(mail, '@', -1) AS domain FROM users;
+-------------+
| domain      |
+-------------+
| yuyat.jp    |
| yuyat.jp    |
| example.com |
| example.org |
| example.com |
| yuyat.jp    |
| example.net |
| yuyat.jp    |
| example.org |
| example.com |
| yuyat.jp    |
| yuyat.jp    |
+-------------+
12 rows in set (0.00 sec)

また、以下の用にグルーピングすることで、ドメイン数順にソートすることも簡単です。

SELECT
  SUBSTRING_INDEX(mail, '@', -1) AS domain,
  COUNT(id) AS count
FROM
  users
GROUP BY
  domain
ORDER BY
  count DESC
mysql> SELECT SUBSTRING_INDEX(mail, '@', -1) AS domain, COUNT(id) AS count FROM users GROUP BY domain ORDER BY count DESC;
+-------------+-------+
| domain      | count |
+-------------+-------+
| yuyat.jp    |     6 |
| example.com |     3 |
| example.org |     2 |
| example.net |     1 |
+-------------+-------+
4 rows in set (0.00 sec)

これらの SQL で重要なのは SUBSTRING_INDEX という関数です。この関数は、ある文字列をデリミタで区切り、その n 番目を取得する というものなのですが、n に負の数を与えることで、末尾の要素を取得しています。

ある学校の、生徒の名前と、その所属クラスを記入した、以下のようなテーブルがあるとします。なお、これらのデータは擬似個人情報データ生成サービスにより出力されたもので、全て架空です。

mysql> SELECT * FROM students;
+-----+---------------------+---------------------------------+---------------------+----------+
| id  | name                | kana                            | roman               | class_id |
+-----+---------------------+---------------------------------+---------------------+----------+
|   1 | 大森 比呂美         | オオモリ ヒロミ                 | Oomori Hiromi       |        1 |
|   2 | 長野 紗矢           | ナガノ サヤ                     | Nagano Saya         |        1 |
|   3 | 杉浦 円             | スギウラ マドカ                 | Sugiura Madoka      |        1 |
~~~
| 198 | 寺本 禎             | テラモト タダシ                 | Teramoto Tadashi    |        5 |
| 199 | 高橋 満雄           | タカハシ ミツオ                 | Takahashi Mitsuo    |        5 |
| 200 | 柴山 謙一           | シバヤマ ケンイチ               | Shibayama Kenichi   |        5 |
+-----+---------------------+---------------------------------+---------------------+----------+
200 rows in set (0.00 sec)

5 クラスあり、どのクラスも 40 名、総勢 200 名の学校です。

さて、それぞれのクラスから 1 名ずつ、学級委員長を選出するとします。当然立候補者はおらず、推薦のなすりつけ合いになることでしょう。そこで、全クラスとも、抽選で学級委員を選出することになりました。

それぞれのクラスでクジを作るのもいい考えですが、我々エンジニアは、MySQL によるソリューションを行うべきでしょう。というわけで以下の SQL を実行。

SELECT
  class_id,
  SUBSTRING_INDEX(GROUP_CONCAT(name ORDER BY RAND()), ',', 1) AS name
FROM
  students
GROUP BY
  class_id

この SQL から得られる結果は以下の通り。

+----------+---------------+
| class_id | name          |
+----------+---------------+
|        1 | 戸田 有沙     |
|        2 | 大森 一行     |
|        3 | 金山 清志     |
|        4 | 藤木 啓一     |
|        5 | 岩渕 雅彦     |
+----------+---------------+
5 rows in set (0.00 sec)

そして、この SQL は、常にランダムな抽出を行うので、当然結果は毎回異なります。例えば以下のように。

+----------+---------------+
| class_id | name          |
+----------+---------------+
|        1 | 高見 幸子     |
|        2 | 森川 里菜     |
|        3 | 塩崎 凛乃     |
|        4 | 高畑 栄三     |
|        5 | 柴山 謙一     |
+----------+---------------+
5 rows in set (0.00 sec)

また、選出された学級委員の名前だけではなく、生徒番号やフリガナ等の情報も紐付ける必要がある場合は、以下のように少々複雑になります。自己結合を使います。

SELECT
  sub.class_id,
  sub.student_id,
  students.name,
  students.kana,
  students.roman
FROM
(
  SELECT
    class_id,
    SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY RAND()), ',', 1) AS student_id
  FROM
    students
  GROUP BY
    class_id
) AS sub
LEFT JOIN
  students
ON
  sub.student_id = students.id
+----------+------------+------------------+------------------------------+-------------------+
| class_id | student_id | name             | kana                         | roman             |
+----------+------------+------------------+------------------------------+-------------------+
|        1 | 32         | 湯川 信明        | ユカワ ノブアキ              | Yukawa Nobuaki    |
|        2 | 78         | 金丸 紗良        | カナマル サラ                | Kanamaru Sara     |
|        3 | 111        | 熊倉 美千子      | クマクラ ミチコ              | Kumakura Michiko  |
|        4 | 150        | 吉田 長次郎      | ヨシダ チョウジロウ          | Yoshida Choujirou |
|        5 | 167        | 日比野 善之      | ヒビノ ヨシユキ              | Hibino Yoshiyuki  |
+----------+------------+------------------+------------------------------+-------------------+
5 rows in set (0.00 sec)

この抽選作業を MySQL で実現する上で、あまり日の当たらない (と、少なくとも私は考えている) 関数が 2 つほど活躍しています。それらについてはまたの機会に紹介しようと思っています。こんな、MySQL クックブックの隅にも載らないような SQL は何人の役に立つかはわかりませんが、GROUP_CONCAT は意外とできる子なので、皆さんも是非使ってみてください。