🐥

MongoDB についてまとめてみた

2021/05/25に公開

「MongoDBの薄い本」を読んでまとめたもの

はじめよう

第1章 基礎

最初に6つの概念を理解する必要がある。

  1. MongoDBインスタンスの中には0個以上のDBを持つことができる。(MySQLなどのRDBと同じ)
  2. 一つのDBで0個以上のコレクションを持つことができる。(テーブルと同じ)
  3. コレクションは、0個以上のドキュメントを作成できる。(のようなもの)
  4. ドキュメントは1つ以上のフィールドを作成できる。(のようなもの)
  5. MongoDBのインデックス機能はRDBMSと似ている。コレクションをインデックス化可能。
    参照やソートの性能を改善する。
  6. カーソルという概念は、RDBMSでは見られない重要な概念。
    カーソルとは、MongoDBにデータを問い合わせて得られた結果データへのポイントのことをいう。
    カーソルは実際のデータを取り出す前にカウントやスキップのような操作を行うことが出来る。
    カーソルを経由してデータ取得などの操作を行う。

リレーショナルデータベースとドキュメント指向データベース(MongoDB)の主な違いは、
リレーショナルデータベースがテーブルのレベルにを定義しているのに対し、
ドキュメント指向データベースはドキュメント(行) のレベルにフィールド(列) を定義している事。
 → RDBMSではテーブルのすべての行が同じカラムを持っているが、ドキュメント指向では、1行ごとに異なる列を持てる。
 → コレクションの中に何が入るかが厳密ではない(スキーマレス) ことが要点。

ハンズオン

  1. データベースの切り替え

    > use [DB名]
    

    指定したデータベースが存在していなくてもOK。

  2. コレクションの存在確認

    > db.getCollectionNames()
    [ ]
    
  3. ドキュメント作成
    unicornコレクションに対してドキュメントをinsert。
    > db.unicorns.insert({name: 'Aurorra', gender: 'f', weight: 450}) WriteResult({ "nInserted" : 1 })
    コレクションがなくても勝手に作成してくれる。(unicornsが作成)
    外側から見るとJSONを使っているように見えるが、MongoDB内部ではBSONというバイナリでシリアライズされたJSONフォーマットを利用している。

  4. ドキュメントのリストを取得

    > db.unicorns.find()
    { "_id" : ObjectId("5fd23871d2745658995a2447"), "name" : "Aurorra", "gender" : "f", "weight" : 450 }
    

    全てのドキュメントはユニークな_idフィールドを持たなくてはいけない。
    → ObjectID型の値を自分で生成するかMongoDBに生成させるか

  5. インデックスの確認

    > db.unicorns.getIndexes()
    [ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" } ]
    

    _idフィールドには、規定でインデックスが貼られる。

  6. 異なるドキュメントを挿入

    > db.unicorns.insert({name: 'Leto', gender: 'm', home: 'Arrakeen', worm: false})
    WriteResult({ "nInserted" : 1 })
    > db.unicorns.find()
    { "_id" : ObjectId("5fd23871d2745658995a2447"), "name" : "Aurorra", "gender" : "f", "weight" : 450 }
    { "_id" : ObjectId("5fd23ef6d2745658995a2448"), "name" : "Leto", "gender" : "m", "home" : "Arrakeen", "worm" : false }
    

1.1 セレクターの習得

もう1つの重要な概念、クエリーセレクター

  • クエリーセレクターは、SQL構文のwhere節に似ている。
     → ドキュメントを見つけ出したり、数えたり、更新、削除できる
  • セレクターは、JSONオブジェクト
    例)メスのユニコーンを見つけたい場合は{gender: 'f'}と指定。

