MongoDB Academy · レッスン

findOne と find:カーソルの仕組み

findOne でドキュメントを取得し、find のカーソルを反復処理して、MongoDB が大きな結果セットをストリーム処理する仕組みを理解します。

レッスン 2/413 ステップ

「findOne と find:カーソルの仕組み」はCoddyKit上の無料MongoDB Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはMongoDB Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 MongoDB Academyコースには全4レッスンが含まれています。

ドキュメントを読み取る 2 つの方法

MongoDB には、コレクションからドキュメントを読み取る主な方法が 2 つあります。

  • findOne(filter, projection) — フィルターに一致する最初のドキュメントを取得し、通常のドキュメントオブジェクトとして返します。一致するものがない場合は null を返します
  • find(filter, projection) — 一致するすべてのドキュメントを取得し、カーソルを返します。カーソルは、サーバーから結果を一度に 1 バッチずつストリーミングする遅延イテレーターです

それぞれのメソッドを使う場面とカーソルの仕組みを理解することは、効率的な MongoDB クエリを書くうえで基本となります。

findOne:シンプルで直接的な方法

findOne() は、単一のドキュメントを取得する最も簡単な方法です。フィルターに一致する最初のドキュメントを返し、一致するドキュメントがない場合は null を返します。複数のドキュメントが一致する場合、MongoDB は内部的な順序で最初に見つかったものを返します。特定のドキュメントが必要な場合は、呼び出す前に .sort() を追加してください。

findOne の一般的な用途には、メールアドレスによるユーザー検索、SKU による商品の取得、レコードの存在確認などがあります。カーソルではなく通常のオブジェクトを返すため、反復処理を行わずに結果を直接使用できます。

// findOne by _id (most common lookup)
const user = await db.collection('users').findOne(
  { _id: ObjectId('64a2f3b1...') }
);
if (!user) {
  throw new Error('User not found');
}
console.log(user.name); // 'Alice'

// findOne with a filter
const admin = await db.collection('users').findOne({ role: 'admin' });
// Returns ONE admin doc (undefined order), or null

カーソルとは

カーソルは、クエリ結果の集合を指し示すポインターです。find() を呼び出しても、MongoDB は一致するすべてのドキュメントを直ちにクライアントへ転送しません。代わりにサーバー側でカーソルを開き、バッチ単位(デフォルトでは 1 バッチ 101 件)でドキュメントを送信します。クライアントは現在のバッチを使い切ったときにだけ、次のバッチを取得します。

この設計はメモリ効率の面で重要です。クエリが 1,000 万件のドキュメントに一致し、それらをすべて一度に読み込むと、クライアントがクラッシュする可能性があります。カーソルを使えば、結果セットのサイズに関係なくメモリ使用量を一定に保ちながら、ドキュメントを一度に 1 バッチずつ処理できます。

// find() returns a cursor, not documents
const cursor = db.collection('orders').find({ status: 'pending' });
// No data fetched yet!

// Data flows as you iterate:
for await (const order of cursor) {
  // Each iteration fetches from server in batches
  console.log(order._id);
}
// Cursor is exhausted — server releases it

Node.js でのカーソルの反復処理

Node.js ドライバーのカーソルは、複数の反復処理パターンに対応しています。現在最も推奨される方法は for await...of(非同期イテレーション)で、バックプレッシャーとエラーハンドリングを適切に処理できます。別の方法として cursor.toArray() がありますが、これはすべての結果をメモリに読み込むため、便利な一方で結果セットが大きい場合は危険です。

反復処理を途中で抜ける場合(必要なものが見つかった場合など)は、処理が終わったら必ずカーソルを閉じてください。開いたカーソルは MongoDB サーバー上のリソースを保持します。cursor.close() を明示的に使用するか、for await...of に任せてください。後者では、ループの完了時またはエラー発生時にカーソルが自動的に閉じられます。

// Pattern 1: async for...of (recommended)
const cursor = db.collection('products').find({ inStock: true });
for await (const product of cursor) {
  await processProduct(product);
}

// Pattern 2: toArray() - loads all into memory
const products = await db.collection('products')
  .find({ inStock: true }).toArray();

// Pattern 3: forEach
await cursor.forEach(product => console.log(product.name));

カーソルのバッチサイズと getMore

内部的には、カーソルプロトコルは 2 つの段階で動作します。

  1. 最初の find コマンドが、最初のバッチ(デフォルトでは 101 件または 16 MB のいずれか早い方)を返します
  2. 以降の各バッチは、カーソル ID を使って getMore コマンドで取得されます

