makoto-developer's テックブログ

Firestoreで10万件超のECサイト顧客データを設計する - 実践的なNoSQLスキーマ設計

Firestoreで10万件超のECサイト顧客データを設計する

本稿では、Google Cloud Firestoreを使用したECサイトにおける大規模顧客データ(10万件以上)の実践的な設計方法について解説する。

Firestoreの概要

Firestoreの特徴

Firestoreは、Google Cloudが提供するNoSQLドキュメント指向データベースである。

主な利点:

  • 自動スケーリング: データ量が増えても自動的にスケール
  • リアルタイム同期: データ変更をリアルタイムで反映
  • 構造化されたクエリ: 単純なクエリに最適化
  • オフライン対応: モバイルアプリとの相性が良い

注意すべき制限:

  • JOIN非対応: 複数のコレクションを結合できない
  • 柔軟な検索の制限: 全文検索や複雑なOR条件に制限
  • 集計関数の非サポート: SUM、COUNT、AVGなどの集計が不可

ECサイトでの利用シーン:

  • 顧客管理(会員情報、購入履歴)
  • 商品カタログ
  • 注文処理
  • 在庫管理

RDBとの比較

項目 RDB(MySQL/PostgreSQL) Firestore(NoSQL)
データ構造 テーブル・行・列 コレクション・ドキュメント
スキーマ 固定(厳密) 柔軟(スキーマレス)
JOIN サポート 非サポート(非正規化で対応)
スケーリング 垂直スケール 水平スケール
トランザクション ACID保証 限定的なトランザクション
全文検索 LIKE、正規表現 非サポート(外部サービス必要)
OR条件 自由に使用可能 制限あり(in演算子で一部対応)
複数フィールドの不等号 可能 1フィールドのみ
集計関数 SUM、COUNT、AVG等 非サポート(クライアント側で計算)

Firestoreのクエリ制限と回避策

主要な制限事項

Firestoreは高速な読み書きに最適化されているが、RDBと比較して以下の制限がある。

1. JOIN非対応

制限:

// ❌ RDBのようなJOINは不可能
SELECT customers.*, orders.*
FROM customers
INNER JOIN orders ON customers.id = orders.customer_id

回避策: 非正規化(データの重複を許容)

// ✅ 注文データに顧客情報を埋め込む
{
  "orderId": "order001",
  "customerId": "user123",
  "customerName": "山田太郎",      // 重複データ
  "customerEmail": "yamada@example.com",  // 重複データ
  "totalAmount": 5000
}
2. 全文検索非対応

制限:

// ❌ 部分一致検索は不可能
db.collection('customers')
  .where('name', 'contains', '山田')  // このような検索はできない

回避策: 外部検索サービスの利用

// ✅ Algoliaを使用した全文検索
const algoliaIndex = algolia.initIndex('customers');

// Algoliaで検索
const { hits } = await algoliaIndex.search('山田');

// 検索結果のIDでFirestoreから詳細を取得
const customerIds = hits.map(hit => hit.objectID);
const customers = await Promise.all(
  customerIds.map(id => db.collection('customers').doc(id).get())
);
3. 複雑なOR条件の制限

制限:

// ❌ 複数フィールドのOR条件は不可能
db.collection('customers')
  .where('membershipTier', '==', 'gold')
  .or()  // ORメソッドは存在しない
  .where('totalSpent', '>', 100000)

回避策: in演算子または複数クエリの統合

// ✅ in演算子(最大10個まで)
db.collection('customers')
  .where('membershipTier', 'in', ['gold', 'platinum'])

// ✅ 複数クエリを個別に実行して統合
const goldMembers = await db.collection('customers')
  .where('membershipTier', '==', 'gold')
  .get();

const highSpenders = await db.collection('customers')
  .where('totalSpent', '>', 100000)
  .get();

// クライアント側で統合
const allCustomers = [...goldMembers.docs, ...highSpenders.docs];
4. 複数フィールドへの不等号演算子

制限:

// ❌ 複数フィールドで不等号演算子は使用不可
db.collection('orders')
  .where('totalAmount', '>', 1000)
  .where('createdAt', '>', new Date('2026-01-01'))  // エラー

回避策: 1つのフィールドのみ不等号を使用

// ✅ 1つのフィールドのみ不等号、他は等号
db.collection('orders')
  .where('status', '==', 'completed')
  .where('totalAmount', '>', 1000)

