---
title: "RLS（Row Level Security）とは？DBの行レベルセキュリティの仕組みと設定例"
url: "https://www.issoh.co.jp/tech/details/5442/"
published: 2025-02-20
updated: 2026-10-22
categories: ["データベース"]
publisher: "株式会社一創"
---

# RLS（Row Level Security）とは？DBの行レベルセキュリティの仕組みと設定例

RLS（Row Level Security：行レベルセキュリティ）は、ITシステムのデータベースで、同じテーブルを読んでいてもユーザーやテナントごとに「見える行」「書き換えられる行」をDB自身が絞り込むセキュリティ機能です。「ロウレベルセキュリティ」「ローレベルセキュリティ」と書かれることもあります。アプリケーションのSQLにWHERE句を付け忘れても、DBが他人の行を返しません。医療分野のRLS（レストレスレッグス症候群＝むずむず脚症候群）とは別の言葉で、この記事はデータベースのRLSだけを扱います。

仕組みと、PostgreSQL・SQL Server・Oracle Database・MySQL・Power BI、クラウドDWH（Snowflake・BigQuery・Databricks・Amazon Redshift）の設定例と製品ごとの対応条件を示します。PostgreSQLについては、2026年8月13日リリースのPostgreSQL 18.6を手元に用意し、掲載したSQLを実行して出力を確かめました。とくに「RLSを入れたのに他テナントの行に届いてしまう経路」は、実行結果をそのまま載せています。

## まとめ：RLSの要点と採用判断

- **仕組み**：テーブルにポリシー（条件式）を結び付け、DBがクエリのたびに自動で評価する。GRANTで読めるテーブルでも、条件に合わない行は結果に出ない。
- **対応製品**：PostgreSQL（9.5以降）、SQL Server（2016以降）とAzure SQL、Oracle Database（VPD・Enterprise Editionのみ）、Power BI、Snowflake・BigQuery・Databricks・Amazon Redshiftが持つ。MySQLには行単位のポリシーが無く、ビューで代替する。複数ポリシーの合成規則（OR・AND・1テーブル1本）は製品ごとに違う。
- **列は守らない**：RLSが絞るのは行だけ。カード番号のような列は、列単位のGRANTかマスキングで別に絞る。
- **効かない相手**：PostgreSQLではスーパーユーザーとBYPASSRLS属性のロールが常に素通りし、テーブル所有者もFORCEを指定しない限り素通りする。TRUNCATEはポリシーを見ずに全テナントの行を消す。
- **性能**：ポリシーが比較する列のインデックスは検索に利用される場合があるが、採用される計画は選択率や統計情報などで変わる。行データに依存せず文内で結果を共用できる関数は、`(SELECT 関数())` で包むと文単位の評価にできる場合がある。

1つのDBを複数の顧客や部門で共有し、画面・API・バッチ・BIツールとアクセス経路が複数あるなら、RLSを入れる価値があります。テナントごとにDBを分けている構成や、経路がAPI1本で認可がそこに集約されている構成なら、アプリ層の制御で足ります。どちらでも、RLSはアプリ層の認可を置き換えるものではなく、その下に敷く二重目の網です。

## RLSの仕組み：GRANTの内側で行を絞るDBのセキュリティ機能

PostgreSQLの公式ドキュメントは、RLSを「行セキュリティポリシー」と呼び、通常のSQL権限に加えて、どの行を問い合わせで返し、どの行を挿入・更新・削除の対象にできるかをユーザー単位で制限する機能だと説明しています（[PostgreSQL公式ドキュメント「Row Security Policies」](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)）。DBはポリシーの条件式をユーザーのクエリに付け加えて評価し、真にならなかった行を処理の対象から外します。

ポリシーの条件は、ユーザーが書いた条件より先に評価されます。先に評価してよいのは、情報を漏らさないと保証されたLEAKPROOFの関数と演算子だけです。18.6で、受け取った行をNOTICEに書き出す自作関数をWHERE句に入れて実行すると、関数に渡ったのはテナント1の2行だけで、他テナントの行は関数にも届きませんでした。利用者が関数を細工しても、ポリシーで弾かれる行の中身は覗けません。

### GRANT・ビュー・アプリ層のWHEREとRLSの制御範囲

| 方式           | 制御の単位  | 経路を問わず効くか   | 主な弱点           |
| ------------ | ------ | ----------- | -------------- |
| GRANT・REVOKE | テーブル・列 | 効く          | 行を区別できない       |
| 条件付きビュー      | ビューごと  | ビュー経由のみ     | 元テーブルの権限で迂回される |
| アプリのWHERE句   | クエリごと  | その経路のみ      | 書き忘れ1か所で漏れる    |
| RLS          | 行      | 効く（除外ロールあり） | 所有者・特権ロールは素通り  |

RLSはGRANTと重ねて使います。GRANTで「そもそもテーブルに触れてよいか」を決め、RLSで「どの行に触れてよいか」を決める順番です。GRANTの無いテーブルは、ポリシーがどうであっても読めません。テーブル単位の権限をロールで束ねる設計は[RBACとは？ロールベースアクセス制御の仕組みと導入判断を実装視点で解説](/tech/details/6488/)、役職や所在地などの属性で許可を決める設計は[ABACとは？属性ベースアクセス制御の仕組みとRBACとの使い分けを実装視点で解説](/tech/details/16407/)で扱っています。

