こんにちは、かつコーチです。
前回はDockerとmongoshを使って、MongoDBに接続するところまで進めました。
今回は一歩踏み込んで、MongoDBのデータ構造の核心であるコレクション・ドキュメント・BSONについて整理します。
「JSONっぽいものを保存するんでしょう?」というざっくりした理解のままだと、あとで型やデータサイズの問題につまずくことがあります。
ここでしっかり基礎を固めておきましょう。
コレクションとドキュメントの関係
コレクションはテーブルに似ているが厳密ではない
コレクションは、ドキュメントの集まりです。
SQLのテーブルに対応する概念ですが、決定的に違うのは「コレクション内のドキュメントが、全く同じ構造である必要がない」という点です。
// 同じusersコレクションの中に、構造の異なるドキュメントが共存できる
db.users.insertMany([
{ name: "田中太郎", age: 28, email: "tanaka@example.com" },
{ name: "佐藤花子", age: 25, hobbies: ["読書", "映画鑑賞"] },
{ name: "鈴木一郎" }
])
SQLのテーブルであれば「emailカラムがない行」「hobbiesカラムがない行」は存在し得ません(NULLを入れるにしても、カラム自体は全行に存在します)。
MongoDBでは、そもそもフィールド自体を持たないドキュメントが普通に共存できます。
これは自由度が高い反面、「このコレクションにはどんな項目が入っているのか」をアプリケーション側で意識して管理する必要があるということでもあります。
自由と引き換えに、設計の責任がスキーマ定義からアプリケーションコードへ移る、とイメージするとよいでしょう。
ドキュメントとは何か
ドキュメントは、MongoDBにおけるデータの最小単位です。
キーと値のペアの集まりで構成され、見た目はJSONオブジェクトそのものです。
{
_id: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1"),
name: "田中太郎",
age: 28,
address: {
city: "東京都",
zipcode: "100-0001"
},
tags: ["新規", "VIP"]
}
このドキュメントの特徴は、値としてオブジェクト(address)や配列(tags)をそのままネストできることです。
SQLで住所情報を持たせようとすると、別テーブルに分けて外部キーで結ぶか、カラムをcity・zipcodeのように分割するのが一般的でした。
MongoDBでは、そのままオブジェクトとして埋め込めます。
BSONとJSONの違い
なぜJSONではなくBSONなのか
MongoDBは、ドキュメントを保存する際に内部的にBSON(Binary JSON)という形式に変換しています。
「見た目はJSON、実体はバイナリ」というのがBSONの正体です。
なぜわざわざバイナリ形式にするのかというと、主に2つの理由があります。
- 処理速度:テキスト形式のJSONをその都度パースするより、バイナリ形式の方が読み書きが高速
- 型情報の保持:JSONでは表現できないデータ型(日付型やバイナリデータなど)を正確に扱える
JSONにはない型がBSONにはある
標準的なJSONでは、数値は「数値型」しかなく、文字列・真偽値・配列・オブジェクト・nullを合わせても表現力は限られています。
特に「日付」を表す型がJSONには存在せず、多くの場合は文字列として扱うしかありません。
BSONは、JSONにはない以下のような型を持っています。
| BSON型 | 説明 |
|---|---|
| ObjectId | MongoDB独自の一意識別子(_idのデフォルト型) |
| Date | 日付・時刻を正確に表現する型 |
| Int32 / Int64 / Double | 数値の精度・サイズを区別できる |
| Binary | 画像データなどのバイナリを直接格納 |
// Date型を明示的に使う例
db.events.insertOne({
title: "リリース会議",
startAt: new Date("2026-09-10T10:00:00Z")
})
// 保存後、startAtはBSONのDate型として扱われるため、
// 日付の範囲検索($gte/$lteなど)が正確に行える
db.events.find({ startAt: { $gte: new Date("2026-09-01") } })
❌よくある失敗:日付を文字列で保存してしまう
初めてMongoDBを触ったとき、私は日付を"2026-09-10"のような文字列でそのまま保存していました。
一見動いているように見えるのですが、範囲検索をした際に文字列の辞書順比較になってしまい、意図した結果にならないことがありました。
❌ Before:日付を文字列として保存
db.events.insertOne({
title: "リリース会議",
startAt: "2026-09-10"
})
// 文字列比較になるため、"2026-9-1"のようなフォーマット揺れがあると
// 正しい範囲検索ができない
✅ After:Date型で保存する
db.events.insertOne({
title: "リリース会議",
startAt: new Date("2026-09-10")
})
// BSONのDate型として保存されるため、$gte/$lteでの範囲検索が
// 常に正確な時刻比較になる
「動いているから正しい」とは限らないという典型例だったので、今でも新しいプロジェクトでは真っ先にDate型を使う癖をつけるようにしています。
ドキュメントサイズの上限を知っておく
1ドキュメントは16MBまで
MongoDBには、1つのドキュメントのサイズに16MBという上限があります。
「テキストデータや数値を扱う分には十分すぎる容量」と感じるかもしれませんが、埋め込み設計を多用しすぎると、この上限に近づくことがあります。
例えば「1つの記事ドキュメントに、すべてのコメントを配列として埋め込む」設計をした場合、人気記事にコメントが殺到するとドキュメントサイズが際限なく膨らみ、16MBの壁にぶつかるリスクがあります。
このような「無限に増え続ける可能性のあるデータ」は、埋め込みではなく別コレクションへの参照で持たせるのが定石です。
埋め込みと参照の使い分けは、MG08で詳しく扱います。
まとめ
この記事のポイント
- コレクション内のドキュメントは、SQLのテーブルと違って構造が統一されていなくてもよい
- ドキュメントはJSONに似た構造で、オブジェクトや配列をそのままネストできる
- MongoDBは内部的にBSON(Binary JSON)形式でデータを保存しており、JSONにはないDate型やObjectId型を持つ
- 日付は文字列ではなくDate型で保存しないと、範囲検索が正しく機能しないことがある
- 1ドキュメントのサイズには16MBの上限があり、無限に増える可能性があるデータは埋め込みを避けるべき
次に読むべき記事
データ構造の基礎が固まったところで、次回は「CRUD操作の基本(insertOne・find・updateOne・deleteOne)」で、実際にデータを操作するコマンドを一通り解説します。
タグ: MongoDB 初心者向け 入門