🎬

MongoDB 勉強用メモ

2022/08/28に公開約33,900字

MongoDB と RDB との概念の違い

まずは基本中の基本である概念の差異から理解する

RDB MongoDB
database database
table collection
column field
index index
primary key _id フィールド

データベース操作

操作系

// データベース作成(既に作成されていた場合は移動する)
use test;

// カレントデータベース表示
db;

// データベース一覧
show dbs;

//データベースの統計確認
> db.stats();
{
	"db" : "test",
	"collections" : 0,
	"views" : 0,
	"objects" : NumberLong(0),
	"avgObjSize" : 0,
	"dataSize" : NumberLong(0),
	"storageSize" : NumberLong(0),
	"totalFreeStorageSize" : NumberLong(0),
	"numExtents" : NumberLong(0),
	"indexes" : 0,
	"indexSize" : NumberLong(0),
	"indexFreeStorageSize" : NumberLong(0),
	"fileSize" : NumberLong(0),
	"nsSizeMB" : 0,
	"ok" : 1
}

//データベース削除
db.dropDatabase();

データベースの統計情報の意味

  • collections
    • データベースの中にあるコレクションの数
  • dataSize
    • 圧縮前のデータサイズ
  • storageSize
    • コレクションに割り当てられたスペースの合計サイズ。
    • 圧縮後サイズ
    • MongoDB ではデフォルトでデータ圧縮が有効。そうすることで、ストレージ容量を削減することができる。
  • indexes
    • インデックス数
  • indexSize
    • インデックスデータ量の合計
  • fileSize
    • ストレージに保存されるファイルサイズ

コレクション操作

操作系

// コレクション作成
db.createCollection("item1");

// コレクション一覧
show collections;

// コレクションの統計情報
db.collection.stats();

> db.item1.stats();
{
	"ns" : "test.item1",
	"size" : 0,
	"count" : 0,
	"storageSize" : 4096,
	"freeStorageSize" : 0,
	"capped" : false,
	"nindexes" : 1,
	"indexBuilds" : [ ],
	"totalIndexSize" : 4096,
	"totalSize" : 8192,
	"indexSizes" : {
		"_id_" : 4096
	},
	"scaleFactor" : 1,
	"ok" : 1,
	"$clusterTime" : {
		"clusterTime" : Timestamp(1661603192, 7),
		"signature" : {
			"hash" : BinData(0,"lHoKI0+rXY2A9YSJvuxsTi9LilY="),
			"keyId" : NumberLong("7090975556698636292")
		}
	},
	"operationTime" : Timestamp(1661603192, 7)
}

//コレクション削除
db.item1.drop();

コレクション統計情報

  • size
    • メモリに展開されるデータの合計サイズ
  • count
    • コレクションのデータ件数
  • storageSize
    • ストレージサイズでデータが圧縮された後のサイズ。
  • capped
    • Capped コレクションかどうかの判定
  • nindexes
    • インデックスの数
  • totalIndexSize
    • インデックスのデータ量合計。
  • indexSizes
    • 各インデックスのデータ量

Capped コレクション

  • 概要

    • 特殊なコレクションタイプ
    • コレクションの中に入るデータの上限が決まっているコレクション
    • 上限以上になると古いデータから自動で削除されていく。
    • サイズにプラスしてドキュメント数も閾値にすることが可能
    • 古いデータを自動削除してくれる関係で主にログ用のコレクションで利用されやすい
  • 制限

    • サイズの上限変更が途中でできない
    • データの削除・更新はできず追記だけされる前提
    • Capped コレクションはシャーディング不可。
  • 作成例

//サイズの上限が1MBのCappedコレクション
db.createCollection("log",{capped: true,size: 1024000});

//サイズの上限が1MB かつ ドキュメント数が 5000 のCappedコレクション
db.createCollection("log_max",{capped: true,size: 1024000,max: 5000});

//通常コレクションを Capped コレクションへ変更
db.runCommand({"convertToCapped": "item1",size: 1000000});

ドキュメントの登録

1 件登録

db.item1.insertOne({name: "AAA",price: 999});

複数件登録

db.item1.insertMany([
	{name: "AAA",price: 999},
	{name: "BBB",price: 999},
	{name: "CCC",price: 999}
	]);

配列型データを登録

db.item1.insertOne({name: "AAA",price: 999,array: ["A","B","C"]});

オブジェクト型データを登録

db.item1.insertOne({name: "AAA",price: 999,array: {"A":1,"B":2,"C":3}});

ドキュメントの検索

ソースデータ
db.item1.drop();
db.createCollection("item1");
db.item1.insertOne({name: "A1",price: 1});
db.item1.insertOne({name: "A2",price: 2});
db.item1.insertOne({name: "A3",price: 3});
db.item1.insertOne({name: "B1",price: 4});
db.item1.insertOne({name: "B2",price: 5});
db.item1.insertOne({name: "B3",price: 6});
db.item1.insertOne({name: "C1",price: 7});
db.item1.insertOne({name: "C2",price: 8});
db.item1.insertOne({name: "C3",price: 9});

全件検索

  • デフォルトの場合、20件ずつ返却される。
  • "it" と実行することで次の20件返却される。
