MongoDB: 高度な集計パイプライン:複雑な式と変換

最終更新:2026-08-26

高度な集計パイプライン—複雑な式と型変換をマスターすれば、データ分析シナリオの90%を解決できます。

1. このレッスンで学ぶこと


100%
graph LR
    A[ドキュメント] -->|$cond<br/>三項演算| B[条件付き射影]
    A -->|$switch<br/>多分岐| C[カテゴリタグ]
    A -->|$ifNull<br/>null値処理| D[デフォルト値置換]
    A -->|$dateToString<br/>日付フォーマット| E[日付文字列]
    A -->|$toInt/$toDecimal<br/>型変換| F[型変換]

    style B fill:#d4edda
    style C fill:#d4edda


2. 条件式

概念説明: 条件式は集計パイプラインの「論理制御フロー」として機能し、ドキュメント内の各値を条件に基づいて動的に計算できます。$condは三項式(if-then-else)、$switchは多分岐マッチ(switch-caseに類似)、$ifNullはnull値置換(デフォルト値提供)です。これらは$project$addFields$groupで広く使用されます。

100%
graph TB
    A[条件式] --> B[$cond<br/>三項if-else]
    A --> C[$switch<br/>多分岐マッチング]
    A --> D[$ifNull<br/>null値置換]
    
    B --> E["price >= 1000 → '高い'<br/>else price >= 100 → '普通'<br/>else → '安い'"]
    C --> F["status = 'paid' → '支払済み'<br/>status = 'shipped' → '発送済み'<br/>default → '不明'"]
    D --> G["nicknameがnull<br/>→ usernameで置換"]
    
    style B fill:#d4edda
    style C fill:#cce5ff
    style D fill:#fff3cd
構文 使用例 可読性
$cond {if, then, else} 2分岐条件 中程度
ネスト$cond "else"内に$condをネスト 3分岐以上
$switch {branches, default} 3分岐以上
$ifNull { $ifNull: [expr, default] } null値処理

(1) $cond:三項演算子

JAVASCRIPT
// === if-elseに類似 ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      priceLevel: {
        $cond: {
          if: { $gte: ['$price', 1000] },
          then: '高い',
          else: {
            $cond: {
              if: { $gte: ['$price', 100] },
              then: '普通',
              else: '安い'
            }
          }
        }
      }
    }
  }
]);

(2) $ifNull:null値処理

JAVASCRIPT
// === null/undefinedを置換 ===
db.users.aggregate([
  {
    $project: {
      name: 1,
      displayName: {
        $ifNull: ['$nickname', '$username']  // nicknameが空ならusernameを使用
      }
    }
  }
]);

(3) $switch:多条件分岐

JAVASCRIPT
// === switch-caseに類似 ===
db.orders.aggregate([
  {
    $project: {
      orderId: '$_id',
      statusLabel: {
        $switch: {
          branches: [
            { case: { $eq: ['$status', 'pending'] }, then: '支払待ち' },
            { case: { $eq: ['$status', 'paid'] }, then: '支払済み' },
            { case: { $eq: ['$status', 'shipped'] }, then: '発送済み' },
            { case: { $eq: ['$status', 'delivered'] }, then: '配達済み' }
          ],
          default: '不明なステータス'
        }
      }
    }
  }
]);


3. 日付操作

概念説明: 日付操作は時系列データ分析の基盤です。MongoDBは3種類の日付演算子を提供します:(1) 抽出演算子($year/$month/$dayOfMonth/$hour等)、(2) フォーマット演算子($dateToString)、(3) 算術演算子($add/$subtract)。

100%
graph LR
    A[日付フィールド<br/>2026-07-01T10:30:00Z] --> B[$year → 2026]
    A --> C[$month → 7]
    A --> D[$dayOfMonth → 1]
    A --> E[$hour → 10]
    
    A --> F["$dateToString<br/>%Y-%m-%d → '2026-07-01'"]
    
    A --> G["$add[date, 7*86400000]<br/>→ 2026-07-08"]
    A --> H["$subtract[now, date]<br/>→ 日数差"]
    
    style B fill:#d4edda
    style F fill:#cce5ff
    style G fill:#fff3cd

