MongoDB Academy · レッスン

パイプラインでの$sort、$limit、$skip

集約結果の並べ替えとページネーションを行い、$groupの前に$matchや$sortを移動するオプティマイザーのルールを理解します。

レッスン 4/413 ステップ

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

概要: 結果の並べ替えとページネーション

集計パイプラインには、結果の順序と件数を制御する3つのステージがあります。$sort は1つ以上のフィールドでドキュメントを並べ替え、$limit は先頭から N 件のドキュメントだけを取得し、$skip は先頭から N 件のドキュメントを破棄します。これらを組み合わせることで、ソートとページネーションを実装できます。パイプラインのどこに配置するかを理解することは、パフォーマンスに大きく影響します。

$sort ステージ

$sort ステージは、1つ以上のフィールドの値でドキュメントを並べ替えます。昇順には 1、降順には -1 を使用します。複数のフィールドを指定した場合、まず最初のフィールドで並べ替え、同値のドキュメントについては次のフィールドで並べ替えるというように処理します。これは、SQL の複合キーによるソートや MongoDB の find().sort() とまったく同じです。

// Sort by revenue descending, then alphabetically by name for ties
db.products.aggregate([
  { $group: { _id: '$category', revenue: { $sum: '$price' } } },
  { $sort: { revenue: -1, _id: 1 } }  // revenue desc, category name asc
]);

$sort とインデックスの使用

$sort がパイプラインの最初のステージである場合(またはインデックスを使用する $match の直後である場合)、MongoDB はインデックスを使ってソートを満たせるため、メモリ内でのソートを完全に回避できます。$sort が $group や $project のように新しいフィールドを生成するステージの後にある場合、使用できるインデックスはなく、MongoDB はメモリ内でソートを実行します。100 MB を超える大規模なメモリ内ソートには allowDiskUse: true が必要です。

// Index-backed sort: $sort on indexed field before any transformation
db.orders.aggregate([
  { $match: { userId: 'u1' } },
  { $sort: { createdAt: -1 } }  // if createdAt is indexed, no in-memory sort
]);

// In-memory sort: $sort after $group (computed field, no index)
db.orders.aggregate([
  { $group: { _id: '$userId', total: { $sum: '$amount' } } },
  { $sort: { total: -1 } }  // in-memory: 'total' is a computed field
]);

$limit ステージ

$limit は先頭から N 件のドキュメントだけを後続ステージに渡し、それ以外を破棄します。使い方は単純で、通過を許可するドキュメントの最大数を整数で指定します。$lookup のような負荷の高いステージの前に $limit を配置すると、実行される結合や検索の数を大幅に減らせます。常に「早い段階で制限できないか」と考えてください。

// Top 10 best-selling products
db.orders.aggregate([
  { $group: { _id: '$productId', sold: { $sum: '$qty' } } },
  { $sort: { sold: -1 } },
  { $limit: 10 },  // only top 10 pass through
  // Only 10 lookups instead of thousands:
  { $lookup: { from: 'products', localField: '_id', foreignField: '_id', as: 'product' } }
]);

$skip ステージ

$skip は、ストリームの先頭から N 件のドキュメントを破棄します。これはオフセット方式のページネーションに相当します。1ページあたり10件の場合、2ページ目は skip 10 から始まります。安定した結果を得るため、$skip は必ず $sort の後に配置してください。ソートされていないドキュメントをスキップすると、ページの内容が予測できなくなります。SQL の OFFSET と同様に、大きなオフセットでの $skip は低速です。MongoDB はスキップするすべてのドキュメントを処理する必要があるためです。

const PAGE = 2;
const PAGE_SIZE = 10;

db.articles.aggregate([
  { $match: { status: 'published' } },
  { $sort: { publishedAt: -1 } },
  { $skip: (PAGE - 1) * PAGE_SIZE },  // skip page 1's 10 docs
  { $limit: PAGE_SIZE }               // take page 2's 10 docs
]);

オプティマイザー: $sort と $limit の統合

MongoDB の集計オプティマイザーは、$sort ステージの直後に $limit ステージが続くことを検出すると、両者を上位 N 件のソートに統合します。すべてのドキュメントをソートしてから大部分を破棄する代わりに、MongoDB はソート中に優先度付きキューで上位 N 件の候補だけを保持します。これにより、メモリ使用量は O(total) ではなく O(N) になります。この最適化は自動的に行われるため、$sort の後に $limit を記述するだけで利用できます。

// This pattern triggers the top-N sort optimisation
db.reviews.aggregate([
  { $match: { productId: 'p1' } },
  { $sort: { rating: -1, helpful: -1 } },  // sort
  { $limit: 5 }   // $sort + $limit merged into top-5 sort internally
]);
// MongoDB never sorts ALL reviews; it tracks only the best 5

オプティマイザー: $match と $sort の順序変更

集計オプティマイザーは、安全に実行できる場合、$match ステージをパイプラインの早い位置に自動的に移動します。たとえば、$sort が計算していないフィールドに対する $match より前に $sort がある場合、オプティマイザーは $match を $sort の前に移動します。これにより、ソートが必要なドキュメント数を減らせます。オプティマイザーによる変更は、explain() の出力で確認できます。

