MongoDB vs PostgreSQL|用途別に選ぶNoSQL・RDBの使い分けガイド

kento_morota 20分で読めます

「新しいプロジェクトのデータベースをMongoDBにするかPostgreSQLにするか迷っている」「NoSQLとRDBの違いがいまいちわからない」「それぞれどんな場面で使うべき?」――データベース選定は、プロジェクトの成功を左右する重要な意思決定です。

本記事では、ドキュメント型NoSQLの代表格MongoDBと、リレーショナルデータベースの代表格PostgreSQLを、データモデル、クエリ、パフォーマンス、スケーラビリティなどの観点からコード例を交えて徹底比較します。プロジェクトの要件に応じた最適な選択ができるようになるでしょう。

データモデルの根本的な違い

MongoDBとPostgreSQLの最大の違いは、データモデルのアプローチです。

PostgreSQL:リレーショナルモデル

PostgreSQLはテーブル(行と列)でデータを管理し、テーブル間をリレーション(外部キー)で結びつけます。

-- PostgreSQL:正規化されたテーブル設計
CREATE TABLE users (
  id    SERIAL PRIMARY KEY,
  name  VARCHAR(100) NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL
);

CREATE TABLE orders (
  id          SERIAL PRIMARY KEY,
  user_id     INTEGER NOT NULL REFERENCES users(id),
  total       NUMERIC(10, 2) NOT NULL,
  status      VARCHAR(20) DEFAULT 'pending',
  created_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE order_items (
  id          SERIAL PRIMARY KEY,
  order_id    INTEGER NOT NULL REFERENCES orders(id),
  product_name VARCHAR(255) NOT NULL,
  quantity    INTEGER NOT NULL,
  unit_price  NUMERIC(10, 2) NOT NULL
);

-- データの取得:JOINで結合
SELECT
  u.name,
  o.id AS order_id,
  o.total,
  oi.product_name,
  oi.quantity
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
WHERE u.id = 1;

MongoDB:ドキュメントモデル

MongoDBはJSON/BSONドキュメントでデータを管理します。関連するデータをネスト(埋め込み)して1つのドキュメントに収められます。

// MongoDB:ドキュメント設計
db.orders.insertOne({
  user: {
    name: "田中太郎",
    email: "tanaka@example.com"
  },
  items: [
    {
      productName: "ノートPC",
      quantity: 1,
      unitPrice: 89800
    },
    {
      productName: "マウス",
      quantity: 2,
      unitPrice: 3980
    }
  ],
  total: 97760,
  status: "pending",
  createdAt: new Date()
});

// データの取得:JOINなしで全情報が取得できる
db.orders.findOne({ "user.email": "tanaka@example.com" });

スキーマの柔軟性

PostgreSQLはスキーマオンライトで、データを書き込む前にテーブル構造を定義する必要があります。一方、MongoDBはスキーマレス(正確にはスキーマオンリード)で、同じコレクション内でもドキュメントごとに異なる構造を持てます。

// MongoDBでは同じコレクションに異なる構造のドキュメントが入る
db.products.insertMany([
  {
    name: "ノートPC",
    category: "電子機器",
    specs: { cpu: "M3", memory: "16GB", storage: "512GB" }
  },
  {
    name: "Tシャツ",
    category: "衣類",
    sizes: ["S", "M", "L", "XL"],
    colors: ["白", "黒", "ネイビー"]
  }
]);

ただし、実際のプロジェクトではMongoDBでもバリデーションスキーマを使って構造を制約することが推奨されます。

// MongoDBのスキーマバリデーション
db.createCollection("users", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "email"],
      properties: {
        name: { bsonType: "string", minLength: 1 },
        email: { bsonType: "string", pattern: "^.+@.+\\..+$" },
        age: { bsonType: "int", minimum: 0 }
      }
    }
  }
});

クエリ言語の比較

両者のクエリ方法を、同じ操作で比較してみましょう。

基本的なCRUD操作

