- データベース
MongoDB入門|mongoshのコマンド一覧とSQLとの対応表
MongoDB は、データを JSON に近い「ドキュメント」の形で保存するデータベースです。 表(テーブル)の代わりに「コレクション」、行の代わりに「ドキュメント」を使い、1つのドキュメントの中に配列や入れ子のオブジェクトをそのまま持てます。操作は mongosh(MongoDB のシェル)で、JavaScript の書き方で行います。
use shop // データベースを選ぶ(無ければ最初の書き込みで作られる)
db.users.insertOne({ name: "Sato", age: 34, city: "Tokyo", tags: ["vip"] })
db.users.find({ city: "Tokyo" }, { _id: 0, name: 1, age: 1 }).sort({ age: 1 })
db.users.updateOne({ name: "Sato" }, { $set: { city: "Osaka" } })
db.users.deleteOne({ name: "Sato" })
| SQL | MongoDB(mongosh) |
|---|---|
| データベース | データベース |
| テーブル | コレクション |
| 行 | ドキュメント |
| 列 | フィールド |
| 主キー | _id(自動で付く) |
SELECT ... WHERE |
find({ 条件 }) |
INSERT |
insertOne() / insertMany() |
UPDATE ... SET |
updateOne() / updateMany() + $set |
DELETE |
deleteOne() / deleteMany() |
GROUP BY・JOIN |
aggregate([...])($group・$lookup) |
CREATE INDEX |
createIndex() |
EXPLAIN |
.explain("executionStats") |
確認環境:Python のドライバー pymongo 4.18.3 のコードを、MongoDB の動きをまねるライブラリ mongomock 4.3.0 で動かし、同じデータを SQLite(Python 3.11 の sqlite3)に入れて、SQL と結果が一致するかを比べました(Ubuntu 24.04)。mongosh(2.13.0)の書き方・出力の形、explain の項目、演算子と集計ステージの仕様は、MongoDB の公式ドキュメント(9.0)で確かめています。エラーの文面・explain の数値はドキュメントの例を載せています(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「MongoDB入門|mongoshのコマンド一覧とSQLとの対応表」
mongosh を使い始める
# 入れ方の一例(npm で入る。ほかに各 OS 向けのパッケージがある)
npm install -g mongosh
mongosh --version
# 接続
mongosh # mongodb://localhost:27017 に接続
mongosh "mongodb://localhost:27017/shop"
mongosh "mongodb+srv://cluster0.example.mongodb.net/" --username alice # Atlas
シェルのコマンド(ヘルパー)
| コマンド | 意味 | SQL で言うと |
|---|---|---|
show dbs |
データベースの一覧 | SHOW DATABASES |
use shop |
データベースを選ぶ | USE shop |
db |
今のデータベース名 | SELECT DATABASE() |
show collections |
コレクションの一覧 | SHOW TABLES |
db.createCollection("logs") |
コレクションを作る(書き込めば自動でもできる) | CREATE TABLE |
db.users.drop() |
コレクションを消す | DROP TABLE |
db.dropDatabase() |
今のデータベースを消す | DROP DATABASE |
db.users.stats() / db.stats() |
サイズ・件数など | ― |
it |
結果の続きを表示 | ― |
help / db.users.help() |
ヘルプ | ― |
exit / quit |
終わる | ― |
use しただけでは、データベースは作られません。 最初にドキュメントを書き込んだときに、データベースとコレクションがまとめて作られます。
旧 mongo シェルとの違い
mongosh は旧 mongo シェルの後継です。ドキュメントで「deprecated」とされているメソッドと、代わりに使うものです。
| 使わないもの(旧) | 代わりに使うもの |
|---|---|
db.collection.insert() |
insertOne()・insertMany()・bulkWrite() |
db.collection.update() |
updateOne()・updateMany()・findOneAndUpdate() |
db.collection.remove() |
deleteOne()・deleteMany()・findOneAndDelete() |
db.collection.save() |
insertOne()・updateOne() など |
db.collection.count() |
countDocuments()・estimatedDocumentCount() |
db.collection.copyTo() |
集計の $out |
旧シェル向けの記事にある db.users.insert(...) や .count() は、mongosh のドキュメントでは deprecated(使わないことが勧められるもの)とされています。また、mongosh は数値を、入る範囲なら Int32、それ以外を Double で保存します(旧シェルは既定で Double)。
作る・読む・更新・削除
作る(insert)
db.users.insertOne({ name: "Sato", age: 34, city: "Tokyo", tags: ["vip", "early"] })
// { acknowledged: true, insertedId: ObjectId('...') } ← _id が自動で付く
db.users.insertMany([
{ _id: 2, name: "Suzuki", age: 27, city: "Osaka", tags: ["early"] },
{ _id: 3, name: "Tanaka", age: 45, city: "Tokyo", tags: [] },
{ _id: 4, name: "Ito", age: 19, city: "Nagoya", tags: ["vip"] }
])
_id を書かないと、ObjectId(12バイトの ID。文字列にすると16進24文字)が自動で付きます。 同じ _id を入れると、ドキュメントの例ではこのエラーになります。
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: sample_mflix.movies index: _id_ dup key: { : 10.0 }"
読む(find)
db.users.find() // 全部
db.users.find({ city: "Tokyo" }) // 条件
db.users.find({ city: "Tokyo" }, { _id: 0, name: 1, age: 1 }) // 列を選ぶ(射影)
db.users.find().sort({ age: -1 }).skip(10).limit(5) // 並べ替え・ページング
db.users.findOne({ _id: 2 }) // 1件
db.users.countDocuments({ age: { $gte: 30 } }) // 件数
db.users.distinct("city") // 重複なしの値
射影(第2引数)では、1 で表示、0 で非表示です。 _id は書かなくても表示されるので、要らないときは _id: 0 を付けます。1 と 0 は _id 以外では混ぜられません。
更新(update)
db.users.updateOne({ _id: 2 }, { $set: { city: "Kyoto" }, $inc: { age: 1 } })
// { acknowledged: true, matchedCount: 1, modifiedCount: 1, ... }
db.users.updateMany({ city: "Tokyo" }, { $push: { tags: "tokyo" } })
db.users.updateOne({ _id: 9 }, { $set: { name: "New" } }, { upsert: true }) // 無ければ作る
db.users.findOneAndUpdate({ _id: 4 }, { $inc: { age: 1 } }, { returnDocument: "after" })
db.users.replaceOne({ _id: 9 }, { name: "Replaced" }) // 丸ごと置き換え
updateOne・updateMany の第2引数には、$set などの更新の演算子を書きます。 演算子の無いドキュメントを渡すとエラーになるので、中身を丸ごと置き換えたいときは replaceOne を使います。
| 結果の項目 | 意味 |
|---|---|
matchedCount |
条件に合った件数 |
modifiedCount |
実際に変わった件数(同じ値を入れると増えない) |
upsertedId |
upsert で新しく作ったときの _id |
削除(delete)
db.orders.deleteOne({ _id: 104 })
db.orders.deleteMany({ status: "cancelled" }) // { acknowledged: true, deletedCount: 1 }
db.orders.deleteMany({}) // 全部消す(コレクションは残る)
演算子一覧
比較
| 演算子 | 意味 | 例 | SQL |
|---|---|---|---|
(そのまま) / $eq |
等しい | { city: "Tokyo" } |
= 'Tokyo' |
$ne |
等しくない | { status: { $ne: "cancelled" } } |
<> |
$gt / $gte |
より大きい / 以上 | { age: { $gt: 20 } } |
> / >= |
$lt / $lte |
より小さい / 以下 | { age: { $lte: 40 } } |
< / <= |
$in / $nin |
どれかに一致 / どれにも一致しない | { city: { $in: ["Osaka", "Nagoya"] } } |
IN / NOT IN |
論理・要素・文字列
| 演算子 | 意味 | 例 |
|---|---|---|
(並べる) / $and |
かつ | { city: "Tokyo", age: { $gt: 30 } } |
$or |
または | { $or: [{ city: "Osaka" }, { age: { $lt: 20 } }] } |
$not |
否定 | { age: { $not: { $gt: 30 } } } |
$nor |
どれでもない | { $nor: [{ city: "Tokyo" }, { vip: true }] } |
$exists |
フィールドがある / 無い | { email: { $exists: false } } |
$type |
型 | { age: { $type: "int" } } |
$regex |
正規表現 | { name: { $regex: "^S" } }、{ name: /^S/i } |
$expr |
集計の式で比べる(フィールドどうし) | { $expr: { $gt: ["$spent", "$budget"] } } |
配列
| 演算子・書き方 | 意味 | 例 |
|---|---|---|
| 値をそのまま | 配列にその値を含む | { tags: "vip" } |
$all |
全部を含む | { tags: { $all: ["vip", "early"] } } |
$size |
要素の数 | { tags: { $size: 0 } } |
$elemMatch |
1つの要素が複数の条件を満たす | { items: { $elemMatch: { sku: "A", qty: { $gte: 2 } } } } |
"items.sku" |
入れ子・配列の中のフィールド | { "items.sku": "A" } |
配列のフィールドに値をそのまま書くと、「その値を含む」という意味になります。 手元のデータで { tags: "vip" } は Sato と Ito、{ tags: { $size: 0 } } は Tanaka を返しました。
更新の演算子
| 演算子 | 意味 | 例 |
|---|---|---|
$set |
値を入れる | { $set: { city: "Kyoto" } } |
$unset |
フィールドを消す | { $unset: { city: "" } } |
$inc |
足す | { $inc: { age: 1 } } |
$mul |
掛ける | { $mul: { price: 1.1 } } |
$min / $max |
小さい / 大きいときだけ入れる | { $max: { best: 90 } } |
$rename |
フィールド名を変える | { $rename: { nm: "name" } } |
$currentDate |
今の日時 | { $currentDate: { updatedAt: true } } |
$setOnInsert |
upsert で作ったときだけ入れる | { $setOnInsert: { createdAt: new Date() } } |
$push |
配列に足す | { $push: { tags: "new" } } |
$addToSet |
無いときだけ配列に足す | { $addToSet: { tags: "vip" } } |
$pull |
条件に合う要素を消す | { $pull: { tags: "early" } } |
$pop |
先頭(-1)・末尾(1)を消す | { $pop: { tags: 1 } } |
$[]・$[elem] |
配列の全要素・条件に合う要素を更新 | { $set: { "items.$[i].qty": 0 } } + arrayFilters |
$addToSet で既にある "vip" を足すと、modifiedCount は 0 でした。$pull: { tags: "early" } で ["vip", "early", "tokyo"] が ["vip", "tokyo"] になりました。
SQL との対応表
左の SQL と右の mongosh は、同じ結果を返す書き方です。 users・orders のデータを SQLite と mongomock の両方に入れ、pymongo で同じ問い合わせをして、結果が一致することを確かめました(表の右端)。
// 使ったデータ
users: { _id: 1, name: "Sato", age: 34, city: "Tokyo", tags: ["vip", "early"] }
{ _id: 2, name: "Suzuki", age: 27, city: "Osaka", tags: ["early"] }
{ _id: 3, name: "Tanaka", age: 45, city: "Tokyo", tags: [] }
{ _id: 4, name: "Ito", age: 19, city: "Nagoya", tags: ["vip"] }
orders: { _id: 101, user_id: 1, item: "keyboard", price: 12000, qty: 1, status: "paid" }
{ _id: 102, user_id: 1, item: "mouse", price: 3000, qty: 2, status: "paid" }
{ _id: 103, user_id: 2, item: "monitor", price: 30000, qty: 1, status: "shipped" }
{ _id: 104, user_id: 3, item: "mouse", price: 3000, qty: 1, status: "cancelled" }
{ _id: 105, user_id: 3, item: "cable", price: 800, qty: 5, status: "paid" }
| SQL | mongosh | 結果(両方で一致) |
|---|---|---|
SELECT name, age FROM users WHERE city='Tokyo' ORDER BY age |
db.users.find({ city: "Tokyo" }, { _id: 0, name: 1, age: 1 }).sort({ age: 1 }) |
Sato 34、Tanaka 45 |
WHERE age > 20 AND age <= 40 |
find({ age: { $gt: 20, $lte: 40 } }) |
Sato、Suzuki |
WHERE city='Osaka' OR age < 20 |
find({ $or: [{ city: "Osaka" }, { age: { $lt: 20 } }] }) |
Suzuki、Ito |
WHERE city IN ('Osaka','Nagoya') |
find({ city: { $in: ["Osaka", "Nagoya"] } }) |
Suzuki、Ito |
WHERE name LIKE 'S%' |
find({ name: { $regex: "^S" } }) |
Sato、Suzuki |
ORDER BY age DESC LIMIT 2 OFFSET 1 |
find().sort({ age: -1 }).skip(1).limit(2) |
Sato、Suzuki |
SELECT COUNT(*) ... WHERE age >= 30 |
countDocuments({ age: { $gte: 30 } }) |
2 |
SELECT DISTINCT city |
distinct("city") |
Nagoya、Osaka、Tokyo |
GROUP BY ... HAVING(下記) |
aggregate([$match, $group, $match, $sort]) |
(2, 30000, 1)、(1, 18000, 2) |
JOIN(下記) |
aggregate([$match, $lookup, $unwind, $project]) |
101 Sato keyboard ほか2件 |
GROUP BY と HAVING
SELECT user_id, SUM(price * qty) AS total, COUNT(*) AS n
FROM orders
WHERE status <> 'cancelled'
GROUP BY user_id
HAVING SUM(price * qty) > 5000
ORDER BY total DESC;
db.orders.aggregate([
{ $match: { status: { $ne: "cancelled" } } }, // WHERE
{ $group: { _id: "$user_id", // GROUP BY
total: { $sum: { $multiply: ["$price", "$qty"] } }, // SUM(price*qty)
n: { $sum: 1 } } }, // COUNT(*)
{ $match: { total: { $gt: 5000 } } }, // HAVING
{ $sort: { total: -1 } } // ORDER BY
])
$group の後ろに置いた $match が HAVING です。 集計ステージは上から順に流れるので、$match を $group の前に置くと WHERE、後に置くと HAVING になります。フィールドの値を参照するときは "$price" のように $ を付けます。結果は SQL・mongomock ともに (2, 30000, 1)・(1, 18000, 2) でした。
JOIN($lookup)
SELECT o.id, u.name, o.item
FROM orders o JOIN users u ON u.id = o.user_id
WHERE o.status = 'paid'
ORDER BY o.id;
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $lookup: { from: "users", localField: "user_id", foreignField: "_id", as: "user" } },
{ $unwind: "$user" }, // 配列 user を1件ずつに開く
{ $project: { _id: 1, item: 1, name: "$user.name" } },
{ $sort: { _id: 1 } }
])
$lookup の結果は、相手が1件でも配列(user: [ {...} ])で入ります。 $unwind で開くと、INNER JOIN と同じ形になります。相手がいない行も残したい(LEFT JOIN)ときは、{ $unwind: { path: "$user", preserveNullAndEmptyArrays: true } } と書きます。結果は 101 Sato keyboard・102 Sato mouse・105 Tanaka cable で、SQL と一致しました。SQL の JOIN の種類は、関連記事「SQLのJOINの違い|INNER JOINとLEFT JOINの使い分け」で扱っています。
DDL(テーブル定義)にあたるもの
| SQL | MongoDB |
|---|---|
CREATE TABLE |
最初の書き込みで自動。明示するなら db.createCollection("users") |
ALTER TABLE ADD COLUMN |
不要(ドキュメントごとに自由)。全件に足すなら updateMany({}, { $set: { joinDate: new Date() } }) |
ALTER TABLE DROP COLUMN |
updateMany({}, { $unset: { joinDate: "" } }) |
NOT NULL・CHECK・型 |
スキーマ検証(db.createCollection("users", { validator: { $jsonSchema: {...} } })) |
INSERT INTO new SELECT |
集計の $out |
MERGE |
集計の $merge |
UNION ALL |
集計の $unionWith |
集計(aggregate)のステージ早見表
| ステージ | 意味 | SQL で言うと |
|---|---|---|
$match |
絞り込み | WHERE($group の後なら HAVING) |
$group |
グループごとに集計 | GROUP BY |
$project |
列を選ぶ・計算した列を作る | SELECT 列 |
$addFields / $set |
列を足す(ほかの列は残る) | SELECT *, 式 AS 名前 |
$unset |
列を外す | ― |
$sort |
並べ替え | ORDER BY |
$limit / $skip |
件数・読み飛ばし | LIMIT / OFFSET |
$count |
件数を1つのドキュメントで返す | SELECT COUNT(*) |
$sortByCount |
値ごとの件数を多い順に | GROUP BY x ORDER BY COUNT(*) DESC |
$lookup |
別のコレクションと結合 | JOIN |
$unwind |
配列を1要素1ドキュメントに開く | ― |
$facet |
1回の入力で複数の集計を並べて行う | 複数のクエリ |
$bucket / $bucketAuto |
範囲ごとに分ける | CASE + GROUP BY |
$replaceRoot / $replaceWith |
入れ子のドキュメントを一番上に | ― |
$unionWith |
別のコレクションの結果を続ける | UNION ALL |
$setWindowFields |
累計・順位などのウィンドウ関数 | OVER (PARTITION BY ...) |
$out |
結果をコレクションに書き出す(置き換え) | INSERT INTO ... SELECT |
$merge |
結果をコレクションに混ぜる | MERGE |
$group で使う集計の演算子
| 演算子 | 意味 | SQL |
|---|---|---|
$sum: 1 |
件数 | COUNT(*) |
$sum: "$price" |
合計 | SUM(price) |
$avg |
平均 | AVG |
$min / $max |
最小 / 最大 | MIN / MAX |
$first / $last |
グループ内の最初 / 最後($sort の後に使う) |
― |
$push |
値を配列に集める | ARRAY_AGG |
$addToSet |
重複なしで配列に集める | ARRAY_AGG(DISTINCT) |
_id: null |
全体で1グループ | GROUP BY なし |
手元で動かした例です。
// 配列 tags を開いてタグごとに数える
db.users.aggregate([{ $unwind: "$tags" }, { $group: { _id: "$tags", n: { $sum: 1 } } }, { $sort: { _id: 1 } }])
// early: 2、vip: 2
// 1回で、状態ごとの件数と最高値を出す
db.orders.aggregate([{ $facet: {
byStatus: [{ $group: { _id: "$status", n: { $sum: 1 } } }, { $sort: { _id: 1 } }],
max: [{ $group: { _id: null, max: { $max: "$price" } } }]
} }])
// byStatus: cancelled 1・paid 3・shipped 1、max: 30000
// 年齢を範囲で分ける
db.users.aggregate([{ $bucket: { groupBy: "$age", boundaries: [0, 20, 30, 40, 100], output: { n: { $sum: 1 } } } }])
// 0〜19: 1、20〜29: 1、30〜39: 1、40〜99: 1
集計は前のステージの出力が次の入力になるので、$match をできるだけ前に置くと、後ろのステージが扱う件数が減ります。 SQL の集計関数の考え方は、関連記事「SQL集計関数一覧|SUM・AVG・MAX・MIN・COUNTとNULLの扱い」で扱っています。
インデックス
db.users.createIndex({ city: 1 }) // 単一(1 は昇順、-1 は降順)
db.users.createIndex({ city: 1, age: -1 }) // 複合 → 名前は city_1_age_-1
db.users.createIndex({ email: 1 }, { unique: true }) // 重複禁止
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 }) // TTL:1時間後に自動削除
db.orders.createIndex({ status: 1 }, { partialFilterExpression: { status: "paid" } }) // 一部だけ
db.users.getIndexes() // 一覧
db.users.dropIndex("city_1_age_-1") // 削除
db.users.hideIndex("city_1") // 消さずに使わないようにする(試すとき)
| 種類 | 作り方 | 用途 |
|---|---|---|
| 単一 | { city: 1 } |
1つのフィールドで探す |
| 複合 | { city: 1, age: -1 } |
複数の条件・並べ替え |
| マルチキー | 配列のフィールドに作ると自動 | 配列の要素で探す({ tags: "vip" }) |
| テキスト | { body: "text" } |
全文検索($text) |
| 2dsphere | { location: "2dsphere" } |
地図上の距離 |
| ハッシュ | { userId: "hashed" } |
シャーディングのキー |
| ワイルドカード | { "attrs.$**": 1 } |
形が決まっていないフィールド |
| ユニーク | { unique: true } |
重複を許さない |
| TTL | { expireAfterSeconds: 秒 } |
期限が来たら消す(セッション・ログ) |
| 部分 | { partialFilterExpression: {...} } |
条件に合うものだけ(小さくできる) |
複合インデックスは、等しさで絞る列 → 並べ替えの列 → 範囲で絞る列の順に並べるのが、MongoDB のドキュメントの目安(ESR ガイドライン:Equality・Sort・Range)です。範囲の条件でかなり絞れる場合は、範囲を並べ替えより前に置く(ERS)こともある、とされています。find({ city: "Tokyo", age: { $gt: 30 } }).sort({ name: 1 }) なら { city: 1, name: 1, age: 1 } になります。
pymongo で create_index([("city", ASCENDING), ("age", DESCENDING)]) を実行すると、名前は city_1_age_-1 になりました。name にユニークインデックスを作ってから同じ名前を入れると、DuplicateKeyError(code 11000)になりました。インデックスの仕組みそのものは、関連記事「SQLインデックスの作成方法と仕組み|CREATE INDEXを実行結果で解説」で扱っています。
explain で実行計画を見る
db.users.find({ city: "Tokyo", age: { $gt: 30 } }).explain("executionStats")
db.orders.explain("executionStats").aggregate([ { $match: { status: "paid" } } ])
| モード | 出るもの |
|---|---|
"queryPlanner"(既定) |
選ばれた計画(winningPlan)と、選ばれなかった計画(rejectedPlans) |
"executionStats" |
上に加えて、実際に動かした件数・時間 |
"allPlansExecution" |
候補の計画それぞれの途中の実行情報も |
見るところ
queryPlanner: {
winningPlan: { stage: 'FETCH', inputStage: { stage: 'IXSCAN', indexName: 'city_1_age_-1', ... } },
rejectedPlans: [ ... ]
},
executionStats: {
nReturned: ...,
executionTimeMillis: ...,
totalKeysExamined: ...,
totalDocsExamined: ...,
executionStages: { ... }
}
| 項目 | 意味(ドキュメントより) | 目安 |
|---|---|---|
winningPlan.stage |
選ばれた計画の処理の種類 | COLLSCAN なら全件を読んでいる |
nReturned |
返したドキュメントの数 | ― |
totalKeysExamined |
調べたインデックスのキーの数 | nReturned に近いほど無駄が少ない |
totalDocsExamined |
調べたドキュメントの数 | nReturned より大きく離れていたら、インデックスが合っていない |
executionTimeMillis |
計画の選択と実行にかかったミリ秒 | ネットワークの時間は含まない |
| stage | 意味 |
|---|---|
COLLSCAN |
コレクションを全部読む(インデックスなし) |
IXSCAN |
インデックスを読む |
FETCH |
インデックスで見つけたドキュメント本体を読む |
SORT |
メモリで並べ替え(無ければインデックスの順で並んでいる) |
EXPRESS_IXSCAN など EXPRESS_ で始まるもの |
8.0 から。単純な問い合わせを、通常の計画選びを省いて実行 |
stage: 'SORT' が出ていたら、並べ替えにインデックスが使われていません。 ESR の順の複合インデックスを作ると、SORT が消えることがあります。MySQL の EXPLAIN との読み比べは、関連記事「MySQL EXPLAINの見方|type・key・rows・Extra早見表」で扱っています。
Python(pymongo)で使う
pip install pymongo
import os
from pymongo import MongoClient, ASCENDING, DESCENDING, ReturnDocument
from pymongo.errors import DuplicateKeyError
client = MongoClient(os.environ.get("MONGODB_URI", "mongodb://localhost:27017"))
db = client["shop"]
users = db["users"]
orders = db["orders"]
# --- Create ---
users.insert_many([
{"_id": 1, "name": "Sato", "age": 34, "city": "Tokyo", "tags": ["vip", "early"]},
{"_id": 2, "name": "Suzuki", "age": 27, "city": "Osaka", "tags": ["early"]},
{"_id": 3, "name": "Tanaka", "age": 45, "city": "Tokyo", "tags": []},
{"_id": 4, "name": "Ito", "age": 19, "city": "Nagoya", "tags": ["vip"]},
])
orders.insert_many([
{"_id": 101, "user_id": 1, "item": "keyboard", "price": 12000, "qty": 1, "status": "paid"},
{"_id": 102, "user_id": 1, "item": "mouse", "price": 3000, "qty": 2, "status": "paid"},
{"_id": 103, "user_id": 2, "item": "monitor", "price": 30000, "qty": 1, "status": "shipped"},
{"_id": 104, "user_id": 3, "item": "mouse", "price": 3000, "qty": 1, "status": "cancelled"},
{"_id": 105, "user_id": 3, "item": "cable", "price": 800, "qty": 5, "status": "paid"},
])
# --- Read ---
tokyo = users.find({"city": "Tokyo"}, {"_id": 0, "name": 1, "age": 1}).sort("age", ASCENDING)
print("Tokyo:", list(tokyo))
print("20 < age <= 40:", [u["name"] for u in users.find({"age": {"$gt": 20, "$lte": 40}})])
print("count age>=30:", users.count_documents({"age": {"$gte": 30}}))
print("tags に vip:", [u["name"] for u in users.find({"tags": "vip"})])
# --- Aggregate ---
pipeline = [
{"$match": {"status": {"$ne": "cancelled"}}},
{"$group": {"_id": "$user_id",
"total": {"$sum": {"$multiply": ["$price", "$qty"]}},
"n": {"$sum": 1}}},
{"$match": {"total": {"$gt": 5000}}},
{"$sort": {"total": -1}},
]
print("売上:", [(d["_id"], d["total"], d["n"]) for d in orders.aggregate(pipeline)])
joined = orders.aggregate([
{"$match": {"status": "paid"}},
{"$lookup": {"from": "users", "localField": "user_id", "foreignField": "_id", "as": "user"}},
{"$unwind": "$user"},
{"$project": {"_id": 1, "item": 1, "name": "$user.name"}},
{"$sort": {"_id": 1}},
])
print("JOIN:", [(d["_id"], d["name"], d["item"]) for d in joined])
# --- Update ---
r = users.update_one({"_id": 2}, {"$set": {"city": "Kyoto"}, "$inc": {"age": 1}})
print("update_one:", r.matched_count, r.modified_count)
r = users.update_one({"_id": 9}, {"$set": {"name": "New"}}, upsert=True)
print("upsert:", r.matched_count, r.upserted_id)
doc = users.find_one_and_update({"_id": 4}, {"$inc": {"age": 1}},
return_document=ReturnDocument.AFTER)
print("find_one_and_update:", doc["age"])
# --- Delete ---
print("delete_many:", orders.delete_many({"status": "cancelled"}).deleted_count)
# --- Index ---
print("index:", users.create_index([("city", ASCENDING), ("age", DESCENDING)]))
users.create_index("name", unique=True)
try:
users.insert_one({"name": "Sato"})
except DuplicateKeyError as e:
print("DuplicateKeyError code:", e.code)
MongoClient を mongomock に差し替えて実行した出力です。
Tokyo: [{'name': 'Sato', 'age': 34}, {'name': 'Tanaka', 'age': 45}]
20 < age <= 40: ['Sato', 'Suzuki']
count age>=30: 2
tags に vip: ['Sato', 'Ito']
売上: [(2, 30000, 1), (1, 18000, 2)]
JOIN: [(101, 'Sato', 'keyboard'), (102, 'Sato', 'mouse'), (105, 'Tanaka', 'cable')]
update_one: 1 1
upsert: 0 9
find_one_and_update: 20
delete_many: 1
index: city_1_age_-1
DuplicateKeyError code: 11000
mongosh と pymongo の書き方の違い
| mongosh | pymongo |
|---|---|
insertOne / findOne / updateMany |
insert_one / find_one / update_many(スネークケース) |
{ city: "Tokyo" } |
{"city": "Tokyo"}(キーは文字列で引用符が要る) |
.sort({ age: -1 }) |
.sort("age", DESCENDING) か .sort([("age", -1)]) |
{ returnDocument: "after" } |
return_document=ReturnDocument.AFTER |
{ upsert: true } |
upsert=True |
ObjectId('...') |
from bson import ObjectId |
new Date() |
datetime.now(timezone.utc) |
countDocuments |
count_documents |
find() が返すのはカーソル(結果を少しずつ取り出すもの)です。 list() で全部を取り出すか、for で1件ずつ読みます。大量のデータで list() すると、全部がメモリに乗ります。
テストで mongomock を使うとき
mongomock は、サーバーなしで pymongo の動きをまねるライブラリで、単体テストで使われます。手元の mongomock 4.3.0 では、$sortByCount は「not implemented in Mongomock」のエラーになり、pymongo 4.18.3 の bulk_write は引数の違いで TypeError になりました。新しい機能まで確かめたいテストは、MongoDB のサーバー(Docker の公式イメージなど)を使います。
よくある質問
Q. テーブルの設計(正規化)はしなくてよいのですか? A. MongoDB では、一緒に読むデータは1つのドキュメントに入れる(埋め込む)設計がよく使われます。ドキュメントには1件あたり 16MB の上限があるので、際限なく増える配列(ログ・コメントなど)は別のコレクションに分けます。
Q. トランザクションは使えますか? A. 使えます。レプリカセット(複製の構成)かシャードクラスター上で、複数のドキュメント・コレクションにまたがるトランザクションが使えます。MongoDB のドキュメントでは、多くの場面で埋め込みのデータモデルのほうが適していると説明されています。
Q. 日本語の部分一致検索はどうしますか?
A. $regex で部分一致はできますが、先頭一致(^)以外はインデックスを効率よく使えません。全文検索が要る場合は、Atlas Search などの検索機能を検討します。
Q. Redis とどう使い分けますか? A. Redis はメモリ上のキーと値の保存(キャッシュ・セッション)、MongoDB はドキュメントを保存して条件で検索・集計する用途が中心です。Redis については、関連記事「Redisとは?使い方入門|データ型・キャッシュ・よくある失敗を実行結果で解説」で扱っています。
Q. PostgreSQL の JSON 型で足りませんか? A. 表の中の一部だけが JSON なら、PostgreSQL の jsonb でも扱えます。関連記事「PostgreSQL jsonb操作一覧|演算子早見表とGINインデックス実測」で扱っています。
まとめ
- テーブル → コレクション、行 → ドキュメント、主キー →
_id(自動で ObjectId) - mongosh では
insert・update・remove・countではなく、insertOne・updateOne・deleteMany・countDocuments - 条件は
{ age: { $gt: 20, $lte: 40 } }、OR は$or: [...]、配列は値をそのまま書くと「含む」 - 更新は
$set・$inc・$push・$addToSet・$pullを使う。丸ごと置き換えはreplaceOne GROUP BYは$group、HAVING は$groupの後の$match、JOINは$lookup+$unwind。SQLite と同じ結果になることを確かめた- 複合インデックスは ESR(等しさ → 並べ替え → 範囲)の順。
explain("executionStats")でCOLLSCAN・totalDocsExaminedを見る
参考・出典
確認日はいずれも 2026年10月13日です。
- MongoDB Docs「SQL to MongoDB Mapping Chart」:https://www.mongodb.com/docs/manual/reference/sql-comparison/
- MongoDB Docs「SQL to Aggregation Mapping Chart」:https://www.mongodb.com/docs/manual/reference/sql-aggregation-comparison/
- MongoDB Docs「Explain Results」:https://www.mongodb.com/docs/manual/reference/explain-results/
- MongoDB Docs「db.collection.insertOne()」:https://www.mongodb.com/docs/manual/reference/method/db.collection.insertOne/
- MongoDB Docs「db.collection.updateOne()」:https://www.mongodb.com/docs/manual/reference/method/db.collection.updateOne/
- MongoDB Docs「Aggregation Stages」:https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/
- MongoDB Docs「Query and Projection Operators」:https://www.mongodb.com/docs/manual/reference/operator/query/
- MongoDB Docs「The ESR (Equality, Sort, Range) Guideline」:https://www.mongodb.com/docs/manual/tutorial/equality-sort-range-guideline/
- MongoDB Shell Docs「Compatibility Changes with Legacy mongo Shell」:https://www.mongodb.com/docs/mongodb-shell/reference/compatibility/
- PyMongo Documentation:https://pymongo.readthedocs.io/
- mongomock(GitHub):https://github.com/mongomock/mongomock
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「MongoDB入門|mongoshのコマンド一覧とSQLとの対応表」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。