MongoDBが導入されたとき、強調された主な機能は、「スキーマレス」である機能でした。どういう意味ですか?これは、それぞれが異なる構造を持つJSONドキュメントを同じコレクションに保存できることを意味します。これはかなりクールです。しかし、問題は、ドキュメントを取得する必要があるときに始まります。取得したドキュメントが特定の構造であるかどうか、または特定のフィールドが含まれているかどうかをどのように判断しますか?すべてのドキュメントをループして、その特定のフィールドを検索する必要があります。これが、特に大規模なアプリケーションの場合、MongoDBスキーマを慎重に計画することが役立つ理由です。
MongoDBに関しては、スキーマを設計する特定の方法はありません。それはすべて、アプリケーションと、アプリケーションがデータをどのように使用するかによって異なります。ただし、データベーススキーマを設計する際に従うことができるいくつかの一般的な方法があります。ここでは、これらのプラクティスとその長所と短所について説明します。
1対数のモデリング(埋め込み)
このデザインは、ドキュメントを埋め込む非常に良い例です。このモデリングを説明するために、Personコレクションのこの例を検討してください。
{
name: "Amy Cooper",
hometown: "Seoul",
addresses: [
{ city: 'New York', state: 'NY', cc: 'USA' },
{ city: 'Jersey City', state: 'NJ', cc: 'USA' }
]
}
長所:
- 1回のクエリですべての情報を取得できます。
短所:
- 埋め込まれたデータは、親ドキュメントに完全に依存しています。埋め込まれたデータを個別に検索することはできません。
- このアプローチを使用してタスク追跡システムを作成している例を検討してください。次に、Personコレクションに1人の人物に固有のすべてのタスクを埋め込みます。次のようなクエリを実行する場合:明日を締め切りとするすべてのタスクを表示します。これは単純なクエリですが、非常に難しい場合があります。この場合、他のアプローチを検討する必要があります。
1対多モデリング(参照)
このタイプのモデリングでは、親ドキュメントは子ドキュメントの参照ID(ObjectID)を保持します。ドキュメントを取得するには、アプリケーションレベルの結合(アプリケーションレベルでDBから取得した後に2つのドキュメントを結合する)を使用する必要があるため、データベースレベルの結合はありません。したがって、データベースの負荷が軽減されます。この例を考えてみましょう:
// Parts collection
{
_id: ObjectID(1234),
partno: '1',
name: ‘Intel 100 Ghz CPU',
qty: 100,
cost: 1000,
price: 1050
}
// Products collection
{
name: 'Computer WQ-1020',
manufacturer: 'ABC Company',
catalog_number: 1234,
parts: [
ObjectID(‘1234’), <- Ref. for Part No: 1
ObjectID('2345'),
ObjectID('3456')
]
}
各製品に数千の部品が関連付けられているとします。この種のデータベースの場合、参照は理想的なタイプのモデリングです。関連するすべての部品の参照IDを製品ドキュメントの下に置きます。次に、アプリケーションレベルの結合を使用して、特定の製品の部品を取得できます。
長所:
- このタイプのモデリングでは、各パーツは個別のドキュメントであるため、これらのドキュメントにすべてのパーツ関連のクエリを適用できます。親ドキュメントに依存する必要はありません。
- 各ドキュメントに対して個別にCRUD(作成、読み取り、更新、書き込み)操作を実行するのは非常に簡単です。
短所:
- この方法の主な欠点の1つは、パーツの詳細を取得するために1つの追加クエリを実行する必要があることです。これにより、製品ドキュメントでアプリケーションレベルの結合を実行して、必要な結果セットを取得できます。そのため、DBのパフォーマンスが低下する可能性があります。
1対100万のモデリング(親参照)
各ドキュメントに大量のデータを保存する必要がある場合、MongoDBにはドキュメントあたり16 MBのサイズ制限があるため、上記のアプローチのいずれも使用できません。この種のシナリオの完璧な例は、さまざまなタイプのマシンからログを収集し、それらをログとマシンコレクションに保存するイベントログシステムです。
ここでは、特定のマシンのすべてのログ情報を1つのドキュメントに保存する埋め込みアプローチの使用について考えることさえできません。これは、わずか数時間でドキュメントサイズが16MBを超えるためです。すべてのログドキュメントの参照IDのみを保存する場合でも、一部のマシンは1日に数百万のログメッセージを生成する可能性があるため、16MBの制限を使い果たします。
したがって、この場合、親参照アプローチを使用できます。このアプローチでは、子ドキュメントの参照IDを親ドキュメントに保存する代わりに、親ドキュメントの参照IDをすべての子ドキュメントに保存します。したがって、この例では、マシンのObjectIDをログドキュメントに保存します。この例を考えてみましょう:
// Machines collection
{
_id : ObjectID('AAA'),
name : 'mydb.example.com',
ipaddr : '127.66.0.4'
}
// Logs collection
{
time : ISODate("2015-09-02T09:10:09.032Z"),
message : 'WARNING: CPU usage is critical!',
host: ObjectID('AAA') -> references Machine document
}
マシン127.66.0.4の最新の3000ログを検索するとします。
machine = db.machines.findOne({ipaddr : '127.66.0.4'});
msgs = db.logmsg.find({machine: machine._id}).sort({time : -1}).limit(3000).toArray()
双方向参照
このアプローチでは、参照を両側に保存します。つまり、親の参照は子ドキュメントに保存され、子の参照は親ドキュメントに保存されます。これにより、1対多のモデリングで比較的簡単に検索できます。たとえば、親ドキュメントとタスクドキュメントの両方を検索できますが、このアプローチでは、1つのドキュメントを更新するために2つの個別のクエリが必要です。
// person
{
_id: ObjectID("AAAA"),
name: "Bear",
tasks [
ObjectID("AAAD"),
ObjectID("ABCD"), -> Reference of child document
ObjectID("AAAB")
]
}
// tasks
{
_id: ObjectID("ABCD"),
description: "Read a Novel",
due_date: ISODate("2015-11-01"),
owner: ObjectID("AAAA") -> Reference of parent document
}
結論
結局、それはすべてアプリケーションの要件に依存します。アプリケーションにとって最も有益で、高いパフォーマンスを提供する方法でMongoDBスキーマを設計できます。スキーマを設計する際に考慮できる考慮事項の概要を次に示します。
- アプリケーションのデータアクセスパターンに基づいてスキーマを設計します。
- 毎回ドキュメントを埋め込む必要はありません。ドキュメントを一緒に使用する場合にのみ、ドキュメントを結合します。
- 現在、ストレージは計算能力よりも安価であるため、データの重複を検討してください。
- より頻繁なユースケースに合わせてスキーマを最適化します。
- 配列は範囲を超えて大きくならないようにする必要があります。子ドキュメントが数百を超える場合は、埋め込まないでください。
- データベースレベルの結合よりもアプリケーションレベルの結合を優先します。適切なインデックス付けと投影フィールドの適切な使用により、時間を大幅に節約できます。