セレクター一覧

  • find({field: value})
    fieldというフィールドがvalueと等しいドキュメントを検索
  • find({field1: value1, field2: value2})
    and式で検索
  • $lt、$lte、$gt、$gte、$ne
    それぞれ未満、以下、より大きい、以上、非等価を意味する。
    例)性別がオスで体重が700ポンドより大きいユニコーンを探す
    > db.unicorns.find({gender: 'm', weight: {$gt: 700}})
    { "_id" : ObjectId("5fd24127d2745658995a244b"), "name" : "Unicrom", "dob" : ISODate("1973-02-09T13:10:00Z"), "loves" : [ "energon", "redbull" ], "weight" : 984, "gender" : "m", "vampires" : 182 }
    { "_id" : ObjectId("5fd24144d2745658995a2454"), "name" : "Dunx", "dob" : ISODate("1976-07-18T09:18:00Z"), "loves" : [ "grape", "watermelon" ], "weight" : 704, "gender" : "m", "vampires" : 165 }
    
  • $exists
    フィールドの存在や欠如のマッチ
    > db.unicorns.find({vampires: {$exists: false}})
    { "_id" : ObjectId("5fd23871d2745658995a2447"), "name" : "Aurorra", "gender" : "f", "weight" : 450 }
    { "_id" : ObjectId("5fd23ef6d2745658995a2448"), "name" : "Leto", "gender" : "m", "home" : "Arrakeen", "worm" : false }
    { "_id" : ObjectId("5fd24142d2745658995a2453"), "name" : "Nimue", "dob" : ISODate("1999-12-20T07:15:00Z"), "loves" : [ "grape", "carrot" ], "weight" : 540, "gender" : "f" }
    
  • $in
    配列で渡した値のいずれかがにマッチ
    > db.unicorns.find({loves: {$in:['apple','orange']}})
    { "_id" : ObjectId("5fd24127d2745658995a244c"), "name" : "Roooooodles", "dob" : ISODate("1979-08-18T09:44:00Z"), "loves" : [ "apple" ], "weight" : 575, "gender" : "m", "vampires" : 99 }
    { "_id" : ObjectId("5fd24127d2745658995a244d"), "name" : "Solnara", "dob" : ISODate("1985-07-03T17:01:00Z"), "loves" : [ "apple", "carrot", "chocolate" ], "weight" : 550, "gender" : "f", "vampires" : 80 }
    { "_id" : ObjectId("5fd24127d2745658995a2450"), "name" : "Raleigh", "dob" : ISODate("2005-05-02T15:57:00Z"), "loves" : [ "apple", "sugar" ], "weight" : 421, "gender" : "m", "vampires" : 2 }
    { "_id" : ObjectId("5fd24127d2745658995a2451"), "name" : "Leia", "dob" : ISODate("2001-10-08T05:53:00Z"), "loves" : [ "apple", "watermelon" ], "weight" : 601, "gender" : "f", "vampires" : 33 }
    { "_id" : ObjectId("5fd24142d2745658995a2452"), "name" : "Pilot", "dob" : ISODate("1997-02-28T20:03:00Z"), "loves" : [ "apple", "watermelon" ], "weight" : 650, "gender" : "m", "vampires" : 54 }
    
  • $or
    ORを取りたいセレクターの配列を指定
    > db.unicorns.find({gender: 'f', $or: [{loves: 'apple'}, {weight: {$lt: 500}}]});
    { "_id" : ObjectId("5fd23871d2745658995a2447"), "name" : "Aurorra", "gender" : "f", "weight" : 450 }
    { "_id" : ObjectId("5fd24127d2745658995a244a"), "name" : "Aurora", "dob" : ISODate("1991-01-24T04:00:00Z"), "loves" : [ "carrot", "grape" ], "weight" : 450, "gender" : "f", "vampires" : 43 }
    { "_id" : ObjectId("5fd24127d2745658995a244d"), "name" : "Solnara", "dob" : ISODate("1985-07-03T17:01:00Z"), "loves" : [ "apple", "carrot", "chocolate" ], "weight" : 550, "gender" : "f", "vampires" : 80 }
    { "_id" : ObjectId("5fd24127d2745658995a2451"), "name" : "Leia", "dob" : ISODate("2001-10-08T05:53:00Z"), "loves" : [ "apple", "watermelon" ], "weight" : 601, "gender" : "f", "vampires" : 33 }
    
    全てのメスのユニコーンの中からりんごが好き、もしくは体重が 500 ポンド未満の条件で検索している。

既にお気づきかと思いますが、データ型で配列が使える!MongoDBはファーストクラスオブジェクトとして配列をサポートしている。
他に…

  • $where
    JavaScriptをサーバー上で実行できる
  • ObjectId
    MongoDBが生成した_idフィールドで検索
    > db.unicorns.find({_id: ObjectId("TheObjectId")})
    

全てのクエリーセレクターは、MongoDBのドキュメントのQuery Selectorsの欄に記載されている。

