マルチテナントDBアーキテクチャ
分離レベルを誤ると1社の障害が全社に波及します。DB単位・スキーマ単位・行単位共有の分離度とコストの原理を比較し、SaaSに合う設計軸がわかります。
- 1.分離レベルはDB単位・スキーマ単位・行単位共有の3段階で、隔離の強さとハードウェア効率がちょうど逆順にトレードオフする。
- 2.ノイジーネイバー問題は共有度が上がるほど深刻化し、行単位共有ではリソースガバナ(テナントごとのCPU/IO割当)が実質必須になる。
- 3.テナントごとのスキーマ移行は、DB単位なら独立実行できる一方、行単位共有では全テナント一括のオンラインDDLとなり複雑度が跳ね上がる。
マルチテナントSaaS特有の分離レベル問題
シャーディング方式は「1つのデータ集合をどう水平分割するか」という一般論でしたが、マルチテナントSaaSではもう1段階手前に固有の意思決定があります。**「テナント(契約企業・ユーザー組織)同士のデータとリソースを、どこまで物理的に分離するか」**という選択です。これは単なるスケール手法ではなく、セキュリティ境界・運用コスト・機能提供速度を同時に左右する設計判断です。
分離レベルは大きく3パターンに整理できます。
- DB単位分離(silo): テナントごとに独立したデータベース(インスタンスまたはクラスタ)を持つ。
- スキーマ単位分離(bridge): 1つのDBサーバー内で、テナントごとに別スキーマ(名前空間)を持つ。
- 行単位共有(pool): 1つのテーブル群をテナントIDカラムで水平共有し、全テナントが同じ行集合を使う。
三者は隔離の強さと運用効率がちょうど反比例する関係にあり、どれか1つが常に正解ということはありません。
パターン1: DB単位分離(silo)
テナントAとテナントBが物理的に別のデータベース(別クラスタ、少なくとも別スキーマ以上の隔離)を持ちます。接続文字列やルーティング層でテナントIDからDB実体を引き当てるのは、ディレクトリ分割方式の考え方そのものです。
- 隔離性は最強。あるテナントの暴走クエリやロック競合が他テナントの性能に及ぶ経路が物理的に存在しない。
- テナントごとにバックアップ、リストア、コンプライアンス対応(データ所在地の指定など)を個別に完結できる。
- 一方でDB接続やメモリ、バックグラウンドプロセスなど固定オーバーヘッドをテナント数だけ乗算するため、テナント数が数千を超えるとハードウェア効率が著しく悪化する。小規模テナントが大量にいるSaaSでは非現実的になりやすい。
金融・医療のように契約でデータ物理分離を求められる大口テナント、あるいはテナント数が数十〜数百規模に収まる場合はDB単位分離が第一候補です。大口ほど専用リソースの正当化がしやすく、コネクションプーリングの管理単位もテナントごとに独立させやすいという利点もあります。
パターン2: スキーマ単位分離(bridge)
1つのDBサーバー(1インスタンス)の中に、テナントごとの**スキーマ(名前空間)**を作り、同名のテーブル群を複製します。接続後に search_path やスキーマ修飾でテナントを切り替えます。
- DB単位分離よりハードウェア効率が良い(接続プールやバッファプールをテナント間で共有できる)一方、隔離性は下がる。同一インスタンス上のリソース(CPU、ディスクIO、バッファキャッシュ)は全テナントで奪い合いになる。
- テナント数が数百〜数千のオーダーで現実的な妥協点になりやすい。
- 欠点はカタログの肥大化です。テーブル数×テナント数ぶんのメタデータをDBが保持するため、システムカタログの検索やプランキャッシュの管理コストが増え、数千テナントを超えると起動やDDL自体が重くなる。
パターン3: 行単位共有(pool)
全テナントが同一のテーブルを使い、各行に tenant_id カラムを持たせて論理的に区別します。アプリ層またはDBのRLS(行レベルセキュリティ)で、クエリに必ず tenant_id 条件を強制します。
- ハードウェア効率は最大。テーブル・インデックス・バッファキャッシュ・接続プールをすべて共有するため、小規模テナントを大量に収容するコストが最も低い。
- 隔離性は最弱。分離はアプリまたはDBのポリシー層に委ねられ、
tenant_idの条件漏れ(バグ)がテナント間データ漏洩に直結する。 - スキーマ変更は全テナントに即座に反映されるが、それは裏を返せば1テナントだけの個別対応ができないということでもある。
| 観点 | DB単位(silo) | スキーマ単位(bridge) | 行単位共有(pool) |
|---|---|---|---|
| 隔離性 | 最強(物理分離) | 中(同一インスタンス内で論理分離) | 最弱(同一行集合をポリシーで分離) |
| ハードウェア効率 | 低い(固定費が乗算) | 中 | 高い(共有度が最大) |
| スキーマ移行の単位 | テナントごとに独立実行 | テナント数ぶん繰り返し実行 | 全テナント一括の単一DDL |
| ノイジーネイバーの経路 | 物理的に遮断 | 同一インスタンスのリソース競合 | 同一テーブル・インデックスの競合 |
| 向く規模 | 数十〜数百テナント、大口契約 | 数百〜数千テナント | 数千〜数百万テナント |
ノイジーネイバー問題とリソース分離
ノイジーネイバー問題とは、あるテナントの過大な負荷(大量書き込み、非効率なクエリ、突発的なバッチ処理)が、同居する他テナントの性能を巻き添えにする現象です。共有度が上がるほど経路が増えます。
- silo: 物理的に別インスタンスなので、他テナントへの波及経路が存在しない。最も安全だが、暇なテナントの空きリソースを忙しいテナントへ融通することもできない。
- bridge: CPU・ディスクIO・バッファプールは同一インスタンス内で共有される。1テナントのフルスキャンがバッファキャッシュを丸ごと入れ替えてしまい、他テナントの体感速度を落とすことがある。
- pool: テーブルやインデックス自体を共有するため、ロック競合や特定インデックスのホットページ争いも加わる。
行単位共有でノイジーネイバーを抑える実務的な対策は次のとおりです。
- リソースガバナでテナントごとにCPU時間・同時接続数・実行時間の上限を強制し、1テナントが持てる分を頭打ちにする。
- クエリタイムアウトの強制と、重いバッチ処理を専用のリードレプリカへ逃がす分離。
- 突出して大きい・重いテナントだけをsiloへ個別に退避する。3パターンは排他ではなく、大口だけ抜き出すハイブリッド運用がよく行われる。
行単位共有のSaaSでは、SLAを「平均レイテンシ」だけで語ると危険です。あるテナントが深夜バッチで大量書き込みをした瞬間、無関係なテナントのp99レイテンシが跳ねる、という形でノイジーネイバーは顕在化します。テナント単位のリソース計測とスロットリングを持たない行単位共有は、規模が大きくなるほど破綻します。
テナントごとのスキーマ移行の複雑さ
分離レベルはスキーマ変更(マイグレーション)の運用難度にも直結します。
- silo: テナントごとに独立したマイグレーション実行が可能。障害があっても影響は1テナントに閉じる。反面、数百〜数千テナント分のマイグレーション完了状態を個別に追跡・管理するコストが発生し、「テナントAはv12、テナントBはまだv10」というバージョンのばらつきが常態化しやすい。
- bridge: スキーマの数だけDDLを繰り返す必要があり、途中失敗時に「どのテナントまで適用済みか」を厳密に管理しないと不整合が生じる。オンラインスキーマ変更の手法をテナント数ぶん反復適用するイメージに近い。
- pool: DDLは1回で全テナントに反映されるため管理は単純に見えるが、テーブルが巨大なぶんオンラインスキーマ変更のコスト(新旧スキーマの二重書き込み、ロック取得の待ち行列)を全テナント分のトラフィックが同時に受ける。ロールバックも全テナントに波及するため、失敗時の影響範囲が最大になる。
行単位共有で1つの巨大テーブルに ALTER TABLE をかけると、ロック待ちや書き込み遅延は原理上すべてのテナントに同時発生します。カナリア的に一部テナントだけへ先行適用する、という段階的ロールアウトができないのが pool 方式の構造的な弱点です。対策として、機能フラグと tenant_id を組み合わせたアプリ層での段階的な有効化(スキーマ自体は先に追加しておき、読み書きの切り替えをテナント単位で制御する)が定石になります。
行単位共有でのテナントIDインデックス設計
pool方式を選ぶなら、tenant_id のインデックス設計が性能を決定づけます。
原則は複合インデックスの先頭列に tenant_id を置くことです。複合インデックスの列順の一般論のとおり、B+Treeは複合キーを辞書式順序で保持するため、先頭に tenant_id を置くと同一テナントの行が物理的に隣接し、テナント単位のクエリが狭い範囲へ絞り込まれます。
-- 良い例: tenant_id を先頭に置き、以後のクエリ条件を続ける
CREATE INDEX idx_orders_tenant_created
ON orders (tenant_id, created_at DESC);
-- クエリはこの並びをそのまま使える
SELECT * FROM orders
WHERE tenant_id = 'acme' AND created_at > now() - interval '7 days'
ORDER BY created_at DESC;
tenant_id を先頭に置かず created_at だけで並べると、インデックスは全テナントの行を時系列に混在させて保持するため、特定テナントの絞り込みには不要な行を大量に読み飛ばす必要が生じます。
PostgreSQLのRLSで tenant_id = current_setting('app.tenant_id') のようなポリシーを課す場合も、オプティマイザはこの条件を通常の WHERE 句同様に扱い、tenant_id 先頭の複合インデックスがあれば同じインデックスシークを使います。RLSは「アプリが条件を書き忘れても強制される」安全弁であって、性能上の特別扱いではない点に注意してください。
もう1つの実務的な論点はカーディナリティの偏りです。巨大テナントと零細テナントが同じテーブルに混在すると、tenant_id のヒストグラムが極端に歪みます。オプティマイザのカーディナリティ推定が不正確になりやすく、巨大テナントに対しては想定外のプラン(インデックスを使わないシーケンシャルスキャンなど)を選ぶことがあるため、大口テナントは統計情報の更新頻度を上げる、またはsiloへ退避するといった対応が必要になります。
分離レベルの選び方
問われるのは「隔離性とハードウェア効率はトレードオフであり、テナント数・契約要件・DDL頻度で選ぶ」という因果です。大口かつ数十社規模ならsilo、中規模SaaSで数百〜数千テナントならbridge、フリーミアムで数万〜数百万テナントならpoolが出発点になります。実運用では単一方式に固定せず、大口テナントだけpool/bridgeからsiloへ退避するハイブリッドが一般的です。
分離レベルの選択は一度決めたら終わりではなく、テナント構成の変化(大口契約の獲得、零細テナントの急増)に応じて見直す対象です。行単位共有を土台にしつつ、突出したテナントだけを個別分離するハイブリッド構成が、多くのSaaSにとって現実的な着地点になります。
データベース Article
マルチテナントDBアーキテクチャを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
マルチテナント
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
ノイジーネイバー問題は共有度が上がるほど深刻化し、行単位共有ではリソースガバナ(テナントごとのCPU/IO割当)が実質必須になる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「マルチテナント / SaaS」に近いか確認する。
- 強みである「分離レベルはDB単位・スキーマ単位・行単位共有の3段階で、隔離の強さとハードウェア効率がちょうど逆順にトレードオフする。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。