// You write:
db.orders.aggregate([
  { $sort: { amount: -1 } },
  { $match: { status: 'completed' } }  // optimizer moves this BEFORE $sort
]);

// Optimized execution (equivalent to):
db.orders.aggregate([
  { $match: { status: 'completed' } },  // fewer docs to sort
  { $sort: { amount: -1 } }
]);

4つすべてを組み合わせる: 完全な一覧エンドポイント

フィルター、ソート、ページネーションを備えた現実的な商品一覧 API エンドポイントでは、$match、$sort、$skip、$limit を正しい順序で組み合わせます。順序は常に「フィルター → ソート → スキップ → 制限」です。この順序により、不要なドキュメントを破棄する前に、MongoDB はフィルタリングと(可能な場合は)ソートの両方でインデックスを使用できます。

async function getProducts({ category, sort = 'price', page = 1, pageSize = 20 }) {
  return db.products.aggregate([
    { $match: { category, inStock: true } },   // 1. filter
    { $sort: { [sort]: 1 } },                  // 2. sort
    { $skip: (page - 1) * pageSize },          // 3. offset
    { $limit: pageSize }                       // 4. take page
  ]).toArray();
}

深いオフセットでの $skip のパフォーマンス

$skip の根本的な問題は、MongoDB がスキップするすべてのドキュメントを処理して破棄しなければならないことです。1ページあたり20件の場合、1000ページ目では、20件を返す前に19,980件のドキュメントを破棄します。ページが深くなるほど、処理は遅くなります。ユーザーが通常5~10ページ目を超えて進まない一般公開の検索 API やフィード API では、オフセット方式のページネーションで問題ありません。深いページを高頻度で取得する場合は、キ​​ーセットページネーションが必要です。

// Page 1000 is SLOW: MongoDB processes and discards 19,980 docs
db.posts.aggregate([
  { $sort: { createdAt: -1 } },
  { $skip: 19980 },  // 999 * 20
  { $limit: 20 }
]);

// Keyset pagination: always fast regardless of page depth
db.posts.aggregate([
  { $match: { createdAt: { $lt: lastSeenTimestamp } } },  // cursor
  { $sort: { createdAt: -1 } },
  { $limit: 20 }
]);

$sort のメモリ制限と allowDiskUse

集計パイプラインのメモリ内 $sort には、デフォルトで100 MBの制限があります。ソートがこの制限を超えると、パイプラインは QueryExceededMemoryLimitNoDiskUseAllowed エラーで失敗します。aggregate() の第2引数として { allowDiskUse: true } を渡すと、ディスクへの退避を有効にできます。または、早い段階で $match を適用するか、対応するインデックスを使用してソート対象の入力サイズを減らしてください。

// Enable disk use for large sorts
db.events.aggregate(
  [
    { $match: { year: 2024 } },
    { $sort: { timestamp: -1 } },
    { $group: { _id: '$userId', events: { $push: '$type' } } }
  ],
  { allowDiskUse: true }  // allows spilling to disk
);

結果とともに合計件数を取得する

ページネーションされた結果と合計件数の両方を、1回のデータベース往復で返すのは一般的な API パターンです。$facet を使うと、パイプラインを2つの並列ブランチに分割できます。一方は $skip/$limit による結果取得用、もう一方は $count による件数取得用です。これにより、集計を2回別々に呼び出す必要がなくなり、すべてのドキュメントを別途数えるより効率的です。

db.products.aggregate([
  { $match: { category: 'electronics' } },
  { $sort: { price: 1 } },
  { $facet: {
    // Branch 1: paginated results
    data: [
      { $skip: 0 },
      { $limit: 20 }
    ],
    // Branch 2: total count
    total: [
      { $count: 'count' }
    ]
  }}
]);
// Output: { data: [...20 docs...], total: [{ count: 348 }] }

理解度チェック

集計パイプラインにおける $sort、$limit、$skip の理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、$sort がドキュメントを並べ替え、早い段階に配置するとインデックスの恩恵を受けられること、$limit が先頭から N 件のドキュメントを取得し、$sort と統合されて効率的な上位 N 件のソートになること、そして$skip がオフセット方式のページネーションを実装する一方、深いオフセットでは性能が低下することを学びました。次は、コレクションの結合に使う $lookup など、高度な集計ステージについて学びます。

無料で開始

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

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

コース
30
レッスン
120

よくある質問

「パイプラインでの$sort、$limit、$skip」レッスンは無料ですか?

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

「パイプラインでの$sort、$limit、$skip」で何を学びますか?

集約結果の並べ替えとページネーションを行い、$groupの前に$matchや$sortを移動するオプティマイザーのルールを理解します。 ブラウザで直接実行するハンズオンコードでMongoDB Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

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

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

「パイプラインでの$sort、$limit、$skip」レッスンにはどのくらい時間がかかりますか?

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

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

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

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

  1. パイプラインの概念:ステージ、演算子、式
  2. $matchと$project:フィルターと形状変更
  3. $group:集約と合計値の計算
  4. パイプラインでの$sort、$limit、$skip
← MongoDB Academyに戻る