// または、クライアント側でフィルタリング
const snapshot = await db.collection('orders')
  .where('totalAmount', '>', 1000)
  .get();

const filtered = snapshot.docs.filter(doc => {
  const data = doc.data();
  return data.createdAt.toDate() > new Date('2026-01-01');
});
5. 集計関数非対応

制限:

// ❌ データベース側での集計は不可能
SELECT SUM(totalAmount) FROM orders WHERE customerId = 'user123'

回避策: 非正規化(集計値を事前計算)

// ✅ 顧客ドキュメントに集計値を保存
{
  "customerId": "user123",
  "name": "山田太郎",
  "totalOrders": 12,      // 集計値
  "totalSpent": 58000,    // 集計値
  "lastOrderDate": "2026-02-05T09:20:00Z"
}

// 注文作成時に顧客の集計値を更新
async function createOrderAndUpdateStats(customerId, orderAmount) {
  const customerRef = db.collection('customers').doc(customerId);

  await db.runTransaction(async (transaction) => {
    const doc = await transaction.get(customerRef);
    const currentTotal = doc.data().totalSpent;
    const currentOrders = doc.data().totalOrders;

    transaction.update(customerRef, {
      totalSpent: currentTotal + orderAmount,
      totalOrders: currentOrders + 1,
      lastOrderDate: admin.firestore.FieldValue.serverTimestamp()
    });
  });
}

全文検索の実装(Algolia連携)

ECサイトでは商品名や顧客名の部分一致検索が必要となる。Firestoreでは全文検索が不可能なため、Algoliaなどの外部サービスとの連携が必須となる。

Algoliaのセットアップ
npm install algoliasearch
Firestoreとの同期
const algoliasearch = require('algoliasearch');
const admin = require('firebase-admin');

const algolia = algoliasearch('YOUR_APP_ID', 'YOUR_API_KEY');
const index = algolia.initIndex('customers');

// Cloud Functionsでリアルタイム同期
exports.syncCustomerToAlgolia = functions.firestore
  .document('customers/{customerId}')
  .onWrite(async (change, context) => {
    const customerId = context.params.customerId;

    // ドキュメント削除時
    if (!change.after.exists) {
      await index.deleteObject(customerId);
      return;
    }

    // ドキュメント作成・更新時
    const customer = change.after.data();
    const algoliaObject = {
      objectID: customerId,
      name: customer.name,
      email: customer.email,
      phoneNumber: customer.phoneNumber,
      membershipTier: customer.membershipTier,
      totalSpent: customer.totalSpent,
      createdAt: customer.createdAt.toDate().getTime()
    };

    await index.saveObject(algoliaObject);
  });
Algoliaでの検索
const algolia = algoliasearch('YOUR_APP_ID', 'YOUR_SEARCH_KEY');
const index = algolia.initIndex('customers');

async function searchCustomers(query) {
  const { hits } = await index.search(query, {
    attributesToRetrieve: ['objectID', 'name', 'email'],
    hitsPerPage: 20,
    filters: 'membershipTier:gold'  // フィルタも可能
  });

  // Firestoreから詳細データを取得(必要に応じて)
  const customers = await Promise.all(
    hits.map(hit =>
      db.collection('customers').doc(hit.objectID).get()
    )
  );

  return customers.map(doc => doc.data());
}

// 使用例
const results = await searchCustomers('山田');

Firestoreの基本概念

ドキュメント指向データベース

Firestoreはドキュメント指向のデータベースである。

データベース
  └── コレクション(Collection)
        └── ドキュメント(Document)
              ├── フィールド(Field)
              └── サブコレクション(Subcollection)

コレクションとドキュメント

// コレクション: customers
// ドキュメント: user123
{
  "id": "user123",
  "name": "山田太郎",
  "email": "yamada@example.com",
  "createdAt": "2026-01-01T00:00:00Z"
}

要点:

  • コレクション: ドキュメントの集合(RDBのテーブルに相当)
  • ドキュメント: JSON形式のデータ(RDBの行に相当)
  • フィールド: ドキュメント内のキー・バリュー(RDBの列に相当)

ECサイトの要件定義

対象データ

顧客データ:

  • 会員情報: 10万件以上
  • 注文履歴: 1人あたり平均10件
  • 配送先住所: 1人あたり1〜3件

必要なクエリパターン:

