はじめに
この記事を書き終える直前に「もしかしてDBサーバのスペックのせいじゃね?」と思ってしまったので数値の類は真に受けないでください。
aggregateを使うとこんなことができるんだー程度でお願いします。
大人の事情でデータ構造やコードなどをかなり改変しています。
この記事は私の個人ブログの記事を再編成して持ってきたものです。
事の発端
他のサーバエンジニアの方「データを抽出するためのMongoShell書いてるけどパフォーマンス出ない」
抽出対象のデータ
実際のサービスのコレクションのバックアップを展開したもの
データ構造(1ドキュメントあたり)
item
{
"inventory": {
"item1": {
"time": 1212121212121
},
"item2": {
"time": 1212121212121,
"count": 2
} ...
},
"used": {
"item3": {
"time": 1212121212121
},
"item2": {
"time": 1212121212121,
"count": 1
} ...
}
}
抽出したいもの
ユーザがアイテムおよび過去に使用したアイテム(used
以下)の合計個数
各アイテムの個数はcount
、ただしcount
が存在しない場合は1とみなす
コード
普通のコード
db.Item.find({}, {used: true}).forEach(function(doc) {
var used = doc.used || {};
var items = Object.keys(used);
var len = items.length;
var count = 0;
for (var i = 0; i < len; i++) {
var code = items[i];
count += used[code].count || 1;
}
print(doc._id + ',' + count);
});
aggregateを使ったコード
db.Item.aggregate([
{$match: {used: {$exists: true}}},
{$project: {usd: {$objectToArray: '$used'}}},
{$unwind: '$usd'},
{$project: {usdLen: {$ifNull: ['$usd.v.count', 1]}}},
{$group: {_id: '$_id', count: {$sum: '$usdLen'}}},
],
{allowDiskUse: true}
).forEach((doc) => {
printjson(doc);
});
実行時間
上記のコードをそれぞれextract.js
として保存し、timeコマンドで計測してみた
time mongo ${HOST}:${PORT}/${DB} extract.js > result.txt
普通のコード
real 1555m5.464s
user 1550m3.492s
sys 1m0.109s
aggregateを使ったコード
real 57m14.554s
user 6m7.054s
sys 0m25.907s
解説
aggregateに配列で渡すOperatorを順番に実行する。
JavaのStreamAPI的なやつ
db.Item.aggregate([
{$match: {used: {$exists: true}}}, // usedが存在するものだけピックアップ
{$project: {usd: {$objectToArray: '$used'}}}, // usdにusedをArrayにしたものを詰める
{$unwind: '$usd'}, // usdをunwindする(下記参照)
{$project: {usdLen: {$ifNull: ['$usd.v.count', 1]}}}, // usd.v.countがnullであれば1を、nullでなければusd.v.countをusdLenに入れる
{$group: {_id: '$_id', count: {$sum: '$usdLen'}}}, // _idが同じものを集約する、countにはusdLenを合計したものを入れる
],
{allowDiskUse: true} // メモリだけで集計すると死ぬことがあるのでtmpファイルを作成することを許可
).forEach((doc) => {
printjson(doc);
});
$unwind
配列を展開して複数のオブジェクトにしてくれる
これが
[
{
"name": "team1"
"member": ["suzuki", "sato"]
},
{
"name": "team2",
"member": ["tanaka"]
}
]
↓こうなる
[
{"name":"team1", "member": "suzuki" },
{"name":"team1", "member": "sato" },
{"name":"team2", "member": "tanaka" }
]
考察
DBサーバとMongoShellを動かしてるサーバのスペックの違いな気がする