-- PostgreSQL:INSERT
INSERT INTO users (name, email) VALUES ('田中太郎', 'tanaka@example.com');

-- PostgreSQL:SELECT
SELECT * FROM users WHERE email = 'tanaka@example.com';

-- PostgreSQL:UPDATE
UPDATE users SET name = '田中一郎' WHERE email = 'tanaka@example.com';

-- PostgreSQL:DELETE
DELETE FROM users WHERE email = 'tanaka@example.com';
// MongoDB:insert
db.users.insertOne({ name: "田中太郎", email: "tanaka@example.com" });

// MongoDB:find
db.users.findOne({ email: "tanaka@example.com" });

// MongoDB:update
db.users.updateOne(
  { email: "tanaka@example.com" },
  { $set: { name: "田中一郎" } }
);

// MongoDB:delete
db.users.deleteOne({ email: "tanaka@example.com" });

集計処理の比較

-- PostgreSQL:カテゴリ別の売上合計
SELECT
  category,
  COUNT(*) AS order_count,
  SUM(total) AS total_revenue,
  AVG(total) AS avg_order_value
FROM orders
WHERE created_at >= '2026-01-01'
GROUP BY category
HAVING SUM(total) > 100000
ORDER BY total_revenue DESC;
// MongoDB:Aggregation Pipeline
db.orders.aggregate([
  { $match: { createdAt: { $gte: ISODate("2026-01-01") } } },
  { $group: {
      _id: "$category",
      orderCount: { $sum: 1 },
      totalRevenue: { $sum: "$total" },
      avgOrderValue: { $avg: "$total" }
    }
  },
  { $match: { totalRevenue: { $gt: 100000 } } },
  { $sort: { totalRevenue: -1 } }
]);

PostgreSQLのSQLは宣言的で読みやすく、SQLの知識がある開発者にとって直感的です。MongoDBのAggregation Pipelineはパイプライン(ステージの連鎖)として表現され、段階的にデータを変換するアプローチです。

フルテキスト検索の比較

-- PostgreSQL:全文検索
-- 日本語対応にはpg_bigmやpg_trgm拡張が必要
CREATE INDEX idx_posts_search ON posts USING GIN (to_tsvector('english', title || ' ' || content));

SELECT title, ts_rank(to_tsvector('english', title || ' ' || content),
                       to_tsquery('english', 'database & performance')) AS rank
FROM posts
WHERE to_tsvector('english', title || ' ' || content)
      @@ to_tsquery('english', 'database & performance')
ORDER BY rank DESC;
// MongoDB:テキストインデックスによる全文検索
db.posts.createIndex({ title: "text", content: "text" });

db.posts.find(
  { $text: { $search: "database performance" } },
  { score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });

// MongoDB Atlas Searchを使えばより高度な検索が可能

パフォーマンスとスケーラビリティ

読み書きパフォーマンス

MongoDBが優位なケース

  • 単一ドキュメントの読み書き:JOINなしで関連データを一度に取得できるため高速
  • 書き込み負荷が高い場合:シャーディングによる水平スケーリングが容易
  • スキーマの頻繁な変更:ALTER TABLEのような重い操作が不要

PostgreSQLが優位なケース

  • 複雑なJOINクエリ:洗練されたクエリオプティマイザによる効率的な実行
  • トランザクション処理:複数テーブルにまたがるACIDトランザクション
  • 集計・分析:ウィンドウ関数やCTEによる高度な分析クエリ

スケーリングアプローチ

MongoDB:水平スケーリング(シャーディング)

// MongoDBのシャーディング設定例
sh.enableSharding("mydb");
sh.shardCollection("mydb.orders", { user_id: "hashed" });

// シャードキーの選択が重要:
// - user_id でシャーディング → ユーザーごとのクエリが高速
// - created_at でシャーディング → 時系列データに最適

MongoDBは設計段階からシャーディング(データの水平分割)を前提としており、大量のデータと高いスループットに対応できます。

