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


### はじめに

---

　「7つのデータベース　7つの世界 -オーム社-」の第5章からMongoDBの2日目を勉強する。

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

---

#### インデックス：あまり高速ではないとき

---

　MongoDBの便利な組み込み機能の中には、クエリ機能を向上させるインデックスがある。MongoDBは、インデックスに最適なデータ構造をいくつか提供している。古典的なBツリーと、2次元インデックスやGeoSpatialインデックスなどの追加的なものである。
　MongoDBのBツリーインデックスのパワーを見るために、ちょっとした実験をしてみる。まずは、ランダムな国番号をつけた電話番号のデータを投入する。以下のコードをコンソールに入力して欲しい。このコードは、「1-800-555-0000」から「1-800-565-9999」までの10万件の電話番号を生成するものである。

```code/populate_phones.js
populatePhones = function(area,start,stop) {
        for(var i=start; i < stop; i++) {
                var country = 1 + ((Math.random() * 8) << 0);
                var num = (country * 1e10) + (area * 1e7) + i;
                db.phones.insert({
                        _id : num,
                        components : {
                                country : country,
                                area : area,
                                prefix : (i * le-4) << 0,
                                number : i
                        },
                        display: "+" + country + " " + area + " " + i
                });
        }
}
```

　3桁のエリアコード(800)と7桁の数値(5,550,000から5,650,000。ゼロの数に注意)を使って、関数を実行する。
　念のため、jsファイルをdbに追加するところから記述する。

```mongo:book
$ mongo book ~/Documents/mongodb/js/populate_phones.js --shell
MongoDB shell version: 2.6.7
connecting to: book
type "help" for help
Server has startup warnings: 
2015-09-19T09:26:29.474+0900 [initandlisten] 
2015-09-19T09:26:29.474+0900 [initandlisten] ** WARNING: soft rlimits too low. Number of files is 256, should be at least 1000
> populatePhones( 800, 5550000, 5650000 )
> db.phones.find().limit(2)
{ "_id" : 48005550000, "components" : { "country" : 4, "area" : 800, "prefix" : 555, "number" : 5550000 }, "display" : "+4 800 5550000" }
{ "_id" : 68005550001, "components" : { "country" : 6, "area" : 800, "prefix" : 555, "number" : 5550001 }, "display" : "+6 800 5550001" }
> db.system.indexes.find()
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "book.towns" }
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "book.phones" }
```

　新しいコレクションを作成すると、Mongoは自動的に_idにインデックスを作成する。このインデックスは、system.indexesコレクションにある。
　db.system.indexes.find()というクエリは、データベースにあるすべてのインデックスを表示する。

　_id以外にも問い合わせるフィールドがあるので、それらのフィールドにもインデックスを作成する必要がある。
　これからdisplayフィールドにBツリーインデックスを作成する。だがその前に、インデックスによって高速化することを検証する。まずは、インデックスのないクエリから確認する。explain()メソッドは、任意の操作を詳細に出力するのに使う。

```mongo:book
> db.phones.find({display: "+1 800-5650001"}).explain()
{
	"cursor" : "BasicCursor",
	"isMultiKey" : false,
	"n" : 0,
	"nscannedObjects" : 100000,
	"nscanned" : 100000,
	"nscannedObjectsAllPlans" : 100000,
	"nscannedAllPlans" : 100000,
	"scanAndOrder" : false,
	"indexOnly" : false,
	"nYields" : 781,
	"nChunkSkips" : 0,
	"millis" : 68,
	"server" : "AlanMac-three.local:27017",
	"filterSet" : false
}
```

　具体的な数値は違うかもしれないが、millisフィールド(クエリの実行時間を表すミリ秒)が、2桁の数値になっていると思う。
　インデックスを作成するには、コレクションのensureIndex(fields,options)を呼び出せばいい。引数fieldsは、インデックスを作成するフィールドを含んだオブジェクトである。引数optionsは、作成するインデックスの種類を表している。ここでは、displayに重複を許さないユニークインデックスを作成する。

```mongo:book
> db.phones.ensureIndex(
... { display : 1 },
... { unique : true, dropDups : true }
... )
{
	"createdCollectionAutomatically" : false,
	"numIndexesBefore" : 1,
	"numIndexesAfter" : 2,
	"ok" : 1
}
```