第2章 更新

2.1 置換と$set

例)Roooooodlesの体重を少し増やしたい場合

> db.unicorns.find({name: 'Roooooodles'})
{ "_id" : ObjectId("5fd24127d2745658995a244c"), "name" : "Roooooodles", "dob" : ISODate("1979-08-18T09:44:00Z"), "loves" : [ "apple" ], "weight" : 575, "gender" : "m", "vampires" : 99 }
> db.unicorns.update({name: 'Roooooodles'}, {weight: 590})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

↓  しかし…

> db.unicorns.find({name: 'Roooooodles'})
('Roooooodles'が見つからない…)

Updateは元のデータを置き換える動作となり、元のドキュメントは見つかりません。
 ↓ では、一つか複数のフィールドの値を更新したい場合は?
$set演算子を利用する

> db.unicorns.update({weight: 590}, {$set: { name: 'Roooooodles',
                                            dob: new Date(1979, 7, 18, 18, 44),
                                            loves: ['apple'],
                                            gender: 'm',
                                            vampires: 99}})
> db.unicorns.find({name: 'Roooooodles'})
{ "_id" : ObjectId("5fd24127d2745658995a244c"), "weight" : 590, "dob" : ISODate("1979-08-18T09:44:00Z"), "gender" : "m", "loves" : [ "apple" ], "name" : "Roooooodles", "vampires" : 99 }

このようにweightを更新しなければ上書きされません。

2.2 更新演算子

  • $inc
    フィールドの値を増やしたり、減らしたり出来ます。
    > db.unicorns.find({name: 'Pilot'})
    { "_id" : ObjectId("5fd24142d2745658995a2452"), "name" : "Pilot", "dob" : ISODate("1997-02-28T20:03:00Z"), "loves" : [ "apple", "watermelon" ], "weight" : 650, "gender" : "m", "vampires" : 52 }
    > db.unicorns.update({name: 'Pilot'}, {$inc: {vampires: -2}})
    WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
    > db.unicorns.find({name: 'Pilot'})
    { "_id" : ObjectId("5fd24142d2745658995a2452"), "name" : "Pilot", "dob" : ISODate("1997-02-28T20:03:00Z"), "loves" : [ "apple", "watermelon" ], "weight" : 650, "gender" : "m", "vampires" : 50 }
    
  • $push
    フィールドに値を追加
    > db.unicorns.find({name: 'Aurora'})
    { "_id" : ObjectId("5fd24127d2745658995a244a"), "name" : "Aurora", "dob" : ISODate("1991-01-24T04:00:00Z"), "loves" : [ "carrot", "grape" ], "weight" : 450, "gender" : "f", "vampires" : 43 }
    > db.unicorns.update({name: 'Aurora'}, {$push: {loves: 'sugar'}})
    WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
    > db.unicorns.find({name: 'Aurora'})
    { "_id" : ObjectId("5fd24127d2745658995a244a"), "name" : "Aurora", "dob" : ISODate("1991-01-24T04:00:00Z"), "loves" : [ "carrot", "grape", "sugar" ], "weight" : 450, "gender" : "f", "vampires" : 43 }
    

もっと知りたい場合は、MongoDBマニュアルの更新演算子に記載されている。

2.3 Upsert

ドキュメントが見つかった場合は、更新を行い、無ければ挿入を行う。
update の 3 番目の引数に{upsert:true}を渡す事で upsert を利用できる。

> db.hits.find();
> db.hits.update({page: 'unicorns'}, {$inc: {hits: 1}}, {upsert:true});
> db.hits.find();
{ "_id" : ObjectId("5fd4814d9fe9ca1215014bab"), "page" : "unicorns", "hits" : 1 }

もう一度実行すると、

> db.hits.update({page: 'unicorns'}, {$inc: {hits: 1}}, {upsert:true});
> db.hits.find();
{ "_id" : ObjectId("5fd4814d9fe9ca1215014bab"), "page" : "unicorns", "hits" : 2 }

2.4 複数自動更新

例)全てのユニコーンたちに予防接種を受けさせた(vaccinated)とする。
普通にやろうとすると、

> db.unicorns.find({vaccinated: true});
> db.unicorns.update({}, {$set: {vaccinated: true }});
> db.unicorns.find({vaccinated: true});
{ "_id" : ObjectId("5fd23871d2745658995a2447"), "name" : "Aurorra", "gender" : "f", "weight" : 450, "vaccinated" : true }

