7つのデータベース　7つの世界　~第5章　MongoDB 2日目2/2~


### はじめに

---

　引き続き、「7つのデータベース　7つの世界 -オーム社-」の第5章からMongoDBの2日目のもう半分を勉強していく。
　1/2で早めに切り上げてしまったので、こっちのほうが少し分量が多くなってしまった。

### 2日目「インデックス・集約・mapreduce」

---

#### 集約クエリ

---

　今まで調べたクエリは、基本的なデータの抽出に便利なものだった。しかし、これだとデータの後処理を自分でやらなければいけない。例えば、5,599,999よりも大きな電話番号をカウントする場合は、データベースにバックエンドでカウントしてもらいたい。PostgreSQLと同様に、count()は最も基本的な集約関数である。クエリを受け取り、(マッチした)数を返す。

```mongo:book
> db.phones.count({'components.number' : { $gt : 5599999 } })
50000
```

　次の集約クエリのパワーを見る前に、新たに電話番号100,000件をphonesコレクションに追加しよう。今回は違うエリアコードをつけている。
　distinct()コマンドは、マッチする値が1つ以上存在する場合に、(ドキュメント全体ではなく)マッチした値を返す。以下のようにすれば、5,550,005未満のコンポーネントナンバーを(重複せずに)取り出すことが出来る。

```mongo:book
> populatePhones( 855, 5550000, 5650000 )
> db.phones.distinct('components.number', {'components.number' : { $lt : 5550005 } })
[ 5550000, 5550001, 5550002, 5550003, 5550004 ]
```

　5,550,000というナンバーは2つあるのだが(エリアコードが800と885)、このリストには1つしか登場しない。
　group()集約クエリは、SQLのGROUP BYと似ている。これは、Mongoで最も複雑な基本クエリでもある。例えば5,599,999より大きな電話番号をカウントして、エリアコードをキーにしたグループを結果にできる。keyはグループにするフィールドである。cond(condition：条件)は値の範囲である。reduceは出力値を制御する関数を受け取る。
　Riakの章でやったコマンドにmapreduceがあるが、このデータは、ドキュメントの既存のコレクションにmapされていて、これ以上のmapは不要なので、reduceだけにする。

```mongo:book
> db.phones.group({
... initial: { count : 0 },
... reduce: function(phone, output) { output.count++; },
... cond: { 'components.number' : { $gt : 5599999 } },
... key: { 'components.area' : true }
... })
[
	{
		"components.area" : 800,
		"count" : 50000
	},
	{
		"components.area" : 855,
		"count" : 50000
	}
]
```

　以下の2つの例は、少し変な使用法みたいだが、group()の柔軟性を見るのには良いようだ。
　まず。group()を以下のように呼び出すことで、count()関数を簡単に再現できる。ここでは、keyを省略している。

```mongo:book
> db.phones.group({
... initial: { count:0 },
... reduce: function(phone, output) { output.count++; },
... cond: { 'components.number' : { $gt : 5599999 } }
... })
[ { "count" : 100000 } ]
```

　この例では、最初にcountフィールドを0にして、オブジェクトを初期化している。ここで生成したフィールドが最終的に出力される。次に、reduce関数を宣言して、ドキュメントが見つからなかったらフィールドに加算するようにしている。最後に、reduceするドキュメントの条件を渡している。条件が同じなので、count()の結果と同じになるはずだ。keyを省略したのは、すべてのドキュメントをリストに追加するからである。
　また、disinct()関数を再現することも出来る。パフォーマンスを高めるために、まず数値をフィールドとして保存するオブジェクトを生成する(その場限りのセットを作成することになる)。続くreduce関数(マッチしたドキュメント毎に実行される)では、その値にプレースホルダーとして1を設定するだけである(これが必要なフィールドになる)。
　技術的に必要なことは以上だが、disinct()を完璧に再現するには、整数の配列を返さなければいけない。そこで、最後に一度だけ実行するfinalize(out)メソッドを追加している。これは、オブジェクトをフィールド値の配列に変換するものである。ここでは、文字列を整数に変換している(途中経過を見たければ、finalize関数無しで実行するといい)。

```mongo:book
> db.phones.group({
... initial: { prefixes : {} },
... reduce: function(phone, output) {
... output.prefixes[phone.components.prefix] = 1;
... },
... finalize: function(out) {
... var ary = [];
... for (var p in out.prefixes) { ary.push( parseInt( p ) ); }
... out.prefixes = ary;
... }
... })[0].prefixes
[ 555, 556, 557, 558, 559, 560, 561, 562, 563, 564 ]
```

　group()関数は、SQLのGROUP BYのように強力である。ただし、MongoDBの実装には弱点がある。まず、結果のドキュメント数の上限が10,000である(バージョンアップして上限数は上がった)。さらに、コレクションをシャード(3日目で説明がある)すると、group()は上手く動かない。クエリを柔軟に組み立てる方法は他にもあるので、mapreduceを使ったやり方を紹介する。だがその前に、クライアントサイドとサーバーサイドのコマンドの境界線を説明する。この違いは、アプリケーションにとって重要だ。