(1) 日付抽出

JAVASCRIPT
// === $year / $month / $dayOfWeek / $hour ===
db.orders.aggregate([
  {
    $project: {
      year: { $year: '$createdAt' },
      month: { $month: '$createdAt' },
      day: { $dayOfMonth: '$createdAt' },
      weekday: { $dayOfWeek: '$createdAt' },  // 1=日曜日
      hour: { $hour: '$createdAt' }
    }
  }
]);

(2) 日付フォーマット

JAVASCRIPT
// === $dateToString 日付フォーマット ===
db.orders.aggregate([
  {
    $project: {
      orderDate: {
        $dateToString: {
          format: '%Y-%m-%d %H:%M:%S',
          date: '$createdAt',
          timezone: 'Asia/Tokyo'
        }
      }
    }
  }
]);
// { orderDate: '2026-07-01 10:30:00' }
フォーマット指定子 意味
%Y 4桁年
%m 2桁月
%d 2桁日
%H 24時間表記
%M
%S

(3) 日付計算

JAVASCRIPT
// === $add / $subtract 日付の加減 ===
db.orders.aggregate([
  {
    $project: {
      createdAt: 1,
      expiryDate: { $add: ['$createdAt', 7 * 24 * 60 * 60 * 1000] },  // 7日追加
      daysSinceCreated: {
        $divide: [
          { $subtract: [new Date(), '$createdAt'] },
          1000 * 60 * 60 * 24
        ]
      }
    }
  }
]);


4. 型変換

概念説明: MongoDBは弱い型付けデータベースで、同じコレクション内のフィールドが異なるデータ型を持つ可能性があります。集計パイプラインは$toString/$toInt/$toLong/$toDouble/$toDecimal/$toDate/$toBool等の変換演算子を提供し、データ型の不整合を解決します。

変換演算子 入力 → 出力 典型的なシナリオ
$toString 任意 → 文字列 数値を表示用文字列に変換
$toInt 文字列/数値→Int32 文字列価格を整数に変換
$toLong 文字列/数値→Int64 大きなID変換
$toDouble 文字列/数値→Double 精密計算
$toDecimal 文字列/数値→Decimal128 通貨の精密計算
$toDate 文字列/数値→Date 文字列日付をDateに変換
$convert onError指定可能 安全な変換(推奨)
JAVASCRIPT
// === 数値型変換 ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      price: 1,
      priceString: { $toString: '$price' },           // Decimal128 → 文字列
      priceInt: { $toInt: '$price' },                  // → Int32
      priceLong: { $toLong: '$price' },                // → Int64
      priceDouble: { $toDouble: '$price' },            // → Double
      priceDecimal: { $toDecimal: '$price' }           // → Decimal128
    }
  }
]);

// === 日付変換 ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      releaseDate: { $toDate: '$releaseDateStr' }     // 文字列 → Date
    }
  }
]);


5. 文字列操作

JAVASCRIPT
// === $substr 文字列切り取り ===
db.users.aggregate([
  {
    $project: {
      email: 1,
      emailPrefix: { $substr: ['$email', 0, 5] }  // メールの最初の5文字
    }
  }
]);

// === $concat 文字列連結 ===
db.users.aggregate([
  {
    $project: {
      fullName: { $concat: ['$firstName', ' ', '$lastName'] }
    }
  }
]);

// === $toUpper / $toLower 大文字・小文字 ===
db.users.aggregate([
  {
    $project: {
      usernameUpper: { $toUpper: '$username' }
    }
  }
]);


6. 配列操作

概念説明: 配列操作はMongoDBの集計パイプラインとSQLを区別する核心機能です。$map$filter$reduceにより、配列内要素の変換・フィルタリング・集計が可能です。

100%
graph LR
    A["tags: ['5g','amoled','fast']"] --> B["$arrayElemAt: 0<br/>→ '5g'"]
    A --> C["$size<br/>→ 3"]
    A --> D["$map: {$toUpper}<br/>→ ['5G','AMOLED','FAST']"]
    A --> E["$filter: len >= 3<br/>→ ['amoled','fast']"]
    
    style B fill:#d4edda
    style C fill:#cce5ff
    style D fill:#fff3cd
    style E fill:#e2d5f1
