はじめに
MongoDB で大量のデータに対して distinct 関数を実行したとき、エラー「MongoDB select count(distinct x) on an indexed column」が発生することがあります。これを回避する方法を紹介します。
重複を取り除くためのコレクションを作成する
大量のデータに対して重複を取り除く場合は、一度重複を取り除きたい項目を ID とするコレクションを別に作成してから、このコレクションに対して目的のクエリーを実行するようにします。元のコレクションから変換先のコレクションに変換するには、mapReduce 関数を使用します。
今回、下記のようなドキュメントを例に挙げます。販売伝票なるものが大量に登録されていて、これまで扱ってきた商品の一覧を取得する際、distinct 関数を使ったところ本題のエラーが発生した、というシナリオを想定します。
> db.sales.findOne()
{
"_id": ObjectId("55f0e01b8150f8de310225bf"),
"item": {
"name": "product-A",
"model": "aaa-aa"
},
"sales": {
"date": "20151015",
"staff": "Yamada"
}
}
> db.sales.distinct('item.name')
2015-10-15T05:13:01.910+0000 distinct failed: {
"errmsg" : "exception: distinct too big, 16mb cap",
"code" : 17217,
"ok" : 0
} at src/mongo/shell/collection.js:1108
集計の結果、下記のようなドキュメントを持つコレクションを得られるように進めていきます。
> db.salesdistinct.findOne()
{
"_id": "product-A",
"value": {
"count": 1234
}
}
map 関数を作成する
map 関数は元のコレクションからデータを取り出すときに呼び出され、reduce 関数に渡されるオブジェクトを作成します。取り出したときに正規化しておきたい処理などを記述します
今回、item[‘name’] をキーにしています。なお、例では個数を count として数えていますが、本来の目的である商品の一覧の取得にはitem[‘name’] だけあれば十分であり不要です。map の使い方を紹介するために加えています。
> map = function() {
emit( this.item['name'] , {
count: 1
});
}
reduce 関数を作成する
reduce 関数は map 関数で準備したキーと、そのキーを持つ複数のドキュメントを受け取り、複数のドキュメントを集計したものを変換後のコレクションのドキュメントとして出力します。
> reduce = function(key, values) {
var count = 0;
values.forEach(function (a_value) {
count += a_value['count'];
});
return {
count: count
};
};
mapReduce 関数を実行する
作成した map 関数と reduce 関数を使って、元のコレクションから変換後のコレクションへの変換を実行します。
> db.salesdistinct.drop();
> db.sales.mapReduce( map, reduce, {
out: 'salesdistinct',
verbose: true
});
重複を取り除いたリストを取得する
変換後のコレクションに対して、リストを取得するクエリーを実行します。
> db.salesdistinct.find()