#### サーバーサイドコマンド

---

　以下の関数をコマンドライン(やドライバ経由)で実行すると、クライアントは100,000研すべての電話番号をローカルに持ってきて、1件ずつサーバーに保存する。

```/js/update_area.js
update_area = function() {
        db.phones.find().forEach(
                function(phone) {
                        phone.components.area++;
                        phone.display = "+"+
                        phone.components.country+" "+
                        phone.components.area+"-"+
                        phone.components.number;
                        db.phones.update({ _id : phone._id }, phone, false);
                }
        )
}
```

　Mongoのdbオブジェクトには、eval()というコマンドがある。これは、受け取った関数をサーバーに渡すものである。リモートで実行されるので、クライアントとサーバーのやり取りが大幅に減る。

```mongo:book
> db.eval(update_area)
```

　JavaScriptの関数を評価するコマンド以外にも、Mongoにはあらかじめ用意されたコマンドがある。その多くはサーバーで実行される。ただし、adminデータはベースでしか実行できないものもある(use adminを入力すればアクセス出来る)。

```mongo:book
> use admin
> db.listCommands()
```

　topコマンドは、すべてのコレクションのアクセス情報を出力する。

```mongo:book
> use book
> db.listCommands()
```

　listCommands()を実行すると、コマンドがたくさんあることがわかる。電話番号をカウントするなどのコマンドもrunCommand()メソッド経由で実行できる。ただし、出力は全く違ったものになる。

```mongo:book
> db.runCommand({ "count" : "phones" })
{ "n" : 200000, "ok" : 1 }
```

　帰ってきた数字(「n」)は正しい数字(200000)である。しかし、このオブジェクトには「ok」フィールドが付いている。その理由は、db.phones.count()がラッパー関数だからである。これは、シェルのJavaScriptインターフェイスから便利に使えるように作られたものである。一方、runCommand()のカウントは、サーバーで実行されるものである。覚えているだろうが、count()のような関数の動きを調べるには、呼び出しの丸括弧を外せば良い。

```mongo:book
> db.runCommand({ "count" : "phones" })
{ "n" : 200000, "ok" : 1 }
> db.phones.count
function ( x ){
    return this.find( x ).count();
}
```

　collection.count()は、find()の結果のcount()を呼び出すラッパー(カーソルを返すクエリオブジェクトのラッパー)にすぎない。そのクエリを実行してみると・・・

```mongo:book
> db.phones.find().count
function ( applySkipLimit ) {
    var cmd = { count: this._collection.getName() };
    if ( this._query ) {
        if ( this._special ) {
            cmd.query = this._query.query;
            if ( this._query.$maxTimeMS ) {
                cmd.maxTimeMS = this._query.$maxTimeMS;
            }
            if ( this._query.$hint ) {
                cmd.hint = this._query.$hint;
            }
        }
        else {
            cmd.query = this._query;
        }
    }
    cmd.fields = this._fields || {};

    if ( applySkipLimit ) {
        if ( this._limit )
            cmd.limit = this._limit;
        if ( this._skip )
            cmd.skip = this._skip;
    }

    var res = this._db.runCommand( cmd );
    if( res && res.n != null ) return res.n;
    throw "count failed: " + tojson( res );
}
```

　もっと大きな関数が手に入る。コードの最後の3行をよく見ると、count()はrunCommand()を実行して、フィールドnから値を返していることがわかる。

##### runCommand

　メッソドの挙動を深掘りしているが、runCommand()関数についても少しだけ見てみる。

```mongo:book
> db.runCommand
function ( obj ){
    if ( typeof( obj ) == "string" ){
        var n = {};
        n[obj] = 1;
        obj = n;
    }
    return this.getCollection( "$cmd" ).findOne( obj );
}
```

　runCommand()も$cmdという名前のコレクションを呼び出すラッパーである。これもヘルパー関数だということがわかった。このコレクションを直接呼び出してもコマンドを実行できる。

```mongo:book
> db.$cmd.findOne({'count' : 'phones' })
{ "n" : 200000, "ok" : 1 }
```

　これがむき出しの状態であり、ドライバはこれでMongoサーバーとやり取りをしている。

##### 寄り道

　以下の2つの理由から、ちょっと寄り道をする。

* mongoコンソールで実行した魔法の多くが、クライアントではなくサーバーで実行されていることを理解するため。MongoDBでは、便利なラッパー関数が提供されている。
* サーバーサイドでコードを実行するため。これは、PostgreSQLのストアドプロシージャとよく似ているらしい。

　JavaScriptの関数は、system.jsという特別なコレクションに保存できる。これは通常のコレクションである。関数の名前を_idに設定して保存する。valueは関数オブジェクトである。