演算子 機能 JavaScript相当
$arrayElemAt インデックスで要素取得 arr[index]
$size 配列長 arr.length
$map 要素ごとに変換 arr.map(fn)
$filter 条件フィルタ arr.filter(fn)
$reduce 累積計算 arr.reduce(fn, init)
$concatArrays 配列結合 [...a, ...b]
$reverseArray 配列反転 arr.reverse()
JAVASCRIPT
// === $arrayElemAt インデックスで要素取得 ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      firstTag: { $arrayElemAt: ['$tags', 0] },
      lastTag: { $arrayElemAt: ['$tags', -1] }
    }
  }
]);

// === $size 配列長 ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      tagCount: { $size: '$tags' }
    }
  }
]);

// === $map 配列変換 ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      tagsUpper: {
        $map: {
          input: '$tags',
          as: 'tag',
          in: { $toUpper: '$$tag' }
        }
      }
    }
  }
]);

// === $filter 配列フィルタ ===
db.products.aggregate([
  {
    $project: {
      title: 1,
      expensiveTags: {
        $filter: {
          input: '$relatedProducts',
          as: 'product',
          cond: { $gte: ['$$product.price', 1000] }
        }
      }
    }
  }
]);


7. 総合実践

(1) 複雑なビジネスシナリオ

JAVASCRIPT
// === シナリオ:ユーザーセグメンテーション分析 ===
db.orders.aggregate([
  { $match: { status: 'paid' } },
  {
    $group: {
      _id: '$userId',
      totalSpent: { $sum: '$total' },
      orderCount: { $sum: 1 },
      avgOrderValue: { $avg: '$total' },
      firstOrderAt: { $min: '$createdAt' },
      lastOrderAt: { $max: '$createdAt' }
    }
  },
  {
    $addFields: {
      userLevel: {
        $switch: {
          branches: [
            { case: { $gte: ['$totalSpent', 10000] }, then: 'VIP' },
            { case: { $gte: ['$totalSpent', 1000] }, then: 'Gold' },
            { case: { $gte: ['$totalSpent', 100] }, then: 'Silver' }
          ],
          default: 'Bronze'
        }
      },
      daysSinceLastOrder: {
        $divide: [
          { $subtract: [new Date(), '$lastOrderAt'] },
          1000 * 60 * 60 * 24
        ]
      }
    }
  },
  { $sort: { totalSpent: -1 } },
  { $limit: 100 }
]);

(2) 売上レポート

JAVASCRIPT
// === 月次売上レポート(前月比含む)===
db.orders.aggregate([
  {
    $group: {
      _id: {
        year: { $year: '$createdAt' },
        month: { $month: '$createdAt' }
      },
      revenue: { $sum: '$total' },
      orderCount: { $sum: 1 },
      avgOrderValue: { $avg: '$total' }
    }
  },
  { $sort: { '_id.year': 1, '_id.month': 1 } },
  {
    $setWindowFields: {
      sortBy: { '_id.year': 1, '_id.month': 1 },
      output: {
        prevMonthRevenue: {
          $shift: {
            output: '$revenue',
            by: -1
          }
        }
      }
    }
  },
  {
    $addFields: {
      growthRate: {
        $cond: {
          if: { $gt: ['$prevMonthRevenue', 0] },
          then: {
            $divide: [
              { $subtract: ['$revenue', '$prevMonthRevenue'] },
              '$prevMonthRevenue'
            ]
          },
          else: 0
        }
      }
    }
  }
]);

▶ サンプル 1:集計パイプラインの高度な実践 - ユーザーセグメンテーション分析(難易度 ⭐⭐)

JAVASCRIPT
// シナリオ:支出に基づいてユーザーをVIP階層に分類
db.orders.insertMany([
  { userId: 'user_001', total: NumberDecimal('15000'), createdAt: new Date('2026-06-01'), status: 'paid' },
  { userId: 'user_002', total: NumberDecimal('500'),   createdAt: new Date('2026-06-05'), status: 'paid' },
  { userId: 'user_003', total: NumberDecimal('50'),    createdAt: new Date('2026-06-10'), status: 'paid' },
  { userId: 'user_001', total: NumberDecimal('800'),   createdAt: new Date('2026-06-15'), status: 'paid' }
]);

