---
name: rls-performance
description: >-
  Postgres + Supabase RLS 效能診斷與優化手冊。Use when 寫/改 RLS policy、
  跑 EXPLAIN ANALYZE、排查 PGRST003 pool timeout、設計 index、
  優化 pagination、使用者抱怨 API 變慢、或需要診斷 connection pool
  問題時。涵蓋 pg_stat_activity 診斷、角色對照、self-hosted LXC
  責任模型、效能基準與事故恢復 SOP。
---
<!-- 🔒 LOCKED — managed by clade · auto-generated by sync-to-agents; edit source in .claude/ then re-run sync -->

# RLS Performance Playbook

大型專案常累積大量 RLS policy 與 SECURITY DEFINER function，加上 self-hosted LXC 的 connection pool 限制，任何 N+1 或 full scan 都會被放大。本 skill 是遇到效能問題時的操作手冊。

決策原則（「MUST 做 / NEVER 做」）仍在 `.claude/rules/{database,database-design}.md`，本檔提供**實際診斷與優化工具**。

## 何時開啟本 skill

- 新增涉及 policy join 的表
- 修改既有 RLS policy 的 WHERE 條件
- 新增 server API endpoint 含 pagination / filter
- 遇到 `PGRST003`（504 timeout）或 pool 耗盡
- 使用者抱怨特定頁面 / endpoint 變慢
- 需要稽核 production 效能或清理無用 index
- 排查 LXC 連線問題 / Tunnel 斷線

**核心原則**：policy 改動前先量，改動後驗證，不要憑感覺優化。

## EXPLAIN ANALYZE — 正確的 RLS 測量方式

**關鍵陷阱**：superuser 跑 EXPLAIN 會 **bypass RLS**，測出來的 plan 跟 production 完全不同。**一定要** `set local role` 模擬目標角色。

```sql
-- 1. 模擬目標 role（不是 postgres superuser！）
set local role authenticated;
set local request.jwt.claims to '{"sub": "<user_uuid>", "role": "authenticated"}';

-- 2. 跑實際 query
explain (analyze, buffers, verbose, format text)
select ... from <schema>.<table> where ...;

-- 3. 還原
reset role;
```

### 讀 plan 重點

| Plan 片段                          | 意義                         | 處理                                               |
| ---------------------------------- | ---------------------------- | -------------------------------------------------- |
| `Seq Scan on xxx`                  | 全表掃描                     | 檢查 WHERE 欄位是否有 index                        |
| `Rows Removed by Filter: > 1000`   | Index 取太多又濾掉           | Composite index 或 partial index                   |
| `Subquery Scan ... InitPlan`       | `(SELECT auth.uid())` 有快取 | ✅ 正確模式                                        |
| `Filter: (auth.uid() = user_id)`   | **沒快取**                   | 改 subselect `(SELECT auth.uid())`                 |
| `Nested Loop` + 高 rows            | Policy 內 per-row JOIN       | 改 subselect 預先算 ID set                         |
| `Planning Time` > `Execution Time` | Plan cache 沒命中            | 通常是 schema cache 過期或 prepared statement 問題 |

## Index 設計

### 必須加 index 的欄位

- **Policy WHERE/USING 引用的欄位**（`user_id`, `tenant_id`, `department_id`）— RLS per-row 檢查
- **FK 欄位**（`xxx_id`）— JOIN 效能 + delete cascade
- **常用 filter 欄位**（`status`, `created_at`, `date`）
- **Order by 欄位**（用於 `limit + offset` pagination）

### Index 類型選擇

| 資料特性                 | Index 類型       | 範例                                                            |
| ------------------------ | ---------------- | --------------------------------------------------------------- |
| 等值查找（`= ?`）        | B-tree（預設）   | `create index on x (user_id)`                                   |
| 範圍查找（`> ?`）        | B-tree           | `create index on x (created_at)`                                |
| 不常更新的遞增 timestamp | BRIN             | `create index on x using brin(created_at)` — 比 B-tree 小 10 倍 |
| JSONB 欄位過濾           | GIN              | `create index on x using gin(details)`                          |
| 全文搜尋                 | GIN (`tsvector`) | `create index on x using gin(to_tsvector('simple', name))`      |