RLSが防ぐのは、他人のIDを指定してデータを取るIDORのような認可の抜けです。攻撃の具体的なパターンは[認可バイパスとは？IDOR・権限昇格の仕組みとサーバ側で止める実装対策](/tech/details/16399/)にまとめています。

### 列単位のGRANT・マスキングとRLSの分担

RLSは「どの行か」を絞る機能で、行に含まれるどの列を見せるかには関与しません。自テナントの注文は見せてよいが、カード番号の列は見せたくない、という要件はRLSだけでは満たせません。PostgreSQLでは列単位のGRANTと組み合わせます。前提は、そのロールがテーブル全体へのSELECT権限を持たないことです。列単位のGRANTはテーブル単位の権限を取り消さないため、既存の `GRANT SELECT ON orders` や、所属ロール・PUBLIC経由で継承した権限が残っていれば全列が読めます。18.6で、注文テーブルにRLSを掛けたうえでアプリ用ロールに3列だけSELECTを許可し、テナント1を設定して試した結果です。

```
GRANT SELECT (order_id, tenant_id, item) ON orders TO app_user;

SELECT * FROM orders;                                -- ERROR: permission denied for table orders
SELECT order_id, item FROM orders;                   -- 2行（テナント1の行だけ）
SELECT order_id FROM orders WHERE card_last4 = '3333';  -- ERROR: permission denied for table orders
```

許可していない列は、SELECTの対象にもWHERE句の条件にも使えません。条件に使えないので、「カード番号が3333の行はあるか」を試して値を推し量ることもできません。`SELECT *` がエラーになるため、ORMが全列を取りにいく実装はそのままでは動かなくなります。列を伏せつつ行は返したい場合は、列を加工したビューか、SnowflakeやBigQueryのマスキングポリシーを使います。MySQL Enterprise Edition 9.7.0で導入された `CREATE MASKING POLICY` も、この列側の機能です（Community Editionの機能ではありません）。

### 製品別のRLS対応状況（2026年10月時点）

| 製品              | 実装                   | 導入                   | 書き込みの制御           | 既定で対象外になる主体                 |
| --------------- | -------------------- | -------------------- | ----------------- | --------------------------- |
| PostgreSQL      | CREATE POLICY        | 9.5（2016年1月）         | WITH CHECK式       | スーパーユーザー・BYPASSRLS・所有者      |
| SQL Server      | 述語関数＋SECURITY POLICY | 2016（13.x）           | ブロック述語            | なし（ポリシー変更権限者は無効化可）          |
| Oracle Database | VPD（DBMS\_RLS）       | Enterprise Editionのみ | update\_check     | SYS・EXEMPT ACCESS POLICY保持者 |
| MySQL           | なし（ビューで代替）           | ―                    | WITH CHECK OPTION | 元テーブルの権限保持者                 |
| Power BI        | ロール＋DAXフィルター         | ―                    | 対象外（閲覧用）          | ワークスペースの管理者・メンバー・共同作成者      |

