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

このエントリはたぶんに煽り要素を含めていますが、意図的なものです。僕は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なり、アプリ側でパースするというやり方であっても十分有益だと思いますので、要はなんであっても使いどころが重要かと。