クエリ Firestoreでの実現性 実装方法
顧客IDで検索 ✅ 可能 ドキュメントID指定
メールアドレスで完全一致検索 ✅ 可能 where('email', '==', '...')
顧客名の部分一致検索 ❌ 不可能 Algolia等の外部サービス
特定期間の顧客取得 ✅ 可能 where('createdAt', '>=', '...')
顧客の注文履歴取得 ✅ 可能 サブコレクション
注文の合計金額でソート ✅ 可能 orderBy('totalAmount', 'desc')
ゴールド会員かつ高額購入者 ⚠️ 制限あり 複数クエリを統合

パフォーマンス要件

  • 読み取り速度: 単一ドキュメント取得 < 100ms
  • 書き込み速度: 新規登録 < 500ms
  • スケーラビリティ: 100万件まで対応可能
  • 可用性: 99.9%以上

スキーマ設計の原則

1. 非正規化(Denormalization)

RDBでは正規化が基本であるが、Firestoreでは非正規化が推奨される。

理由:

  • JOINが使えない
  • 読み取り速度を最適化
  • データの重複を許容

: 注文データに顧客名を含める

// ❌ RDBスタイル(正規化)
// orders テーブル
{
  "orderId": "order001",
  "customerId": "user123",  // 外部キー
  "totalAmount": 5000
}
// → 顧客名を取得するにはJOINが必要

// ✅ Firestoreスタイル(非正規化)
{
  "orderId": "order001",
  "customerId": "user123",
  "customerName": "山田太郎",  // 重複を許容
  "customerEmail": "yamada@example.com",
  "totalAmount": 5000
}
// → 1回のクエリで完結

2. コレクション分割の戦略

ルートコレクション vs サブコレクション

ルートコレクション:
  /customers/{customerId}
  /orders/{orderId}

サブコレクション:
  /customers/{customerId}/orders/{orderId}

判断基準:

パターン 用途
ルートコレクション グローバルな検索が必要 全注文を検索
サブコレクション 特定の親に紐づくデータ 特定顧客の注文

3. インデックス設計

Firestoreは自動インデックス複合インデックスをサポートします。

// 自動インデックス(単一フィールド)
db.collection('customers').where('email', '==', 'yamada@example.com')

// 複合インデックス(複数フィールド)
db.collection('orders')
  .where('customerId', '==', 'user123')
  .where('status', '==', 'completed')
  .orderBy('createdAt', 'desc')

複合インデックスは手動での作成が必要となる(後述)。


実践: 顧客データの設計

コレクション構造

/customers/{customerId}
  - name: string
  - email: string
  - phoneNumber: string
  - createdAt: timestamp
  - totalOrders: number (非正規化)
  - totalSpent: number (非正規化)

  /orders/{orderId} (サブコレクション)
    - orderId: string
    - items: array
    - totalAmount: number
    - status: string
    - createdAt: timestamp

  /addresses/{addressId} (サブコレクション)
    - name: string
    - postalCode: string
    - prefecture: string
    - city: string
    - street: string
    - isDefault: boolean

ドキュメント設計

顧客ドキュメント:

{
  "customerId": "user123",
  "name": "山田太郎",
  "email": "yamada@example.com",
  "phoneNumber": "090-1234-5678",
  "createdAt": "2026-01-15T10:30:00Z",
  "updatedAt": "2026-02-10T15:45:00Z",

  // 非正規化データ(集計値)
  "totalOrders": 12,
  "totalSpent": 58000,
  "lastOrderDate": "2026-02-05T09:20:00Z",

  // メタデータ
  "isActive": true,
  "membershipTier": "gold"  // bronze, silver, gold
}

注文ドキュメント:

{
  "orderId": "order001",
  "customerId": "user123",

  // 顧客情報の非正規化
  "customerName": "山田太郎",
  "customerEmail": "yamada@example.com",

  // 注文内容
  "items": [
    {
      "productId": "prod001",
      "productName": "ワイヤレスイヤホン",
      "price": 5000,
      "quantity": 2
    }
  ],
  "subtotal": 10000,
  "tax": 1000,
  "shippingFee": 500,
  "totalAmount": 11500,

  // ステータス
  "status": "completed",  // pending, processing, shipped, completed, cancelled
  "paymentStatus": "paid",

  // タイムスタンプ
  "createdAt": "2026-02-05T09:20:00Z",
  "shippedAt": "2026-02-06T14:00:00Z",
  "completedAt": "2026-02-08T10:00:00Z"
}

配送先住所ドキュメント:

{
  "addressId": "addr001",
  "name": "山田太郎",
  "postalCode": "100-0001",
  "prefecture": "東京都",
  "city": "千代田区",
  "street": "千代田1-1-1",
  "building": "千代田ビル101",
  "phoneNumber": "090-1234-5678",
  "isDefault": true,
  "createdAt": "2026-01-15T10:30:00Z"
}

サンプルコード(再現可能)

セットアップ

# Firebase Admin SDKをインストール
npm install firebase-admin

# サービスアカウントキーを取得
# Firebase Console > プロジェクト設定 > サービスアカウント > 秘密鍵を生成

初期化コード

const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

const db = admin.firestore();

module.exports = db;

顧客データの作成

const db = require('./firebase-setup');

async function createCustomer(customerData) {
  const customersRef = db.collection('customers');
  const customerRef = customersRef.doc(customerData.customerId);

  await customerRef.set({
    customerId: customerData.customerId,
    name: customerData.name,
    email: customerData.email,
    phoneNumber: customerData.phoneNumber,
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
    updatedAt: admin.firestore.FieldValue.serverTimestamp(),
    totalOrders: 0,
    totalSpent: 0,
    isActive: true,
    membershipTier: 'bronze'
  });

  console.log(`顧客 ${customerData.customerId} を作成`);
  return customerRef;
}

// 使用例
createCustomer({
  customerId: 'user123',
  name: '山田太郎',
  email: 'yamada@example.com',
  phoneNumber: '090-1234-5678'
});

大量データの投入

const db = require('./firebase-setup');

async function seedCustomers(count) {
  const batch = db.batch();
  const customersRef = db.collection('customers');

  for (let i = 0; i < count; i++) {
    const customerId = `user${String(i).padStart(6, '0')}`;
    const customerRef = customersRef.doc(customerId);

    batch.set(customerRef, {
      customerId: customerId,
      name: `顧客 ${i}`,
      email: `customer${i}@example.com`,
      phoneNumber: `090-${String(i).padStart(8, '0')}`,
      createdAt: admin.firestore.Timestamp.now(),
      updatedAt: admin.firestore.Timestamp.now(),
      totalOrders: Math.floor(Math.random() * 20),
      totalSpent: Math.floor(Math.random() * 100000),
      isActive: Math.random() > 0.1,
      membershipTier: ['bronze', 'silver', 'gold'][Math.floor(Math.random() * 3)]
    });

    // Firestoreのバッチは500件まで
    if ((i + 1) % 500 === 0) {
      await batch.commit();
      console.log(`${i + 1}件のデータを投入`);
      batch = db.batch();
    }
  }

  // 残りをコミット
  await batch.commit();
  console.log(`合計 ${count}件のデータを投入`);
}

// 10万件のデータを投入
seedCustomers(100000);

クエリ例

const db = require('./firebase-setup');

// 1. 顧客IDで検索
async function getCustomerById(customerId) {
  const customerRef = db.collection('customers').doc(customerId);
  const doc = await customerRef.get();

  if (!doc.exists) {
    console.log('顧客が見つかりません');
    return null;
  }

  return doc.data();
}

// 2. メールアドレスで検索
async function getCustomerByEmail(email) {
  const snapshot = await db.collection('customers')
    .where('email', '==', email)
    .limit(1)
    .get();

  if (snapshot.empty) {
    console.log('顧客が見つかりません');
    return null;
  }

  return snapshot.docs[0].data();
}

// 3. アクティブなゴールド会員を取得
async function getActiveGoldMembers() {
  const snapshot = await db.collection('customers')
    .where('isActive', '==', true)
    .where('membershipTier', '==', 'gold')
    .orderBy('totalSpent', 'desc')
    .limit(100)
    .get();

  return snapshot.docs.map(doc => doc.data());
}

// 4. 特定期間に登録された顧客を取得
async function getCustomersByDateRange(startDate, endDate) {
  const snapshot = await db.collection('customers')
    .where('createdAt', '>=', startDate)
    .where('createdAt', '<=', endDate)
    .orderBy('createdAt', 'desc')
    .get();

  return snapshot.docs.map(doc => doc.data());
}

// 5. 顧客の注文履歴を取得
async function getCustomerOrders(customerId) {
  const snapshot = await db.collection('customers')
    .doc(customerId)
    .collection('orders')
    .orderBy('createdAt', 'desc')
    .get();

  return snapshot.docs.map(doc => doc.data());
}