最初の1レコードしか更新されない。

↓ どうすれば?

第3引数に{multi: true}に設定。

> db.unicorns.update({}, {$set: {vaccinated: true }}, {multi: true});
> db.unicorns.find({vaccinated: true});
{ "_id" : ObjectId("5fd23871d2745658995a2447"), "name" : "Aurorra", "gender" : "f", "weight" : 450, "vaccinated" : true }
{ "_id" : ObjectId("5fd23ef6d2745658995a2448"), "name" : "Leto", "gender" : "m", "home" : "Arrakeen", "worm" : false, "vaccinated" : true }
︙

第3章 検索の習得

findコマンドがカーソルを返す。

3.1 フィールド選択

findには、任意で設定できるprojectionという2番目のパラメータが存在する。
このパラメータは、取得、もしくは除外したいフィールドのリスト
(SQLのSELECT句と似ている)

例)
全てのユニコーンの名前のみを取得

> db.unicorns.find({}, {name: 1});
{ "_id" : ObjectId("5fd23871d2745658995a2447"), "name" : "Aurorra" }
{ "_id" : ObjectId("5fd23ef6d2745658995a2448"), "name" : "Leto" }
{ "_id" : ObjectId("5fd24127d2745658995a2449"), "name" : "Horny" }
︙

3.2 順序

findはそのデータが必要な時に遅延して実行され、カーソルを返す。
(即時実行されるのはシェルだけ)

カーソルのメソッドをこれから確認していく。

  • ソート
    昇順はフィールドに1と、降順にしたい場合は-1と指定する。
    例)重いユニコーンの順

    > db.unicorns.find().sort({weight: -1});
    { "_id" : ObjectId("5fd24127d2745658995a244b"), "name" : "Unicrom", "dob" : ISODate("1973-02-09T13:10:00Z"), "loves" : [ "energon", "redbull" ], "weight" : 984, "gender" : "m", "vampires" : 182, "vaccinated" : true }
    { "_id" : ObjectId("5fd24127d2745658995a244e"), "name" : "Ayna", "dob" : ISODate("1998-03-06T23:30:00Z"), "loves" : [ "strawberry", "lemon" ], "weight" : 733, "gender" : "f", "vampires" : 40, "vaccinated" : true }
    { "_id" : ObjectId("5fd24144d2745658995a2454"), "name" : "Dunx", "dob" : ISODate("1976-07-18T09:18:00Z"), "loves" : [ "grape", "watermelon" ], "weight" : 704, "gender" : "m", "vampires" : 165, "vaccinated" : true }
    ︙
    

    MongoDBにはインデックスを利用できる。
    インデックスを使用しない場合、ソートのサイズ制限がある。
    → 巨大なデータにインデックスを貼ってない場合、エラーが返ってくる。

3.3 ページング

limitメソッド(SQLのLIMIT)とskipメソッド(SQLのOFFSET)で実現。

> db.unicorns.find().sort({weight: -1}).limit(2).skip(1);
{ "_id" : ObjectId("5fd24127d2745658995a244e"), "name" : "Ayna", "dob" : ISODate("1998-03-06T23:30:00Z"), "loves" : [ "strawberry", "lemon" ], "weight" : 733, "gender" : "f", "vampires" : 40, "vaccinated" : true }
{ "_id" : ObjectId("5fd24144d2745658995a2454"), "name" : "Dunx", "dob" : ISODate("1976-07-18T09:18:00Z"), "loves" : [ "grape", "watermelon" ], "weight" : 704, "gender" : "m", "vampires" : 165, "vaccinated" : true }

3.4 カウント

> db.unicorns.find({vampires: {$gt: 50}}).count()
5

第4章 データモデリング

4.1 Joinがない

Joinはスケーラブルでない
→1度水平分割をすると、アプリケーションサーバー側でJoinを行う必要が出てしまう。
例)
社員がいて、Letoという名前のマネージャがいる。

> db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d730"), name: 'Leto'})
> db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d731"), name: 'Duncan',
manager: ObjectId("4d85c7039ab0fd70a117d730")});
> db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d732"), name: 'Moneo',
manager: ObjectId("4d85c7039ab0fd70a117d730")});