### Partial Index

對「只關心部分資料」的 query 效果顯著：

```sql
-- 只查未完成的任務
create index idx_tasks_pending on tasks (assigned_to)
where status = 'pending';
```

### Composite Index 順序

欄位順序影響命中：`(a, b)` 能用於 `WHERE a=? AND b=?` 與 `WHERE a=?`，但**不能**用於 `WHERE b=?`。放最常過濾的欄位在前。

### 過度 index 的代價

- 每個 index 都會拖慢 INSERT / UPDATE / DELETE
- Index 佔 disk、記憶體
- **MUST** 定期用 `pg_stat_user_indexes` 找出零使用量的 index 並移除

## Pagination

- **NEVER** 用純 `limit + offset` 做深層 pagination（offset 很大時 Postgres 仍會讀過所有 rows 再丟掉）
- **Cursor-based pagination**：用 `where id > :last_id order by id limit :page_size`
- `PAGE_SIZE_MAX` 從 `shared/schemas/pagination` 取用 — 避免前端傳巨大 pageSize

## Connection Pool 診斷（pg_stat_activity）

Self-hosted Supabase 的連線結構：

- **PostgREST (`authenticator` role)** 佔用 pool，所有 `/rest/v1/*` 請求共用
- **Auth (`supabase_auth_admin`)**、**Storage (`supabase_storage_admin`)**、**Realtime (`supabase_admin`)** 各自佔用
- **Supavisor pool** 是所有 client 的總閘口，預設 pool size 不應超過 DB `max_connections` 的 **40%**（若重度使用 PostgREST），其餘留給 Auth / Storage / admin

### 診斷 query

當 API 變慢或 `PGRST003`（504 timeout）出現，在目標 LXC 上跑：

```sql
-- 所有 live connection
SELECT
  pid, usename, application_name, client_addr,
  state, query_start, backend_start,
  left(query, 80) as query
FROM pg_stat_activity
WHERE datname = 'postgres'
ORDER BY backend_start DESC;

-- 按角色分組統計
SELECT usename, state, count(*)
FROM pg_stat_activity
WHERE datname = 'postgres'
GROUP BY usename, state
ORDER BY count DESC;

-- 找 idle connection 超過 5 分鐘
SELECT pid, usename, state, query_start, left(query, 80)
FROM pg_stat_activity
WHERE state = 'idle' AND state_change < now() - interval '5 minutes';
```

### 角色對照表

| `usename`                    | 來源                      |
| ---------------------------- | ------------------------- |
| `authenticator`              | PostgREST（Data API）     |
| `supabase_auth_admin`        | GoTrue                    |
| `supabase_storage_admin`     | Storage                   |
| `supabase_admin`             | Realtime / 監控           |
| `supabase_replication_admin` | Read replica sync         |
| `postgres`                   | Dashboard / psql / Prisma |

### 症狀 → 原因速查

- **大量 `authenticator` idle in transaction** → 前端有未完成的 transaction，或 `db_pre_request` hook 異常
- **`supabase_admin` 連線數突然暴增** → Studio schema introspection（通常來自 MCP `list_tables` / Kong 8001）
- **pool 滿到拒絕新連線** → 檢查 Supavisor pool size 配置、Compute add-on 的 max_connections

## pg_stat_statements — 找慢 query

```sql
-- 開啟 pg_stat_statements（已啟用）
select
  calls,
  total_exec_time::int as total_ms,
  mean_exec_time::int as mean_ms,
  query
from pg_stat_statements
where query ilike '%<your-schema>.%'  -- 換成業務 schema（或拿掉 where 子句看全部）
order by total_exec_time desc
limit 20;
```

## 效能基準

### Query 延遲