// 6. 顧客名の部分一致検索(Firestore単体では不可能)
// ❌ このようなクエリは実行できない
async function searchCustomersByNameNaive(query) {
  // Firestoreには LIKE や CONTAINS がない
  const snapshot = await db.collection('customers')
    .where('name', 'contains', query)  // エラー: 存在しない演算子
    .get();
}

// ✅ 回避策1: 前方一致のみ可能(制限的)
async function searchCustomersByPrefix(prefix) {
  const snapshot = await db.collection('customers')
    .where('name', '>=', prefix)
    .where('name', '<=', prefix + '\uf8ff')  // Unicodeの最大文字
    .get();

  return snapshot.docs.map(doc => doc.data());
}

// ✅ 回避策2: Algoliaで全文検索(推奨)
async function searchCustomersWithAlgolia(query) {
  const algolia = algoliasearch('YOUR_APP_ID', 'YOUR_SEARCH_KEY');
  const index = algolia.initIndex('customers');

  const { hits } = await index.search(query);
  return hits;
}

// 7. 複雑なOR条件(制限あり)
// ❌ 複数フィールドのOR条件は不可能
async function getVIPCustomersNaive() {
  // このようなクエリは実行できない
  const snapshot = await db.collection('customers')
    .where('membershipTier', '==', 'gold')
    .or()  // ORメソッドは存在しない
    .where('totalSpent', '>', 100000)
    .get();
}

// ✅ 回避策: 複数クエリを統合
async function getVIPCustomers() {
  const goldMembers = await db.collection('customers')
    .where('membershipTier', '==', 'gold')
    .get();

  const highSpenders = await db.collection('customers')
    .where('totalSpent', '>', 100000)
    .get();

  // クライアント側で重複を削除しつつ統合
  const customerMap = new Map();

  goldMembers.docs.forEach(doc => {
    customerMap.set(doc.id, doc.data());
  });

  highSpenders.docs.forEach(doc => {
    customerMap.set(doc.id, doc.data());
  });

  return Array.from(customerMap.values());
}

ページネーション(カーソルベース)

const db = require('./firebase-setup');

async function getCustomersWithPagination(pageSize = 50, lastDoc = null) {
  let query = db.collection('customers')
    .orderBy('createdAt', 'desc')
    .limit(pageSize);

  if (lastDoc) {
    query = query.startAfter(lastDoc);
  }

  const snapshot = await query.get();

  return {
    customers: snapshot.docs.map(doc => doc.data()),
    lastDoc: snapshot.docs[snapshot.docs.length - 1],
    hasMore: snapshot.docs.length === pageSize
  };
}

// 使用例
async function paginateAllCustomers() {
  let lastDoc = null;
  let page = 1;

  do {
    const result = await getCustomersWithPagination(50, lastDoc);
    console.log(`ページ ${page}: ${result.customers.length}件`);

    lastDoc = result.lastDoc;
    page++;

    if (!result.hasMore) break;
  } while (true);
}

パフォーマンス最適化

1. 複合インデックスの作成

複合クエリには複合インデックスが必要となる。

// このクエリは複合インデックスが必要
db.collection('customers')
  .where('isActive', '==', true)
  .where('membershipTier', '==', 'gold')
  .orderBy('totalSpent', 'desc')

インデックスの作成方法:

  1. Firebase Consoleで作成:

    • Firestore > インデックス > 複合インデックスを追加
  2. firestore.indexes.json で定義:

{
  "indexes": [
    {
      "collectionGroup": "customers",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "isActive", "order": "ASCENDING" },
        { "fieldPath": "membershipTier", "order": "ASCENDING" },
        { "fieldPath": "totalSpent", "order": "DESCENDING" }
      ]
    },
    {
      "collectionGroup": "orders",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "customerId", "order": "ASCENDING" },
        { "fieldPath": "status", "order": "ASCENDING" },
        { "fieldPath": "createdAt", "order": "DESCENDING" }
      ]
    }
  ]
}

デプロイ:

firebase deploy --only firestore:indexes

2. キャッシング戦略

const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // 5分間キャッシュ

async function getCustomerWithCache(customerId) {
  // キャッシュを確認
  const cached = cache.get(customerId);
  if (cached) {
    console.log('Cache hit');
    return cached;
  }

  // Firestoreから取得
  const doc = await db.collection('customers').doc(customerId).get();
  const data = doc.data();

  // キャッシュに保存
  cache.set(customerId, data);

  return data;
}

