2013/08/01 by Stephane Combaudon
リレーショナルデータベースに慣れている人がMongoDBのようなNoSQLのソリューションを使うのは、面白いチャレンジになるだろう。そのうちのひとつが、スキーマのデザインだ。リレーショナルな世界では、正規化がいいとっかかりだが、新しくMongoDBのアプリケーションを作るときには、データ保存についてどうデザインすべきだろうか?
簡単な例を挙げて、MySQL(というかあらゆるリレーショナルデータベース)でデータ構造をどう作るか、そしてMongoDBではどうかを見てみよう。個人情報(名前)とその人のパスポートの詳細(国籍と有効期限)を保存したい、という状況で考えてみる。
リレーショナルなデザイン
リレーショナルな世界での基本的な考え方は、第3正規形から外れないようにして、2つのテーブルを作る(ここでは分かりやすさのためにインデックスと外部キーは考慮しない。ちなみにMongoDBはインデックスはサポートしているが外部キーはしていない)。
mysql> select * from people;
+----+------------+
| id | name |
+----+------------+
| 1 | Stephane |
| 2 | John |
| 3 | Michael |
| 4 | Cinderella |
+----+------------+
mysql> select * from passports;
+----+-----------+---------+-------------+
| id | people_id | country | valid_until |
+----+-----------+---------+-------------+
| 4 | 1 | FR | 2020-01-01 |
| 5 | 2 | US | 2020-01-01 |
| 6 | 3 | RU | 2020-01-01 |
+----+-----------+---------+-------------+
こういったデザインのいいところは、どんなクエリも同じように簡単に実行できるところだ(使いこなすのが難しい JOIN を考えない限りは)。
人数を知りたいときは?
SELECT count(*) FROM people
Stephaneのパスポートの有効期限を知りたいときは?
SELECT valid_until from passports ps join people pl ON ps.people_id = pl.id WHERE name = 'Stephane'
パスポートを持っていない人の人数を知りたいときは?
SELECT name FROM people pl LEFT JOIN passports ps ON ps.people_id = pl.id WHERE ps.id IS NULL
MongoDBのデザイン
クエリを簡単にするには、MongoDBではどういうコレクションをデザインすべきだろうか?
第3正規形を使うことももちろん可能だが、アプリケーション内で JOIN が必ず発生してしまうので、おそらく非効率になってしまう。一旦上で出てきた3つのクエリは置いておいたとして、1番目のクエリだけは簡単に実行できそうだ。どんなデザインが考えられるだろうか?
第1のやり方は、全てのデータを一緒に保存してしまうことだ。
> db.people_all.find().pretty()
{
"_id" : ObjectId("51f7be1cd6189a56c399d3bf"),
"name" : "Stephane",
"country" : "FR",
"valid_until" : ISODate("2019-12-31T23:00:00Z")
}
{
"_id" : ObjectId("51f7be3fd6189a56c399d3c0"),
"name" : "John",
"country" : "US",
"valid_until" : ISODate("2019-12-31T23:00:00Z")
}
{
"_id" : ObjectId("51f7be4dd6189a56c399d3c1"),
"name" : "Michael",
"country" : "RU",
"valid_until" : ISODate("2019-12-31T23:00:00Z")
}
{ "_id" : ObjectId("51f7be5cd6189a56c399d3c2"), "name" : "Cinderella" }
ところで、この例で、MongoDBがスキーマレスだというのが分かっただろう。同一構造でないドキュメントを保存するのに何の問題もない。
この方法の欠点は、どの要素がパスポートについてのものだかはっきりしなくなってしまうことだ。Michaelのパスポート情報を全て取得したいときには、データ構造を正しく理解している必要がある。
2つ目のやり方は、個人の情報の中にパスポート情報を埋め込んでしまうことだ。MongoDBでは、ドキュメントの中にドキュメントを埋め込むリッチドキュメントをサポートしている。
> db.people_embed.find().pretty()
{
"_id" : ObjectId("51f7c0048ded44d5ebb83774"),
"name" : "Stephane",
"passport" : {
"country" : "FR",
"valid_until" : ISODate("2019-12-31T23:00:00Z")
}
}
{
"_id" : ObjectId("51f7c70e8ded44d5ebb83775"),
"name" : "John",
"passport" : {
"country" : "US",
"valid_until" : ISODate("2019-12-31T23:00:00Z")
}
}
{
"_id" : ObjectId("51f7c71b8ded44d5ebb83776"),
"name" : "Michael",
"passport" : {
"country" : "RU",
"valid_until" : ISODate("2019-12-31T23:00:00Z")
}
}
{ "_id" : ObjectId("51f7c7258ded44d5ebb83777"), "name" : "Cinderella" }
あるいは違う方法でもドキュメントを埋め込める(ただしこの方法だとCinderellaのようにパスポートを持っていない人に関してはちょっとおぼつかない)。
> db.passports_embed.find().pretty()
{
"_id" : ObjectId("51f7c7e58ded44d5ebb8377b"),
"country" : "FR",
"valid_until" : ISODate("2019-12-31T23:00:00Z"),
"person" : {
"name" : "Stephane"
}
}
{
"_id" : ObjectId("51f7c7ec8ded44d5ebb8377c"),
"country" : "US",
"valid_until" : ISODate("2019-12-31T23:00:00Z"),
"person" : {
"name" : "John"
}
}
{
"_id" : ObjectId("51f7c7fa8ded44d5ebb8377d"),
"country" : "RU",
"valid_until" : ISODate("2019-12-31T23:00:00Z"),
"person" : {
"name" : "Michael"
}
}
{
"_id" : ObjectId("51f7c8058ded44d5ebb8377e"),
"person" : {
"name" : "Cinderella"
}
}
色んな方法がある! どれを選んだらいい? スキーマをデザインする際に気を付けるべき、MongoDBとリレーショナルデータベースの間にある基本的な違いを挙げよう。
MongoDBのコレクションは、アプリケーションが一番よくアクセスするパターンを念頭に置いてデザインすべきだ。一方で、リレーショナルな世界では、正規化さえされていれば、どうアクセスされるかは忘れてもいい。
従って、
- 99%が個人の情報の読み出しなら、2つのコレクションに分けるのが得策だ。これで、ほとんど使われない情報(パスポート情報)をメモリに載せておくのを避けられるし、指定された人の全情報が欲しい時だけ JOIN をするのならば許容範囲だろう。
- ある画面で個人の情報を表示し、別の画面でパスポート情報を表示する場合も、上と同じでよい。
- 指定された人の全情報を表示したいのなら、同じコレクションに全てを保存してしまう(ネストしてもフラットにしてもよい)のが最善の策だろう。
結論
この記事では、個々のアプリケーションに最適なデータ構造を作る場合に、MySQLとMongoDBとの基本的に異なる点について見てきた。MongoDBの場合、データへのアクセスパターンを知っている必要があり、間違ったスキーマデザインは大問題を引き起こす原因となるので、これは無視してはならないことである。クエリを書くこと、最適化することは難しくなり、実行は遅く、場合によってはカスタムコードに置き換えねばならないかもしれない。そういったことは全て、パフォーマンス低下とフラストレーションにつながる。
そして次の疑問は、どっちの方法がよいのか?だが、もちろん決まった答えはない。MongoDBのファンは、アクセスパターンを一定にするなら、正規化なんてしたらどのクエリも遅くなってしまうじゃないか、と言うだろうし、正規化の支持者なら、正規化されたスキーマはほとんどのアプリケーションでいいパフォーマンスが出るのに、非正規化したらごく少数のクエリだけしか速くならないじゃないか、と言うだろう。