```mongo:book
> db.system.js.save({
... _id : 'getLast',
... value : function(collection){
... return collection.find({}).sort({'_id':1}).limit(1)[0]
... }
... })
WriteResult({ "nMatched" : 0, "nUpserted" : 1, "nModified" : 0, "_id" : "getLast" })
```

　次にやるのは、サーバーで直接実行することである。eval()関数はサーバーに文字列を渡して、JavaScriptのコードとして評価して、結果を返す。

```mongo:book
> db.eval('getLast(db.phones)')
{
	"_id" : 18005550010,
	"components" : {
		"country" : 1,
		"area" : 801,
		"prefix" : 555,
		"number" : 5550010
	},
	"display" : "+1 801-5550010"
}
```

　ローカルでgetLast(collection)を呼び出したのと同じ値になるだろう。

```mongo:book
> db.system.js.findOne({
... '_id': 'getLast' }).value(db.phones)
{
	"_id" : 18005550010,
	"components" : {
		"country" : 1,
		"area" : 801,
		"prefix" : 555,
		"number" : 5550010
	},
	"display" : "+1 801-5550010"
}
```

　eval()は、実行時にmongodをブロックするので、プロダクション環境ではなくテスト環境で実行するといいだろう。この関数は、$whereやmapreduceの内部でも使われている。それでは、最終兵器のmapreduceを紹介する。

#### mapreduce(とfinalize)

---

　Mongoのmapreduceは、多少のちがいはあるものの、Riakのmapreduceとよく似ている。Mongoは、変換した値を返すmap()関数よりも、emit()関数を呼び出すmapperを必要とする。これには、ドキュメント毎に複数回emit出来るという利点がある。reduce()関数は、1つのキーとそのキーにemitされた値のリストを受け取る。Mongoは3番目の手順も提供している。finalize()である。これは、reduceを実行したあとに、mapした値毎に実行するものである。最終的な計算やクリーンナップに使える。
　国番号と同じ番号を含む電話番号を全てカウントするレポートに生成してみる。まずは、disinctした番号の配列を抽出するヘルパー関数を保存する(このヘルパーの挙動は、mapreduceの理解に必須ではない)。

```/js/distinct_digits.js
distinctDigits = function(phone) {
        var
          number = phone.components.number + '',
          seen = [],
          result = [],
          i = number.length;
        while(i--) {
          seen[+number[i]] = 1;
        }
        for (i=0; i<10; i++) {
          if (seen[i]) {
                result[result.length] = i;
          }
        }
        return result;
}
db.system.js.save({_id: 'distinctDigits', value: distinctDigits})
```

　mongoコマンドラインで読み取る方法をここで紹介しよう。ファイルがmongoを起動したディレクトリにあれば、ファイル名の指定だけで良い。自分のようにほかのディレクトリにあれば、フルパスで指定する。

```mongo:book
> load('~/Documents/mongodb/js/distinct_digits.js')
```

　準備ができたらテストしてみよう。

```mongo:book
> db.eval("distinctDigits(db.phones.findOne({ 'components.number' : 5551213 }))")
[ 1, 2, 3, 5 ]
```

　これでmapperにとりかかることができる。mapreduce関数もそうだが、どのフィールドをmapするかが重要である。それが最終的に集約する値になるからである。このレポートはdistinctした数字を探しているので、distinctした値の配列が1つのフィールドになる。ただし、国ごとにクエリを発行する必要があるので、それもまた別のフィールドになる。したがって、両方の値を{digits : X, country : Y}のように複合キーで表す。
　これらのカウントすることが目標である。したがって、1の値をemitする(各ドキュメントがカウントする項目になる)。reducerの仕事は、これらの1を合計することである。

```/js/map_1.js
map = function() {
        var digits = distinctDigits(this);
        emit({digits : digits, country : this.components.country}, {count : 1});
}
```

```/js/reduce_1.js
reduce = function(key, value) { 
        var total = 0;
        for(var i=0; i<values.length; i++) {
                total += value[i].count;
        }
        return { count : total };
}       

results = db.runCommand({
        mapReduce: 'phones',
        map: map,
        reduce: reduce,
        out: 'phones.report'
})
```

　コレクションの名前は引数outで設定するので(out : 'phones.report')、他と同じように結果を問い合わせることができる。以下は、show tablesのリストにあるマテリアライズドビューである。

```mongo:book
> db.phones.report.find({'_id.country' : 8}
```

　結果の続きを見るにはitを入力する。emitした一意なキーは、_idフィールドにある。reducerから戻されたすべてのデータは、valueフィールドにある。
　mapreduceの結果だけを出力したい場合は、outの値を{ inline : 1 }に設定すれば良い。ただし、出力する結果にはサイズ制限があるので注意しよう。

### おわりに

---

　2日目では、集約クエリを扱い、クエリのパワーを広げた。MongoDBのインデックスを使って、クエリの応答時間を短くした。もっとパワーが必要なら、mapreduce()が利用できる。