// 完全パイプライン:ユーザーセグメンテーション + タグ変換 + 月次統計
db.orders.aggregate([
  // ステップ1:支払済み注文のみカウント
  { $match: { status: 'paid' } },

  // ステップ2:ユーザーでグループ化
  {
    $group: {
      _id: '$userId',
      totalSpent: { $sum: '$total' },
      orderCount: { $sum: 1 },
      avgOrderValue: { $avg: '$total' },
      lastOrderAt: { $max: '$createdAt' }
    }
  },

  // ステップ3:$switchでユーザーを分類
  {
    $addFields: {
      userLevel: {
        $switch: {
          branches: [
            { case: { $gte: ['$totalSpent', 10000] }, then: 'VIP' },
            { case: { $gte: ['$totalSpent', 1000] },  then: 'Gold' },
            { case: { $gte: ['$totalSpent', 100] },   then: 'Silver' }
          ],
          default: 'Bronze'
        }
      },
      // 最終注文からの日数
      daysSinceLastOrder: {
        $divide: [
          { $subtract: [new Date(), '$lastOrderAt'] },
          1000 * 60 * 60 * 24
        ]
      }
    }
  },

  // ステップ4:日付フォーマット
  {
    $project: {
      userId: '$_id',
      totalSpent: 1,
      avgOrderValue: { $toString: '$avgOrderValue' },  // Decimal128 → 文字列
      userLevel: 1,
      daysSinceLastOrder: { $round: ['$daysSinceLastOrder', 0] },  // 整数に丸める
      lastOrderDate: {
        $dateToString: {
          format: '%Y-%m-%d',
          date: '$lastOrderAt',
          timezone: 'Asia/Tokyo'
        }
      }
    }
  },

  // ステップ5:支出額でソート
  { $sort: { totalSpent: -1 } }
]);

出力: 3人のユーザーが支出額で自動的にセグメント分けされます。user_001は総支出15,800でVIPとマークされ、最終注文から16日経過、日付は東京時間でフォーマット済み。


▶ サンプル 2:ShopHub売上レポート + 日付フォーマット(難易度 ⭐)

JAVASCRIPT
// シナリオ:ShopHub運営チームが月次売上レポートを生成。フォーマット済み日付と前月比を含む
db.orders.insertMany([
  { orderId: 'ORD-001', userId: 'user_001', total: NumberDecimal('1500'), status: 'paid', createdAt: new Date('2026-05-15') },
  { orderId: 'ORD-002', userId: 'user_002', total: NumberDecimal('800'),  status: 'paid', createdAt: new Date('2026-06-01') },
  { orderId: 'ORD-003', userId: 'user_001', total: NumberDecimal('2200'), status: 'paid', createdAt: new Date('2026-06-20') },
  { orderId: 'ORD-004', userId: 'user_003', total: NumberDecimal('300'),  status: 'paid', createdAt: new Date('2026-07-05') }
]);

// 月次レポート:月フォーマット、前月比計算、ユーザーセグメンテーションタグ
db.orders.aggregate([
  { $match: { status: 'paid' } },
  {
    $group: {
      _id: {
        year: { $year: '$createdAt' },
        month: { $month: '$createdAt' }
      },
      revenue: { $sum: '$total' },
      orderCount: { $sum: 1 },
      avgOrderValue: { $avg: '$total' }
    }
  },
  { $sort: { '_id.year': 1, '_id.month': 1 } },
  {
    $addFields: {
      monthLabel: {
        $dateToString: {
          format: '%Y-%m',
          date: { $dateFromParts: { year: '$_id.year', month: '$_id.month' } }
        }
      },
      revenueStr: { $toString: '$revenue' },
      performance: {
        $switch: {
          branches: [
            { case: { $gte: ['$revenue', 2000] }, then: '優秀' },
            { case: { $gte: ['$revenue', 1000] }, then: '良好' },
            { case: { $gte: ['$revenue', 500] }, then: '普通' }
          ],
          default: '目標未達'
        }
      }
    }
  }
]);

