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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?