ノーコードバックエンド向け:マルチテナントSaaSのデータモデル選択
マルチテナントSaaSのデータモデルの選択はセキュリティ、レポート、速度に影響します。tenant_id、スキーマ分離、テナントごとのデータベースを比較し、それぞれのトレードオフを明確に示します。

問題:テナントを分離しつつパフォーマンスを落とさない\n\nマルチテナンシーとは、1つのソフトウェアが多数の顧客(テナント)を提供し、各テナントが自分のデータのみを見る必要があることを指します。難しいのはそれを一貫して実現することです:単一画面だけでなく、すべてのAPI呼び出し、管理画面、エクスポート、バックグラウンドジョブに渡ってです。\n\nデータモデルは多くのチームが想像するより日々の運用に影響します。権限、レポーティング、成長に伴うクエリ速度、そして“小さな”バグがどれだけ危険になるかを左右します。フィルタを1つ見落とすとデータが漏れる。分離をやり過ぎるとレポーティングが面倒になります。\n\nマルチテナントSaaSデータモデルを構成する一般的な方法は3つあります:\n\n- すべてのテーブルに tenant_id を含めた1つのデータベース\n- 1つのデータベース内でテナントごとにスキーマを分ける\n- テナントごとに別データベースを用意する\n\nノーコードバックエンドで視覚的に構築しても、同じトレードオフが適用されます。AppMasterのようなツールは設計から実際のバックエンドコードやデータベース構造を生成するため、初期のモデリング判断が本番の挙動や性能にすぐに現れます。\n\nヘルプデスクツールを想像してください。チケット行に tenant_id があると「すべての未解決チケット」を簡単に問合せできますが、すべての箇所でテナントチェックを強制する必要があります。各テナントが独自スキーマやDBを持つと、分離はより強固になりますが、クロステナント集計(例:「全顧客の平均クローズ時間」)は手間が増えます。\n\n目標は、レポーティングやサポート、成長に摩擦を生まない信頼できる分離を作ることです。\n\n## 選び方の近道:絞り込むための4つの質問\n\nデータベース理論から始めないでください。まずは製品の使われ方と週次で何を運用する必要があるかを考えましょう。\n\n### 答えを明らかにする4つの質問\n\n1) データの機微性はどれくらいか、厳格な規則下にあるか? 医療、金融、厳しい顧客契約は、より強い分離(スキーマ分離や別DB)を選ばせることが多いです。リスクを減らし、監査を簡単にします。\n\n2) クロステナントのレポーティングを頻繁に行うか? 全顧客の指標(利用、収益、パフォーマンス)を定期的に必要とするなら、tenant_id を使う単一データベースが最も簡単なことが多いです。別DBは多くの場所から結果を集める必要があり難しくなります。\n\n3) テナント間の違いはどれくらいか? テナントがカスタムフィールドやワークフロー、固有の連携を必要とするなら、スキーマ分離や別DBで変更が他に波及する可能性を下げられます。大半が同じ構造なら tenant_id が適しています。\n\n4) チームが現実的にどこまで運用できるか? 分離を強めるほど作業は増えます:バックアップ、マイグレーション、構成要素、障害箇所が増えます。\n\n実務的には上位2案をプロトタイプにして、権限ルール、レポートクエリ、モデルの変化に伴う展開をテストするのが良いアプローチです。\n\n## アプローチ1:すべての行に tenant_id を持つ1つのデータベース\n\n最も一般的な設定です:すべての顧客が同じテーブルを共有し、テナント所有のレコードには tenant_id が付与されます。運用は単純で、1つのDBと1セットのマイグレーションで済みます。\n\nルールは厳格です:行がテナントに属するなら、必ず tenant_id を含め、すべてのクエリでフィルタする必要があります。テナント所有テーブルには通常、users、roles、projects、tickets、invoices、messages、ファイルメタデータ、テナントデータを繋ぐ結合テーブルなどが含まれます。\n\nデータ漏洩を減らすために tenant_id を必須扱いにしましょう:\n\n- テナント所有テーブルで tenant_id を NOT NULL にする\n- tenant_id から始まるインデックスを追加する(例:tenant_id, created_at)\n- 一意制約に tenant_id を含める(例:テナントごとのメール一意性)\n- tenant_id をAPIやビジネスフロー全体で受け渡す(UIだけに頼らない)\n- クライアント側フィルターだけでなくバックエンドで強制する\n\nPostgreSQLでは、行レベルセキュリティポリシーが強力な安全網になります。\n\n参照データは通常、共有テーブル(国リストなどで tenant_id なし)か、テナント範囲のカタログ(カスタムタグやパイプラインで tenant_id あり)のどちらかに分類されます。\n\nAppMasterで構築する場合、事故の多くを防ぐ簡単な習慣があります:認証済みユーザーのテナントから tenant_id を Business Process ロジックの読み書き前に設定し、そのパターンを一貫して適用することです。\n\n## 権限の影響:アプローチごとに何が変わるか\n\n権限はマルチテナンシーの成否を決めます。選んだデータ配置はユーザーの保存方法、クエリのスコーピング、管理画面でのミス防止方法を変えます。\n\n単一DBで tenant_id を使う場合、チームは共通の Users テーブルを使い、各ユーザーをテナントと一つ以上のロールに関連づけることが多いです。重要なルールは変わりません:読み書きは小さなテーブル(settings、tags、logsなど)でさえテナントスコープを含めること。\n\nスキーマ分離では、共通のID層(ログイン、パスワード、MFA)は共有のままにして、テナントデータをテナントごとのスキーマに置くことが多いです。権限はルーティングの問題の一部になり、アプリはビジネスロジック実行前に正しいスキーマを指す必要があります。\n\n別DBでは分離が最も強くなりますが、権限ロジックはインフラ寄りになります:正しいDB接続を選ぶ、資格情報を管理する、グローバルスタッフアカウントを扱うなどです。\n\nどのアプローチでも、テナントのリスクを減らす共通パターンがあります:\n\n- tenant_id をセッションや認証トークンのクレームに入れて必須扱いにする\n- テナントチェックをミドルウェアや単一の Business Process に集約して散らばらせない\n- 管理ツールではテナントコンテキストを明示し、明確なテナント切替を要求する\n- サポートアクセスはインパーソネーションと監査ログで行う\n\nAppMasterでは通常、認証後にテナントコンテキストを保存し、APIエンドポイントとBusiness Processで再利用してクエリが常にスコープされるようにします。サポート担当者はUIのフィルターだけでなく、アプリがテナントコンテキストを設定した後に注文を見られるべきです。\n\n## tenant_idモデルでのレポーティングとパフォーマンス\n\ntenant_id を使う単一データベースでは、レポーティングは概して簡単です。グローバルダッシュボード(MRR、サインアップ、利用)も1つのクエリで済み、テナントレベルのレポートはそのクエリにフィルタを追加するだけです。\n\nトレードオフは時間とともに現れるパフォーマンスです。テーブルが大きくなると、1つのアクティブなテナントが多量の行を作りノイジー・ネイバーになり得ます。書き込みが増え、共通クエリが遅くなる可能性があります。\n\nインデックスがこのモデルを健康に保ちます。ほとんどのテナントスコープの読み取りが tenant_id から始まるインデックスを使えるようにして、データベースがそのテナントの範囲に直接ジャンプできるようにします。\n\n良いベースライン:\n\n- 複合インデックスでは tenant_id を最初の列にする(例:tenant_id + created_at、tenant_id + status、tenant_id + user_id)\n- 本当にグローバルなインデックスはクロステナントクエリが必要な場合のみにする\n- ジョインやフィルタで tenant_id を忘れていないか監視する(忘れると全表スキャンの原因に)\n\n保持(retention)や削除の方針も必要です。あるテナントの履歴がテーブルを膨らませると全員に影響します。テナントごとに保持方針が異なる場合はソフトデリートと定期アーカイブ、あるいは tenant_id でキー付けされたアーカイブテーブルへの移動を検討してください。\n\n## アプローチ2:テナントごとにスキーマを分ける\n\nスキーマ分離では1つのPostgreSQLデータベースを使い、各テナントにスキーマ(例:tenant_42)を割り当てます。そのスキーマ内のテーブルはそのテナント専用です。多くの顧客に「ミニデータベース」を与えるような感覚で、複数データベースを運用するオーバーヘッドを避けられます。\n\n一般的な構成では、グローバルなサービスを共有スキーマに置き、ビジネスデータはテナントスキーマに入れます。共有すべきか混ぜてはならないかの判断が分割の基準です。\n\n典型的な分割:\n\n- 共有スキーマ:tenantsテーブル、プラン、請求レコード、機能フラグ、監査設定\n- テナントスキーマ:orders、tickets、在庫、プロジェクト、カスタムフィールドなどのビジネステーブル\n- ユーザーとロールはプロダクト次第でどちら側に置くか決める(複数テナントへのアクセスがある場合は共有にすることが多い)\n\nこのモデルはクロステナント結合のリスクを減らし、1つのスキーマをバックアップやリストアのターゲットにできるため単一テナントの復旧が容易です。\n\nチームが驚くのはマイグレーションです。新しいテーブルやカラムを追加するとき、すべてのテナントスキーマに変更を適用する必要があります。テナントが10なら対処しやすいですが、1,000になればスキーマバージョン管理、バッチでのマイグレーション、1つの失敗が他を止めないフェイルセーフが必要になります。\n\n認証や請求といった共有サービスは通常テナントスキーマ外に置きます。実践的なパターンは共有認証(ユーザーテーブル+テナント参加テーブル)や共有請求(Stripeの顧客ID、請求書)を保ち、テナントスキーマはテナント所有のビジネスデータを保持することです。\n\nAppMasterを使う場合は、Data Designerのモデルが共有スキーマとテナントスキーマにどうマップされるかを早めに計画し、ログインや支払いを壊さずにテナントスキーマを進化させられるようにします。\n\n## スキーマ分離でのレポーティングとパフォーマンス\n\nスキーマ分離はデフォルトで tenant_id ベースより強い分離を提供します。テーブルが別の名前空間にあるため、権限をスキーマ単位で設定できます。\n\nテナントごとのレポートが中心なら扱いやすいです。クエリはそのテナントのテーブルだけを読めばよく、常に共有テーブルをフィルタし続ける必要がありません。このモデルは「特別」なテナントに追加テーブルやカスタムカラムを与えやすく、他のテナントに負担を強いません。\n\nただし、全テナントを横断する集計レポートは面倒です。多くのスキーマをクエリするレポーティング層が必要か、共通スキーマにサマリを保持する運用が実務的です。\n\nよくあるパターン:\n\n- テナント別ダッシュボードはそのテナントのスキーマだけをクエリする\n- 各テナントから夜間にロールアップする中央分析用スキーマを持つ\n- テナントデータをデータウェアハウス向けにエクスポートするバッチを走らせる\n\nパフォーマンスは通常、テナント単位のワークロードで良好です。インデックスは各テナントで小さく、あるスキーマでの大量書き込みが他を遅くする可能性は低くなります。トレードオフは運用オーバーヘッドで、新しいテナントのプロビジョニングはスキーマ作成、マイグレーション実行、スキーマ整合性の維持が必要になります。\n\nテナントごとにカスタマイズや厳格な分離を望む場合、スキーマは多くのチームにとって良い中間点です。\n\n## アプローチ3:テナントごとに別データベース\n\nテナントごとに別データベースを用意すると、各顧客に専用のデータベース(同一サーバ上でも可)を割り当てます。最も分離が強く、あるテナントのデータ破損や設定ミス、過負荷が他へ波及する可能性は低くなります。\n\n規制が厳しい環境(医療、金融、政府)やエンタープライズ顧客で厳格な分離、専用の保持ルール、専用パフォーマンスが要求される場合に適しています。\n\nオンボーディングはプロビジョニングワークフローになります。新しいテナントがサインアップしたらデータベースを作成またはクローンし、基本スキーマ(テーブル、インデックス、制約)を適用し、資格情報を安全に保存し、APIリクエストを正しいデータベースへルーティングする必要があります。\n\nAppMasterで構築する場合、重要な設計はテナントディレクトリ(テナント->DB接続のマップ)をどこに置くか、そしてすべてのリクエストが正しい接続を使うことをどう保証するかです。\n\nアップグレードとマイグレーションが主なトレードオフです。スキーマ変更は「1回実行」ではなく「すべてのテナントに対して実行」になるため運用作業とリスクが増えます。多くのチームはスキーマをバージョン管理し、テナントごとの進捗を追う制御されたジョブとしてマイグレーションを実行します。\n\n利点は制御性です。大きなテナントから先に移行して挙動を監視し、段階的に展開できます。\n\n## 別データベースでのレポーティングとパフォーマンス\n\n別DBは理解しやすい利点があります。誤ったクロステナント読み取りは起きにくく、権限ミスがあっても影響はそのテナントに限定されることが多いです。\n\nパフォーマンス面でも強みがあります。重いクエリや大規模インポート、暴走するレポートがテナントAを遅くしてもテナントBには影響しません。ノイジーネイバー対策として有効で、テナントごとにリソース調整ができます。\n\nトレードオフはレポーティングと運用コストです。データが物理的に分かれているため、グローバル分析は難しくなります。実践的なパターンは、重要イベントやテーブルを中央のレポートDBへコピーする、イベントをデータウェアハウスへ送る、テナントごとに集計して結果を合算する(テナント数が少ないとき)などです。\n\nまた接続プールの上限に早く達する可能性があるため、各テナントの接続数を考慮した設計が必要です。\n\n## データ漏洩や後悔を招く一般的なミス\n\n多くのマルチテナント問題は大きな設計ミスではなく、小さな見落としが成長とともにセキュリティバグや散らかったレポーティング、高コストの修復につながるケースです。マルチテナンシーは機能として後付けするのではなく、習慣として扱うべきです。\n\nよくある漏洩原因は、結合テーブル(user_roles、invoice_items、タグ類)でテナントフィールドを忘れることです。見た目は問題なくても、レポートや検索がそのテーブルを経由すると別テナントの行を引いてしまいます。\n\n管理ダッシュボードがテナントフィルタをバイパスするのも頻繁に起きます。「サポート用にだけ」と始めたものが再利用され拡大すると危険です。ノーコードツールでもリスクは変わりません:すべてのクエリ、ビジネスプロセス、エンドポイントで同じテナントスコープが必要です。\n\nIDも落とし穴になります。order_number = 1001 のような人間に優しいIDをテナント間で共有していると、サポートツールや統合がレコードを混同します。内部の主キーとテナントスコープされた識別子を分け、ルックアップにはテナントコンテキストを含めてください。\n\n最後に、チームは規模が大きくなったときのマイグレーションとバックアップを過小評価しがちです。10テナントでは簡単でも1,000テナントでは遅くて危険です。\n\nほとんどの痛みを防ぐクイックチェック:\n\n- すべてのテーブル(結合テーブルを含む)でテナント所有を明示する\n- テナントスコーピングパターンを1つに統一し再利用する\n- レポートやエクスポートがテナントスコープなしでは実行できないようにする(真にグローバルな場合を別扱いに)\n- APIやサポートツールでテナントが曖昧になる識別子を避ける\n- リストアとマイグレーション手順を成長前に練習する\n\n例:サポート担当者が「invoice 1001」を検索して誤ったテナントのレコードを引いてしまう。小さなバグが大きな影響を生む典型です。\n\n## 確定前にやるべきクイックチェックリスト\n\nマルチテナントSaaSデータモデルを確定する前に数個のテストを実行してください。目的はデータ漏洩を早期に見つけ、テーブルが大きくなったときにも選択が機能することを確認することです。\n\n### 1日でできる速いチェック\n\n- データ分離の証明: テナントAとBを作り類似レコードを追加して、すべての読み取りと更新がアクティブなテナントにスコープされることを検証します。UIフィルターだけに頼らないでください。\n- 権限破壊テスト: テナントAのユーザーでログインし、レコードIDだけを変えてテナントBのレコードを開いたり編集・削除できないか試す。成功する場合はリリースブロッカーです。\n- 書き込み経路の安全性: バックグラウンドジョブ、インポート、自動化経路でも新規レコードが正しいテナント値(または正しいスキーマ/DB)に入ることを確認する。\n- レポーティング試験: テナント専用レポートと全テナント(内部スタッフ向け)のレポートがそれぞれ実行できるか、誰がグローバルビューを見られるかのルールを明確にして確認する。\n- パフォーマンスチェック: 今のうちにインデックス戦略を入れて(特に (tenant_id, created_at) 等)、少なくとも1つの遅いクエリを意図的に実行して「悪い状態」がどんなものかを把握する。\n\nレポーティングテストを具体化するには、テナント単位とグローバルの1問ずつ、実際に必要になる質問を選びサンプルデータで実行してみてください。\n\nsql\n-- Tenant-only: last 30 days, one tenant\nSELECT count(*)\nFROM tickets\nWHERE tenant_id = :tenant_id\n AND created_at \u003e= now() - interval '30 days';\n\n-- Global (admin): compare tenants\nSELECT tenant_id, count(*)\nFROM tickets\nWHERE created_at \u003e= now() - interval '30 days'\nGROUP BY tenant_id;\n\n\nAppMasterでプロトタイプするなら、これらのチェックをBusiness Processフロー(読み・書き・削除)に組み込み、Data Designerで2つのテナントをシードしてください。現実的なデータ量でこれらのテストが通れば、自信をもって採用できます。\n\n## 例:最初の顧客からスケールまでの道筋\n\n20人の会社が顧客ポータル(請求書、チケット、簡易ダッシュボード)を立ち上げます。初月で10テナント、1年で1,000テナントに成長する見込みです。\n\n初期は通常、顧客データを保存するテーブルすべてに tenant_id を含めた単一データベースが最もシンプルです。構築が速く、レポートも容易で、セットアップの重複を避けられます。\n\n10テナントの段階で最大のリスクはパフォーマンスではなく権限です。1つのフィルタ漏れ(例:「請求一覧」クエリが tenant_id を忘れる)がデータ漏洩につながります。チームはテナントチェックを一貫した場所(共有ビジネスロジックや再利用可能なAPIパターン)で強制し、テナントスコーピングを非交渉に扱うべきです。\n\n10から1,000に移るに従いニーズは変わります。レポート負荷が増え、サポートから「このテナントの全エクスポート」が求められ、大手テナントがトラフィックを支配して共有テーブルを遅くすることがあります。\n\n現実的なアップグレードパス例:\n\n1) 同じアプリロジックと権限ルールを維持しつつ、高負荷テナントをスキーマ分離へ移す。\n2) 最大級のテナント(またはコンプライアンス要件のある顧客)は別DBへ移行する。\n3) すべてのテナントから読む共有のレポーティングレイヤーを維持し、重いレポートはオフピークで実行する。\n\n最初から「巨大テナント問題」に最適化するより、今日データを安全に分離できる最も単純なモデルを選び、後で「数件の巨大テナント」を移行する計画を立てるほうが現実的です。\n\n## 次のステップ:モデルを選んでノーコードバックエンドでプロトタイプする\n\nまず守るべきもの(データ分離、運用の単純さ、テナント単位のスケーリング)で選択してください。確信は小さなプロトタイプを作り、実際の権限とレポートケースで壊してみることで得られます。\n\n簡単な出発ガイド:\n\n- ほとんどのテナントが小さくクロステナントの集計が必要なら、すべての行に tenant_id を置いた単一データベースから始める。\n- より強い分離が欲しくても1つのDBで管理したいなら、テナントごとにスキーマを分けることを検討する。\n- テナントが厳格な分離(コンプライアンス、専用バックアップ、ノイジーネイバー対策)を要求するなら別DBを検討する。\n\n構築前にテナント境界を平易な言葉で書き出してください。役割(owner、admin、agent、viewer)を定義し、それぞれ何ができるか、"グローバル"データ(プラン、テンプレート、監査ログ)が何を意味するかを決めます。レポーティングがテナント専用か内部スタッフ向けの全テナントかを決めておきましょう。\n\nAppMasterを使うなら、Data Designerでテーブル(tenant_id、一意制約、クエリが依存するインデックスを含む)をモデリングし、Business Process Editorで読み書きごとにルールを強制することでこれらのパターンを素早くプロトタイプできます。AppMaster は appmaster.io にあります。\n\n実践的な最終テスト:テナントAとBを作り、同じユーザーと注文を追加して同じフローを実行します。テナントAのレポートをエクスポートした後、わざとテナントBのIDを同じエンドポイントに渡してみてください。プロトタイプが「十分安全」なのは、これらの試行が常に失敗し、主要レポートが実データ量で高速に動くときです。
よくある質問
シンプルな運用と頻繁なクロステナント分析を重視するなら、まずはテナント所有テーブルにtenant_idを置いた単一データベースをデフォルトにしましょう。より強い分離やテナントごとのカスタマイズが必要になったらスキーマ分離へ。コンプライアンスや専用のパフォーマンス制御が要求される場合はテナントごとの別データベースを検討してください。
テナントスコーピングはUIのフィルターではなくバックエンドで必須に扱ってください。テナント所有テーブルにtenant_idを必須にし、クライアントの入力を信用せず認証済みユーザーのコンテキストから決定します。可能ならPostgreSQLの行レベルセキュリティなどの安全網を追加し、別テナントのレコードにIDだけを変えてアクセスできないかを試すテストを作りましょう。
一般的なフィルターに合うインデックスでは、先頭にtenant_idを置くのが重要です。例えば時間ベースのビューには(tenant_id, created_at)を、ダッシュボード用には(tenant_id, status)や(tenant_id, user_id)を追加します。ユニーク制約もテナント単位にして(例:テナントごとのメール一意性)衝突を避けましょう。
スキーマ分離はテーブルが名前空間ごとに分かれるため、誤ったクロステナント結合が起きにくく、スキーマ単位で権限を設定できます。欠点はマイグレーションで、スキーマ数が増えるとすべてのスキーマに同じ変更を適用する運用が必要になります。tenant_idより強い分離を求めつつも単一DBで管理したい場合の中間案です。
別データベースはブラスト半径(被害範囲)を最小化します。パフォーマンスの急上昇や設定ミス、破損の影響はそのテナントに留まりやすいです。代償は運用コストで、プロビジョニング、バックアップ、監視、マイグレーションがテナント数に応じて増えます。信頼できるテナントディレクトリとリクエストごとの接続ルーティングが必要です。
単一DBとtenant_idならグローバル集計がそのままクエリになるため最も簡単です。スキーマや別DBに分かれている場合は、重要イベントやサマリを定期的に共有のレポーティングストアへコピーするのが実務的な方法です。日次ロールアップやイベントストリームで中央の分析基盤に送るパターンがよく使われます。
サポート用のツールではテナントコンテキストを明示的に表示し、レコードを閲覧する前に意図的なテナント切替を必須にしてください。インパーソネーション(代理ログイン)を使うなら誰がいつアクセスしたかを監査ログに残し、時間制限を設けます。レコードIDだけで検索を許すワークフローは“invoice 1001”問題の温床になります。
テナントごとにフィールドやワークフローが異なるなら、スキーマ分離や別DBにすることで一テナントの変更が他に影響するリスクを下げられます。ほとんどのテナントが似ているならtenant_idベースの共有モデルにして、機能フラグやオプショナルフィールドで差分を扱うのが運用面で楽です。重要なのは共有とテナント固有の所有権を曖昧にしないことです。
早い段階でテナント境界線を設計し、認証後にテナントコンテキストをどこに保存するか決め、読み書き時に必ずそれを使うようにしてください。AppMasterでは通常、Business Processロジック内で認証済みユーザーからtenant_idを設定してからテナント所有レコードの作成・検索を行うことで、エンドポイントがスコープを忘れないようにします。このパターンを全体で再利用することが重要です。
2つのテナントを作成して類似データを用意し、レコードIDだけを変えて読み取り・更新・削除できないか試して分離を破れる箇所を見つけてください。バックグラウンドジョブ、インポート、エクスポートが正しいテナントスコープに書き込むことも必ず確認します。また、実際のサンプルボリュームでテナント別レポートと管理者向けのグローバルレポートを実行して性能とアクセスルールを検証してください。


