スキーマレスについてちょっと考えてみた

このエントリはたぶんに煽り要素を含めていますが、意図的なものです。僕は NoSQL は素晴らしいと思います。

さて、NoSQL なんて言葉に踊らされてる人は置いといて、最近 RDBMS 以外のデータストアというのが色々でてきてます。今時点で見渡す限りにおいては、安定性、耐障害性、パフォーマンス、情報量、開発者の慣れ、全体のバランスで言えば RDBMS にかなうものはないわけですが、今後どうなっていくかはまぁ分かりません。

一方で、RDBMS がどうしても苦手とする分野というのは存在します。例えば 1 サーバに収まりきらない様な大容量データに対するバッチ処理、リアルタイムなランキング、アクティビティなどのフィード情報、そして構造化されたデータの取り扱い。何でもかんでも NoSQL に置き換えればいいなんて考えは現時点では到底受け入れがたいですが、例として挙げた様なピンポイントな部分ではそれに適したデータストアを柔軟に選択できるという能力がオペレーションエンジニアには要求されます。よくちまたで言われるのはこんな感じでしょうか。

  • 大容量データに対するバッチ処理

    • →Hadoop など
  • リアルタイムなランキング、フィード情報

    • →Redis など
  • 構造化されたデータ

    • →MongoDB など

ただ、そもそもこうした分野に対してどうして RDBMS だと厳しいのかということをよく理解しておかないと、「NoSQL マンセー」な人を黙らせる(!)のがめんどくさいですね。この辺について 2010 年代の今を詳細に分析した技術書としてはこちらを一読することをおすすめします。

スキーマレスは別に JSON 使えばできるでしょ?

で、僕自身はどうかといえば、RDBMS というか MySQL が大好きなので、大抵のことは MySQL でできるだろと思ってるわけですが、Hadoop とか Redis はまぁまぁ使いどころあるかなぁと最近思い始めてきました。ただ、MongoDB みたいな「スキーマレス」をおしだしているやつは、別にそんなに意味ないんじゃないの?と思ってました。

なぜなら、構造化されたデータであっても JSON の様に一定の法則でシリアライズしてしまえば、それを文字列として保存しておけるわけです。そして MySQL には kazuho さんが作られた mysql_json という UDF(ユーザ定義関数)があるので、JSON を MySQL 上でパースすることさえできてしまうのです。

CREATE TABLE nosql (value mediumblob);
INSERT INTO nosql (value) VALUES ('{"key": 1, "data": "hoge"}');
SELECT json_get(value, 'data') FROM nosql WHERE json_get(value, 'key') = 1;
+-------------------------+
| json_get(value, 'data') |
+-------------------------+
| hoge                    |
+-------------------------+

どうせこういう類で複雑な検索クエリを投げる場合、MongoDB だとしても効果的にインデックスを持っておくことは難しいわけで、別にフルスキャンとしてもいいやと考えてしまえばこれで困ることはあまりないかな、と思ってました。

こうしたスキーマレスなデータが最も効果的なのは、僕は管理系のアプリケーションだと思ってます。つまりパフォーマンスは要らないアプリ。そういったアプリは、大抵開発工数も十分に使わせてもらえません。事前に要求を満たすスキーマを設計することもできず中途半端なスキーマになるくらいなら、ドーンとスキーマレスにぶち込んでしまった方が作るのもメンテナンスも効率的だなぁと。

んで、ちょうどそういうのを書く機会ができたので「いざ、UDF」と思って始めてみました。インストールは簡単です。ここでは homebrew で mysql をいれているとします。

$ git clone https://github.com/kazuho/mysql_json.git
$ cd mysql_json
$ git submodule init
$ git submodule update
$ vi mysql_json.cc
#include <mysql/mysql.h>

#include <mysql.h>
$ g++ -I/usr/local/Cellar/mysql/5.5.15/include/ -shared -fPIC -g mysql_json.cc -o mysql_json.so
$ cp mysql_json.so /usr/local/Cellar/mysql/5.5.15/lib/plugin/
$ mysql -uroot
mysql> create function json_get returns string soname 'mysql_json.so';
mysql> SELECT json_get('{"a":1}', 'a');

json_get ではちょっときつい

ただ、いざいじりはじめてみたら現状の mysql_json だとちょっと問題がありました。

INSERT INTO nosql (value) VALUES ('{"key": 2, "data": {"aaa":1, "bbb":2}}');
SELECT json_get(value, 'data') FROM nosql WHERE json_get(value, 'key') = 2;
+-------------------------+
| json_get(value, 'data') |
+-------------------------+
| object                  |
+-------------------------+
SELECT json_get(value, 'data', 'aaa') FROM nosql WHERE json_get(value, 'key') = 2;
+--------------------------------+
| json_get(value, 'data', 'aaa') |
+--------------------------------+
| 1                              |
+--------------------------------+

