RLS 效能診斷:EXPLAIN ANALYZE、PGRST003 pool timeout、connection pool 耗盡、API 變慢。NOT for RLS policy 的正確性設計(走 supabase-rls)。
Scanned 9/5/2026
Install to Claude Code
npx -y skills add Charles5277/nuxt-supabase-starter --skill rls-performance --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Rls Performance?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/charles5277-rls-performance)More formats (shields.io, HTML) on the badges page.
---
name: rls-performance
description: >-
RLS 效能診斷:EXPLAIN ANALYZE、PGRST003 pool timeout、connection pool 耗盡、API 變慢。NOT for RLS policy 的正確性設計(走 supabase-rls)。
---
<!-- 🔒 LOCKED — managed by clade · auto-generated by sync-to-cursor; 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 做」)仍在 `.cursor/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 設計(RLS 專屬部分)
**Policy WHERE/USING 引用的欄位**(`user_id`、`tenant_id`、`department_id`)**MUST** 有 index ——
RLS 是 per-row 檢查,這些欄位沒 index 等於每列都掃一次。這條是 RLS 特有的,一般 query 的索引
直覺涵蓋不到它。
index 類型選擇、partial / composite / covering index、FK index、找出零使用量 index:走上游
`supabase-postgres-best-practices` 的 `references/query-*.md` 與 `schema-foreign-key-indexes.md`,
本檔不重複。
## Pagination
深層 pagination 的通則(cursor-based 優於 `limit + offset`)見上游
`references/data-pagination.md`。自家慣例只有一條:`PAGE_SIZE_MAX` **MUST** 從
`shared/schemas/pagination` 取用,不要各處自訂上限。
## 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 — 找連線洩漏
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!