| Query 類型                    | 可接受延遲 | 異常訊號             |
| ----------------------------- | ---------- | -------------------- |
| Single row by PK              | < 5ms      | > 50ms 要查          |
| List with filter + pagination | < 50ms     | > 200ms 要查         |
| Aggregation / group by        | < 200ms    | > 1s 告警            |
| Full-text search              | < 100ms    | > 500ms 查 GIN index |

### RLS policy 開銷

| Policy 類型                          | 可接受延遲增量 |
| ------------------------------------ | -------------- |
| 單欄位比對（`user_id = auth.uid()`） | < 1ms          |
| 單層 subselect（`IN (select ...)`）  | < 5ms          |
| JOIN 到另一表（應避免）              | > 10ms（警訊） |

**異常訊號**：若 `Execution Time` 在有/無 policy 差異 > 50%，或出現 `Seq Scan` → 檢查 index 與 policy 寫法。

## Transaction 與批次寫入

- **批次寫入 ≤500 筆/transaction** — 超過會造成 WAL bloat 與 replica lag
- **避免 long-running transaction** — `idle in transaction` > 5 分鐘會卡 vacuum、blocking lock
- **Server 端遇到 `40001`（serialization_failure）** — retry transaction，但需有 idempotency 保證（見 `api-patterns.md`）
- **NEVER** 在 RLS policy 中放重型 JOIN 或外部函式 — 會被 per-row 執行
- **Dry run**：production 大量 UPDATE/DELETE 前先跑 `EXPLAIN` 確認 plan，避免 full scan

## Self-Hosting LXC 責任模型

當專案自建 dev / prod LXC（Docker Compose 部署），Supabase Cloud 提供的功能必須自行維護：

| 面向              | Cloud 提供                 | 自建需自行處理                              |
| ----------------- | -------------------------- | ------------------------------------------- |
| OS 與 kernel 更新 | 自動                       | `apt upgrade` 週期 + 重啟排程               |
| Postgres 備份     | PITR + 每日 snapshot       | `pg_dump` 排程 + Tailscale 拉到 NAS         |
| 監控告警          | Dashboard charts + Grafana | 目前無 — 倚賴 `docker logs` + 手動 SSH 檢查 |
| SSL / HTTPS       | 自動                       | Cloudflare Tunnel 代管（cloudflared）       |
| Rate limiting     | Platform 層                | Nuxt 層自行實作（目前無）                   |
| Schema cache 重載 | 自動                       | `notify pgrst, 'reload schema'` 手動        |
| Postgres 升級     | 一鍵                       | 需手動規劃 + 停機                           |

### 關鍵依賴

- **Cloudflare Tunnel**（若採此架構）：production LXC 對外唯一入口。Tunnel 斷 = 全站掛（登入 / API 都不通）。任何影響 host 頻寬的操作都會波及（見 `database.md` Production 存取安全規範）
- **Tailscale**（若採此架構）：Dev / 備份 / SSH 的通道。若 relay mode 密集連線會癱瘓整條網路
- **備份策略**：自架沒有內建 PITR / 自動 snapshot — **MUST** 規劃排程備份（例：`pg_dump` 經 Tailscale 推到 NAS / 物件儲存）；無備份 = 復原倚賴最近一次手動 dump

## 效能事故處理 SOP

API 變慢或 `PGRST003` 出現時：

1. 先看 `log.error` 是否有 `PGRST003`（pool 耗盡）
2. SSH 到目標 LXC 跑 `pg_stat_activity`（上方診斷 query）找 long-running query
3. 取該 query 跑 `EXPLAIN ANALYZE`（記得 `set local role`）
4. 若是 RLS policy 問題 → 改 policy 或加 index
5. 若是 N+1 → 改用 `select(*, related(*))` embed 或 RPC

## 定期稽核

- `pg_stat_user_indexes` — 找零使用量 index
- `pg_stat_statements` — 找 total_exec_time 最高的 query
- `pg_stat_activity` idle > 5 min — 找連線洩漏