PostgreSQLは10で、AND条件で重ねるRESTRICTIVEポリシーが加わりました。SQL ServerのRLSは、Azure SQL Database、Azure SQL Managed Instance、Azure Synapse AnalyticsやMicrosoft Fabricにも対応していますが、製品ごとの制約があります。少なくともAzure Synapse AnalyticsとFabric Warehouseではフィルター述語のみが対応し、後述するブロック述語付きの例をそのまま適用できません（[Microsoft Learn「行レベルのセキュリティ」](https://learn.microsoft.com/ja-jp/sql/relational-databases/security/row-level-security)）。MySQLは、ソースコードの構文定義（sql\_yacc.yy）を9.7.0と2026年7月の26.7.0のタグで確かめても行単位のポリシー構文が無く、9.7.0でEnterprise Edition向けに入ったのは列の値を伏せる `CREATE MASKING POLICY` です（[MySQL 9.7.0リリースノート](https://dev.mysql.com/doc/relnotes/mysql/9.7/en/news-9-7-0.html)）。行を絞る機能とは別物なので混同しないでください。

## PostgreSQLでRLSを設定する手順：ENABLE・CREATE POLICY・FORCE

以下のSQLは、対象テーブルとロールを準備し、通常のアクセス権を付与した環境で使う設定例です。PostgreSQLのRLSは、テーブルでRLSを有効にする（ENABLE）、操作ごとにポリシーを作る（CREATE POLICY）、所有者でないロールで見え方を確かめる、の3段階で設定します。次の例は、従業員が自分の行だけを見られるようにするものです。

```
CREATE TABLE employees (
  employee_id   integer PRIMARY KEY,
  login_name    text    NOT NULL UNIQUE,  -- DBのロール名と一致させる
  department_id integer NOT NULL,
  full_name     text    NOT NULL
);
ALTER TABLE employees ENABLE ROW LEVEL SECURITY;

CREATE POLICY employee_self ON employees
  FOR SELECT
  USING (login_name = current_user);
```

解説記事でよく見かける `employee_id = current_user` は動きません。current\_userはロール名（name型）を返すため、整数の列とは比較できず「operator does not exist」エラーになります。ロール名と照合する列を別に持たせるのが正しい形です。

ENABLEだけでポリシーが1つも無い状態は、公式ドキュメントのいう「default-deny（既定で拒否）」です。18.6で確かめると、3行入ったテーブルをアプリ用ロールで数えた結果は0行、テーブル所有者で数えると3行でした。所有者で動作確認すると「全行見えるのでRLSが効いていない」と誤解しやすいので、確認は必ず所有者でないロールで行います。

### USINGとWITH CHECKによる読み取りと書き込みの条件

CREATE POLICYには2つの式があります。USINGは読み取りと、更新・削除の対象になる既存行を絞ります。WITH CHECKは、INSERTやUPDATEで書き込まれる新しい行を検査します。FOR INSERTのポリシーにはWITH CHECKしか書けず、FOR ALLとFOR UPDATEでWITH CHECKを省くとUSINGの式が流用されます（[PostgreSQL公式ドキュメント「CREATE POLICY」](https://www.postgresql.org/docs/current/sql-createpolicy.html)）。流用に頼るとレビューで書き込み側の条件を見落としやすいため、書き込みのあるテーブルでは両方を明示します。

2026年10月15日にRC1が出たPostgreSQL 19（正式版は11月19日の予定）では、`INSERT … ON CONFLICT DO SELECT … RETURNING` が加わり、ポリシーの適用表にも行が足されました。競合した既存行を返すこの構文では、既存行にSELECTまたはALLのポリシーが検査として掛かります（[PostgreSQL 19「CREATE POLICY」](https://www.postgresql.org/docs/19/sql-createpolicy.html)）。19へ上げてこの構文を使う場合は、INSERTのポリシーだけでなくSELECTのポリシーも確認します。

書き込み側の挙動は、読み取りと違ってエラーになる場合とならない場合があります。18.6での結果は次のとおりです。

- 他テナントのtenant\_idを持つ行のINSERT：`ERROR: new row violates row-level security policy for table "orders"`
- 自テナントの行のtenant\_idを書き換えて他テナントへ移すUPDATE：同じエラー
- 見えない行（他テナントの行）を主キー指定でUPDATE・DELETE：エラーにならず `UPDATE 0`・`DELETE 0`

3つ目は見落としやすい点です。更新対象が見えないとき、DBは失敗を返さず「0件更新した」と答えます。アプリが更新件数を確かめずに「更新しました」と表示すると、利用者には成功に見えます。更新・削除のAPIでは件数0を「対象なし」として扱う処理を入れてください。

### PERMISSIVEとRESTRICTIVEの合成規則

1つのテーブルに複数のポリシーを置くと、既定のPERMISSIVE同士はORで合成されます。「部門が一致」と「所在地が一致」を並べれば両方を満たす行だけが見える、と考えがちですが、実際はどちらか一方を満たす行が見えます。両方を必須にするには、外せない方をRESTRICTIVEにします。

```
-- emp: (1,部門10,tokyo) (2,部門10,osaka) (3,部門20,tokyo) (4,部門20,osaka)
CREATE POLICY by_dept ON emp FOR SELECT TO app_user USING (dept = 10);
CREATE POLICY by_loc  ON emp FOR SELECT TO app_user USING (loc = 'tokyo');
-- 両方PERMISSIVE     → 見える行 {1,2,3}（OR）
-- by_locをRESTRICTIVE → 見える行 {1}    （AND）
-- RESTRICTIVEだけ残す → 0行（RESTRICTIVEは単独では何も許可しない）
```

コメントの結果は18.6での実測値です。基本の許可をPERMISSIVE、絶対に外せない制約をRESTRICTIVEと役割を分けると、ポリシーが増えても合成結果を追えます。

条件に使う属性は別テーブルに切り出します。ポリシーの中で同じテーブル自身を参照すると `ERROR: infinite recursion detected in policy for relation "members"` で止まりました。ユーザーと部門の対応表を別に置けば、異動のたびにポリシーを書き換えずに済みます。「管理職なら全行」のような例外は、行の列（role = ‘manager’）ではなく `pg_has_role(current_user, 'managers', 'MEMBER')` のようにアクセスしている側のロール所属で判定します。行の列で判定すると「その行の人物が管理職か」を見ているだけになります。ロール属性とGRANTの設計は[PostgreSQLのロールと権限設計｜CREATE ROLEの属性とGRANT・既定権限の決め方](/tech/details/16984/)が詳しいです。

## マルチテナントSaaSのRLS：テナントIDの渡し方と接続プール

SaaSのアプリは多くの場合、DBに1つの共通ユーザーで接続するため、ロール名ではテナントを区別できません。アプリがトランザクションの開始時にテナントIDをセッション変数へ入れ、ポリシーがそれを読む形にします。共有DB・共有スキーマを選ぶかどうかという前段の判断は[シングルテナントとマルチテナントの違い｜比較表・選定基準とテナント分離の実装](/tech/details/7733/)、テナントごとにスキーマを分ける方式は[PostgreSQLのスキーマ運用｜search\_pathの解決順とpublic権限・テナント分割の判断基準](/tech/details/16980/)で扱っています。

```
CREATE POLICY tenant_isolation ON orders
  FOR ALL
  TO app_user
  USING      (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
  WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
```

この1文の書き方には、18.6で確かめた理由があります。

- 第2引数の `true` を外すと、変数が未設定の接続で `ERROR: unrecognized configuration parameter "app.tenant_id"` になる
- `::integer` を外すと、current\_settingが文字列を返すため `ERROR: operator does not exist: integer = text` でポリシー作成自体が失敗する
- 上の形なら、テナントIDが未設定の接続では条件がNULLになり、結果は0行（安全側）

### SETとSET LOCALの違いと接続プールでの持ち越し

マルチテナントのRLSでは、接続プールで再利用する接続にセッション変数が残ると、別テナントの情報を参照する原因になります。`SET` で入れた値はセッションが続く限り残ります。18.6で、テナント2を `SET` してCOMMITした後に件数を数えると、値は2のまま残り、テナント2の行が1件見えました。セッションの値をRESETしてから `SET LOCAL` を試すと、COMMIT後の値は空になり、件数は0でした。SET LOCALの終了後は元のセッション値に戻るため、以前のSET値が残っていれば、その値が再び有効になります。

```
BEGIN;
SET LOCAL app.tenant_id = '42';   -- トランザクション終了時に元の値へ戻る
SELECT order_id, item FROM orders;
COMMIT;

-- 関数で設定する場合は第3引数をtrueにする（SET LOCALと同じ効果）
BEGIN;
SELECT set_config('app.tenant_id', '42', true);
SELECT order_id, item FROM orders;
COMMIT;
```

接続をプールへ返す構成で `SET` を使うと、次にその接続を受け取った別テナントのリクエストが前のテナントIDのまま動きます。「トランザクションを開始したら最初にテナントIDを設定する」処理を共通部品に閉じ込め、画面ごとの実装者が触らないようにします。

### テナント内の役割とRESTRICTIVEによるテナントの壁

テナントの中でも、管理者は自社の全請求書、一般ユーザーは自分が作った請求書だけ、と分けたい場合は、テナント分離をRESTRICTIVEで固定し、テナント内の見え方をPERMISSIVEで書きます。

```
-- テナントの分離は必ず満たす（ANDで合成）
CREATE POLICY invoices_tenant ON invoices
  AS RESTRICTIVE FOR ALL TO app_user
  USING      (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
  WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);

-- テナント内の見え方（ORで合成）
CREATE POLICY invoices_admin_or_owner ON invoices
  AS PERMISSIVE FOR ALL TO app_user
  USING (
    current_setting('app.user_role', true) = 'admin'
    OR created_by = NULLIF(current_setting('app.user_id', true), '')::integer
  );
```

テナント分離をRESTRICTIVEにしておけば、後から誰かがPERMISSIVEのポリシーを足しても、テナントの壁を越える行は見えません。テナントID・ユーザーID・役割は、認証済み情報から信頼できるアプリが設定する前提です。app\_userとして任意のSQLを実行できる利用者はこれらのカスタム設定を書き換えられるため、この方式だけではSQLインジェクションやDBへの直接接続に対するテナント分離を保証できません。

## PostgreSQL 18.6で確かめたRLSのすり抜け経路

ポリシーの条件式が正しくても、所有者権限や対象外の操作を通じて分離が崩れる場合があります。3行（テナント1が2行、テナント2が1行）のordersテーブルを所有者ロールで作り、上記のtenant\_isolationポリシーを掛けたうえで、別に作ったアプリ用ロール（検証のためTRUNCATE権限も付与）をテナント1に設定した状態で試した結果です。

| 経路          | 18.6での結果                  | 対策                       |
| ----------- | ------------------------- | ------------------------ |
| テーブル所有者で接続  | FORCE無しで3行すべて見える          | アプリ用ロールを所有者と分ける          |
| スーパーユーザーで接続 | FORCE後も3行すべて見える           | アプリの接続に使わない              |
| 既定のビュー経由    | 3行（security\_invokerなら2行） | security\_invoker = true |
| TRUNCATE    | テナント2の行も含め全行が消える          | アプリ用ロールにGRANTしない         |
| 主キーの重複      | 他テナントの行の存在がエラーで分かる        | 推測できない採番にする              |

### 所有者・スーパーユーザー・BYPASSRLSとFORCEの効く範囲

スーパーユーザーとBYPASSRLS属性を持つロールは、常にRLSを素通りします。テーブル所有者も既定では素通りし、`ALTER TABLE … FORCE ROW LEVEL SECURITY` を指定したときだけポリシーの対象になります。FORCEを「全ユーザーにRLSを適用する指定」と説明する記事がありますが、正確ではありません。18.6でFORCEを掛けると所有者は0行になりましたが、スーパーユーザーは3行のままでした。マイグレーション用に作ったBYPASSRLSのロールを、アプリが使い回していないかも確認します。

「ポリシーを書いたのにENABLEを忘れた」抜けは、次の一覧で機械的に見つけられます。relrowsecurityがfalseのテーブルには、ポリシーがあっても何も効いていません。

0行しか返らないときは、まず今の接続ロールにRLSが適用される状態かを、組み込み関数 `row_security_active('orders')` で確かめます。この関数が返すのは適用状態で、実際に何行が除外されたかではありません。適用されていれば `t` を返します。18.6では、アプリ用ロールで `t`、FORCE前の所有者で `f`、FORCE後の所有者で `t` でした。`f` なのに行が絞られていれば、原因はRLSではなくクエリ側にあります。

```
SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class
WHERE relkind IN ('r', 'p')
  AND relnamespace = 'public'::regnamespace
ORDER BY relname;

SELECT tablename, policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
ORDER BY tablename, policyname;
```

### PostgreSQLのビュー所有者権限とsecurity\_invokerによるRLS適用

PostgreSQLのビューは、既定では「ビューの所有者」の権限で元のテーブルを読み、RLSもビューの所有者に対して評価されます。ビューの所有者がテーブルの所有者と同じだと、所有者の素通りがビューにも及びます。18.6で、テーブル所有者が作ったビューをアプリ用ロールから読むと3行すべてが返りました。PostgreSQL 15以降は `WITH (security_invoker = true)` を付けたビューが使う人の権限で読むようになり、同じ条件で2行に絞られました。

```
CREATE VIEW sales_summary
  WITH (security_invoker = true) AS
SELECT owner_login, date_trunc('month', sold_at) AS month, sum(amount) AS total
FROM sales
GROUP BY owner_login, date_trunc('month', sold_at);
```

### TRUNCATEと一意制約：ポリシーが見ない操作

TRUNCATEはRLSの対象外です。アプリ用ロールにTRUNCATE権限を与えた状態で、テナント1を設定してTRUNCATEを実行すると、テナント2の行も含めて全行が消えました。`GRANT ALL` で権限をまとめて付けるとTRUNCATEも含まれるので、アプリ用ロールには `SELECT, INSERT, UPDATE, DELETE` だけを明示して付けます。

一意制約はテナントの壁の向こうの行も見ます。テナント1の立場で、テナント2が持つorder\_id=3と同じ値をINSERTすると `ERROR: duplicate key value violates unique constraint "orders_pkey"` が返りました。行の中身は見えなくても、「その番号の行が存在する」ことは分かります。連番の主キーを外部に見せる設計では、他社の注文件数を推し量る手掛かりになります。外部に出すIDは `gen_random_uuid()` で作るUUIDv4のような、暗号学的乱数による推測しにくい値にし、業務キーの一意制約は `(tenant_id, 業務キー)` の組にします。

### row\_securityとpg\_dump：素通りでなくエラーになる設定

設定パラメータ `row_security = off` は、RLSを外すスイッチではありません。BYPASSRLSを持たないアプリ用ロールでoffにして問い合わせると、`ERROR: query would be affected by row-level security policy for table "orders"` で止まりました。pg\_dumpは既定でrow\_securityをoffにして全行を取り出そうとするため、RLSを素通りできないロールでバックアップを取ると同じくエラーになります（[PostgreSQL公式ドキュメント「pg\_dump」](https://www.postgresql.org/docs/current/app-pgdump.html)）。一部の行だけのバックアップにはならず、失敗します。全行を取得するには、必要なSELECT権限に加えてRLSを迂回できる権限が必要です。所有者でもFORCEの対象なら迂回できないため、バックアップ専用ロールの権限を確認します。

## SQL Server・Oracle・MySQL・Power BI・クラウドDWHのRLS設定

考え方は共通ですが、「誰が素通りするか」「書き込みを何で止めるか」は製品ごとに違います。DBを移し替えるときは、条件式の書き換えよりもこの2点の確認が漏れやすい部分です。

### SQL Server：SESSION\_CONTEXTとフィルター述語・ブロック述語

SQL Serverは、条件を返すインライン テーブル値関数と、それをテーブルに結び付けるセキュリティ ポリシーでRLSを組みます。アプリが共通ユーザーで接続する構成では、SESSION\_CONTEXTにテナントIDを入れて述語関数から読みます。

```
CREATE FUNCTION Security.fn_tenant_predicate(@TenantId int)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS fn_result
       WHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS int);
GO
CREATE SECURITY POLICY Security.TenantPolicy
ADD FILTER PREDICATE Security.fn_tenant_predicate(TenantId) ON dbo.Orders,
ADD BLOCK PREDICATE  Security.fn_tenant_predicate(TenantId) ON dbo.Orders AFTER INSERT,
ADD BLOCK PREDICATE  Security.fn_tenant_predicate(TenantId) ON dbo.Orders AFTER UPDATE
WITH (STATE = ON);
GO
-- アプリ側：接続ごとに設定（@read_only = 1 で同じ接続中の上書きを禁止）
EXEC sp_set_session_context @key = N'TenantId', @value = 42, @read_only = 1;
```

フィルター述語は読み取りと更新・削除の対象を絞るだけで、他社のTenantIdを持つ行のINSERTは止めません。書き込みを止めるのはブロック述語（AFTER INSERT・AFTER UPDATE・BEFORE UPDATE・BEFORE DELETE）です。Microsoft Learnは、RLSがあってもエラーを使って細工したクエリで情報が漏れうると注意しており、給与が100,000ドルのときだけゼロ除算を起こす例を挙げています。PostgreSQLの一意制約と同じく、「見えないが、存在や値の手掛かりが漏れる」経路は残ります。

### Oracle Database：VPD（DBMS\_RLS）とEXEMPT ACCESS POLICY

OracleではRLSに相当する機能をVirtual Private Database（VPD）と呼びます。ポリシー関数が「WHERE句に足す条件の文字列」を返し、DBがそれを対象テーブルへのクエリに自動で付け加えます。次の例は、アプリケーションコンテキストAPP\_CTXを `CREATE CONTEXT` で作成し、信頼できるパッケージが認証済みの情報からTENANT\_IDを設定する処理を別に実装してある前提です。

```
CREATE OR REPLACE FUNCTION app.tenant_policy_fn (
  p_schema IN VARCHAR2,
  p_object IN VARCHAR2
) RETURN VARCHAR2 AS
BEGIN
  RETURN 'tenant_id = SYS_CONTEXT(''APP_CTX'', ''TENANT_ID'')';
END;
/
BEGIN
  DBMS_RLS.ADD_POLICY(
    object_schema   => 'APP',
    object_name     => 'ORDERS',
    policy_name     => 'TENANT_POLICY',
    function_schema => 'APP',
    policy_function => 'TENANT_POLICY_FN',
    statement_types => 'SELECT, INSERT, UPDATE, DELETE',
    update_check    => TRUE);
END;
/
```

Oracle Database 26のリファレンスは「DBMS\_RLS is available with the Enterprise Edition only.」と明記しており、Standard Edition 2では使えません（[Oracle公式リファレンス「DBMS\_RLS」](https://docs.oracle.com/en/database/oracle/oracle-database/26/arpls/DBMS%5FRLS.html)）。update\_checkの既定はFALSEで、statement\_typesにINSERTを含めるならTRUEが必須です（FALSEのままだとORA-28104）。SYSとEXEMPT ACCESS POLICYシステム権限を持つユーザーはポリシーの対象外になるため、この権限の付与先を定期的に棚卸しします。なお、Oracle Database 26のセキュリティガイドはVPDの章の冒頭で、VPDとReal Application Securityを宣言型のSQLポリシーへ置き換える「Oracle Deep Data Security」の利用を推奨しています（[Oracle Database Security Guide「Using Oracle Virtual Private Database to Control Data Access」](https://docs.oracle.com/en/database/oracle/oracle-database/26/dbseg/using-oracle-vpd-to-control-data-access.html)）。新規に組む場合は、VPDと並べて検討対象に入れます。

### MySQL：ビューとUSER()による代替

MySQLには行単位のポリシーが無いため、利用者に元テーブルの権限を与えず、行を絞ったビューだけに権限を与えます。

```
CREATE SQL SECURITY DEFINER VIEW hr.my_employee AS
SELECT employee_id, login_name, department_id, full_name
FROM hr.employees
WHERE login_name = SUBSTRING_INDEX(USER(), '@', 1)
WITH CHECK OPTION;

GRANT SELECT, UPDATE ON hr.my_employee TO 'taro'@'%';
```

使う関数はCURRENT\_USER()ではなくUSER()です。SQL SECURITY DEFINERのビューの中では、CURRENT\_USER()はビューの定義者を返すため、全員が定義者として扱われます。USER()はクライアントが接続時に指定した名前を返し、認証に使われたアカウントと一致しない場合があります。この例は匿名・プロキシ認証を使わず、ホスト部分が異なる同名アカウントも同じ利用者として扱う前提です。元テーブルへの権限を誰かに与えた時点でこの仕組みは迂回されるので、権限の付け方まで含めて設計します。

### Power BI：ロールとDAXフィルター、閲覧者だけに効く制限

Power BIでは、Power BI Desktopの「モデリング」タブの「ロールの管理」でロールを作り、テーブルにDAX式のフィルターを書きます。`[Department] = USERPRINCIPALNAME()` はよく見る誤りで、USERPRINCIPALNAME()はメールアドレス形式の名前を返すため部門名とは一致しません。ユーザーと部門の対応表に `[Email] = USERPRINCIPALNAME()` を掛け、リレーションシップでフィルターを売上テーブルへ伝えます。

見落としやすいのは効く相手です。Microsoft Learnは「RLS only restricts data access for users with Viewer permissions. It doesn’t apply to workspace Admin, Member, or Contributor roles.」と書いています（[Microsoft Learn「Power BIでの行レベルのセキュリティ（RLS）」](https://learn.microsoft.com/ja-jp/power-bi/enterprise/service-admin-rls)）。絞りたい利用者を共同作成者としてワークスペースに招くと、全データが見えます。BIツールとしての全体像は[Power BIとは？できること・料金・Excelやtableauとの違いから導入判断まで解説](/column/details/13227/)を参照してください。

### Snowflake・BigQuery・Databricks・Amazon Redshiftの行アクセス制御

クラウドDWHのRLSは、BIツールやノートブックなど利用者が直接SQLを書く経路を絞るために使われます。構文はどれも「条件を返す関数や式をテーブルに結び付ける」形ですが、複数のポリシーがあるときの合成規則が製品ごとに異なります。

| 製品              | 構文                       | 複数ポリシーの扱い      | 主な前提・注意                      |
| --------------- | ------------------------ | -------------- | ---------------------------- |
| Snowflake       | CREATE ROW ACCESS POLICY | 1テーブルに1本だけ     | Enterprise Edition以上         |
| BigQuery        | CREATE ROW ACCESS POLICY | OR（和集合）        | 1本でも作ると対象外の人は0行              |
| Databricks      | SET ROW FILTER（関数）       | 1テーブルに1つ       | Unity Catalog・対応コンピュート（本文参照） |
| Amazon Redshift | CREATE／ATTACH RLS POLICY | 既定はAND（ORに変更可） | ROW LEVEL SECURITY ONで有効化    |

同じ「複数ポリシー」でも、PostgreSQLのPERMISSIVE同士とBigQueryはORで合成して見える行が増え、Amazon Redshiftは既定のANDで減ります（[Amazon Redshift「ALTER TABLE」](https://docs.aws.amazon.com/redshift/latest/dg/r%5FALTER%5FTABLE.html)）。DBを移し替えるときにポリシーを1本ずつ書き写すと、意味が逆になる場合があります。Snowflakeは「A table or view can only be protected by one row access policy at a time.」と1本に限っており、条件は1つのポリシー本体にまとめます（[Snowflake「ALTER TABLE」](https://docs.snowflake.com/en/sql-reference/sql/alter-table)）。同じテーブルにマスキングポリシーもある場合、Snowflakeは行アクセスポリシーを先に評価します。

BigQueryで見落としやすいのは、行アクセスポリシーを1本作った瞬間に、どのポリシーにも名前の無い利用者が何も読めなくなる点です。公式ドキュメントは、それまで全行を読めていた利用者は `TRUE` のフィルターに加えないと読めなくなると注意しています（[BigQuery「行レベルのセキュリティの概要」](https://cloud.google.com/bigquery/docs/row-level-security-intro)）。集計バッチのサービスアカウントを忘れると、夜間の集計結果が黙って0件になります。

```
CREATE ROW ACCESS POLICY apac_filter
ON project.dataset.sales
GRANT TO ('group:sales-apac@example.com')
FILTER USING (region = 'APAC');

-- 全行が必要な集計用サービスアカウントは TRUE で明示的に残す
CREATE ROW ACCESS POLICY full_access
ON project.dataset.sales
GRANT TO ('serviceAccount:etl@example-project.iam.gserviceaccount.com')
FILTER USING (TRUE);
```

DatabricksはUnity Catalogの行フィルターと列マスクで行と列を分けて制御し、行フィルターは1テーブルに1つです。フィルター付きテーブルを読めるのは、SQL Warehouse、StandardアクセスモードのDatabricks Runtime 12.2 LTS以上、Dedicatedアクセスモードの15.4 LTS以上（ワークスペースでサーバーレスコンピュートが有効な場合）です。Dedicatedでは15.4〜16.2は読み取りだけで、INSERT・UPDATE・DELETEには16.3以上と、MERGE INTOなど対応する書き込みパターンが必要です（[Databricks「Row filters and column masks」](https://docs.databricks.com/aws/en/data-governance/unity-catalog/filters-and-masks/)）。12.2 LTS未満のRuntimeは、データを返さない側に倒れます。製品の全体像と料金は[Databricksとは？できること・DBU課金の料金・業種別の導入効果を2026年版で解説](/tech/details/3557/)、Redshiftの構成の違いは[Amazon Redshift（レッドシフト）とは？RG・RA3・Serverlessの違いと東京の料金・使い方を解説](/tech/details/5170/)で扱っています。

## RLSの性能：インデックスと条件式の評価回数

RLSの条件は、クエリにWHERE句が1つ足されるのに近い形で処理されます。約20万行のordersに `(tenant_id, created_at)` のインデックスを張り、アプリ用ロールで `SELECT * FROM orders` の実行計画を取ると、ポリシーの条件がそのままIndex Condとして使われました。

```
SET ROLE app_user;
BEGIN;
SET LOCAL app.tenant_id = '5';
EXPLAIN (COSTS OFF) SELECT * FROM orders;

 Bitmap Heap Scan on orders
   Recheck Cond: (tenant_id = (NULLIF(current_setting('app.tenant_id'::text, true), ''::text))::integer)
   ->  Bitmap Index Scan on orders_tenant_idx
         Index Cond: (tenant_id = (NULLIF(current_setting('app.tenant_id'::text, true), ''::text))::integer)
```

ポリシーが計画に入っているかを確かめるときは、WHERE句を付けずに、検証したいロールへ切り替えてから実行します。WHERE句を書いた場合も計画にはポリシーの条件と検索条件の両方が反映されますが、どちらの条件がインデックスを使っているのか読み分けにくくなります。RLSの適用確認はWHERE句なしで行い、性能の評価は本番と同じ検索条件を付けたクエリで別に行います。計画にポリシーの条件が現れず全行を読んでいるなら、接続ロールが所有者やBYPASSRLSでないかを疑います。

遅くなるのは、ポリシーが比較する列にインデックスが無い場合と、条件式の中の関数やサブクエリが行ごとに実行される場合です。Supabaseの公式ドキュメントは、ポリシー内の `auth.uid()` を `(select auth.uid())` と副問い合わせで包み、PostgreSQLに一度だけ評価させる書き方を勧めています（[Supabase公式ドキュメント「Row Level Security」](https://supabase.com/docs/guides/database/postgres/row-level-security)）。前掲のtenant\_isolationを同じ形に書き換えると次のとおりです。

```
DROP POLICY tenant_isolation ON orders;
CREATE POLICY tenant_isolation ON orders
  FOR ALL TO app_user
  USING      (tenant_id = (SELECT NULLIF(current_setting('app.tenant_id', true), '')::integer))
  WITH CHECK (tenant_id = (SELECT NULLIF(current_setting('app.tenant_id', true), '')::integer));

-- 同じ EXPLAIN の結果（18.6）
 Bitmap Heap Scan on orders
   Recheck Cond: (tenant_id = (InitPlan 1).col1)
   InitPlan 1
     ->  Result
   ->  Bitmap Index Scan on orders_tenant_idx
         Index Cond: (tenant_id = (InitPlan 1).col1)
```

包んだ式は「InitPlan 1」として文の先頭で1回だけ計算され、その値でインデックスを引く計画になりました。この書き換えは、関数の結果が行のデータに依存しない場合に限ります。行ごとに結果が変わる関数を包むと意味が変わります。

もう1つの落とし穴が評価順です。ユーザーのクエリにLEAKPROOFでない関数を使った条件があると、その条件はポリシーの後にしか評価できず、インデックスで絞る順序が崩れることがあります。性能のためにRLSを外すのではなく、インデックスと条件式の書き方で解決します。ブラウザからData APIを経由してPostgreSQLへアクセスするSupabaseの構成でのポリシー設計は、[Supabase RLSの設定手順とAuth連携ポリシー設計｜有効化漏れの検知まで](/tech/details/17056/)で扱っています。

## RLSを採用すべき構成と見送るべき構成

RLSの採用を優先して検討したいのは、次の2つが揃う構成です。

- 1つのデータベース・1つのスキーマを、複数の顧客や部門で共有している
- DBへのアクセス経路が複数ある（Web画面・API・バッチ・BIツール・管理画面）

経路が1つ増えるたびに、アプリ層でテナント条件を書き忘れる機会も1つ増えます。開発者の入れ替わりがあり、全員が規律を守り続けることを前提にできないチームや、顧客への説明で「DBの層でも分離している」と示す必要がある場合も、採用に傾きます。

逆に、次の場面では見送ってアプリ層の制御かDBの分離で対応します。

- テナントごとにデータベースやスキーマを分けており、分離がすでに達成されている
- アクセス経路がAPIサーバー1つで、認可がそこに集約されている
- 行の見え方が画面の都合で頻繁に変わり、DBのポリシーとして固定しにくい
- チームにDBのロールと権限を設計・運用できる人がいない

判断を最も左右するのは最後の項目です。RLSは、アプリ用ロールと所有者ロールの分離、TRUNCATEやBYPASSRLSの権限の棚卸しという土台の上でしか働きません。土台が無いまま入れると、「RLSを入れたので安全」という誤った安心だけが残ります。テナント識別子を持たせるテーブル設計や、既存DBへ後からRLSを入れる移行手順は[データベース設計・移行支援](/service/system/database/)で、稼働中のシステムに他テナントのデータへ届く経路が残っていないかは[脆弱性診断・セキュリティ診断](/service/system/vulnerability/)で相談できます。

## よくある質問

### RLSとは何の略で、ITではどういう意味ですか？

RLSはRow Level Securityの略で、日本語では「行レベルセキュリティ」と訳されます。データベースのテーブルの行ごとに、どのユーザーが閲覧・更新できるかをDBが制御するセキュリティ機能です。医療ではレストレスレッグス症候群（むずむず脚症候群）の略としても使われますが、ITシステムやデータベース、BIツールの文脈ではこの行レベルセキュリティを指します。

### RLSを有効にしたのに全行が見えるのはなぜですか？

PostgreSQLでは、スーパーユーザーとBYPASSRLS属性を持つロールが常にポリシーを素通りし、テーブル所有者もFORCE ROW LEVEL SECURITYを指定しない限り素通りします。動作確認やアプリの接続にこれらのロールを使っていないかを確かめてください。ビュー所有者が元テーブルのRLSを迂回できる場合は、既定のビュー経由でも全行が見えることがあります。Power BIでは、ワークスペースの管理者・メンバー・共同作成者にRLSが効かないことが同じ症状の原因になります。

### RLSを使うとクエリは遅くなりますか？

ポリシーが単純な比較ならインデックスを利用できる場合がありますが、実行計画や追加コストはデータ分布・取得件数・条件式によって変わります。対象ロールでEXPLAIN ANALYZEを実行し、実行時間と処理行数を確認してください。遅くなるのは、条件の列にインデックスが無い場合と、条件式の中で関数やサブクエリが行ごとに実行される場合です。行データに依存せず文内で結果を共用できる関数については、`(SELECT 関数())` で包む方法を検討します。

### MySQLでRLSは使えますか？

MySQLには行単位のポリシー機能がありません。2026年7月の26.7.0でも構文が無く、9.7.0でEnterprise Edition向けに入ったMASKING POLICYは列の値を伏せる機能です。元テーブルの権限を与えず、USER()で行を絞ったビューだけに権限を与える方式で近い動作を作ります。

### マルチテナントはRLSとDB分離のどちらがよいですか？

顧客数が多く1社あたりのデータ量が小さいSaaSでは、共有スキーマにRLSを掛ける構成が運用とコストの面で有利です。顧客ごとの個別バックアップや厳格な分離が契約で求められる場合、1社のデータ量が突出して大きい場合はDBやスキーマを分けます。大口顧客だけを専用DBに逃がし、それ以外を共有スキーマとRLSで扱う混在型も選べます。

## 関連記事

- [Supabase RLSの設定手順とAuth連携ポリシー設計｜有効化漏れの検知まで](/tech/details/17056/)
- [PostgreSQLのロールと権限設計｜CREATE ROLEの属性とGRANT・既定権限の決め方](/tech/details/16984/)
- [シングルテナントとマルチテナントの違い｜比較表・選定基準とテナント分離の実装](/tech/details/7733/)
- [認可バイパスとは？IDOR・権限昇格の仕組みとサーバ側で止める実装対策を解説](/tech/details/16399/)
- [RBACとは？ロールベースアクセス制御の仕組みと導入判断を実装視点で解説](/tech/details/6488/)
- [GETTIIS不正アクセスの件数と対応：主催者向けGettii-Liteから会員DBを分ける設計](https://www.issoh.co.jp/tech/details/18769/)：主催者向けの管理系アプリから会員DBへの到達を絞る設計を、RLSと列権限で示した事故事例です。

---

出典: [RLS（Row Level Security）とは？DBの行レベルセキュリティの仕組みと設定例](<https://www.issoh.co.jp/tech/details/5442/>)（株式会社一創）
