Created
July 6, 2025 10:00
-
-
Save k12u/6c056363d51091f80071858b28430b82 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # PostgreSQL Row-Level Security (RLS) パフォーマンス検証レポート | |
| ## 概要 | |
| このレポートは、PostgreSQLのRow-Level Security (RLS)機能のパフォーマンスへの影響を検証した結果をまとめたものです。特に、インデックスの有無によるクエリ性能の違いに焦点を当てています。 | |
| ## 検証環境 | |
| - **PostgreSQL**: 16.9 (Docker) | |
| - **データ量**: 100万件(テナント2社、各50万件) | |
| - **テーブル構造**: | |
| ```sql | |
| CREATE TABLE tenant ( | |
| id SERIAL PRIMARY KEY, | |
| name VARCHAR(100) NOT NULL UNIQUE, | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
| ); | |
| CREATE TABLE project ( | |
| id SERIAL PRIMARY KEY, | |
| name VARCHAR(255) NOT NULL, | |
| description TEXT, | |
| tenant_id INTEGER NOT NULL REFERENCES tenant(id), | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| data JSONB DEFAULT '{}'::jsonb | |
| ); | |
| ``` | |
| ## 検証内容 | |
| ### 1. RLSポリシーの設定 | |
| ```sql | |
| -- RLSを有効化 | |
| ALTER TABLE project ENABLE ROW LEVEL SECURITY; | |
| -- RLSポリシーの作成 | |
| CREATE POLICY project_rls_policy ON project | |
| FOR ALL | |
| USING (tenant_id = current_setting('rls.tenant_id', true)::integer); | |
| ``` | |
| ### 2. データ分布 | |
| ```sql | |
| SELECT t.name, COUNT(p.id) as project_count | |
| FROM tenant t | |
| LEFT JOIN project p ON t.id = p.tenant_id | |
| GROUP BY t.id, t.name; | |
| ``` | |
| **結果**: | |
| ``` | |
| name | project_count | |
| ----------+--------------- | |
| tenant_b | 500000 | |
| tenant_a | 500000 | |
| ``` | |
| ## パフォーマンス検証結果 | |
| ### 仮説A: RLSはPostgreSQL内部でクエリにWHERE句を追加する | |
| **検証結果: ✅ 正しい** | |
| RLSが有効な状態での実行計画: | |
| ```sql | |
| EXPLAIN (VERBOSE) | |
| SELECT * FROM project WHERE name LIKE 'Project A-%' LIMIT 10; | |
| ``` | |
| **結果**: | |
| ``` | |
| Limit | |
| Output: id, name, description, tenant_id, created_at, data | |
| -> Index Scan using idx_project_tenant_id on public.project | |
| Output: id, name, description, tenant_id, created_at, data | |
| Index Cond: (project.tenant_id = (current_setting('rls.tenant_id'::text, true))::integer) | |
| Filter: ((project.name)::text ~~ 'Project A-%'::text) | |
| ``` | |
| RLSポリシーが`Index Cond`として内部的に追加されていることが確認できます。 | |
| ### 仮説B: インデックスがなければフルテーブルスキャンになる | |
| **検証結果: ✅ 正しい** | |
| #### インデックスなしでのクエリ実行 | |
| ```sql | |
| DROP INDEX IF EXISTS idx_project_tenant_id; | |
| EXPLAIN (ANALYZE, BUFFERS) | |
| SELECT COUNT(*) FROM project; | |
| ``` | |
| **実行計画**: | |
| ``` | |
| Finalize Aggregate (cost=30013.62..30013.63 rows=1 width=8) (actual time=99.431..102.048 rows=1 loops=1) | |
| Buffers: shared hit=12639 read=7520 | |
| -> Gather (cost=30013.41..30013.62 rows=2 width=8) | |
| Workers Planned: 2 | |
| Workers Launched: 2 | |
| -> Partial Aggregate (cost=29013.41..29013.42 rows=1 width=8) | |
| -> Parallel Seq Scan on project | |
| Filter: (tenant_id = (current_setting('rls.tenant_id'::text, true))::integer) | |
| Rows Removed by Filter: 166667 | |
| Buffers: shared hit=12639 read=7520 | |
| Execution Time: 102.129 ms | |
| ``` | |
| **実行時間**: 81.830 ms | |
| ### 仮説C: tenant_idにインデックスがあれば高速化される | |
| **検証結果: ✅ 正しい** | |
| #### インデックスありでのクエリ実行 | |
| ```sql | |
| CREATE INDEX idx_project_tenant_id ON project(tenant_id); | |
| ANALYZE project; | |
| EXPLAIN (ANALYZE, BUFFERS) | |
| SELECT COUNT(*) FROM project; | |
| ``` | |
| **実行計画**: | |
| ``` | |
| Finalize Aggregate (cost=9068.98..9068.99 rows=1 width=8) (actual time=30.502..33.133 rows=1 loops=1) | |
| Buffers: shared hit=3 read=423 | |
| -> Gather (cost=9068.77..9068.98 rows=2 width=8) | |
| Workers Planned: 2 | |
| Workers Launched: 2 | |
| -> Partial Aggregate (cost=8068.77..8068.78 rows=1 width=8) | |
| -> Parallel Index Only Scan using idx_project_tenant_id on project | |
| Index Cond: (tenant_id = (current_setting('rls.tenant_id'::text, true))::integer) | |
| Heap Fetches: 0 | |
| Buffers: shared hit=3 read=423 | |
| Execution Time: 33.167 ms | |
| ``` | |
| **実行時間**: 14.909 ms (**5.5倍高速**) | |
| ## パフォーマンス比較サマリー | |
| | クエリタイプ | インデックスなし | インデックスあり | 改善率 | | |
| |------------|---------------|---------------|-------| | |
| | COUNT(*) | 81.830 ms | 14.909 ms | 5.5x | | |
| | 名前パターン検索 (LIKE) | - | 52.754 ms | - | | |
| | ランダムアクセス (id=100) | - | 0.557 ms | - | | |
| | ページネーション (OFFSET 1000 LIMIT 10) | - | 0.784 ms | - | | |
| ## 追加の発見事項 | |
| ### 1. スーパーユーザーのRLSバイパス | |
| スーパーユーザーとして実行した場合: | |
| ```sql | |
| SELECT current_user, current_setting('is_superuser'); | |
| -- 結果: testuser | on | |
| SELECT COUNT(*) FROM project; | |
| -- 結果: 1000000 (全レコード) | |
| ``` | |
| 通常ユーザーとして実行した場合: | |
| ```sql | |
| SET ROLE test_user; | |
| SET rls.tenant_id = 1; | |
| SELECT COUNT(*) FROM project; | |
| -- 結果: 500000 (tenant_aのレコードのみ) | |
| ``` | |
| ### 2. 各テナントのデータアクセス確認 | |
| **Tenant A としてアクセス**: | |
| ```sql | |
| SET rls.tenant_id = 1; | |
| SELECT id, name, tenant_id FROM project LIMIT 5; | |
| ``` | |
| 結果: | |
| ``` | |
| id | name | tenant_id | |
| ----+-------------+----------- | |
| 1 | Project A-1 | 1 | |
| 2 | Project A-2 | 1 | |
| 3 | Project A-3 | 1 | |
| 4 | Project A-4 | 1 | |
| 5 | Project A-5 | 1 | |
| ``` | |
| **Tenant B としてアクセス**: | |
| ```sql | |
| SET rls.tenant_id = 2; | |
| SELECT id, name, tenant_id FROM project LIMIT 5; | |
| ``` | |
| 結果: | |
| ``` | |
| id | name | tenant_id | |
| --------+-------------+----------- | |
| 500001 | Project B-1 | 2 | |
| 500002 | Project B-2 | 2 | |
| 500003 | Project B-3 | 2 | |
| 500004 | Project B-4 | 2 | |
| 500005 | Project B-5 | 2 | |
| ``` | |
| ## 結論 | |
| 1. **RLSは内部的にWHERE句相当の処理を追加**: 実行計画でFilter条件として確認できる | |
| 2. **インデックスなしではフルテーブルスキャン**: Parallel Seq Scanが発生し、パフォーマンスが大幅に低下 | |
| 3. **適切なインデックスで5.5倍の高速化**: tenant_idへのインデックスによりIndex Only Scanが可能になる | |
| 4. **スーパーユーザーはRLSをバイパス**: アプリケーションでは通常ユーザーを使用すべき | |
| ## 推奨事項 | |
| 1. RLSを使用する場合は、ポリシーで使用するカラムに必ずインデックスを作成する | |
| 2. アプリケーションからはスーパーユーザーではなく、適切な権限を持つ通常ユーザーを使用する | |
| 3. 大量データを扱う場合は、実行計画を確認して適切なインデックス戦略を立てる | |
| 4. セッション変数の設定漏れを防ぐため、アプリケーション側で確実に設定する仕組みを構築する |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment