LoginSignup
2
1

More than 3 years have passed since last update.

MongoDBのdb.Collection.aggregateを使うお話

Posted at

はじめに

この記事を書き終える直前に「もしかして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を動かしてるサーバのスペックの違いな気がする

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1