MongoDB: 高度な集計パイプライン:複雑な式と変換
最終更新:2026-08-26
高度な集計パイプライン—複雑な式と型変換をマスターすれば、データ分析シナリオの90%を解決できます。
1. このレッスンで学ぶこと
- $groupアキュムレータの高度な使用法
- 条件式:$cond / $ifNull / $switch
- 日付抽出:$year / $month / $dayOfWeek
- 日付フォーマット:$dateToString
- 型変換:$toString / $toInt / $toDecimal
- 配列操作:$arrayElemAt / $map / $filter
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で広く使用されます。
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:三項演算子
// === 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値処理
// === null/undefinedを置換 ===
db.users.aggregate([
{
$project: {
name: 1,
displayName: {
$ifNull: ['$nickname', '$username'] // nicknameが空ならusernameを使用
}
}
}
]);
(3) $switch:多条件分岐
// === 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)。
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) 日付抽出
// === $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) 日付フォーマット
// === $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) 日付計算
// === $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指定可能 | 安全な変換(推奨) |
// === 数値型変換 ===
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. 文字列操作
// === $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により、配列内要素の変換・フィルタリング・集計が可能です。
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() |
// === $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) 複雑なビジネスシナリオ
// === シナリオ:ユーザーセグメンテーション分析 ===
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) 売上レポート
// === 月次売上レポート(前月比含む)===
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:集計パイプラインの高度な実践 - ユーザーセグメンテーション分析(難易度 ⭐⭐)
// シナリオ:支出に基づいてユーザーを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売上レポート + 日付フォーマット(難易度 ⭐)
// シナリオ: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:前月比付き月次売上レポート(難易度 ⭐⭐)
// シナリオ: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}
❓ よくある質問
timezoneパラメータをサポート(IANAタイムゾーン名、例:'Asia/Tokyo')。$convertでonErrorハンドラを指定可能。📖 まとめ
- 条件式:$cond(if-else)、$switch(多分岐)、$ifNull(null値処理)
- 日付操作:$year/$month/$dayOfWeek + $dateToString
- 型変換:$toString/$toInt/$toDecimal/$toDate
- 文字列操作:$concat、$substr、$toUpper
- 配列操作:$arrayElemAt/$size/$map/$filter
📝 練習問題
- 基本問題(⭐):$switch文を使用して注文ステータスに日本語ラベルを追加せよ。
- 基本問題(⭐):$dateToStringを使用して注文日をフォーマットせよ。
- 応用問題(⭐⭐):ユーザーセグメンテーション($switchでVIP、Gold、Silverを区別)を実装せよ。
- 応用問題(⭐⭐):$mapを使用して全タグを大文字に変換せよ。
- チャレンジ問題(⭐⭐⭐):月次売上レポート + 前月比成長率($setWindowFields + $shift)を作成せよ。