3. バッチ処理

// 複数の顧客を一括取得
async function getCustomersByIds(customerIds) {
  const promises = customerIds.map(id =>
    db.collection('customers').doc(id).get()
  );

  const docs = await Promise.all(promises);
  return docs.map(doc => doc.data());
}

// トランザクションで一括更新
async function updateCustomerStats(customerId, orderAmount) {
  const customerRef = db.collection('customers').doc(customerId);

  await db.runTransaction(async (transaction) => {
    const doc = await transaction.get(customerRef);

    if (!doc.exists) {
      throw new Error('顧客が存在しません');
    }

    const newTotalOrders = doc.data().totalOrders + 1;
    const newTotalSpent = doc.data().totalSpent + orderAmount;

    transaction.update(customerRef, {
      totalOrders: newTotalOrders,
      totalSpent: newTotalSpent,
      lastOrderDate: admin.firestore.FieldValue.serverTimestamp(),
      updatedAt: admin.firestore.FieldValue.serverTimestamp()
    });
  });
}

セキュリティルール

Firestoreのセキュリティルールで、アクセス制御とバリデーションを実装します。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // 認証済みユーザーのみアクセス可能
    match /customers/{customerId} {
      // 読み取り: 本人または管理者のみ
      allow read: if request.auth != null &&
                     (request.auth.uid == customerId ||
                      request.auth.token.admin == true);

      // 作成: 管理者のみ
      allow create: if request.auth != null &&
                       request.auth.token.admin == true;

      // 更新: 本人または管理者のみ
      allow update: if request.auth != null &&
                       (request.auth.uid == customerId ||
                        request.auth.token.admin == true);

      // 削除: 管理者のみ
      allow delete: if request.auth != null &&
                       request.auth.token.admin == true;

      // サブコレクション: 注文
      match /orders/{orderId} {
        allow read: if request.auth != null &&
                       (request.auth.uid == customerId ||
                        request.auth.token.admin == true);
        allow write: if request.auth != null &&
                        request.auth.token.admin == true;
      }

      // サブコレクション: 配送先住所
      match /addresses/{addressId} {
        allow read, write: if request.auth != null &&
                              request.auth.uid == customerId;
      }
    }

    // バリデーション
    function isValidCustomer() {
      return request.resource.data.keys().hasAll([
        'customerId', 'name', 'email', 'createdAt'
      ]) &&
      request.resource.data.email.matches('.*@.*\\..*') &&
      request.resource.data.name.size() > 0;
    }
  }
}

まとめ

Firestoreでの大規模データ設計のポイント

  1. 非正規化を活用: JOIN非対応のため、データの重複を許容
  2. 適切なコレクション分割: ルート vs サブコレクション
  3. 複合インデックスの設計: 複雑なクエリに対応
  4. ページネーション: カーソルベースで効率的に取得
  5. キャッシング: アプリケーション層でキャッシュ
  6. セキュリティルール: アクセス制御とバリデーション
  7. 全文検索にはAlgolia: 部分一致検索が必要な場合
  8. 集計値の事前計算: SUM/COUNTなどは非正規化で対応
  9. 複雑なOR条件は統合: 複数クエリをクライアント側で結合

スケーリングの考慮事項

  • ドキュメントサイズ: 1MBまで(通常は数KBに抑える)
  • コレクションサイズ: 無制限(10億件以上も可能)
  • 書き込み速度: 1ドキュメントあたり1秒に1回
  • 読み取り速度: 制限なし(ただしコスト考慮)

次のステップ

  • Algolia連携: 全文検索機能の実装
  • Cloud Functions: バッチ処理と定期実行
  • BigQuery エクスポート: 複雑な集計やデータ分析
  • Elasticsearch: Algoliaの代替として自前で全文検索
  • Firebase Extensions: バックアップや削除機能の自動化

Firestoreが適さないケース

以下の要件がある場合、PostgreSQLなどのRDBを検討すべきである:

  • 複雑なJOINが頻繁に必要
  • リアルタイムな集計(SUM、AVG等)が必須
  • 複雑なトランザクション処理(複数のコレクションを跨ぐACID保証)
  • 高度な全文検索を自前で実装(Algoliaなどの外部サービスを使いたくない)

Firestoreを使用することで、10万件を超える大規模データの効率的な管理が可能である。本稿で示した設計パターンを参考に、スケーラブルなECサイトの構築を行われたい。