はじめに
「テーブル設計が複雑すぎる」「スキーマの変更が大変」「大量のデータを高速に処理したい」
これらの課題を解決するのがドキュメント指向データベースのMongoDBです。柔軟なスキーマ、高いスケーラビリティ、そしてJSONライクなデータ構造により、現代のアプリケーション開発で広く採用されています。
この記事では、MongoDBの基礎から実践的なクエリ、パフォーマンス最適化まで詳しく解説します。
MongoDBは便利ですが「RDBが辛いから」で導入すると事故りがちです。
実務で重要なのは
- どのデータをMongoDBに置くべきか
- スキーマレスをどう運用するか
- 埋め込みと参照をどう決めるか
の判断です。
この記事では基本説明に入る前に、まず意思決定の軸を置きます。
まず決める採用判断の軸
MongoDBが向くケース
- 取得単位がドキュメントで自然にまとまる(プロフィール、設定、コンテンツ)
- スキーマが進化しやすい(フィールド追加が頻繁)
- 読みが中心で、ドキュメント単位で完結しやすい
向かないケース
- 厳密な整合性が最優先(残高、在庫、二重更新が致命的)
- 複雑な結合や集計が主戦場で、ドキュメント境界が定まらない
- 更新が多く、ドキュメントが肥大化しやすい
埋め込み 参照 を決める判断軸
迷ったら次で判断するとブレません。
- 一緒に取得する頻度が高いか
- 一緒に更新する必要があるか
- サイズは増え続けるか(配列が無限に伸びないか)
基本戦略
- 基本は埋め込みで読みを速くする
- 増え続ける配列や独立更新が必要なら参照に逃がす
スキーマレス運用の落とし穴
スキーマレスは「スキーマがない」ではなく「スキーマがコードとデータに散る」という意味です。
運用で壊れやすいポイントは
- 書き込み経路が複数あって形が揺れる
- 型が揃わず、集計や検索が不安定になる
- optional フィールドの扱いが曖昧で、アプリ側が複雑化する
これを避けるには
- 書き込み時のバリデーション
- バージョニング(schemaVersion)
- 移行手順(バックフィル)
をセットで持つのがコツです。
最小チェックリスト(設計レビュー用)
- ドキュメント境界が説明できる
- 増え続ける配列を埋め込んでいない
- schemaVersion と移行方針がある
- インデックス設計がある(検索条件に合わせる)
- バックアップとリストア手順がある
MongoDBとは
なぜMongoDBが必要なのか
RDBMSは長年にわたりデータベースの標準でした。しかし、現代のアプリケーション開発では以下のような課題が生じることがあります。
- スキーマ変更のコスト: テーブル構造を変更するたびにALTER TABLEが必要。大規模なテーブルでは数時間かかることも
- 複雑なJOIN: 正規化されたデータを取得するのに何テーブルも結合が必要
- スケールの難しさ: 単一サーバーの性能限界に達したとき、水平スケールが困難
MongoDBは**「ドキュメント指向」**という異なるアプローチでこれらを解決します。
ドキュメント指向とは?
データをテーブルの行ではなく、JSONのようなドキュメントとして保存します。1つのドキュメントに関連データをすべて含められるため、JOINが不要になることが多いです。
// RDBMSの場合: 3つのテーブルをJOINして取得
// users + addresses + orders → 複雑なクエリ
// MongoDBの場合: 1つのドキュメントに含める
{
"name": "田中太郎",
"address": { "city": "東京", "zipcode": "100-0001" },
"orders": [
{ "product": "商品A", "price": 1000 },
{ "product": "商品B", "price": 2000 }
]
}
いつMongoDBを選ぶべきか?
| ユースケース | MongoDB向き | RDBMS向き |
|---|---|---|
| スキーマが頻繁に変わる | ◎ | △ |
| 階層的なデータ構造 | ◎ | △ |
| 大量の読み書き | ◎(水平スケール) | △ |
| 複雑なトランザクション | △ | ◎ |
| 厳密なデータ整合性 | △ | ◎ |
MongoDBは「銀の弾丸」ではありません。トランザクションが複雑な銀行システムや、厳密な整合性が必要な在庫管理にはRDBMSが適しています。一方、コンテンツ管理、ユーザープロフィール、IoTデータ、ログ収集などにはMongoDBが威力を発揮します。
RDBMSとの比較
| 特徴 | RDBMS(MySQL等) | MongoDB |
|---|---|---|
| データモデル | テーブル(行と列) | ドキュメント(JSON/BSON) |
| スキーマ | 固定(事前定義必要) | 柔軟(スキーマレス) |
| 結合 | JOINで実現 | 埋め込みor参照 |
| スケーリング | 垂直スケール中心 | 水平スケール(シャーディング) |
| トランザクション | 完全サポート | サポート(4.0以降) |
| ユースケース | 複雑な関係データ | 柔軟なデータ構造 |
用語の対応
RDBMSの知識がある方は、以下の用語対応を覚えておくと理解しやすいです。
| RDBMS | MongoDB | 説明 |
|---|---|---|
| Database | Database | データベース(同じ概念) |
| Table | Collection | ドキュメントのグループ |
| Row | Document | 1つのデータレコード(JSONオブジェクト) |
| Column | Field | ドキュメント内の属性 |
| Primary Key | _id | 一意識別子(自動生成) |
| Index | Index | 検索高速化(同じ概念) |
| JOIN | $lookup / 埋め込み | 関連データの取得方法 |
ドキュメント構造
MongoDBのドキュメントは**BSON(Binary JSON)**形式で保存されます。JSONと似ていますが、日付型やObjectIdなど追加の型をサポートしています。
// MongoDBのドキュメント例
{
// _id: 一意識別子。指定しなければ自動生成される
// ObjectIdは12バイトで、タイムスタンプ+マシンID+プロセスID+カウンター
"_id": ObjectId("507f1f77bcf86cd799439011"),
// 基本的なフィールド
"name": "田中太郎",
"email": "tanaka@example.com",
"age": 30,
// 埋め込みドキュメント(ネスト構造)
// 関連データを1つのドキュメントにまとめられる
"address": {
"city": "東京",
"zipcode": "100-0001"
},
// 配列(複数の値を保持)
"tags": ["developer", "javascript", "mongodb"],
// 日付型(ISODate)
"createdAt": ISODate("2024-01-01T00:00:00Z")
}
埋め込み vs 参照
関連データの持ち方には2つのアプローチがあります:
// 埋め込み(Embedding): 1つのドキュメントに含める
// メリット: 1回のクエリで取得可能
// デメリット: ドキュメントサイズが大きくなる(上限16MB)
{
"_id": ObjectId("..."),
"name": "田中太郎",
"orders": [
{ "product": "商品A", "price": 1000 },
{ "product": "商品B", "price": 2000 }
]
}
// 参照(Reference): 別コレクションへの参照
// メリット: 柔軟性が高い、重複を避けられる
// デメリット: $lookupが必要(性能コスト)
{
"_id": ObjectId("user1"),
"name": "田中太郎",
"orderIds": [ObjectId("order1"), ObjectId("order2")]
}
選択の指針: 「一緒に取得することが多いデータ」は埋め込み、「独立して操作することが多いデータ」は参照がおすすめです。
# 環境構築
## インストール
```bash
# macOS (Homebrew)
brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community
# Ubuntu
wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt update
sudo apt install -y mongodb-org
sudo systemctl start mongod
# Docker
docker run -d --name mongodb -p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=password \
mongo:latest
# Docker Compose
# docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:latest
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
volumes:
- mongodb_data:/data/db
volumes:
mongodb_data:
接続
# MongoDB Shellで接続
mongosh
# 認証付き接続
mongosh "mongodb://admin:password@localhost:27017"
# 接続文字列の例
mongosh "mongodb+srv://cluster.mongodb.net/mydb" --username user
基本操作(CRUD)
データベースとコレクション
// データベース一覧
show dbs
// データベースを選択/作成
use myapp
// コレクション一覧
show collections
// コレクションの作成(明示的)
db.createCollection("users")
// コレクションの削除
db.users.drop()
// データベースの削除
db.dropDatabase()
ドキュメントの挿入(Create)
// 1件挿入
db.users.insertOne({
name: "田中太郎",
email: "tanaka@example.com",
age: 30,
createdAt: new Date()
})
// 複数件挿入
db.users.insertMany([
{
name: "佐藤花子",
email: "sato@example.com",
age: 25
},
{
name: "鈴木一郎",
email: "suzuki@example.com",
age: 35
}
])
// 挿入結果
// {
// acknowledged: true,
// insertedId: ObjectId("...")
// }
ドキュメントの検索(Read)
// 全件取得
db.users.find()
// 整形して表示
db.users.find().pretty()
// 1件取得
db.users.findOne({ name: "田中太郎" })
// 条件付き検索
db.users.find({ age: { $gt: 25 } })
// 複数条件(AND)
db.users.find({
age: { $gte: 25, $lte: 35 },
"address.city": "東京"
})
// OR条件
db.users.find({
$or: [
{ age: { $lt: 25 } },
{ age: { $gt: 50 } }
]
})
// フィールド指定(プロジェクション)
db.users.find(
{ age: { $gt: 25 } },
{ name: 1, email: 1, _id: 0 }
)
// ソート
db.users.find().sort({ age: -1 }) // 降順
db.users.find().sort({ name: 1 }) // 昇順
// リミットとスキップ(ページネーション)
db.users.find().skip(10).limit(10)
// カウント
db.users.countDocuments({ age: { $gt: 25 } })
比較演算子
| 演算子 | 意味 | 例 |
|---|---|---|
| $eq | 等しい | { age: { $eq: 25 } } |
| $ne | 等しくない | { status: { $ne: "inactive" } } |
| $gt | より大きい | { age: { $gt: 25 } } |
| $gte | 以上 | { age: { $gte: 25 } } |
| $lt | より小さい | { age: { $lt: 30 } } |
| $lte | 以下 | { age: { $lte: 30 } } |
| $in | いずれかに一致 | { status: { $in: ["active", "pending"] } } |
| $nin | いずれにも一致しない | { status: { $nin: ["deleted"] } } |
論理演算子
// AND(暗黙的)
db.users.find({ age: { $gte: 20 }, status: "active" })
// AND(明示的)
db.users.find({
$and: [
{ age: { $gte: 20 } },
{ age: { $lte: 30 } }
]
})
// OR
db.users.find({
$or: [
{ status: "active" },
{ premium: true }
]
})
// NOT
db.users.find({
age: { $not: { $gt: 30 } }
})
// NOR(すべての条件に一致しない)
db.users.find({
$nor: [
{ status: "deleted" },
{ banned: true }
]
})
配列とネストドキュメントの検索
// 配列に要素が含まれる
db.users.find({ tags: "developer" })
// 配列のすべての要素が一致
db.users.find({ tags: { $all: ["developer", "javascript"] } })
// 配列のサイズ
db.users.find({ tags: { $size: 3 } })
// 配列の要素に対する条件
db.users.find({ "scores": { $elemMatch: { $gte: 80, $lt: 90 } } })
// ネストドキュメント
db.users.find({ "address.city": "東京" })
db.users.find({ "address.zipcode": { $regex: /^100/ } })
ドキュメントの更新(Update)
// 1件更新
db.users.updateOne(
{ email: "tanaka@example.com" },
{ $set: { age: 31, updatedAt: new Date() } }
)
// 複数件更新
db.users.updateMany(
{ status: "pending" },
{ $set: { status: "active" } }
)
// フィールドの追加/更新
db.users.updateOne(
{ _id: ObjectId("...") },
{ $set: { "address.city": "大阪" } }
)
// フィールドの削除
db.users.updateOne(
{ _id: ObjectId("...") },
{ $unset: { temporaryField: "" } }
)
// 数値のインクリメント
db.users.updateOne(
{ _id: ObjectId("...") },
{ $inc: { loginCount: 1, points: 100 } }
)
// 配列に要素を追加
db.users.updateOne(
{ _id: ObjectId("...") },
{ $push: { tags: "newTag" } }
)
// 配列に複数要素を追加(重複なし)
db.users.updateOne(
{ _id: ObjectId("...") },
{ $addToSet: { tags: { $each: ["tag1", "tag2"] } } }
)
// 配列から要素を削除
db.users.updateOne(
{ _id: ObjectId("...") },
{ $pull: { tags: "oldTag" } }
)
// 存在しなければ挿入(Upsert)
db.users.updateOne(
{ email: "new@example.com" },
{ $set: { name: "新規ユーザー", createdAt: new Date() } },
{ upsert: true }
)
// ドキュメント全体を置換
db.users.replaceOne(
{ _id: ObjectId("...") },
{ name: "新しい名前", email: "new@example.com" }
)
ドキュメントの削除(Delete)
// 1件削除
db.users.deleteOne({ email: "tanaka@example.com" })
// 複数件削除
db.users.deleteMany({ status: "deleted" })
// 全件削除
db.users.deleteMany({})
// 条件付き削除
db.users.deleteMany({
lastLoginAt: { $lt: new Date("2023-01-01") },
status: "inactive"
})
Aggregation Pipeline
集計処理のための強力なフレームワークです。
基本構造
db.collection.aggregate([
{ $stage1: { ... } },
{ $stage2: { ... } },
{ $stage3: { ... } }
])
主要なステージ
// $match: フィルタリング
db.orders.aggregate([
{ $match: { status: "completed", createdAt: { $gte: new Date("2024-01-01") } } }
])
// $group: グループ化と集計
db.orders.aggregate([
{ $match: { status: "completed" } },
{
$group: {
_id: "$customerId",
totalAmount: { $sum: "$amount" },
orderCount: { $sum: 1 },
avgAmount: { $avg: "$amount" },
maxAmount: { $max: "$amount" },
minAmount: { $min: "$amount" }
}
}
])
// $sort: ソート
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } }
])
// $limit と $skip: ページネーション
db.orders.aggregate([
{ $sort: { createdAt: -1 } },
{ $skip: 20 },
{ $limit: 10 }
])
// $project: フィールドの選択と変換
db.users.aggregate([
{
$project: {
fullName: { $concat: ["$firstName", " ", "$lastName"] },
email: 1,
age: 1,
_id: 0
}
}
])
// $unwind: 配列の展開
db.orders.aggregate([
{ $unwind: "$items" },
{
$group: {
_id: "$items.productId",
totalQuantity: { $sum: "$items.quantity" },
totalRevenue: { $sum: { $multiply: ["$items.price", "$items.quantity"] } }
}
}
])
// $lookup: 結合(LEFT OUTER JOIN相当)
db.orders.aggregate([
{
$lookup: {
from: "users",
localField: "customerId",
foreignField: "_id",
as: "customer"
}
},
{ $unwind: "$customer" },
{
$project: {
orderNumber: 1,
amount: 1,
"customer.name": 1,
"customer.email": 1
}
}
])
// $addFields: フィールドの追加
db.orders.aggregate([
{
$addFields: {
totalWithTax: { $multiply: ["$amount", 1.1] },
isLargeOrder: { $gte: ["$amount", 10000] }
}
}
])
// $bucket: 範囲でグループ化
db.users.aggregate([
{
$bucket: {
groupBy: "$age",
boundaries: [0, 20, 30, 40, 50, 100],
default: "Other",
output: {
count: { $sum: 1 },
avgAge: { $avg: "$age" }
}
}
}
])
// $facet: 複数の集計を並列実行
db.products.aggregate([
{
$facet: {
"byCategory": [
{ $group: { _id: "$category", count: { $sum: 1 } } }
],
"priceStats": [
{
$group: {
_id: null,
avgPrice: { $avg: "$price" },
maxPrice: { $max: "$price" },
minPrice: { $min: "$price" }
}
}
],
"topProducts": [
{ $sort: { sales: -1 } },
{ $limit: 5 }
]
}
}
])
実践的な集計例
// 月別売上レポート
db.orders.aggregate([
{ $match: { status: "completed" } },
{
$group: {
_id: {
year: { $year: "$createdAt" },
month: { $month: "$createdAt" }
},
totalRevenue: { $sum: "$amount" },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: "$amount" }
}
},
{ $sort: { "_id.year": -1, "_id.month": -1 } },
{
$project: {
_id: 0,
yearMonth: {
$concat: [
{ $toString: "$_id.year" },
"-",
{ $cond: [{ $lt: ["$_id.month", 10] }, { $concat: ["0", { $toString: "$_id.month" }] }, { $toString: "$_id.month" }] }
]
},
totalRevenue: { $round: ["$totalRevenue", 0] },
orderCount: 1,
avgOrderValue: { $round: ["$avgOrderValue", 0] }
}
}
])
// ユーザーの購入履歴分析
db.orders.aggregate([
{
$lookup: {
from: "users",
localField: "customerId",
foreignField: "_id",
as: "customer"
}
},
{ $unwind: "$customer" },
{
$group: {
_id: "$customerId",
customerName: { $first: "$customer.name" },
customerEmail: { $first: "$customer.email" },
totalSpent: { $sum: "$amount" },
orderCount: { $sum: 1 },
firstOrder: { $min: "$createdAt" },
lastOrder: { $max: "$createdAt" },
products: { $push: "$items.productId" }
}
},
{
$addFields: {
avgOrderValue: { $divide: ["$totalSpent", "$orderCount"] },
daysSinceLastOrder: {
$divide: [
{ $subtract: [new Date(), "$lastOrder"] },
1000 * 60 * 60 * 24
]
}
}
},
{ $sort: { totalSpent: -1 } },
{ $limit: 100 }
])
インデックス
インデックスの作成
// 単一フィールドインデックス
db.users.createIndex({ email: 1 }) // 昇順
db.users.createIndex({ createdAt: -1 }) // 降順
// ユニークインデックス
db.users.createIndex({ email: 1 }, { unique: true })
// 複合インデックス
db.orders.createIndex({ customerId: 1, createdAt: -1 })
// 部分インデックス
db.orders.createIndex(
{ createdAt: 1 },
{ partialFilterExpression: { status: "active" } }
)
// TTLインデックス(自動削除)
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 } // 1時間後に削除
)
// テキストインデックス(全文検索)
db.articles.createIndex({ title: "text", content: "text" })
// 地理空間インデックス
db.locations.createIndex({ coordinates: "2dsphere" })
// ハッシュインデックス(シャーディング用)
db.users.createIndex({ email: "hashed" })
インデックスの管理
// インデックス一覧
db.users.getIndexes()
// インデックスの削除
db.users.dropIndex("email_1")
// 全インデックスの削除(_idを除く)
db.users.dropIndexes()
// インデックスの使用状況
db.users.aggregate([{ $indexStats: {} }])
実行計画の確認
// explain()で実行計画を確認
db.users.find({ email: "test@example.com" }).explain()
// 詳細な実行計画
db.users.find({ email: "test@example.com" }).explain("executionStats")
// 重要な指標
// - COLLSCAN: コレクション全スキャン(インデックスなし)
// - IXSCAN: インデックススキャン
// - totalDocsExamined: 調べたドキュメント数
// - executionTimeMillis: 実行時間
Node.jsでの使用
インストールと接続
// npm install mongodb
const { MongoClient, ObjectId } = require('mongodb');
const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri);
async function main() {
try {
await client.connect();
console.log('Connected to MongoDB');
const db = client.db('myapp');
const users = db.collection('users');
// 操作を実行
} finally {
await client.close();
}
}
main().catch(console.error);
CRUD操作
const { MongoClient, ObjectId } = require('mongodb');
class UserRepository {
constructor(db) {
this.collection = db.collection('users');
}
// 作成
async create(userData) {
const result = await this.collection.insertOne({
...userData,
createdAt: new Date(),
updatedAt: new Date()
});
return result.insertedId;
}
// ID検索
async findById(id) {
return await this.collection.findOne({
_id: new ObjectId(id)
});
}
// 条件検索
async findByEmail(email) {
return await this.collection.findOne({ email });
}
// 一覧取得
async findAll(filter = {}, options = {}) {
const { page = 1, limit = 10, sort = { createdAt: -1 } } = options;
const skip = (page - 1) * limit;
const [users, total] = await Promise.all([
this.collection
.find(filter)
.sort(sort)
.skip(skip)
.limit(limit)
.toArray(),
this.collection.countDocuments(filter)
]);
return {
users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
};
}
// 更新
async update(id, updateData) {
const result = await this.collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
...updateData,
updatedAt: new Date()
}
}
);
return result.modifiedCount > 0;
}
// 削除
async delete(id) {
const result = await this.collection.deleteOne({
_id: new ObjectId(id)
});
return result.deletedCount > 0;
}
// 検索
async search(query, options = {}) {
const { page = 1, limit = 10 } = options;
const filter = {
$or: [
{ name: { $regex: query, $options: 'i' } },
{ email: { $regex: query, $options: 'i' } }
]
};
return await this.findAll(filter, { page, limit });
}
}
// 使用例
async function main() {
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('myapp');
const userRepo = new UserRepository(db);
// ユーザー作成
const userId = await userRepo.create({
name: '田中太郎',
email: 'tanaka@example.com',
age: 30
});
console.log('Created user:', userId);
// ユーザー取得
const user = await userRepo.findById(userId);
console.log('Found user:', user);
// 一覧取得
const { users, pagination } = await userRepo.findAll({}, { page: 1, limit: 10 });
console.log('Users:', users);
console.log('Pagination:', pagination);
await client.close();
}
Mongoose(ODM)
// npm install mongoose
const mongoose = require('mongoose');
// スキーマ定義
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, '名前は必須です'],
trim: true,
maxlength: [100, '名前は100文字以内で入力してください']
},
email: {
type: String,
required: [true, 'メールアドレスは必須です'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, '有効なメールアドレスを入力してください']
},
password: {
type: String,
required: true,
minlength: 8,
select: false // デフォルトで取得しない
},
age: {
type: Number,
min: [0, '年齢は0以上で入力してください'],
max: [150, '年齢は150以下で入力してください']
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
profile: {
bio: String,
avatar: String,
website: String
},
tags: [String],
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true, // createdAt, updatedAtを自動追加
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// インデックス
userSchema.index({ email: 1 });
userSchema.index({ createdAt: -1 });
userSchema.index({ name: 'text', 'profile.bio': 'text' });
// バーチャルフィールド
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
// インスタンスメソッド
userSchema.methods.isAdmin = function() {
return this.role === 'admin';
};
// 静的メソッド
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email });
};
// ミドルウェア(フック)
userSchema.pre('save', async function(next) {
if (this.isModified('password')) {
const bcrypt = require('bcrypt');
this.password = await bcrypt.hash(this.password, 10);
}
next();
});
// モデル作成
const User = mongoose.model('User', userSchema);
// 使用例
async function main() {
await mongoose.connect('mongodb://localhost:27017/myapp');
// 作成
const user = await User.create({
name: '田中太郎',
email: 'tanaka@example.com',
password: 'password123',
age: 30,
tags: ['developer', 'javascript']
});
// 検索
const foundUser = await User.findById(user._id);
const userByEmail = await User.findByEmail('tanaka@example.com');
// 更新
await User.findByIdAndUpdate(user._id, { age: 31 }, { new: true });
// クエリビルダー
const activeUsers = await User.find()
.where('isActive').equals(true)
.where('age').gte(20).lte(40)
.select('name email age')
.sort('-createdAt')
.limit(10);
// 全文検索
const searchResults = await User.find({
$text: { $search: 'developer' }
});
// 集計
const stats = await User.aggregate([
{ $match: { isActive: true } },
{
$group: {
_id: '$role',
count: { $sum: 1 },
avgAge: { $avg: '$age' }
}
}
]);
await mongoose.disconnect();
}
データモデリング
埋め込み vs 参照
// 埋め込み(Embedding)
// 1対少数、頻繁に一緒に読み取る場合に適している
{
"_id": ObjectId("..."),
"title": "ブログ記事",
"content": "...",
"author": {
"name": "田中太郎",
"email": "tanaka@example.com"
},
"comments": [
{ "text": "いい記事ですね", "author": "佐藤" },
{ "text": "参考になりました", "author": "鈴木" }
]
}
// 参照(Reference)
// 1対多、独立して更新する場合に適している
// articles コレクション
{
"_id": ObjectId("article1"),
"title": "ブログ記事",
"authorId": ObjectId("user1")
}
// comments コレクション
{
"_id": ObjectId("comment1"),
"articleId": ObjectId("article1"),
"text": "いい記事ですね",
"authorId": ObjectId("user2")
}
選択の指針
| 基準 | 埋め込み | 参照 |
|---|---|---|
| 関係性 | 1対少数 | 1対多、多対多 |
| 読み取りパターン | 一緒に読み取る | 独立して読み取る |
| 更新頻度 | 低い | 高い |
| ドキュメントサイズ | 16MB未満 | 大きくなる可能性 |
| データの重複 | 許容する | 避ける |
パフォーマンス最適化
インデックス戦略
// 複合インデックスの順序
// クエリパターン: { status: "active", createdAt: { $gte: ... } }
// 正しい順序: 等価条件 → 範囲条件
db.orders.createIndex({ status: 1, createdAt: -1 })
// カバリングインデックス
// インデックスだけで結果を返せるようにする
db.users.createIndex({ email: 1, name: 1 })
db.users.find({ email: "test@example.com" }, { name: 1, _id: 0 })
// スパースインデックス
// nullや存在しないフィールドを含めない
db.users.createIndex({ nickname: 1 }, { sparse: true })
クエリ最適化
// プロジェクションで必要なフィールドのみ取得
db.users.find({}, { name: 1, email: 1 })
// 大量データの処理にはカーソルを使用
const cursor = db.users.find({}).batchSize(100);
while (await cursor.hasNext()) {
const doc = await cursor.next();
// 処理
}
// $existsよりスパースインデックス
// 遅い
db.users.find({ optionalField: { $exists: true } })
// 速い(スパースインデックスがある場合)
db.users.find({ optionalField: { $ne: null } })
書き込み最適化
// バルク操作
const bulk = db.users.initializeUnorderedBulkOp();
for (const user of users) {
bulk.insert(user);
}
await bulk.execute();
// bulkWrite
await db.users.bulkWrite([
{ insertOne: { document: { name: "User1" } } },
{ updateOne: { filter: { _id: id1 }, update: { $set: { status: "active" } } } },
{ deleteOne: { filter: { _id: id2 } } }
]);
まとめ
この記事では、MongoDBの基礎から実践的なクエリまで解説しました。
基本操作
| 操作 | コマンド |
|---|---|
| 挿入 | insertOne, insertMany |
| 検索 | find, findOne |
| 更新 | updateOne, updateMany |
| 削除 | deleteOne, deleteMany |
集計パイプライン
| ステージ | 用途 |
|---|---|
| $match | フィルタリング |
| $group | グループ化・集計 |
| $sort | ソート |
| $project | フィールド選択・変換 |
| $lookup | 結合 |
| $unwind | 配列展開 |
ベストプラクティス
- 適切なインデックスを作成する
- 埋め込みと参照を適切に使い分ける
- プロジェクションで必要なフィールドのみ取得
- explain()で実行計画を確認
- バルク操作で書き込みを効率化
MongoDBは柔軟なデータモデリングと高いスケーラビリティを持つ強力なデータベースです。ぜひプロジェクトに活用してみてください!