4.1.1 配列と埋め込みドキュメント

Joinはないが、以下2つを駆使してJoinを再現。

  • 配列を利用
> db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d733"), name: 'Siona',manager: [ObjectId("4d85c7039ab0fd70a117d730"),ObjectId("4d85c7039ab0fd70a117d732")]});
> db.employees.find({manager: ObjectId("4d85c7039ab0fd70a117d730")})
{ "_id" : ObjectId("4d85c7039ab0fd70a117d731"), "name" : "Duncan", "manager" : ObjectId("4d85c7039ab0fd70a117d730") }
{ "_id" : ObjectId("4d85c7039ab0fd70a117d732"), "name" : "Moneo", "manager" : ObjectId("4d85c7039ab0fd70a117d730") }
{ "_id" : ObjectId("4d85c7039ab0fd70a117d733"), "name" : "Siona", "manager" : [ ObjectId("4d85c7039ab0fd70a117d730"), ObjectId("4d85c7039ab0fd70a117d732") ] }
  • 埋め込みドキュメント(入れ子)
> db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d734"), name: 'Ghanima',
  family: {mother: 'Chani',
         father: 'Paul',
           brother: ObjectId("4d85c7039ab0fd70a117d730")}})

埋め込みドキュメントはクエリーにドット表記を使用。

> db.employees.find({'family.mother': 'Chani'})
{ "_id" : ObjectId("4d85c7039ab0fd70a117d734"), "name" : "Ghanima", "family" : { "mother" : "Chani", "father" : "Paul", "brother" : ObjectId("4d85c7039ab0fd70a117d730") } }

もちろん、配列と埋め込みドキュメントを利用することも可能。

db.employees.insert({_id: ObjectId(
"4d85c7039ab0fd70a117d735"),
name: 'Chani',
family: [ {relation:'mother',name: 'Chani'},
{relation:'father',name: 'Paul'},
{relation:'brother', name: 'Duncan'}]})

4.1.2 非正規化

Joinを実現するもう一つの方法は、データの非正規化。
これは各ドキュメントで各情報の断片を重複させろという意味ではありません。データの重複を恐れながら設計するのではなく、データがどの情報に基づいていてどのドキュメントに属しているかをよく考えてモデリングしましょう。

例)
掲示板の WEB アプリケーションを作っているとします。RDBMSでは
posts テーブルにユーザー ID を持たせてユーザーと投稿を関連付けます。この様なモデ
ルでは users テーブルを検索 (もしくは join) しなければ投稿を表示することが出来ませ
ん。

↓ MongoDBでは?

各投稿エントリに単純にユーザー ID に対応するユーザー名を保持する。
→ もちろんこうした場合、ユーザーが名前を変更すると全てのドキュメントを更新しなければならない(1 回のマルチアップデートで済むが)。
ただ、これは場合によっては最良の対処になる。

4.1.3 IDの配列 VS 埋め込みドキュメントでの非正規化

各ドキュメントのサイズは16MByteまでに制限されている

普通は、RDBMSのようにIDの配列を保持し、手動で依存関係先の参照を手動を選択しがちですが、大抵の場合取得したいドキュメントの情報は小さいデータのことが多い。

  • 依存関係先の参照データが巨大な場合はIDの配列を所持する
  • 依存関係先の参照データが小さなデータ歯科必要としない場合、埋め込みドキュメントで直接保持する

4.2 少ないコレクションと多いコレクション

リレーショナルデータベースのテーブルは、多くは MongoDB のコレクションで置き換えることが可能です。(多対多の Join は例外です。)

例)
ブログシステムで、posts コレクションと comments コレクションを別々に持つべきか、post ドキュメントにコメントの配列を埋め込むべきでしょうか?

上の2つのアプローチを組み合わせて、
posts コレクションと comments コレクションを用意し、少ない数のコメントを投稿に埋め込むことで、表示に必要な最低限のコメントは1回のクエリでまとめて取得できる。

第5章 どんな時MongoDBを利用するか

  • MongoDBはリレーショナル・データベースとの代替になる(中央リポジトリ)
  • Luceneは全文検索インデックスによってリレーショナル・データベースを強化
  • Redisが永続的なKey-Valueストア