　再びfind()とexplain()を実行して、高速化されたことを確認する。

```mongo:book
> db.phones.find({ display: "+1 800-5650001" }).explain()
{
	"cursor" : "BtreeCursor display_1",
	"isMultiKey" : false,
	"n" : 0,
	"nscannedObjects" : 0,
	"nscanned" : 0,
	"nscannedObjectsAllPlans" : 0,
	"nscannedAllPlans" : 0,
	"scanAndOrder" : false,
	"indexOnly" : false,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"millis" : 6,
	"indexBounds" : {
		"display" : [
			[
				"+1 800-5650001",
				"+1 800-5650001"
			]
		]
	},
	"server" : "AlanMac-three.local:27017",
	"filterSet" : false
}
```

　millisの値が68から6になった。桁違いの高速化ができた。それから、cursorがBasicCursorからBtreeCursorに変わっている(値ではなく値が保存されたポイントを指すので、カーソルと呼ばれるらしい)。これで、コレクションをフルスキャンせずに、ツリーを走査するようになった。ここで重要なのは、スキャンするオブジェクトが109999から1に減ったことである。つまり、一意な検索になったというわけである。
　explain()は便利な関数だが、特定のクエリ呼び出しをテストするときにしか使えない。通常のテストやプロダクション環境では、システムプロファイラが必要になる。
　プロファイリングレベルを2に設定して(レベル2はすべてのクエリを保存する。レベル1は100ミリ秒より遅いクエリのみを保存する)、いつもの様にfind()を実行しよう。

```mongo:book
> db.setProfilingLevel(2)
{ "was" : 0, "slowms" : 100, "ok" : 1 }
> db.phones.find({ display : "+1 800-5650001" })
```

　新しいオブジェクトがsystem.profileコレクションに作成される。これは、他のテーブルからでも読み取れる。「ts」はクエリを実行したタイムスタンプ、「info」は操作名、「millis」はかかった時間である。
　ちなみに、昨日の記事で出力結果を整形してほしいな〜と思って調べたら、find()の後ろに.pretty()をつけるだけでいいみたい。

```mongo:book
> db.system.profile.find().pretty()
```

　昨日のクエリのように、Mongoはネストした値にもインデックスを作成できる。すべてのエリアコードにインデックスが必要であれば、components.areaのようにドットで区切ってフィールドを表現する。インデックスをバックグラウンドで作るには、{ background : 1 }オプションを使う。

```mongo:book
> db.phones.ensureIndex({ "components.area" : 1 }, { background : 1 })
{
	"createdCollectionAutomatically" : false,
	"numIndexesBefore" : 2,
	"numIndexesAfter" : 3,
	"ok" : 1
}
```

　phonesコレクションのシステムインデックスをfind()すると、新しいものが最後に登場する。1つ目のインデックスは、常に自動的に作られる_idのインデックスである。2つ目は、先ほど作ったユニークインデックスである。

```mongo:book
> db.system.indexes.find({ "ns" : "book.phones" }).pretty()
{
	"v" : 1,
	"key" : {
		"_id" : 1
	},
	"name" : "_id_",
	"ns" : "book.phones"
}
{
	"v" : 1,
	"unique" : true,
	"key" : {
		"display" : 1
	},
	"name" : "display_1",
	"ns" : "book.phones",
	"dropDups" : true
}
{
	"v" : 1,
	"key" : {
		"components.area" : 1
	},
	"name" : "components.area_1",
	"ns" : "book.phones",
	"background" : 1
}
```

　「book.phones」インデックスも上手くできているみたいだ。
　最後に１つ注意点がある。それは、大きなコレクションにインデックスを作ろうとすると、時間とリソースを浪費してしまうことである。こうした影響を考慮して、インデックスはオフピーク時間に、バックグラウンドで、自動よりも手動で作成するといいだろう。

### おわりに

---

　データを整形して出力する時は、.pretty()を最後につける。

　MongoDB編は長いので、2日目も半分に分けました。

### 参考

---

- [すぐ忘れちゃうのでMongoクエリ(検索系)のメモ - チョコパイを240こ食べたプログラマ](http://chooringo.hatenablog.com/entry/2014/06/01/001044)