cursor.batchSize(n) でバッチサイズを変更できます。バッチサイズを小さくすると双方のメモリ使用量を減らせますが、ネットワーク往復が増えます。バッチサイズを大きくすると、大規模なシーケンシャルスキャンがより効率的になります。通常はデフォルト値が最適なので、特定のワークロードがある場合にだけ調整してください。

// Set a custom batch size (rarely needed)
const cursor = db.collection('logs')
  .find({})
  .batchSize(500);

// Count documents in a cursor without loading them
// (MongoDB 4.4+ supports .count() on cursor for backwards compat)
// Prefer countDocuments() for accurate counts:
const count = await db.collection('logs').countDocuments({});
console.log('Total logs:', count);

カーソルのタイムアウトとセッション

デフォルトでは、MongoDB のカーソルはサーバー側で10 分間操作がないとタイムアウトします。各バッチの処理にそれ以上かかると、カーソルが削除され、次のバッチを取得しようとしたときに CursorNotFound エラーが発生します。

長時間実行する処理では、noCursorTimeout: true を設定するか、セッションを使用してカーソルを維持してください。ただし、noCursorTimeout はサーバー上のカーソルを無期限に開いたままにします。リソースリークを避けるため、処理が終わったら必ずこれらのカーソルを明示的に閉じてください。

// Long-running cursor that won't time out
const cursor = db.collection('bigCollection').find(
  {},
  { noCursorTimeout: true }
);

try {
  for await (const doc of cursor) {
    await slowProcessing(doc); // Takes > 10 minutes total
  }
} finally {
  // Always close explicitly when using noCursorTimeout
  await cursor.close();
}

find() での修飾子のチェーン

find() が返すカーソルは fluent API に対応しており、反復処理を開始する前にメソッドをチェーンしてクエリを変更できます。可読性のための順序は重要ですが、実行には影響しません(MongoDB はすべての修飾子をまとめて送信します)。

  • .sort({ field: 1 }) — ソート方向
  • .limit(n) — 最大ドキュメント数
  • .skip(n) — 最初の n 件をスキップ
  • .projection({ field: 1 }) — フィールドを選択
  • .hint({ index: 1 }) — 特定のインデックスを強制的に使用
  • .maxTimeMS(ms) — クエリに時間がかかりすぎた場合に中止
// Full chained query: filter → sort → skip → limit → projection
const page2Products = db.collection('products').find(
  { category: 'Electronics', inStock: true },
  { name: 1, price: 1, _id: 0 }   // projection as 2nd arg
)
  .sort({ price: -1 })  // Descending price
  .skip(20)             // Skip page 1 (20 items)
  .limit(20)            // Page size 20
  .maxTimeMS(5000);     // Abort if > 5s

Capped コレクション用の Tailable カーソル

tailable カーソルと呼ばれる特殊なカーソル型は、capped コレクションでのみ動作します。すべての結果を読み終えると閉じる通常のカーソルとは異なり、tailable カーソルはブロックして新しいドキュメントを待機します。これはログファイルに対する Unix の tail -f コマンドに似ています。

tailable カーソルは、Change Streams が導入される前に MongoDB でリアルタイムデータをストリーミングするために使われていた元来の仕組みです。Change Streams を使うほどではない capped コレクションの軽量なログ追跡には、現在でも役立ちます。

// Tailable cursor on a capped collection
const tailCursor = db.collection('appLogs').find(
  {},
  { tailable: true, awaitData: true }
);

// Blocks and awaits new log entries indefinitely
for await (const log of tailCursor) {
  console.log('[' + log.level + '] ' + log.message);
  // Prints each new log as it is inserted
}

findOne と find:適切な使い分け

findOne と find を使い分ける際は、次の目安に従ってください。

  • findOne を使う場合:結果が 1 件だけだと予想される(一意キーによる検索)、存在確認だけが必要、または単一レコードの API エンドポイントで最もシンプルなコードにしたい
  • find を使う場合:クエリが 0 件、1 件、または複数件を返す可能性がある、一覧エンドポイントを作成している、カーソルを制御する必要がある(batchSize、maxTimeMS)、またはすべてをメモリに読み込まずに結果を処理したい

大規模なコレクションで find({}).toArray() を使うのは避けてください。すべての結果がメモリに読み込まれます。代わりに for await...of で処理してください。