5.1 動的スキーマ

  • ドキュメント指向データベースの最もの利点は、動的スキーマです。
    スキーマレスは、ごちゃまぜなデータを格納し始めるのではないかという欠点があるが、NULLカラムによる解決できない問題はなくなる。
  • セットアップの省略とオブジェクト指向プログラミングとの摩擦の低減
    ドライバ視点で、オブジェクトを保存する際にJSONにシリアライズしてMongoDBに送信するだけでよい。

5.2 書き込み

MongoDBの使用する目的として、ロギングがある。
MongoDBには書き込みを速くする 2 つの性質がある。

  1. 送信した書き込み命令は実際の書き込み完了を待たず即座に戻ってくるオプションがある。
  2. データの耐久性に関する動作を制御できる。
    何台のサーバーに書き込んだら成功とするかを設定することで書き込み性能と耐久性を絶妙にコントロール出来る。

ログデータとして活用するのは、パフォーマンスに加え、スキーマレスの利点も活かす事が出来る。

  • Cappedコレクション
    今までは暗黙的に普通のコレクションを作成してきた。
    capped コレクションは db.createCollection コマンドにフラグを指定して作成。
    // Cappedコレクションを1MByteで制限
    > db.createCollection('logs', {capped: true, size: 1048576})
    
    Cappedコレクションの性質
    • 1MByteの上限に達すると、古いドキュメントは自動的に削除される。
      maxオプションを指定することでドキュメントのサイズではなく数で制限できる。
    • ドキュメントの更新を行なってもサイズは増えない。
    • 挿入した順序を維持するので時間でソートする為のインデックスを貼る必要はない。
    • Unix の tail -f コマンドのように、1 回のクエリーで Capped コレクションを tail できる。
    • データを時間で有効期限切れにしたい場合、TTL(有効期間) インデックスを利用できる。

5.3 耐久性

MongoDBを複数台で構成するしかなかった (MongoDB はレプリケーションをサポートしている)。
MongoDB バージョン 2.0 以降ジャーナリングは既定で有効になっているのでクラッシュや電源喪失からの迅速な復旧が可能。

5.4 全文検索

  • ステミング(語幹抽出) とストップワードに対応

5.5 トランザクション

MongoDB はトランザクションを持っていない。

↓ 代替案

  1. アトミック操作
    $inc$setなどには既に内包されている。
    findAndMondifyコマンドでドキュメントの更新と削除をアトミックに行える。
  2. アトミック操作が不十分で二層コミットをフォールバックする
    二層コミットは Join に手動参照するトランザクション。
    基本的な考え方は、実際のドキュメントの中にトランザクションの状態を格納し、init-pending-commit/rollback の段階を手動で行う。

5.6 データ処理

  • アグリゲーション・パイブライン
    グループ化するための機能豊富なライブラリ
  • MapReduce
    アグリゲーション・パイブラインでも処理しきれない複雑な関数での集約時に利用
  • Hadoop
    巨大なデータを並列処理

5.7 位置情報

位置情報インデックスをサポートしている。
geoJSON または x と y の座標をドキュメントに格納し、near で指定した座標で検索したり、within で指定した四角や円で検索を行える。

第6章 データの集計

6.1 アグリゲーション・パイプライン

アグリゲーション・パイプラインは、コレクション内のドキュメントを変換・結合する方法。

ドキュメントをパイプラインに渡すというのは、Unixの「パイプ」に似ている。
アグリゲーション(集約)は、最も単純なものはSQLのGROUP BY

  • パイプライン演算子を配列で渡すaggregateヘルパー関数を利用
    • $group
      グループ化して集約(SQLのGROUP BY)
      _idで指定したフィールドでグループ化
      フィールド名の前の$はドキュメントのフィールド値に置き換えられる
      例)
      ユニコーンを雄と雌で別々に集約したい

      > db.unicorns.aggregate([{$group:{_id:'$gender', total: {$sum:1}}}]);
      { "_id" : "f", "total" : 4 }
      { "_id" : "m", "total" : 6 }
      
    • $match
      findメソッドと同じように条件にマッチする、マッチしないドキュメントの部分集合を集約
      $group演算子の前後でよく使われる

      db.unicorns.aggregate([{$match: {weight:{$lt:600}}},
                              {$group: {_id:'$gender', total:{$sum:1},
                                        avgVamp:{$avg:'$vampires'}}},
                              {$sort:{avgVamp:-1}} ])
      
    • $sort

    • $limit

    • $skip

    • $unwind
      DBのデータを全て正しく数え上げるにはデータを「平ら」にしてやる必要がある
      例)
      最も多くのユニコーン達に好まれている食べ物と各好物を好きなユニコーンの名前のリストを表示

      > db.unicorns.aggregate([{$unwind:'$loves'},
                              {$group: {_id:'$loves', total:{$sum:1},
                              unicorns:{$addToSet:'$name'}}},
                              {$sort:{total:-1}},
                              {$limit:1} ])
      { "_id" : "carrot", "total" : 4, "unicorns" : [ "Aurora", "Horny", "Solnara", "Nimue" ] }
      
    • $project
      findで指定するprojectionと似たもの
      既存のフィールドの値に基づいて新しいフィールドを作成・計算することができる

      • 算術演算子を利用して複数のフィールドを足し合わせて平均値を求める
      • 文字列演算子を利用して既存のフィールドの文字列を結合したフィールドを生成
    • $out
      結果を新しいコレクションに書き込むことが出来る