出力: 月次レポートにはフォーマット済み月ラベル(2026-05)、変換済み売上文字列、$switchによる自動パフォーマンス評価タグが含まれます。


▶ サンプル 3:前月比付き月次売上レポート(難易度 ⭐⭐)

JAVASCRIPT
// シナリオ:ShopHub売上ダッシュボードの当月と前月比較
db.orders.insertMany([
  { orderId: 'ORD-001', total: 15000, status: 'paid', createdAt: new Date('2026-06-15') },
  { orderId: 'ORD-002', total: 8500, status: 'paid', createdAt: new Date('2026-06-25') },
  { orderId: 'ORD-003', total: 12000, status: 'paid', createdAt: new Date('2026-07-05') },
  { orderId: 'ORD-004', total: 9500, status: 'paid', createdAt: new Date('2026-07-15') },
  { orderId: 'ORD-005', total: 18000, status: 'paid', createdAt: new Date('2026-07-20') }
]);

// パイプライン:月次集計 + ウィンドウ関数で成長率
const monthlyReport = db.orders.aggregate([
  // ステージ1:支払済み注文のみマッチ
  { $match: { status: 'paid' } },
  // ステージ2:年月でグループ化
  { $group: {
    _id: {
      year: { $year: '$createdAt' },
      month: { $month: '$createdAt' }
    },
    revenue: { $sum: '$total' },
    orderCount: { $sum: 1 },
    avgOrderValue: { $avg: '$total' }
  }},
  // ステージ3:時系列でソート
  { $sort: { '_id.year': 1, '_id.month': 1 } },
  // ステージ4:ウィンドウ関数で前月売上を取得
  { $setWindowFields: {
    sortBy: { '_id.year': 1, '_id.month': 1 },
    output: {
      prevRevenue: { $shift: { output: '$revenue', by: -1 } }
    }
  }},
  // ステージ5:成長率を計算
  { $addFields: {
    growthRate: {
      $cond: {
        if: { $gt: ['$prevRevenue', 0] },
        then: {
          $multiply: [
            { $divide: [{ $subtract: ['$revenue', '$prevRevenue'] }, '$prevRevenue'] },
            100
          ]
        },
        else: 0
      }
    }
  }},
  // ステージ6:出力フォーマット
  { $project: {
    month: { $concat: [
      { $toString: '$_id.year' }, '-',
      { $toString: '$_id.month' }
    ]},
    revenue: 1,
    orderCount: 1,
    avgOrderValue: { $round: ['$avgOrderValue', 2] },
    growthRate: { $round: ['$growthRate', 1] }
  }}
]);

monthlyReport.forEach(r => console.log(JSON.stringify(r)));

出力:

TEXT 📖 参照専用
{"month":"2026-6","revenue":23500,"orderCount":2,"avgOrderValue":11750,"growthRate":0}
{"month":"2026-7","revenue":39500,"orderCount":3,"avgOrderValue":13166.67,"growthRate":68.1}

❓ よくある質問

Q $condと$switchどちらのパフォーマンスが良い?
A $condがわずかに高速(CPU命令数が少ない)。多分岐の場合のみ$switchを使用。
Q $dateToStringはタイムゾーンをサポート?
A timezoneパラメータをサポート(IANAタイムゾーン名、例:'Asia/Tokyo')。
Q 型変換が失敗したらどうなる?
A デフォルトではnullを返す。$convertでonErrorハンドラを指定可能。

📖 まとめ


📝 練習問題

  1. 基本問題(⭐):$switch文を使用して注文ステータスに日本語ラベルを追加せよ。
  2. 基本問題(⭐):$dateToStringを使用して注文日をフォーマットせよ。
  3. 応用問題(⭐⭐):ユーザーセグメンテーション($switchでVIP、Gold、Silverを区別)を実装せよ。
  4. 応用問題(⭐⭐):$mapを使用して全タグを大文字に変換せよ。
  5. チャレンジ問題(⭐⭐⭐):月次売上レポート + 前月比成長率($setWindowFields + $shift)を作成せよ。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%