// GOOD: findOne for unique key lookup
const user = await db.collection('users').findOne({ email: 'alice@test.com' });

// GOOD: find with streaming for large sets
for await (const doc of db.collection('users').find({ active: true })) {
  await sendNewsletter(doc);
}

// BAD: loading millions of docs into memory
const allUsers = await db.collection('users').find({}).toArray();
// Could OOM crash your server!

カーソルでの explain() メソッド

カーソルに .explain('executionStats') を追加すると、ドキュメントを返す代わりに MongoDB がクエリをどのように実行するかを確認できます。出力には次の情報が含まれます。

  • winningPlan.stage:IXSCAN(インデックスを使用)または COLLSCAN(全件スキャン。大規模なコレクションでは不適切)
  • nReturned:返されたドキュメント数
  • totalDocsExamined:結果を見つけるために MongoDB が調べたドキュメント数(インデックスを使用している場合は nReturned に近い値になるはずです)
  • executionTimeMillis:合計実行時間

重要なクエリに対して定期的に explain() を実行することは、MongoDB のパフォーマンスチューニングの基礎です。

// Check query execution plan
const plan = await db.collection('users')
  .find({ email: 'alice@test.com' })
  .explain('executionStats');

console.log(plan.queryPlanner.winningPlan.stage);
// 'IXSCAN' if email is indexed, 'COLLSCAN' if not

console.log(plan.executionStats.nReturned);        // 1
console.log(plan.executionStats.totalDocsExamined); // 1 (indexed) or 50000 (COLLSCAN)

API レスポンスでの ObjectId の変換

findOne または find().toArray() が ObjectId フィールドを含むドキュメントを返す場合、JSON API のレスポンスとして返す前に ObjectId を特別に処理する必要があります。古いドライバーのバージョンでは、JSON.stringify が ObjectId をオブジェクト {} としてシリアライズするため値が失われます。新しいバージョンでは文字列表現としてシリアライズされます。

最も安全な方法は、マッピング関数でクライアントに送信する前に、ObjectId フィールドに対して明示的に .toString() を呼び出すことです。クライアントは ID を文字列として送り返し、サーバー側でクエリを実行する前に new ObjectId(idString) で変換します。

function toPublicDoc(doc) {
  if (!doc) return null;
  return {
    ...doc,
    _id: doc._id.toString(),  // ObjectId -> string for JSON
    authorId: doc.authorId ? doc.authorId.toString() : null
  };
}

// Usage:
const post = await db.collection('posts').findOne({ slug: 'intro' });
res.json(toPublicDoc(post));
// Client receives: { _id: '64a2f3b1c9e7...', title: '...' }

クイックチェック

このレッスンで学んだ MongoDB & NoSQL Databases の概念について理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、findOneは単一のドキュメントを直接返す一方、findはカーソルを返すことを学びました。カーソルは結果をバッチ単位で遅延ストリーミングするため、大量の結果セットによるメモリの問題を回避できます。また、カーソルは流暢なチェーンAPI(.sort()、.limit()、.skip()、.maxTimeMS())をサポートしており、MongoDBはこれらを最適化された1つのクエリとして送信します。さらに、explain('executionStats')を使うと、クエリがインデックス(IXSCAN)を使用しているか、コレクション全体をスキャンしているか(COLLSCAN)を確認できます。次は、ドット記法を使ってネストされたフィールドや配列をクエリする方法を学びます。

無料で開始

AI チューターと学ぶ JavaScript — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
30
レッスン
120

よくある質問

「findOne と find:カーソルの仕組み」レッスンは無料ですか?

はい。「findOne と find:カーソルの仕組み」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、MongoDB Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 MongoDB Academyコースには全4レッスンが含まれています。

「findOne と find:カーソルの仕組み」で何を学びますか?

findOne でドキュメントを取得し、find のカーソルを反復処理して、MongoDB が大きな結果セットをストリーム処理する仕組みを理解します。 ブラウザで直接実行するハンズオンコードでMongoDB Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

MongoDB Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのMongoDB Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「findOne と find:カーソルの仕組み」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このMongoDB Academyレッスンでコードを書いて実行できますか?

はい。すべてのMongoDB Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. insertOne と insertMany
  2. findOne と find:カーソルの仕組み
  3. ネストされたフィールドと配列のクエリ
  4. Node.js Driver によるドキュメントの読み取り
← MongoDB Academyに戻る