PostgreSQL:垂直スケーリング + リードレプリカ

# PostgreSQLのリードレプリカ構成
# プライマリサーバー(書き込み用)
# ├── リードレプリカ1(読み取り専用)
# ├── リードレプリカ2(読み取り専用)
# └── リードレプリカ3(読み取り専用)

# テーブルパーティショニングによるスケーリング
CREATE TABLE access_logs (
  id BIGSERIAL,
  created_at TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (created_at);

PostgreSQLは基本的に垂直スケーリング(サーバースペックの強化)とリードレプリカで対応しますが、Citus拡張を使えば水平スケーリングも可能です。

アプリケーションコードでの使い方比較

Node.js/TypeScriptでの実装例を比較します。

PostgreSQL(Prisma)

// schema.prisma
model User {
  id     Int     @id @default(autoincrement())
  name   String
  email  String  @unique
  orders Order[]
}

model Order {
  id        Int         @id @default(autoincrement())
  user      User        @relation(fields: [userId], references: [id])
  userId    Int
  total     Decimal
  status    String      @default("pending")
  items     OrderItem[]
  createdAt DateTime    @default(now())
}

model OrderItem {
  id          Int    @id @default(autoincrement())
  order       Order  @relation(fields: [orderId], references: [id])
  orderId     Int
  productName String
  quantity    Int
  unitPrice   Decimal
}
// PostgreSQL + Prisma でのクエリ
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// ユーザーの注文履歴を取得(リレーション付き)
const userWithOrders = await prisma.user.findUnique({
  where: { email: 'tanaka@example.com' },
  include: {
    orders: {
      include: { items: true },
      orderBy: { createdAt: 'desc' },
      take: 10,
    },
  },
});

// 売上集計
const salesByMonth = await prisma.$queryRaw`
  SELECT
    DATE_TRUNC('month', created_at) AS month,
    COUNT(*) AS order_count,
    SUM(total) AS revenue
  FROM orders
  WHERE status = 'completed'
  GROUP BY month
  ORDER BY month DESC
`;

MongoDB(Mongoose)

// MongoDB + Mongoose のスキーマ定義
import mongoose from 'mongoose';

const orderSchema = new mongoose.Schema({
  user: {
    name: { type: String, required: true },
    email: { type: String, required: true },
  },
  items: [{
    productName: { type: String, required: true },
    quantity: { type: Number, required: true, min: 1 },
    unitPrice: { type: Number, required: true },
  }],
  total: { type: Number, required: true },
  status: {
    type: String,
    enum: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'],
    default: 'pending',
  },
  createdAt: { type: Date, default: Date.now },
});

// インデックスの作成
orderSchema.index({ 'user.email': 1, createdAt: -1 });
orderSchema.index({ status: 1 });

const Order = mongoose.model('Order', orderSchema);
// MongoDB + Mongoose でのクエリ
// ユーザーの注文履歴を取得
const orders = await Order.find({ 'user.email': 'tanaka@example.com' })
  .sort({ createdAt: -1 })
  .limit(10);

// 売上集計
const salesByMonth = await Order.aggregate([
  { $match: { status: 'completed' } },
  { $group: {
      _id: {
        year: { $year: '$createdAt' },
        month: { $month: '$createdAt' },
      },
      orderCount: { $sum: 1 },
      revenue: { $sum: '$total' },
    },
  },
  { $sort: { '_id.year': -1, '_id.month': -1 } },
]);

ユースケース別の選定ガイド

PostgreSQLを選ぶべきケース

1. ECサイト・業務システム:在庫管理、注文処理、会計処理など、データの整合性が重要なシステムではACIDトランザクションが必須です。

2. 複雑なレポーティング:売上分析、KPIダッシュボードなど、複雑なJOINや集計が多いケースではSQLの表現力が活きます。

3. 地理空間データ:PostGIS拡張を使えば、位置情報の検索や距離計算が効率的に行えます。

4. 既存のSQLスキル:チームにSQL経験者が多い場合、学習コストが低くなります。

MongoDBを選ぶべきケース

1. コンテンツ管理システム(CMS):記事やページの構造が柔軟に変化するCMSでは、スキーマレスの利点が活きます。

2. IoT・ログデータ:大量のセンサーデータやアクセスログなど、書き込みが多く構造が変わりやすいデータに適しています。

3. プロトタイピング:スキーマ定義なしでデータを投入できるため、初期段階の素早い開発に向いています。

4. 大規模な水平スケーリング:数TB以上のデータを複数サーバーに分散する必要がある場合、MongoDBのシャーディングが有利です。

両方を併用するケース

実際のプロジェクトでは、両方を併用するケースもあります。

# マイクロサービスアーキテクチャでの併用例
# ├── ユーザーサービス:PostgreSQL(認証、プロフィール、権限管理)
# ├── 注文サービス:PostgreSQL(注文処理、在庫管理、決済)
# ├── 商品カタログサービス:MongoDB(商品情報、カテゴリ別の属性)
# ├── ログ・分析サービス:MongoDB(アクセスログ、行動データ)
# └── 検索サービス:Elasticsearch(全文検索)

PostgreSQLのJSONBで「いいとこ取り」

実は、PostgreSQLにはJSONB型があり、ドキュメント的なデータモデルも扱えます。

JSONBを使ったハイブリッド設計

-- リレーショナル + ドキュメントのハイブリッド
CREATE TABLE products (
  id          SERIAL PRIMARY KEY,
  name        VARCHAR(255) NOT NULL,
  price       NUMERIC(10, 2) NOT NULL,
  category    VARCHAR(100) NOT NULL,
  attributes  JSONB DEFAULT '{}'  -- カテゴリ別の可変属性
);

-- カテゴリごとに異なる属性をJSONBで保存
INSERT INTO products (name, price, category, attributes) VALUES
('MacBook Pro', 248800, 'ノートPC',
 '{"cpu": "M3 Pro", "memory": "18GB", "storage": "512GB", "display": "14.2インチ"}'),
('Tシャツ', 3980, '衣類',
 '{"size": "L", "color": "ネイビー", "material": "コットン100%"}');

-- JSONBの検索
SELECT name, price, attributes->>'cpu' AS cpu
FROM products
WHERE category = 'ノートPC'
  AND (attributes->>'memory')::int >= 16;

-- GINインデックスで高速検索
CREATE INDEX idx_products_attributes ON products USING GIN (attributes);

PostgreSQLのJSONB型は、リレーショナルモデルの堅牢さとドキュメントモデルの柔軟性を1つのデータベースで実現できる強力な機能です。多くのケースで、MongoDBを使わずにPostgreSQLだけで対応できます。

まとめ:選定の判断基準

MongoDBとPostgreSQLの選択は、プロジェクトの要件によって決まります。

PostgreSQLを選ぶ判断基準

  • データの整合性・一貫性が最重要
  • 複雑なリレーションを持つデータ構造
  • JOINや複雑な集計クエリが多い
  • ACIDトランザクションが必須
  • チームにSQLのスキルがある

MongoDBを選ぶ判断基準

  • データ構造が頻繁に変わる
  • ネストされたドキュメントが多い
  • 書き込み負荷が非常に高い
  • 大規模な水平スケーリングが必要
  • 素早いプロトタイピングが優先

迷った場合は、まずPostgreSQLを選択し、JSONBで柔軟性が必要な部分に対応するアプローチがおすすめです。PostgreSQLのJSONBは非常に高機能で、多くのユースケースをカバーできます。MongoDBの導入は、明確にMongoDBが適しているユースケース(大量ログデータ、IoTデータなど)に限定するとよいでしょう。

#MongoDB#PostgreSQL#比較
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

起業準備に役立つ情報、もっとありますよ。

まずは話だけ聞いてもらう