このように、今の json_get 関数では、数値か文字列になるまでプロパティを指定してあげないと中身を見ることができません。mysql_json/picojson はとても小さいコードなので、よっしゃいっちょ、こういう場合はシリアライズして JSON の形で返すように改造して pull req するか!と思ったのが明け方だったんですが、僕の C++力では到底不可能でした。。。

MongoDB が意外と快適だった

kazuho さんに改造の方針は伺ったので、やろうと思えばやれるとは思いますがここに時間使うのもアレだなぁと思って、スキーマレスに構造化したデータを扱えるということで、MongoDB を改めて学習してみました。すると、この他にも色々とメリットがあるなぁと思ってきたところです。

タグデータ

まだまとめるには学習時間が短すぎるのですが、とりあえずこれはいいなと思ったのが、いわゆる「タグ」データの扱い。RDBMS でタグを表現するには、

CREATE TABLE `users` (
  `id` int(11) NOT NULL,
  `name` char(100) NOT NULL,
  PRIMARY KEY (`id`)
);

CREATE TABLE `tags` (
  `tag` char(100) NOT NULL,
  `id` int(11) NOT NULL,
  PRIMARY KEY (`tag`, `id`),
  KEY `i1` (`id`)
);

みたいな感じにするのかなと思います(適当に書いたので DDL 間違ってたらすみません)。でも、タグをつけたいものが出てくる度にこうしたスキーマを追加したりするのは面倒ですよね。そこでスキーマレスなわけですが、JSON でぶち込んだものをいざ探そうとするとそれはそれでまた大変です。

例えばタグを

{"id": 1, "tags": ["tag1", "tag2"]}

みたいに持たせたとしても、先程の json_get では配列の中を検索することができないです。結局 JSON 全体を SELECT してしまって、アプリ側で JSON をパースするのかぁとか考えるとなんか萎えます。

ところが、MongoDB の検索クエリはかなり柔軟に作られていて、

> db.users.insert({"id": 1, "tags": ["tag1", "tag2"]});
> db.users.find({"tags": "tag1"}, {_id:0});
{ "id" : 1, "tags" : [ "tag1", "tag2" ] }

とやるだけでオッケーなんですね。これはいい!(後ろの{_id:0}ってのは OID を消すためのフィールド指定です)

構造化したものの検索

他にも、せっかく構造化できるので、

{"id": 2, "data": {"hoge": {"a": 1}, "fuga": { ... }}}

みたいにネストさせてみたくなるものです。こうしたデータに対して検索をかける時も、

> db.users.insert({"id": 2, "data": {"hoge": {"a": 1}}});
> db.users.find({"data.hoge.a": 1}, {_id:0});
{ "id" : 2, "data" : { "hoge" : { "a" : 1 } } }

の様にキーをドットで繋げることで検索できちゃっていい感じです。

参考

MongoDB で管理系アプリ作ってみる予定

今作ろうとしてるのは、現状 RDBMS のスキーマ上で動いているアプリがメンテナンスしきれなくなってきたものを、どうやって並行稼動させながら新しいアプリに移行していくかが重要です。新しいアプリ側ではいずれはスキーマが MongoDB に移ることを想定しながら、まずは RDBMS のスキーマに対して新しいインタフェースを提供する予定です。

そのために、テストが重要で、まずは今のスキーマを再現してテストとかができるように Test::mysqld を使ってテストライブラリを準備しましてテストデータをぶち込みました。次に、今のスキーマ上のテストデータを新しいスキーマレスな形式で MongoDB にコンバートして入れます。そのために Test::mongod というのをググってコピペして作りました。

これで同じデータを MySQL と MongoDB に配置できたので、MongoDB に対して操作した結果を期待値として新しいアプリのインタフェースを設計しつつ、同じように動作するように MySQL 版の方を実装していって、ひたすらテストで突き合わせる、というのをやり始めたのが今日の夕方という感じです。Amon2 使ってるので、plugin で MongoDB モジュールを使える様にもしましたが、これもコピペです。

実態をみないと何を言ってるのかさっぱりだと思いますが、自分としては「新しいアプリは MongoDB で」と決めたことで一気に色々な問題が解決して実装をガリガリ書き始められたので幸せなのでした。

おわりに

管理系のアプリを書くときはスキーマレスというのはかなり強い武器になるなぁと思いました。マジメに調べてまだ 1 日なので全然全体像見えてないですが、パフォーマンス要求されないアプリならそもそも細かいことを知る必要性もないので、ライトに使い始められて良いですね。

え?サービスの最前線で使えるかって?それは僕は知りませんが、すでに使われている例もでてきていますし、僕自身も勉強はするつもりです。とはいえ MySQL の優位性はまだまだ高いと思っていますので、「ピンポイントで」というのは外せないと思います。逆に言えば、ピンポイントなら今でも十分使う価値はあるのではないかと思っています。

とはいえ、余程混みいった検索をしないとかシンプルな使い方であれば mysql_json なり、アプリ側でパースするというやり方であっても十分有益だと思いますので、要はなんであっても使いどころが重要かと。