6.2 MapReduce

MapReduce は 2 段階のステップに分かれています。最初に map を行い、次に reduce を行う。

  1. mapping の段階で入力されたドキュメントを変換し、key=>value のペアを出力する (キーと値は複合できる)。
  2. key/value ペアを key 毎にグループ化。reduce の段階で出力されたキーと値の配列から最終的な結果を生成。
    (map とreduce 関数は JavaScript で記述)

※あまり自分で使うことはないので、詳しくしらなくてもOK

第7章 パフォーマンスとツール

7.1 インデックス

  • インデックスの作成
    > db.unicorns.ensureIndex({name: 1});
    
  • インデックスの削除
    > db.unicorns.dropIndex({name: 1});
    
  • ユニーク制約
    第2引数に{unique: true}を設定
    > db.unicorns.ensureIndex({name: 1}, {unique: true});
    
  • 複合インデックス
    インデックスの順序 (1 は昇順、-1 は降順) は単一キーのインデックスでは影響はないが、複合キーで複数のインデックスフィールドをソートする際に違いがでる。
    > db.unicorns.ensureIndex({name: 1, vampires: -1});
    

7.2 Explain

カーソルに対して、実行計画を取ることが出来る

> db.unicorns.find({name: 'Pilot'}).explain()

7.3 レプリケーション

理想的に、本番環境には同じデータを持つ 3 台以上のレプリカセットを配備。単一のプライマリサーバーに対し書き込みが行われると、すべてのセカンダリサーバへ非同期に複製。

7.4 シャーディング

シャーディングはデータを複数のサーバーやクラスターに分割してスケーラビリティを高める手法。
例)
単純な実装はデータの名前が A~M で始まるものをサーバー 1 に、残りをサーバー 2 に格納。

単一レプリカセットのデータが限界まで増えた時、あなたはシャーディングの存在を思い出して利用することを検討。

7.5 統計

  • db.stats()
    データベースの統計を取得
    例)
    データベースのサイズ

7.6 Webインターフェース

ブラウザで http://localhost:27017/ を開いてアクセス出来る。
設定ファイルに rest=true を追加して mongod プロセスを再起動すると、サーバーの状態などを知れる。

7.7 プロファイラ

  • setProfilingLevel
    • プロファイルの有効化
      何がいつどれ程のドキュメントを走査し、どれ程のデータが返却されたかを教えてくれる。
      // 有効化
      > db.setProfilingLevel(2);
      > db.unicorns.find({weight: {$gt: 600}});
      // プロファイルを観察
      > db.system.profile.find();
      
    • 無効化
      > db.setProfilingLevel(0);
      
    • プロファイルするクエリを限定
      引数を1に設定。デフォルトでは、100ミリ秒以上かかるクエリーをプロファイリングする。
      > db.setProfilingLevel(1, 1000);
      

7.8 バックアップとリストア

  • バックアップ
    • $ mongodump
      ローカルホストに接続して全てのデータベースをdumpサブフォルダ以下にバックアップ。
    • $ mongodump --db DBNAME
      指定したデータベースをバックアップ
    • $ mongodump --collection COLLECTIONNAME
      指定したコレクションをバックアップ
  • リストア
    • $ mongorestore
      バックアップをリストア
      mongodumpとオプションは同じ

Discussion