MongoDB: ドキュメント検索の基礎:findメソッドとプロジェクション
最終更新:2026-08-26
検索はMongoDBからデータを取得するための核心的な操作です。findメソッドをマスターすることが、データベースとの対話における第一歩となります。
本コースでは、findとfindOneの検索構文、フィールドプロジェクション、ページネーションとソート、検索結果のフォーマットと処理について詳しく解説します。
1. 学習内容
findとfindOneメソッドの主な違い- 検索フィルターの基本構文
- プロジェクション:返却フィールドの選択
- pretty()によるフォーマット出力
- limit / skip / sort:ページネーションとソート
- countDocuments:ドキュメント数のカウント
- Node.js / Mongooseでの検索結果の処理
2. フルスタックエンジニアの実話
(1) 課題:全フィールドを返す検索がネットワーク負荷を引き起こす
Charlieはeコマースのフルスタックエンジニアで、商品一覧APIのパフォーマンスを最適化しています:
「商品一覧APIは100件の商品を返します。各商品は5 KBのサイズですが、フロントエンドはtitle、price、imageの3フィールドしか表示しません。50 KBの不要なデータを返し、APIレスポンスに800 msかかり、帯域幅とパース時間を浪費しています。」
元の検索コード:
// ❌ 反例:全フィールドを返す
app.get('/api/products', async (req, res) => {
const products = await Product.find(); // 全フィールドを返す
res.json(products);
});
// 各商品 5KB、100件 = 500KB
// ネットワークが遅い + フロントエンドのパースが遅い
(2) プロジェクションによる解決策
// ✅ 正例:必要なフィールドのみ返す
app.get('/api/products', async (req, res) => {
const products = await Product.find(
{ isActive: true },
{
projection: {
sku: 1,
title: 1,
price: 1,
thumbnail: 1,
_id: 0 // _idを除外
}
}
)
.sort({ createdAt: -1 })
.limit(20)
.lean(); // mongooseのhydrateをスキップ、パフォーマンス↑3-5倍
res.json(products);
});
// 各商品 200 Byte、20件 = 4KB(パフォーマンス↑100倍)
(3) 効果
| 観点 | プロジェクションなし | プロジェクションあり |
|---|---|---|
| レスポンスサイズ | 500 KB | 4 KB |
| APIレイテンシ | 800 ms | 50 ms |
| フロントエンドのパース時間 | 200 ms | 5 ms |
| ネットワーク帯域幅 | 高 | 1/100 |
3. findとfindOne
概念説明明: findとfindOneはMongoDBの2つの主要な検索メソッドです。findは一致するドキュメントのカーソルを返し、一覧検索に適しています。findOneは単一のドキュメントを返し、詳細検索に適しています。構文は似ていますが、戻り値の型が異なるため、この違いを理解することが検索結果を正しく処理する鍵となります。
仕組み: findは全データを即座に返すのではなく、カーソルオブジェクトを作成します。カーソルは遅延読み込み戦略を採用しており、イテレートまたはtoArray()を呼び出したときにのみ、サーバーからバッチ単位(デフォルトで101件または1 MB)でデータを取得します。この設計により、findは数百万件のデータセットでもメモリを使い果たすことはありません。findOneは本質的にfind().limit(1)と同等ですが、カーソルではなくドキュメントを直接返します。
sequenceDiagram
participant App as アプリケーション
participant Mongo as MongoDBサーバー
App->>Mongo: find({ category: "Electronics" })
Mongo-->>App: カーソルオブジェクト(データ未取得)
App->>Mongo: cursor.next() / toArray()
Mongo-->>App: 最初のバッチ 101件のドキュメント
App->>Mongo: イテレート継続
Mongo-->>App: 後続バッチ(最大16MB/バッチ)
| 観点 | find | findOne |
|---|---|---|
| 戻り値の型 | カーソル | ドキュメントまたはnull |
| 一致件数 | 全一致 | 最初の1件 |
| メモリ使用量 | ストリーミング(遅延読み込み) | 一括 |
| パフォーマンス | 高速 | わずかに高速(カーソル作成なし) |
| 用途 | 一覧検索 | 詳細検索 |
(1) findで複数ドキュメントを検索
// === find基本構文 ===
db.products.find();
// 全ドキュメントを返す(カーソル)
// === find条件指定 ===
db.products.find({ category: "Electronics" });
// 全Electronics製品を返す
// === find配列として返す ===
db.products.find({ category: "Electronics" }).toArray();
// Array<Document>を返す
// === findイテレート(カーソル)===
db.products.find({ category: "Electronics" }).forEach(printjson);
(2) findOne:単一ドキュメントを検索
概念説明明: findOneは単一ドキュメントを検索する便利なメソッドです。内部的にはfind().limit(1)と同等ですが、カーソルではなくドキュメントオブジェクトを直接返します。nullを返すことは一致するドキュメントが見つからなかったことを意味します。これはfindが空のカーソルではなくnullを返すという重要な違いです。
用途: _idで詳細を検索、一意インデックスで単一ドキュメントを取得、存在チェック(特定の条件を満たすドキュメントが存在するかどうかを判定)。
// === findOneは単一ドキュメントを返す ===
db.products.findOne({ sku: "PHONE-001" });
// 最初に一致したドキュメントを返す(またはnull)
// === findOne vs find().limit(1)の違い ===
const doc1 = db.products.findOne({ sku: "PHONE-001" });
const doc2 = db.products.find({ sku: "PHONE-001" }).limit(1).next();
// 結果は同じ、findOneの方が簡潔
(3) findとfindOneの比較
要点分析:
findが返すカーソルは全データを即座に読み込まず、メモリを節約findOneは本質的にfind().limit(-1)であり、カーソルを構築せずドキュメントを直接返す- Mongooseでは、
findは配列Array<T>を返し、findOneはオブジェクトT | nullを返す - ドキュメントの存在を判定するには、
findで配列長をチェックするよりfindOneでnullをチェックする方が効率的
| 観点 | find | findOne |
|---|---|---|
| 戻り値の型 | カーソル | ドキュメントまたはnull |
| 一致件数 | 全一致 | 最初の1件 |
| メモリ使用量 | ストリーミング(遅延読み込み) | 一括 |
| パフォーマンス | 高速 | わずかに高速(カーソル作成なし) |
| 用途 | 一覧検索 | 詳細検索 |
(4) Mongooseでの検索結果
// === mongoose: findは配列を返す ===
const products = await Product.find({ category: "Electronics" });
// Array<Product>
// === mongoose: findOneはオブジェクトを返す ===
const product = await Product.findOne({ sku: "PHONE-001" });
// Product | null
// === 検索結果が空の場合の処理 ===
const product = await Product.findOne({ sku: "NOT_EXIST" });
if (!product) {
return res.status(404).json({ error: "Product not found" });
}
▶ サンプル 1:findの完全な使用方法
// === mongoshでの検索 ===
// 全ドキュメントを検索
db.products.find();
// 指定条件で検索
db.products.find({ category: "Electronics" });
// 複数条件検索(AND)
db.products.find({
category: "Electronics",
stock: { $gt: 0 } // 在庫が0より大きい
});
// 検索してフォーマット
db.products.find({ category: "Electronics" }).pretty();
// 検索してカウント
db.products.find({ category: "Electronics" }).count();
// === Node.jsでの検索 ===
const { MongoClient } = require('mongodb');
async function findProducts() {
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const collection = client.db('shopdb').collection('products');
// 1. find()はカーソルを返す
const cursor = collection.find({ category: "Electronics" });
const products = await cursor.toArray();
console.log(`Found ${products.length} products`);
// 2. findOne()はドキュメントを返す
const product = await collection.findOne({ sku: "PHONE-001" });
console.log(product);
// 3. カーソルをイテレート(ストリーム)
for await (const doc of collection.find({ category: "Electronics" })) {
console.log(doc.title);
}
await client.close();
}
findProducts();
4. 検索フィルター
概念説明明: 検索フィルターはfind / findOneの最初のパラメータで、一致条件を指定するために使用します。フィルターはJSON/BSON構文を使用し、完全一致、比較演算子、論理組み合わせ、ネスト検索など様々なパターンをサポートします。フィルター構文を理解することはMongoDB検索の基礎です。
仕組み: MongoDBは検索フィルターをクエリプランに変換し、インデックスまたは全表スキャンを使用してドキュメントを一致させます。フィルター内の各フィールド条件は独立してインデックスを使用でき、複数条件を組み合わせると、MongoDBオプティマイザーが最適な実行パスを自動選択します。
graph TB
A[検索フィルター] --> B[完全一致<br/>{ field: value }]
A --> C[比較演算子<br/>{ field: { $gt: N } }]
A --> D[論理組み合わせ<br/>{ $and / $or / $not }]
A --> E[ネスト検索<br/>{ "path.field": value }]
A --> F[配列検索<br/>{ array: value }]
style A fill:#cce5ff
| フィルタータイプ | 構文 | 例 |
|---|---|---|
| 完全一致 | { field: value } |
{ sku: "PHONE-001" } |
| 複数条件AND | { f1: v1, f2: v2 } |
{ category: "E", stock: 50 } |
| フィールド不在 | { field: { $exists: false } } |
{ discount: { $exists: false } } |
| ネストドキュメント | { "path.field": value } |
{ "specs.battery": "4500mAh" } |
| 配列要素 | { array: value } |
{ tags: "5g" } |
(1) 基本フィルタリング
// === 完全一致 ===
db.products.find({ sku: "PHONE-001" });
// === 複数条件AND ===
db.products.find({
category: "Electronics",
stock: 50,
isActive: true
});
// === フィールド不在 ===
db.products.find({ discount: { $exists: false } });
// === ネストドキュメント検索 ===
db.products.find({ "specs.battery": "4500mAh" });
// === 配列要素マッチング ===
db.products.find({ tags: "5g" });
// === 配列内の複数要素マッチング ===
db.products.find({ tags: { $all: ["5g", "amoled"] } });
(2) 比較演算子
概念概要: 比較演算子は検索フィルターの核であり、範囲検索、複数値マッチング、除外などの操作をサポートします。MongoDBは8つの比較演算子を提供します:$eq、$ne、$gt、$gte、$lt、$lte、$in、$nin。このうち$eqはデフォルトの動作({ price: 599 }は{ price: { $eq: 599 } }と同等)で、$inは最もよく使われる演算子です。
| 演算子 | 意味 | SQL相当 | インデックス対応 |
|---|---|---|---|
$eq |
等しい | WHERE field = value |
✅ |
$ne |
等しくない | WHERE field != value |
⚠️ |
$gt/$gte |
より大きい / 以上 | WHERE field > />= value |
✅ |
$lt/$lte |
より小さい / 以下 | WHERE field < /<= value |
✅ |
$in |
含まれる | WHERE field IN (...) |
✅ |
$nin |
含まれない | WHERE field NOT IN (...) |
⚠️ |
// === $eq(等しい、デフォルト)===
db.products.find({ price: { $eq: 599.99 } });
// { price: 599.99 } と同等
// === $ne(等しくない)===
db.products.find({ category: { $ne: "Books" } });
// === $gt / $gte(より大きい / 以上)===
db.products.find({ price: { $gt: 100 } }); // > 100
db.products.find({ price: { $gte: 100 } }); // >= 100
// === $lt / $lte(より小さい / 以下)===
db.products.find({ price: { $lt: 1000 } });
db.products.find({ price: { $lte: 1000 } });
// === $in / $nin(含む / 含まない)===
db.products.find({ category: { $in: ["Electronics", "Books"] } });
db.products.find({ category: { $nin: ["Clothing"] } });
// === 範囲検索 ===
db.products.find({
price: { $gte: 100, $lte: 1000 } // 100 <= price <= 1000
});
(3) 論理演算子
概念説明明: 論理演算子は複数の検索条件を組み合わせて複雑なフィルタリングロジックを実装します。MongoDBは4つの論理演算子をサポートします:$and(全条件を満たす)、$or(いずれかの条件を満たす)、$not(条件を満たさない)、$nor(全条件を満たさない)。このうち暗黙のAND(複数フィールドをカンマ区切り)が最もよく使われる構文です。明示的な$andは「同一フィールドに複数条件がある」場合にのみ必要です。
| 演算子 | 意味 | SQL相当 | 使用頻度 |
|---|---|---|---|
| 暗黙のAND | カンマ区切り | WHERE a=1 AND b=2 |
⭐⭐⭐ 最も使用頻度が高い |
$and |
明示的AND | WHERE (a=1 AND b=2) |
⭐ 同一フィールドに複数条件 |
$or |
いずれかを満たす | WHERE a=1 OR b=2 |
⭐⭐ |
$not |
満たさない | WHERE NOT (condition) |
⭐ |
$nor |
全て満たさない | WHERE NOT (a=1 OR b=2) |
低使用頻度 |
// === $and(暗黙のAND)===
db.products.find({
category: "Electronics",
stock: { $gt: 0 } // 暗黙のAND
});
// === $and(明示的AND)===
db.products.find({
$and: [
{ category: "Electronics" },
{ $or: [{ stock: { $gt: 10 } }, { isFeatured: true }] }
]
});
// === $or ===
db.products.find({
$or: [
{ category: "Electronics" },
{ tags: "bestseller" }
]
});
// === $not ===
db.products.find({ price: { $not: { $gt: 1000 } } });
// 価格 <= 1000
// === $nor(全て満たさない)===
db.products.find({
$nor: [
{ category: "Electronics" },
{ category: "Books" }
]
});
▶ サンプル 2:複合検索の例
// === シーン:価格100-1000、在庫0より大、ElectronicsまたはBooksカテゴリの商品を検索 ===
db.products.find({
price: { $gte: 100, $lte: 1000 },
stock: { $gt: 0 },
$or: [
{ category: "Electronics" },
{ category: "Books" }
],
isActive: true
}).sort({ price: 1 }).limit(20);
// === Mongoose相当の記述 ===
const products = await Product.find({
price: { $gte: 100, $lte: 1000 },
stock: { $gt: 0 },
$or: [{ category: "Electronics" }, { category: "Books" }],
isActive: true
})
.sort({ price: 1 })
.limit(20)
.lean();
5. プロジェクション
概念説明明: プロジェクションは検索でどのフィールドを返すかを制御し、ネットワークトラフィックとフロントエンドのパース負荷を削減する重要な最適化技術です。デフォルトではMongoDBはドキュメント内の全フィールドを返しますが、一覧ページやAPIレスポンスなどのシーンでは通常3-5個の主要フィールドのみが必要です。プロジェクションを適切に使用すると、レスポンスサイズを90%以上削減できます。
仕組み: プロジェクションはサーバー側で実行されます。MongoDBがドキュメント全体を読み込んだ後、プロジェクションルールに従ってフィールドを削除してから結果を返します。つまり、プロジェクションはディスクI/Oを削減しません(ドキュメント全体を読み込む必要がある)。ただし、ネットワークトラフィックとクライアントのデシリアライズ時間は大幅に削減できます。唯一の例外はカバードクエリです。検索とプロジェクションの全フィールドがインデックスに含まれる場合、MongoDBはドキュメントを読み込むことなくインデックスから直接データを返します。
graph LR
A[完全なドキュメント<br/>20フィールド ~5KB] --> B{プロジェクションルール}
B -->|ホワイトリストモード<br/>{ sku: 1, title: 1, price: 1 }| C[3フィールド ~200B]
B -->|ブラックリストモード<br/>{ description: 0, images: 0 }| D[18フィールド ~4.5KB]
style C fill:#d4edda
| プロジェクションモード | 構文 | 特徴 | 用途 |
|---|---|---|---|
| ホワイトリスト | { field: 1 } |
指定フィールドのみ返す | 一覧ページ(少数フィールド必要) |
| ブラックリスト | { field: 0 } |
指定フィールドを除外 | 詳細ページ(機密フィールド除外) |
| 混合 | ❌ 不可 | ホワイトリストとブラックリストは併用不可(_idを除く) |
— |
| _id制御 | { _id: 0 } |
デフォルトで返却、明示的に除外が必要 | APIレスポンスから_idを削除 |
(1) プロジェクションとは
プロジェクションは返却する特定フィールドを制御し、ネットワーク転送とフロントエンドパースの負担を軽減します。
graph LR
A[完全なドキュメント<br/>20フィールド] --> B{プロジェクション}
B -->|フィールドホワイトリスト| C[3フィールドのみ返す<br/>~10 KB]
B -->|フィールドブラックリスト| D[2フィールド除外<br/>~18 KB]
style C fill:#d4edda
(2) プロジェクション構文
// === フィールドホワイトリスト(指定フィールドのみ返す)===
db.products.find(
{ category: "Electronics" },
{ sku: 1, title: 1, price: 1 }
);
// 返却:{ _id, sku, title, price }
// === _idはデフォルトで返却、明示的に除外が必要 ===
db.products.find(
{},
{ sku: 1, title: 1, _id: 0 } // _id: 0 で_idを除外
);
// === フィールドブラックリスト(指定フィールドを除外)===
db.products.find(
{},
{ internalNotes: 0, debugInfo: 0 } // 機密フィールドを除外
);
// === ネストドキュメントのプロジェクション ===
db.products.find(
{ sku: "PHONE-001" },
{
sku: 1,
title: 1,
"specs.screen": 1, // specs.screenのみ返す
"specs.battery": 1 // specs.batteryのみ返す
}
);
// === 配列要素のプロジェクション($slice)===
db.reviews.find(
{ productId: "PHONE-001" },
{
title: 1,
content: 1,
comments: { $slice: 3 } // 最初の3件のコメントのみ返す
}
);
(3) プロジェクションのパフォーマンスへの影響
// === パフォーマンステスト:100万ドキュメント、100件を検索 ===
// ❌ プロジェクションなし:5MB返却
db.products.find({ category: "Electronics" }).limit(100);
// 所要時間 800ms
// ✅ プロジェクションあり:200KB返却
db.products.find(
{ category: "Electronics" },
{ sku: 1, title: 1, price: 1, _id: 0 }
).limit(100);
// 所要時間 80ms(パフォーマンス↑10倍)
(4) Mongooseでのプロジェクション
// === 方法1:projectionオプション ===
const products = await Product.find({ category: "Electronics" }, "sku title price");
// 文字列構文(スペース区切り)
// === 方法2:select()チェーン ===
const products = await Product.find()
.select("sku title price")
.select("-description -images"); // 特定フィールドを除外
// === 方法3:オブジェクト構文 ===
const products = await Product.find(
{ category: "Electronics" },
{ sku: 1, title: 1, price: 1, _id: 0 }
);
// === 方法4:lean() + select()で最高パフォーマンス ===
const products = await Product.find()
.select("sku title price")
.lean() // mongooseのhydrateをスキップ
.limit(100);
▶ サンプル 3:eコマース一覧APIのベストプラクティス
// === eコマースサイト一覧API完全版 ===
app.get('/api/products', async (req, res) => {
const {
category,
minPrice,
maxPrice,
search,
sort = 'createdAt',
order = 'desc',
page = 1,
limit = 20
} = req.query;
// 1. 検索条件を構築
const query = { isActive: true };
if (category) query.category = category;
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = NumberDecimal(minPrice);
if (maxPrice) query.price.$lte = NumberDecimal(maxPrice);
}
if (search) query.title = new RegExp(search, 'i');
// 2. ソート
const sortObj = { [sort]: order === 'desc' ? -1 : 1 };
// 3. ページネーション
const skip = (page - 1) * limit;
// 4. 検索(プロジェクション + lean)
const products = await Product.find(query)
.select('sku title price thumbnail rating reviewCount') // 6フィールドのみ返す
.sort(sortObj)
.skip(skip)
.limit(Number(limit))
.lean(); // 重要:mongooseのhydrateをスキップ
// 5. 総件数
const total = await Product.countDocuments(query);
res.json({
products,
pagination: {
page: Number(page),
limit: Number(limit),
total,
pages: Math.ceil(total / limit)
}
});
});
6. pretty()と結果のフォーマット
概念説明明: pretty()はmongoshのフォーマットメソッドで、コンパクトなJSON出力を読みやすいインデント形式に変換します。検索ロジックや返却データには影響せず、mongoshターミナルでの出力表示のみを変更します。スクリプト実行やNode.jsコードではpretty()は機能せず、printjson()またはJSON.stringify(obj, null, 2)を使用する必要があります。
| フォーマット方法 | 環境 | 説明 |
|---|---|---|
.pretty() |
mongosh対話モード | インdent形式、最高の可読性 |
printjson() |
mongoshスクリプト | 完全なJSON構造を出力 |
JSON.stringify(obj, null, 2) |
Node.js | 標準JSONフォーマット |
console.dir(obj, { depth: null }) |
Node.js | 深くネストされた構造を完全出力 |
(1) pretty()でフォーマット出力
// === デフォルト出力(コンパクト)===
db.products.findOne({ sku: "PHONE-001" });
// { _id: ObjectId('...'), sku: 'PHONE-001', title: 'Phone', ... }
// === pretty()フォーマット ===
db.products.findOne({ sku: "PHONE-001" }).pretty();
// {
// _id: ObjectId('507f1f77bcf86cd799439011'),
// sku: 'PHONE-001',
// title: 'Smartphone X',
// price: NumberDecimal('599.99'),
// ...
// }
// === findもpretty対応 ===
db.products.find({ category: "Electronics" }).pretty();
(2) スクリプトでのprettyの影響
# prettyは対話モードで有効、スクリプト出力に違いなし
mongosh "mongodb://localhost:27017" --eval "db.products.find().pretty()"
(3) カスタムフォーマット
// === printjson()の使用 ===
db.products.find().forEach(printjson);
// 完全なJSON構造を出力
// === tojson()の使用 ===
const doc = db.products.findOne();
print(tojson(doc));
// === フォーマット出力(pretty 2)===
printjson(doc, null, 2);
7. limit / skip / sort
概念説明明: limit、skip、sortは検索結果を変更する3つの主要な方法で、それぞれ返却件数、スキップ件数、ソートルールを制御します。適用順序はsort → skip → limitであり、コードでの記述順序に関係なく、MongoDBは常にソート→スキップ→制限の順で処理します。
仕組み: sortは一致したドキュメントを返却前にソートする必要があります。ソートフィールドにインデックスがあればインデックス順序を使用し(非常に効率的)、なければメモリ内でソートします(32 MBを超えるとエラー)。skip(N)は最初のN件のドキュメントをスキャンして破棄する必要があり、Nが大きいほどパフォーマンスが悪くなります。これがディープページネーション問題の根本原因です。limit(N)は返却ドキュメント数を制限し、スキャンを早期終了できます。
graph TB
A[検索結果セット<br/>1000件一致] --> B[sort ソート<br/>指定フィールドで]
B --> C[skip スキップ<br/>最初のN件]
C --> D[limit 制限<br/>M件返却]
B --> B1{ソートフィールドにインデックスあり?}
B1 -->|あり| B2[インデックススキャン<br/>O(log N)]
B1 -->|なし| B3[メモリソート<br/>O(N log N)<br/>32MB超過でエラー]
style B2 fill:#d4edda
style B3 fill:#f8d7da
| メソッド | 機能 | パフォーマンスへの影響 | 注意点 |
|---|---|---|---|
sort({ field: 1/-1 }) |
ソート | インデックスなしでメモリソート | 1: 昇順、-1: 降順 |
skip(N) |
最初のN件をスキップ | Nが大きいほど遅くなる | ディープページネーション回避 |
limit(N) |
結果件数を制限 | 効率向上 | 推奨:≤ 100 |
(1) limit:返却件数を制限
// === 10件返却 ===
db.products.find().limit(10);
// === 条件との組み合わせ ===
db.products.find({ category: "Electronics" }).limit(5);
// === limit(0)はlimit(1)と同等 ===
db.products.find().limit(0); // 1件返却
// === limit(-1)は全件返却(特殊)===
db.products.find().limit(-1); // 全件返却(逆順ソートとして使用)
(2) skip:ドキュメントをスキップ
// === 最初の10件をスキップ、11-20件を返却 ===
db.products.find().skip(10).limit(10);
// === ページ番号の公式 ===
// Nページ目(1ページ20件):skip = (N - 1) * 20
db.products.find().skip((page - 1) * 20).limit(20);
// === skip + sortの一貫性 ===
db.products.find().sort({ _id: 1 }).skip(10).limit(10);
(3) sort
// === 昇順(1)===
db.products.find().sort({ price: 1 }); // 価格(昇順)
// === 降順(-1)===
db.products.find().sort({ createdAt: -1 }); // 最新順
// === 複数フィールドでソート ===
db.products.find().sort({ category: 1, price: -1 });
// まずcategoryで昇順、次にpriceで降順
// === ネストフィールドのソート ===
db.products.find().sort({ "specs.rating": -1 });
// === 配列フィールドのソート ===
db.products.find().sort({ "tags.0": 1 }); // tagsの最初の要素でソート
(4) limit / skip / sortの組み合わせ
// === 完全なページネーション検索 ===
db.products
.find({ category: "Electronics", isActive: true })
.sort({ price: 1, createdAt: -1 }) // 価格昇順、作成日逆順
.skip(20) // 20件スキップ
.limit(10); // 10件返却
// === Mongoose相当の記述 ===
const products = await Product
.find({ category: "Electronics", isActive: true })
.sort({ price: 1, createdAt: -1 })
.skip(20)
.limit(10)
.lean();
(5) ページネーションのパフォーマンス最適化
概念説明明: 従来のskip + limitページネーションは、深いページに移動するとパフォーマンスが急激に低下します。skip(10000)は10000件のドキュメントをスキャンしてから破棄する必要があります。カーソルベースのページネーションは_idまたはソートキーを使用して開始位置を特定し、ターゲットドキュメントに直接ジャンプするため、ページの深さによるパフォーマンスへの影響を受けません。
比較分析:
| 観点 | skip + limit | カーソルページネーション |
|---|---|---|
| ディープページのパフォーマンス | ❌ O(N) 線形低下 | ✅ O(log N) 安定 |
| ページジャンプ対応 | ✅ 任意のページ番号 | ❌ 前後移動のみ |
| 総件数 | countDocuments必要 | 不要 |
| 用途 | 管理画面(ページ番号) | 無限スクロール、フィード |
graph TB
A[ページネーション] --> B[skip + limit 従来型]
A --> C[カーソルベースページネーション<br/>推奨]
B --> B1[skip(10000) 遅い<br/>10000件スキャン]
C --> C1[lastIdで検索<br/>直接ターゲット特定]
style C1 fill:#d4edda
// === 従来のページネーション(遅いページ送り)===
const page1 = await Product.find().skip(0).limit(20);
const page1000 = await Product.find().skip(20000).limit(20); // 遅い!
// === カーソルベースページネーション(推奨)===
const lastId = null; // 初回
const products1 = await Product.find({ _id: { $gt: lastId } }).limit(20);
const nextLastId = products1[products1.length - 1]._id;
const products2 = await Product.find({ _id: { $gt: nextLastId } }).limit(20);
// パフォーマンス安定、ページ深度の影響なし
▶ サンプル 4:完全なページネーション + ソート
// === 総合実習:商品一覧APIのページネーション ===
app.get('/api/products', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
const sortBy = req.query.sort || 'createdAt';
const order = req.query.order === 'asc' ? 1 : -1;
const products = await Product.find({ isActive: true })
.select('sku title price thumbnail rating')
.sort({ [sortBy]: order })
.skip((page - 1) * limit)
.limit(limit)
.lean();
const total = await Product.countDocuments({ isActive: true });
res.json({
data: products,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
});
});
8. countDocuments カウント
概念説明明: countDocumentsとestimatedDocumentCountはMongoDBの2つのカウントメソッドです。前者は正確なカウントを提供しますが一致するドキュメントをスキャンする必要があり、後者はコレクションのメタデータに基づいて推定カウントを行います(非常に高速)。ただしフィルター条件をサポートしません。両者の違いを理解することは、一覧ページのページネーションや統計シーンで重要です。
仕組み: countDocumentsはクエリプランを実行して一致する全ドキュメントをカウントします。パフォーマンスは一致件数に正比例します。estimatedDocumentCountはクエリを実行せず、コレクションのメタデータ(ドキュメント数)を直接読み取ります。パフォーマンスはO(1)です。大規模データコレクションでは、両者のパフォーマンス差は100倍以上になることがあります。
| 観点 | countDocuments | estimatedDocumentCount |
|---|---|---|
| 正確性 | ✅ 正確 | ⚠️ 推定(誤差 < 5%) |
| パフォーマンス | ⚠️ 遅い(全表スキャン) | ⚡⚡ 超高速(O(1)) |
| フィルター条件 | ✅ 対応 | ❌ 非対応 |
| 大規模集計 | ⚠️ 遅い | ⚡ 高速 |
| リアルタイム性 | ✅ リアルタイム | ⚠️ ほぼリアルタイム |
(1) countDocuments:正確なカウント
// === countDocuments:正確なカウント ===
// 全ドキュメントをカウント
db.products.countDocuments();
出力:
TEXT 📖 参照専用1250
// 条件を満たす件数を統計
db.products.countDocuments({ category: "Electronics" });
出力:
TEXT 📖 参照専用250
// === オプション付き ===
db.products.countDocuments(
{ category: "Electronics" },
{ limit: 1000 } // 最大1000件スキャン
);
// === Mongoose相当 ===
const count = await Product.countDocuments({ category: "Electronics" });
// 250
(2) estimatedDocumentCount 推定(高速)
// === 総数の推定(メタデータベース、超高速)===
db.products.estimatedDocumentCount();
// 1250(概算値)
// === 適用シーン ===
// - 一覧ページで「全1000件」と表示(正確さ不要)
// - リアルタイム不要の統計
// === パフォーマンス比較 ===
// countDocuments({}): ~100ms(全表スキャン)
// estimatedDocumentCount(): ~1ms(メタデータ読み取り)
(3) countDocuments vs estimatedDocumentCount
| 観点 | countDocuments | estimatedDocumentCount |
|---|---|---|
| 正確性 | ✅ 正確 | ⚠️ 推定(誤差 < 5%) |
| パフォーマンス | ⚠️ 遅い(全表スキャン) | ⚡⚡ 超高速(O(1)) |
| フィルター条件 | ✅ 対応 | ❌ 非対応 |
| 大規模集計 | ⚠️ 遅い | ⚡ 高速 |
| リアルタイム性 | ✅ リアルタイム | ⚠️ ほぼリアルタイム |
▶ サンプル 5:countの実践的な使い方
// === 商品カテゴリ別統計(正確)===
const stats = await Product.aggregate([
{ $group: { _id: "$category", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]);
// [
// { _id: 'Electronics', count: 250 },
// { _id: 'Books', count: 200 },
// { _id: 'Clothing', count: 180 }
// ]
// === 一覧ページの総件数(推定)===
const totalProducts = await Product.estimatedDocumentCount();
const electronicsCount = await Product.countDocuments({ category: "Electronics" });
res.json({
total: totalProducts, // 推定 1250
electronics: electronicsCount // 正確 250
});
9. 検索結果の処理
(1) カーソルのイテレート
// === mongoshでイテレート ===
db.products.find({ category: "Electronics" }).forEach(doc => {
print(`SKU: ${doc.sku}, Title: ${doc.title}`);
});
// === Node.jsでイテレート ===
const cursor = collection.find({ category: "Electronics" });
// 方法1:toArray()
const products = await cursor.toArray();
// 方法2:for await...of
for await (const doc of collection.find({ category: "Electronics" })) {
console.log(doc.title);
}
// 方法3:手動next()
const cursor2 = collection.find({ category: "Electronics" });
while (await cursor2.hasNext()) {
const doc = await cursor2.next();
console.log(doc);
}
(2) カーソル設定
// === バッチサイズの設定 ===
const cursor = collection.find({ category: "Electronics" })
.batchSize(100); // 1バッチ100件
// === 最大返却数の制限 ===
const cursor = collection.find({ category: "Electronics" })
.limit(1000);
// === カーソルタイムアウトの制限 ===
const cursor = collection.find({ category: "Electronics" })
.maxTimeMS(5000); // 5秒タイムアウト
(3) Mongooseクエリチェーン
// === 完全なMongooseクエリチェーン ===
const products = await Product.find({ category: "Electronics" })
.where('price').gt(100).lt(1000) // 価格 100-1000
.where('stock').gt(0) // 在庫あり
.select('sku title price') // プロジェクション
.sort({ price: 1 }) // ソート
.skip(20) // ページネーション
.limit(10) // 制限
.populate('categoryId', 'name slug') // 結合検索
.lean(); // パフォーマンス最適化
// === 同等の簡潔な記述 ===
const products2 = await Product.find({
category: "Electronics",
price: { $gt: 100, $lt: 1000 },
stock: { $gt: 0 }
})
.select('sku title price')
.sort({ price: 1 })
.skip(20)
.limit(10)
.lean();
▶ サンプル 6:複合検索の実践ガイド
// === シーン:eコマース商品検索API ===
app.get('/api/products/search', async (req, res) => {
const { q, category, minPrice, maxPrice, sortBy = 'relevance' } = req.query;
// 1. クエリを構築
const query = { isActive: true };
if (q) query.$text = { $search: q };
if (category) query.category = category;
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = NumberDecimal(minPrice);
if (maxPrice) query.price.$lte = NumberDecimal(maxPrice);
}
// 2. ソート
const sortObj = sortBy === 'price_asc' ? { price: 1 } :
sortBy === 'price_desc' ? { price: -1 } :
sortBy === 'newest' ? { createdAt: -1 } :
{ score: { $meta: 'textScore' } }; // 全文検索関連度順
// 3. 検索
const products = await Product.find(query, sortObj.score ? { score: { $meta: 'textScore' } } : {})
.sort(sortObj)
.limit(40)
.lean();
// 4. 統計
const total = await Product.countDocuments(query);
res.json({
query: { q, category, minPrice, maxPrice },
total,
products
});
});
❓ よくある質問
findとfindOneのパフォーマンス差はどの程度ですか?findはカーソルを作成(遅延読み込み)、findOneはドキュメントを直接返します。パフォーマンス差はわずかです(< 5%)が、findOneの方がシンプルです。一覧にはfind、詳細にはfindOneを使用してください。_idはデフォルトで含まれるフィールドであり、明示的に_id: 0で除外する必要があります。プロジェクションで_idを省略しても、返却されます。countDocumentsは遅いのですか?estimatedDocumentCount()(メタデータベース)の使用、またはページネーションAPIで推定値を返すことを推奨します。skipは深くなるほど遅くなりますか?skip(N)は結果を返す前に最初のN件のドキュメントをスキャンする必要があるため、Nが大きいほど遅くなります。ディープページネーションにはカーソルページネーション({ _id: { $gt: lastId } })の使用を推奨します。Sort exceeded memory limitエラーが発生します。インデックスがあれば、ソートはO(log N)です。find().limit(0)はどういう意味ですか?find({ _id: null })を使用してください。lean()の役割は何ですか?save()やpopulate()など)にアクセスできなくなります。純粋な検索シーンに適しています。📖 まとめ
findは複数ドキュメントを検索する際にカーソルを返し、findOneは単一ドキュメントを返す- 検索フィルターは比較、論理、要素、配列など30以上の演算子をサポート
- 「プロジェクション」は返却フィールドを制御し、レスポンスサイズを90%以上削減可能
- limit:返却件数を制限、skip:ドキュメントをスキップ、sort:結果をソート
- countDocuments:正確なカウント、estimatedDocumentCount:高速推定
- ディープページネーションにはカーソルベース(_idベース)を使用、
skipは使用しない - Mongooseチェーンクエリ +
lean()が最高パフォーマンス
📝 練習問題
-
基礎問題(⭐):Mongoshに10件の商品ドキュメントを挿入し、
findで"Electronics"カテゴリの全ドキュメントを検索し、pretty()でフォーマット出力してください。 -
基礎問題(⭐):
findOneでSKU "PHONE-001"の商品を検索し、プロジェクションでSKU、title、priceの3フィールドのみ返してください。 -
応用問題(⭐⭐):Node.js APIを作成し、商品一覧のページネーション検索を実装してください(
pageとlimitパラメータを使用)。projectionとlean()でパフォーマンスを最適化し、ページネーション情報(total、pages、hasNext)を返してください。 -
応用問題(⭐⭐):価格が100〜1000の間、在庫> 0、ElectronicsまたはBooksカテゴリの商品を検索し、価格の昇順でソートし、結果を20件に制限してください。
-
応用問題(⭐⭐):100万ドキュメントのコレクションで
skip(0).limit(20)とskip(10000).limit(20)のクエリ実行時間を比較し、ディープページネーションの問題を理解してください。 -
チャレンジ(⭐⭐⭐):カーソルベースのページネーションAPIを実装してください(
lastIdを使用しskipを使用しない)。任意の深度のページネーションにパフォーマンスを損なわずに対応し、完全なAPIドキュメントとテストケースを含めてください。