db.item1.find()
{ "_id" : ObjectId("630a1587cb873500bb3231cd"), "name" : "A1", "price" : 1 }
{ "_id" : ObjectId("630a1587cb873500bb3231ce"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630a1587cb873500bb3231cf"), "name" : "A3", "price" : 3 }
{ "_id" : ObjectId("630a1587cb873500bb3231d0"), "name" : "B1", "price" : 4 }
{ "_id" : ObjectId("630a1587cb873500bb3231d1"), "name" : "B2", "price" : 5 }
{ "_id" : ObjectId("630a1587cb873500bb3231d2"), "name" : "B3", "price" : 6 }
{ "_id" : ObjectId("630a1587cb873500bb3231d3"), "name" : "C1", "price" : 7 }
{ "_id" : ObjectId("630a1587cb873500bb3231d4"), "name" : "C2", "price" : 8 }
{ "_id" : ObjectId("630a1587cb873500bb3231d5"), "name" : "C3", "price" : 9 }

取得フィールド指定

  • 取得フィールド名をキーにして0の場合取得しない、0以外の場合取得とできる
  • _id フィールドは明治的に 0 にすることで取得しないようにできる
// _id と name フィールドだけ返却
> db.item1.find({},{name:1});
{ "_id" : ObjectId("630a1587cb873500bb3231cd"), "name" : "A1" }
{ "_id" : ObjectId("630a1587cb873500bb3231ce"), "name" : "A2" }
{ "_id" : ObjectId("630a1587cb873500bb3231cf"), "name" : "A3" }
{ "_id" : ObjectId("630a1587cb873500bb3231d0"), "name" : "B1" }
{ "_id" : ObjectId("630a1587cb873500bb3231d1"), "name" : "B2" }
{ "_id" : ObjectId("630a1587cb873500bb3231d2"), "name" : "B3" }
{ "_id" : ObjectId("630a1587cb873500bb3231d3"), "name" : "C1" }
{ "_id" : ObjectId("630a1587cb873500bb3231d4"), "name" : "C2" }
{ "_id" : ObjectId("630a1587cb873500bb3231d5"), "name" : "C3" }

// _id 以外全て返却
> db.item1.find({},{_id:0});
{ "name" : "A1", "price" : 1 }
{ "name" : "A2", "price" : 2 }
{ "name" : "A3", "price" : 3 }
{ "name" : "B1", "price" : 4 }
{ "name" : "B2", "price" : 5 }
{ "name" : "B3", "price" : 6 }
{ "name" : "C1", "price" : 7 }
{ "name" : "C2", "price" : 8 }
{ "name" : "C3", "price" : 9 }

// name フィールドだけ返却
> db.item1.find({},{_id:0,name:1});
{ "name" : "A1" }
{ "name" : "A2" }
{ "name" : "A3" }
{ "name" : "B1" }
{ "name" : "B2" }
{ "name" : "B3" }
{ "name" : "C1" }
{ "name" : "C2" }
{ "name" : "C3" }

完全一致

> db.item1.find({name:"A1"});
{ "_id" : ObjectId("630a1587cb873500bb3231cd"), "name" : "A1", "price" : 1 }

部分一致

> db.item1.find({name:/A/});
{ "_id" : ObjectId("630a1587cb873500bb3231cd"), "name" : "A1", "price" : 1 }
{ "_id" : ObjectId("630a1587cb873500bb3231ce"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630a1587cb873500bb3231cf"), "name" : "A3", "price" : 3 }

前方一致

> db.item1.find({name:/^A/});
{ "_id" : ObjectId("630a1587cb873500bb3231cd"), "name" : "A1", "price" : 1 }
{ "_id" : ObjectId("630a1587cb873500bb3231ce"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630a1587cb873500bb3231cf"), "name" : "A3", "price" : 3 }

後方一致

> db.item1.find({name:/1$/});
{ "_id" : ObjectId("630a1587cb873500bb3231cd"), "name" : "A1", "price" : 1 }
{ "_id" : ObjectId("630a1587cb873500bb3231d0"), "name" : "B1", "price" : 4 }
{ "_id" : ObjectId("630a1587cb873500bb3231d3"), "name" : "C1", "price" : 7 }

範囲指定

// 以下
> db.item1.find({price:{$lte: 5}});
{ "_id" : ObjectId("630a16b9cb873500bb3231d6"), "name" : "A1", "price" : 1 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d7"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d8"), "name" : "A3", "price" : 3 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d9"), "name" : "B1", "price" : 4 }
{ "_id" : ObjectId("630a16b9cb873500bb3231da"), "name" : "B2", "price" : 5 }

// 未満
> db.item1.find({price:{$lt: 5}});
{ "_id" : ObjectId("630a16b9cb873500bb3231d6"), "name" : "A1", "price" : 1 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d7"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d8"), "name" : "A3", "price" : 3 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d9"), "name" : "B1", "price" : 4 }

// 以上
> db.item1.find({price:{$gte: 5}});
{ "_id" : ObjectId("630a16b9cb873500bb3231da"), "name" : "B2", "price" : 5 }
{ "_id" : ObjectId("630a16b9cb873500bb3231db"), "name" : "B3", "price" : 6 }
{ "_id" : ObjectId("630a16b9cb873500bb3231dc"), "name" : "C1", "price" : 7 }
{ "_id" : ObjectId("630a16b9cb873500bb3231dd"), "name" : "C2", "price" : 8 }
{ "_id" : ObjectId("630a16b9cb873500bb3231de"), "name" : "C3", "price" : 9 }

// より大きい
> db.item1.find({price:{$gt: 5}});
{ "_id" : ObjectId("630a16b9cb873500bb3231db"), "name" : "B3", "price" : 6 }
{ "_id" : ObjectId("630a16b9cb873500bb3231dc"), "name" : "C1", "price" : 7 }
{ "_id" : ObjectId("630a16b9cb873500bb3231dd"), "name" : "C2", "price" : 8 }
{ "_id" : ObjectId("630a16b9cb873500bb3231de"), "name" : "C3", "price" : 9 }

// Between
> db.item1.find({price:{$gt: 3,$lt:7}});
{ "_id" : ObjectId("630a16b9cb873500bb3231d9"), "name" : "B1", "price" : 4 }
{ "_id" : ObjectId("630a16b9cb873500bb3231da"), "name" : "B2", "price" : 5 }
{ "_id" : ObjectId("630a16b9cb873500bb3231db"), "name" : "B3", "price" : 6 }

And/Or

//And
> db.item1.find({$and:
[
{name:"A1"},
{price:1}
]
});
{ "_id" : ObjectId("630a16b9cb873500bb3231d6"), "name" : "A1", "price" : 1 }

//Or
> db.item1.find({$or:
[
{name:"A1"},
{price:9}
]
});
{ "_id" : ObjectId("630a16b9cb873500bb3231d6"), "name" : "A1", "price" : 1 }
{ "_id" : ObjectId("630a16b9cb873500bb3231de"), "name" : "C3", "price" : 9 }

特定フィールドが存在するか、しないか

//nameフィールドが存在するドキュメントを検索
> db.item1.find({name:{$exists:true}});
{ "_id" : ObjectId("630a16b9cb873500bb3231d6"), "name" : "A1", "price" : 1 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d7"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d8"), "name" : "A3", "price" : 3 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d9"), "name" : "B1", "price" : 4 }
{ "_id" : ObjectId("630a16b9cb873500bb3231da"), "name" : "B2", "price" : 5 }
{ "_id" : ObjectId("630a16b9cb873500bb3231db"), "name" : "B3", "price" : 6 }
{ "_id" : ObjectId("630a16b9cb873500bb3231dc"), "name" : "C1", "price" : 7 }
{ "_id" : ObjectId("630a16b9cb873500bb3231dd"), "name" : "C2", "price" : 8 }
{ "_id" : ObjectId("630a16b9cb873500bb3231de"), "name" : "C3", "price" : 9 }

//nameフィールドが存在しないドキュメントを検索
> db.item1.find({name:{$exists:false}});

ソート

  • .sort の中で、key:1 => 昇順ソート、key:-1 => 降順ソートで指定する。
  • 複数フィールド指定することも可能
//降順
> db.item1.find().sort({price:-1});
{ "_id" : ObjectId("630a16b9cb873500bb3231de"), "name" : "C3", "price" : 9 }
{ "_id" : ObjectId("630a16b9cb873500bb3231dd"), "name" : "C2", "price" : 8 }
{ "_id" : ObjectId("630a16b9cb873500bb3231dc"), "name" : "C1", "price" : 7 }
{ "_id" : ObjectId("630a16b9cb873500bb3231db"), "name" : "B3", "price" : 6 }
{ "_id" : ObjectId("630a16b9cb873500bb3231da"), "name" : "B2", "price" : 5 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d9"), "name" : "B1", "price" : 4 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d8"), "name" : "A3", "price" : 3 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d7"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d6"), "name" : "A1", "price" : 1 }

Limit

  • フェッチ件数を制限。
> db.item1.find().limit(3);
{ "_id" : ObjectId("630a16b9cb873500bb3231d6"), "name" : "A1", "price" : 1 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d7"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630a16b9cb873500bb3231d8"), "name" : "A3", "price" : 3 }

Skip

  • 先頭から指定した件数をスキップ
// Skip
> db.item1.find().skip(4);
{ "_id" : ObjectId("630a16b9cb873500bb3231da"), "name" : "B2", "price" : 5 }
{ "_id" : ObjectId("630a16b9cb873500bb3231db"), "name" : "B3", "price" : 6 }
{ "_id" : ObjectId("630a16b9cb873500bb3231dc"), "name" : "C1", "price" : 7 }
{ "_id" : ObjectId("630a16b9cb873500bb3231dd"), "name" : "C2", "price" : 8 }
{ "_id" : ObjectId("630a16b9cb873500bb3231de"), "name" : "C3", "price" : 9 }

//Limit と組み合わせ
> db.item1.find().limit(3).skip(3);
{ "_id" : ObjectId("630a16b9cb873500bb3231d9"), "name" : "B1", "price" : 4 }
{ "_id" : ObjectId("630a16b9cb873500bb3231da"), "name" : "B2", "price" : 5 }
{ "_id" : ObjectId("630a16b9cb873500bb3231db"), "name" : "B3", "price" : 6 }

ドキュメントの削除

ソースデータ
db.item1.drop();
db.createCollection("item1");
db.item1.insertOne({name: "A1",price: 1});
db.item1.insertOne({name: "A1",price: 1});
db.item1.insertOne({name: "A2",price: 2});
db.item1.insertOne({name: "A2",price: 2});
db.item1.insertOne({name: "A3",price: 3});
db.item1.insertOne({name: "A3",price: 3});

1 件だけ削除

deleteOne でマッチしたドキュメントを1件だけ削除

> db.item1.deleteOne({name: "A1"});
{ "acknowledged" : true, "deletedCount" : 1 }

> db.item1.find();
{ "_id" : ObjectId("630ac7b06b9740af37086922"), "name" : "A1", "price" : 1 }
{ "_id" : ObjectId("630ac7b06b9740af37086923"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630ac7b06b9740af37086924"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630ac7b06b9740af37086925"), "name" : "A3", "price" : 3 }
{ "_id" : ObjectId("630ac7b06b9740af37086926"), "name" : "A3", "price" : 3 }

複数ドキュメント削除

deleteMany でマッチしたドキュメントを全て削除

> db.item1.deleteMany({name: "A1"});
{ "acknowledged" : true, "deletedCount" : 2 }

> db.item1.find();
{ "_id" : ObjectId("630ac8146b9740af37086929"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630ac8146b9740af3708692a"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630ac8146b9740af3708692b"), "name" : "A3", "price" : 3 }
{ "_id" : ObjectId("630ac8146b9740af3708692c"), "name" : "A3", "price" : 3 }

全件削除

db.item1.deleteMany({})

ドキュメントの更新

ソースデータ
db.item1.drop();
db.createCollection("item1");
db.item1.insertOne({name: "A1",price: 1});
db.item1.insertOne({name: "A2",price: 2});
db.item1.insertOne({name: "A3",price: 3});
db.item1.insertOne({name: "B1",price: 4});
db.item1.insertOne({name: "B2",price: 5});
db.item1.insertOne({name: "B3",price: 6});
db.item1.insertOne({name: "C1",price: 7});
db.item1.insertOne({name: "C2",price: 8});
db.item1.insertOne({name: "C3",price: 9});

1件のみ更新

db.item1.updateOne(
{ name: "A1"},
{ $set: {price:999}}
)

> db.item1.find({name:"A1"})
{ "_id" : ObjectId("630aec911d196e9e528b2d92"), "name" : "A1", "price" : 999 }

複数ドキュメント更新

db.item1.updateMany(
{$or: [{ name: "A1"},{ name: "A2"}]},
{ $set: {price:999}}
)

> db.item1.find({$or: [{ name: "A1"},{ name: "A2"}]})
{ "_id" : ObjectId("630aec911d196e9e528b2d92"), "name" : "A1", "price" : 999 }
{ "_id" : ObjectId("630aec911d196e9e528b2d93"), "name" : "A2", "price" : 999 }

ドキュメントの入れ替え

なお、_id は入れ替えても変わらない。

db.item1.replaceOne(
{ name: "A1"},
{ name: "AX1",price: 10}
)

> db.item1.find()
{ "_id" : ObjectId("630aef701d196e9e528b2d9b"), "name" : "AX1", "price" : 10 }
{ "_id" : ObjectId("630aef701d196e9e528b2d9c"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630aef701d196e9e528b2d9d"), "name" : "A3", "price" : 3 }
{ "_id" : ObjectId("630aef701d196e9e528b2d9e"), "name" : "B1", "price" : 4 }
{ "_id" : ObjectId("630aef701d196e9e528b2d9f"), "name" : "B2", "price" : 5 }
{ "_id" : ObjectId("630aef701d196e9e528b2da0"), "name" : "B3", "price" : 6 }
{ "_id" : ObjectId("630aef701d196e9e528b2da1"), "name" : "C1", "price" : 7 }
{ "_id" : ObjectId("630aef701d196e9e528b2da2"), "name" : "C2", "price" : 8 }
{ "_id" : ObjectId("630aef711d196e9e528b2da3"), "name" : "C3", "price" : 9 }

新規フィールドの追加

updateOne か updateMany で新しいフィールドをそのまま追加すればOK

db.item1.updateOne(
{ name: "A1"},
{ $set: {xxx:10000}}
)

db.item1.updateMany(
{$or: [{ name: "A2"},{ name: "A3"}]},
{ $set: {yyy:20000}}
)

> db.item1.find()
{ "_id" : ObjectId("630afb101d196e9e528b2dad"), "name" : "A1", "price" : 1, "xxx" : 10000 }
{ "_id" : ObjectId("630afb101d196e9e528b2dae"), "name" : "A2", "price" : 2, "yyy" : 20000 }
{ "_id" : ObjectId("630afb101d196e9e528b2daf"), "name" : "A3", "price" : 3, "yyy" : 20000 }
{ "_id" : ObjectId("630afb101d196e9e528b2db0"), "name" : "B1", "price" : 4 }
{ "_id" : ObjectId("630afb101d196e9e528b2db1"), "name" : "B2", "price" : 5 }
{ "_id" : ObjectId("630afb101d196e9e528b2db2"), "name" : "B3", "price" : 6 }
{ "_id" : ObjectId("630afb101d196e9e528b2db3"), "name" : "C1", "price" : 7 }
{ "_id" : ObjectId("630afb101d196e9e528b2db4"), "name" : "C2", "price" : 8 }
{ "_id" : ObjectId("630afb101d196e9e528b2db5"), "name" : "C3", "price" : 9 }

フィールド名の変更

$rename を利用する。

db.item1.updateMany(
{},
{ $rename: {name:"finalName"}}
)

> db.item1.find()
{ "_id" : ObjectId("630afc0b1d196e9e528b2db6"), "price" : 1, "finalName" : "A1" }
{ "_id" : ObjectId("630afc0b1d196e9e528b2db7"), "price" : 2, "finalName" : "A2" }
{ "_id" : ObjectId("630afc0b1d196e9e528b2db8"), "price" : 3, "finalName" : "A3" }
{ "_id" : ObjectId("630afc0b1d196e9e528b2db9"), "price" : 4, "finalName" : "B1" }
{ "_id" : ObjectId("630afc0b1d196e9e528b2dba"), "price" : 5, "finalName" : "B2" }
{ "_id" : ObjectId("630afc0b1d196e9e528b2dbb"), "price" : 6, "finalName" : "B3" }
{ "_id" : ObjectId("630afc0b1d196e9e528b2dbc"), "price" : 7, "finalName" : "C1" }
{ "_id" : ObjectId("630afc0b1d196e9e528b2dbd"), "price" : 8, "finalName" : "C2" }
{ "_id" : ObjectId("630afc0b1d196e9e528b2dbe"), "price" : 9, "finalName" : "C3" }

upsert

  • {upsert:true} を updateOne、updateMany 時につければOK。
  • 存在せずにinsertされた場合、upsertedId がレスポンスで返る。
db.item1.updateMany(
{ name: "A1"},
{ $set: {price:999}},
{ upsert : true}
)
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }

db.item1.updateMany(
{ name: "Z1"},
{ $set: {price:999}},
{ upsert : true}
)
{
	"acknowledged" : true,
	"matchedCount" : 0,
	"modifiedCount" : 0,
	"upsertedId" : ObjectId("630afd160cc18cfb3b937a7e")
}

> db.item1.find()
{ "_id" : ObjectId("630afcca1d196e9e528b2dbf"), "name" : "A1", "price" : 999 }
{ "_id" : ObjectId("630afcca1d196e9e528b2dc0"), "name" : "A2", "price" : 2 }
{ "_id" : ObjectId("630afcca1d196e9e528b2dc1"), "name" : "A3", "price" : 3 }
{ "_id" : ObjectId("630afcca1d196e9e528b2dc2"), "name" : "B1", "price" : 4 }
{ "_id" : ObjectId("630afcca1d196e9e528b2dc3"), "name" : "B2", "price" : 5 }
{ "_id" : ObjectId("630afcca1d196e9e528b2dc4"), "name" : "B3", "price" : 6 }
{ "_id" : ObjectId("630afcca1d196e9e528b2dc5"), "name" : "C1", "price" : 7 }
{ "_id" : ObjectId("630afcca1d196e9e528b2dc6"), "name" : "C2", "price" : 8 }
{ "_id" : ObjectId("630afcca1d196e9e528b2dc7"), "name" : "C3", "price" : 9 }
{ "_id" : ObjectId("630afd160cc18cfb3b937a7e"), "name" : "Z1", "price" : 999 }

集計(Aggregation) & 結合(Join)

テストデータの登録
db.employee.deleteMany({})
db.employee.insertMany([
    { employeeID: "emp001", divisionID: "ka001",positionID: "yaku001", employeeName: "田村", age: 30, salary:400000 },
    { employeeID: "emp002", divisionID: "ka001",positionID: "yaku002", employeeName: "山田", age: 31, salary:350000 },
    { employeeID: "emp003", divisionID: "ka002",positionID: "yaku003", employeeName: "中山", age: 37, salary:300000 },
    { employeeID: "emp004", divisionID: "ka002",positionID: "yaku004", employeeName: "佐藤", age: 23, salary:250000 },
    { employeeID: "emp005", divisionID: "ka003",positionID: "yaku002", employeeName: "佐々木", age: 33, salary:350000 },
    { employeeID: "emp006", divisionID: "ka004",positionID: "yaku002", employeeName: "吉田", age: 35, salary:350000 }
])

db.division.deleteMany({})
db.division.insertMany([
    { divisionID: "ka001", departmentID: "bu001", divisionName: "営業1課" },
    { divisionID: "ka002", departmentID: "bu001", divisionName: "営業2課" },
    { divisionID: "ka003", departmentID: "bu002", divisionName: "企画課" },
    { divisionID: "ka004", departmentID: "bu003", divisionName: "経理課" }
])


db.department.deleteMany({})
db.department.insertMany([
    { departmentID: "bu001", departmentName: "営業部" },
    { departmentID: "bu002", departmentName: "企画部" },
    { departmentID: "bu003", departmentName: "経理部" }
])


db.position.deleteMany({})
db.position.insertMany([
    { positionID: "yaku001", positionName: "社長" },
    { positionID: "yaku002", positionName: "部長" },
    { positionID: "yaku003", positionName: "課長" },
    { positionID: "yaku004", positionName: "一般社員" }
])

employee コレクションについて、division ごとの給料の合計値集計

db.employee.aggregate([
    { $group: { _id: "$divisionID", salary_total: { $sum: "$salary" }}}
])

{ "_id" : "ka004", "salary_total" : 700000 }
{ "_id" : "ka003", "salary_total" : 700000 }
{ "_id" : "ka002", "salary_total" : 1100000 }
{ "_id" : "ka001", "salary_total" : 1500000 }

employee コレクションについて、division ごとの給料の合計値集計。ただし30歳以上の従業員について

db.employee.aggregate([
    { $match: { age: { $gt: 30 }}},
    { $group: { _id: "$divisionID", salary_total: { $sum: "$salary" }}}
])

{ "_id" : "ka002", "salary_total" : 600000 }
{ "_id" : "ka003", "salary_total" : 700000 }
{ "_id" : "ka001", "salary_total" : 700000 }
{ "_id" : "ka004", "salary_total" : 700000 }

集計しつつ条件に該当する division のみ表示

db.employee.aggregate([
    { $group: { _id: "$divisionID", salary_total: { $sum: "$salary" }}},
    { $match: { _id : "ka001" }}
])

{ "_id" : "ka001", "salary_total" : 1500000 }

db.employee.aggregate([
    { $match: { age: { $gt: 30 }}},
    { $group: { _id: "$divisionID", salary_total: { $sum: "$salary" }}},
    { $match: { _id : "ka001" }}
])

{ "_id" : "ka001", "salary_total" : 700000 }

division ごとの最大、最小、平均、合計給料

db.employee.aggregate([
    { $group: { _id: "$divisionID", 
    max_salary: { $max: "$salary" },
    min_salary: { $min: "$salary" },
    avg_salary: { $avg: "$salary" },
    sum_salary: { $sum: "$salary" },
    }}
])

{ "_id" : "ka002", "max_salary" : 300000, "min_salary" : 250000, "avg_salary" : 275000, "sum_salary" : 1100000 }
{ "_id" : "ka001", "max_salary" : 400000, "min_salary" : 350000, "avg_salary" : 375000, "sum_salary" : 1500000 }
{ "_id" : "ka004", "max_salary" : 350000, "min_salary" : 350000, "avg_salary" : 350000, "sum_salary" : 700000 }
{ "_id" : "ka003", "max_salary" : 350000, "min_salary" : 350000, "avg_salary" : 350000, "sum_salary" : 700000 }

division ごとの給料の合計と平均年齢

db.employee.aggregate([
    { $group: { _id: "$divisionID", sum_salary: { $sum: "$salary" }, avg_age: { $avg: "$age"}}}
])

{ "_id" : "ka002", "sum_salary" : 1100000, "avg_age" : 30 }
{ "_id" : "ka001", "sum_salary" : 1500000, "avg_age" : 30.5 }
{ "_id" : "ka004", "sum_salary" : 700000, "avg_age" : 35 }
{ "_id" : "ka003", "sum_salary" : 700000, "avg_age" : 33 }

全従業員の合計給料

グルーピングしないので _id に対して null を指定する

db.employee.aggregate([
    { $group: { _id: null, total: { $sum: "$salary" }}}
])

{ "_id" : null, "total" : 4000000 }

全従業員の人数

db.employee.aggregate([
    { $count: "employee_count" }
])

{ "employee_count" : 12 }

特定の条件に該当する従業員の人数

db.employee.aggregate([
    { $match: {age: { $gt: 30 }}},
    { $count: "employee_count" }
])

{ "employee_count" : 8 }

グループごとのドキュメント数のカウント

この場合、count は使えないので、sum を使う。

db.employee.aggregate([
    { $group: { _id: "$divisionID", employee_count: { $sum: 1 }}}
])

{ "_id" : "ka002", "employee_count" : 4 }
{ "_id" : "ka001", "employee_count" : 4 }
{ "_id" : "ka004", "employee_count" : 2 }
{ "_id" : "ka003", "employee_count" : 2 }

コレクションの結合

  • バージョン3.2から MongoDB では結合処理ができる。NoSQL ではあるが、結合ができるというのは RDB に近く使いやすい気がする。その分クエリの内部処理は複雑になり性能ネックは出やすいが。
  • $lookup 構文を利用する。
  • わかりづらいが以下の例では、結合元コレクションは employee の方。division_docs 配列に含まれているのが結合先コレクションである division の各フィールドという意味。
db.employee.aggregate([
    { $lookup: {
        from: "division",
        localField: "divisionID",
        foreignField: "divisionID",
        as: "division_docs"
    }}
])

{ "_id" : ObjectId("630b25197c85fe1350d1da92"), "employeeID" : "emp001", "divisionID" : "ka001", "positionID" : "yaku001", "employeeName" : "田村", "age" : 30, "salary" : 400000, "division_docs" : [ { "_id" : ObjectId("630b25197c85fe1350d1da98"), "divisionID" : "ka001", "departmentID" : "bu001", "divisionName" : "営業1課" } ] }
{ "_id" : ObjectId("630b25197c85fe1350d1da93"), "employeeID" : "emp002", "divisionID" : "ka001", "positionID" : "yaku002", "employeeName" : "山田", "age" : 31, "salary" : 350000, "division_docs" : [ { "_id" : ObjectId("630b25197c85fe1350d1da98"), "divisionID" : "ka001", "departmentID" : "bu001", "divisionName" : "営業1課" } ] }
{ "_id" : ObjectId("630b25197c85fe1350d1da94"), "employeeID" : "emp003", "divisionID" : "ka002", "positionID" : "yaku003", "employeeName" : "中山", "age" : 37, "salary" : 300000, "division_docs" : [ { "_id" : ObjectId("630b25197c85fe1350d1da99"), "divisionID" : "ka002", "departmentID" : "bu001", "divisionName" : "営業2課" } ] }
{ "_id" : ObjectId("630b25197c85fe1350d1da95"), "employeeID" : "emp004", "divisionID" : "ka002", "positionID" : "yaku004", "employeeName" : "佐藤", "age" : 23, "salary" : 250000, "division_docs" : [ { "_id" : ObjectId("630b25197c85fe1350d1da99"), "divisionID" : "ka002", "departmentID" : "bu001", "divisionName" : "営業2課" } ] }
{ "_id" : ObjectId("630b25197c85fe1350d1da96"), "employeeID" : "emp005", "divisionID" : "ka003", "positionID" : "yaku002", "employeeName" : "佐々木", "age" : 33, "salary" : 350000, "division_docs" : [ { "_id" : ObjectId("630b25197c85fe1350d1da9a"), "divisionID" : "ka003", "departmentID" : "bu002", "divisionName" : "企画課" } ] }
{ "_id" : ObjectId("630b25197c85fe1350d1da97"), "employeeID" : "emp006", "divisionID" : "ka004", "positionID" : "yaku002", "employeeName" : "吉田", "age" : 35, "salary" : 350000, "division_docs" : [ { "_id" : ObjectId("630b25197c85fe1350d1da9b"), "divisionID" : "ka004", "departmentID" : "bu003", "divisionName" : "経理課" } ] }

上記では不要なフィールドまで含まれてしまう。結合後に取り出すフィールドを制限するには、$project を使う。

  • 結合元コレクションのフィールド employeeName と結合先コレクションの divisionName のみ取り出す場合。
db.employee.aggregate([
    { $lookup: {
        from: "division",
        localField: "divisionID",
        foreignField: "divisionID",
        as: "division_docs"
    }},
    { $project: {
        "_id": 0,
        "employeeName": 1,
        "division_docs.divisionName": 1
    }}
])

{ "employeeName" : "田村", "division_docs" : [ { "divisionName" : "営業1課" } ] }
{ "employeeName" : "山田", "division_docs" : [ { "divisionName" : "営業1課" } ] }
{ "employeeName" : "中山", "division_docs" : [ { "divisionName" : "営業2課" } ] }
{ "employeeName" : "佐藤", "division_docs" : [ { "divisionName" : "営業2課" } ] }
{ "employeeName" : "佐々木", "division_docs" : [ { "divisionName" : "企画課" } ] }
{ "employeeName" : "吉田", "division_docs" : [ { "divisionName" : "経理課" } ] }

このままだとdivision_docsが配列([])だしその中にオブジェクト型({})でdivisionNameが入っているしで整形した形でデータを取り出せないので、その辺りの配慮。

  • $unwind で配列を外す。
  • $group で division_docs の中の divisionName を取り出す。
db.employee.aggregate([
    { $lookup: {
        from: "division",
        localField: "divisionID",
        foreignField: "divisionID",
        as: "division_docs"
    }},
    { $project: {
        "_id": 0,
        "employeeID":1 ,
        "employeeName": 1,
        "division_docs.divisionName": 1
    }},
    { $unwind: "$division_docs" },
    { $group: {
        _id: "$employeeID",
        employeeName: { $max: "$employeeName" },
        divisionName: { $max: "$division_docs.divisionName"}
    }}
])

{ "_id" : "emp004", "employeeName" : "佐藤", "divisionName" : "営業2課" }
{ "_id" : "emp002", "employeeName" : "山田", "divisionName" : "営業1課" }
{ "_id" : "emp005", "employeeName" : "佐々木", "divisionName" : "企画課" }
{ "_id" : "emp001", "employeeName" : "田村", "divisionName" : "営業1課" }
{ "_id" : "emp006", "employeeName" : "吉田", "divisionName" : "経理課" }
{ "_id" : "emp003", "employeeName" : "中山", "divisionName" : "営業2課" }

集計結果のソート

$sort を使う

// division ごとの給料の最大値を昇順ソート
db.employee.aggregate([
    { $group: { _id: "$divisionID", max_salary: { $max: "$salary" }}},
    { $sort: { max_salary: 1 }}
])

{ "_id" : "ka002", "max_salary" : 300000 }
{ "_id" : "ka003", "max_salary" : 350000 }
{ "_id" : "ka004", "max_salary" : 350000 }
{ "_id" : "ka001", "max_salary" : 400000 }

// division ごとの給料の最大値を降順ソート
db.employee.aggregate([
    { $group: { _id: "$divisionID", max_salary: { $max: "$salary" }}},
    { $sort: { max_salary: -1 }}
])

{ "_id" : "ka001", "max_salary" : 400000 }
{ "_id" : "ka003", "max_salary" : 350000 }
{ "_id" : "ka004", "max_salary" : 350000 }
{ "_id" : "ka002", "max_salary" : 300000 }

集計の表示件数制御

$limit を使う

db.employee.aggregate([
    { $group: { _id: "$divisionID", max_salary: { $max: "$salary" }}},
    { $sort: { max_salary: -1 }},
    { $limit: 1 }
])

{ "_id" : "ka001", "max_salary" : 400000 }

集計結果のスキップ件数

$skip を使う

db.employee.aggregate([
    { $group: { _id: "$divisionID", max_salary: { $max: "$salary" }}},
    { $sort: { max_salary: -1 }},
    { $skip: 1 },
    { $limit: 1 }
])

{ "_id" : "ka003", "max_salary" : 350000 }

ジャーナル

  • 書き込み時に一時的にデータをディスクに書き込んで保存しておくファイルのこと。

  • ジャーナルがない場合の動作はというと、書き込みデータはメモリにバッファされて、storage.syncPeriodSecs ごとにディスクにflushされる。そのため、その間に障害が発生すると書き込みデータは吹っ飛ぶ可能性がある。本番でこの設定値を何かに設定することは非推奨らしい。しょっちゅうチェックポイントが走っても困るし。
    https://www.mongodb.com/docs/v6.0/reference/configuration-options/#mongodb-setting-storage.syncPeriodSecs

この際には、以下のログが出力される。v4.4 で確認

{"t":{"$date":"2022-08-30T14:07:04.434+09:00"},"s":"I",  "c":"STORAGE",  "id":22430,   "ctx":"WTCheckpointThread","msg":"WiredTiger message","attr":{"message":"[1661836024:434746][14692:0x7f2427789700], WT_SESSION.checkpoint: [WT_VERB_CHECKPOINT_PROGRESS] saving checkpoint snapshot min: 39, snapshot max: 39 snapshot count: 0, oldest timestamp: (1661836009, 1) , meta checkpoint timestamp: (1661836014, 1) base write gen: 13624"}}

レプリカセット

概要

  • レプリカセットはDBサーバをスケールアウトして冗長化するための MongoDB の手法。
  • Primary に書き込まれた後に Secondary に非同期で反映される。
  • 最小でも 1 つのレプリカセットを作るために 3 台のサーバが必要。各サーバは常にお互いをヘルスチェックして生死確認を行なっている。各サーバはどのサーバを Primary にするかの投票権を持っている。
  • 3台構成であれば、Primary 以外は 1 台 Secondary、1 台は Arbiter (投票権だけ持っているサーバでワークロードには関与しない) でも可能。Secondary 2 台でももちろん可能。
  • MongoDB のレプリカセットでは Primary 障害時に特段手動の介入なく自動でフェイルオーバーする。

同期の仕組み

レプリカセットを構築すると oplog というコレクション(厳密にはコレクション名 oplog.rs)が local データベース上に作成される。イメージとしては RDB の WAL ログのようなもの。同期の流れとしては以下になる。

  1. Primary で書き込み
  2. Primary にて書き込まれたデータの更新内容が oplog に記録される。
  3. Secondary が Primary の oplog を非同期で確認を行い、差分があれば Secondary の oplog にその差分が書き込まれる。
  4. Secondary の oplog の更新内容より、Secondary のコレクションに反映。

oplog のサイジング

oplog は capped コレクションで上限値に達すると古いデータが消えていくため、Secondary に障害が起きて復旧したらもう反映すべきデータが消えている可能性もある。そのため、なるべく余裕を持ったサイジングをしましょう。

Upper Bound でも 50 GB が指定できる最大サイズの模様。

https://www.mongodb.com/docs/manual/core/replica-set-oplog/

主な設定値

replication:
  replSetName: "rs000"
  oplogSizeMB: 1024
  • replSetName
    • レプリカセット名。所属するレプリカセットのメンバーはこの値を同一にする必要がある。
  • oplogSizeMB
    • そのまま oplog のサイズ。
    • Arbiter はデータを持たないのでこの設定値は不要。

書き込み保証

クエリ実行時に、どの程度書き込みを保証させるのかのパラメータを付与する。
writeConcern の w オプションと j オプション(journal)の組み合わせで動作が変わる

  • w:0
    • 書き込み保証なし。
  • w:1 j:false
    • Primary のメモリーに書き込まれたことを保証する。
  • w:1 j:true
    • Primary のディスク上のジャーナルに書き込まれたことを保証する。
  • w:2 j:false
    • Primary と 1 台の Seconday のメモリーに書き込まれたことを保証する。
  • w:2 j:true
    • Primary と 1 台の Seconday のディスク上のジャーナルに書き込まれたことを保証する。
  • w:majority j:false
    • Primary 含む過半数のノードのメモリーに書き込まれたことを保証する。
  • w:majority j:true
    • Primary 含む過半数のノードのディスク上のジャーナルに書き込まれたことを保証する。
  • w の数値が増えるほど、書き込み保証するSecondary の台数が増える。

なお、クエリ実行時にオプションを付与しない場合の暗黙的な値(デフォルト値)は以下のロジックとなる。

https://www.mongodb.com/docs/v5.0/reference/write-concern/#implicit-default-write-concern
if [ (#arbiters > 0) AND (#non-arbiters <= majority(#voting-nodes)) ]
    defaultWriteConcern = { w: 1 }
else
    defaultWriteConcern = { w: "majority" }

インデックス

RDB 同様に、MongoDB にもインデックスの概念が存在する。アルゴリズムは B-Tree が採用されているのでこれまた RDB と似ている。

(参考画像)

https://www.mongodb.com/docs/manual/indexes/

ソースデータ
db.item1.drop();
db.createCollection("item1");
db.item1.insertOne({name: "A1",price: 1});
db.item1.insertOne({name: "A2",price: 2});
db.item1.insertOne({name: "A3",price: 3});
db.item1.insertOne({name: "B1",price: 4});
db.item1.insertOne({name: "B2",price: 5});
db.item1.insertOne({name: "B3",price: 6});
db.item1.insertOne({name: "C1",price: 7});
db.item1.insertOne({name: "C2",price: 8});
db.item1.insertOne({name: "C3",price: 9});

インデックスの作成

  • インデックスを作成するフィールドに加えて、1か-1を値として指定する。
  • 1 の場合は対象フィールド値の昇順、-1 の場合は対象フィールドの降順でインデックス作成
  • 元になるコレクションを削除するとインデックスは自動的に削除される。
// 単一フィールド
db.item1.createIndex({name:1})

// 複数フィールドに対して指定することで複合インデックスにもできる
db.item1.createIndex({name:1,price:1})
  • オプション

インデックスの定義

db.item1.getIndexes()

インデックスの削除

// 特定インデックス削除
db.item1.dropIndex(<インデックス名>)

// 全インデックス削除
db.item1.dropIndexes()

Read Preference

クライアント側の MongoDB のライブラリによる設定となるが、設定値によって読み取りクエリをレプリカセット内のどのノードに接続して実行するかを制御する。

https://www.mongodb.com/docs/manual/core/read-preference/
  • primary
    • デフォルト。primary で読み取り実行。
  • primaryPreferred
    • primary で読み取りを実行できれば実行。primary の障害等で実行できなければ secondary で実行。
  • secondary
    • 読み取りは secondary で実行。
  • secondaryPreferred
    • secondary で読み取りを実行できれば実行。secondary の障害等で実行できなければ secondary で実行。
  • nearest
    • (各ノードへのレイテンシで決めるらしいですが詳細未調査)

注意点として、primary 以外は stale なデータを読み取る可能性があると考えた方が良い。

pymongo で secondaryPreferred 指定コード例。
import pymongo
import datetime
import time

# 以下の client オブジェクトを使っていくことで読み取りクエリは優先的にセカンダリに割り振る。書き込みはそのまま primary で実行される
client = pymongo.MongoClient(
		'localhost:27017',
		replicaSet='rs000',
		readPreference='secondaryPreferred'
         )


mydb = client["test"]
mycol = mydb["users"]

for x in mycol.find():
  print(x)

バックアップ・リストア

mondodump/mongorestore という純正ツールで行える。
mongodump を実行するとバックアップ対象の各コレクションに対して、データをバイナリ形式で保管した bson ファイル、及び、メタデータを保管した json ファイルが作成される。

mongodump
# データベース指定
mongodump \
--host localhost:27017 \
--username admin \
--password P#ssw0rd \
--db=test \
-o /tmp/dump-`date +%Y%m%d-%H%M%S`

# コレクション指定
mongodump \
--host localhost:27017 \
--username admin \
--password P#ssw0rd \
--db=test \
--collection=item1 \
-o /tmp/dump-`date +%Y%m%d-%H%M%S`

# 1 ファイルにアーカイブ
mongodump \
--host localhost:27017 \
--username admin \
--password P#ssw0rd \
--db=test \
--archive=/tmp/dump-`date +%Y%m%d-%H%M%S`

# アーカイブ & gzip
mongodump \
--host localhost:27017 \
--username admin \
--password P#ssw0rd \
--db=test \
--archive=/tmp/dump-`date +%Y%m%d-%H%M%S`.gz \
--gzip

Discussion

ログインするとコメントできます