連線管理與 Pooler:Supavisor 實戰 | Supabase 完整教學

2026/09/27
連線管理與 Pooler:Supavisor 實戰 | Supabase 完整教學

在 Supabase 的世界裡,寫好查詢只是第一步,能不能在高流量下「穩得住」,關鍵往往在連線管理。每一條 Postgres 連線都要吃掉伺服器記憶體,而 Serverless 環境動不動就想開一條新連線——這篇帶你搞懂 Supabase 自研的連線池器 Supavisor、transaction mode 與 session mode 的差異、direct 與 pooled 的埠與連線字串,以及為什麼 Serverless 一定要走 transaction pooler。

前言

一句話定義本篇主題:這篇要教你搞懂 Supabase 的連線管理——為什麼需要 connection pooler(連線池)、Supabase 自研的 Supavisor 如何運作、Direct Connection 與 pooled 連線的差別、transaction mode 與 session mode 各自的埠與適用場景,以及在 Serverless/Edge 情境為何非用 transaction pooler 不可。

上一篇《Database Functions 與 Triggers》我們讓資料庫「動了起來」——用函式封裝邏輯、用觸發器在資料變更瞬間自動反應。但當你的應用開始有真實流量,尤其是部署到 Vercel、Netlify、Supabase Edge Functions 這類 Serverless 平台時,一個新的瓶頸會浮現:連線數。你會發現查詢明明沒問題,網站卻突然回報「too many connections」而整個掛掉。這一篇就是要解決這個問題,也為整個「資料庫段」畫下句點。

用一個現實類比:Postgres 的連線就像一家餐廳的「桌位」,數量有限;連線池(Pooler)則像餐廳門口的「帶位服務生」。 如果沒有服務生,每位客人(請求)都自己衝進去佔一桌,就算只是進來喝杯水(跑一句短查詢)也要獨佔整桌,人一多桌位立刻被佔滿,後面的客人只能吃閉門羹。而帶位服務生(Pooler)的做法是:讓多位客人輪流共用少數幾桌——你點餐、上菜、結帳(一個交易)的期間才佔桌,一結束馬上讓出來給下一位。於是同樣的桌位數,能服務的客人翻了好幾倍。Supabase 的這位「服務生」就叫 Supavisor。

本篇你會學到:

  • 為何需要連線池:Postgres 連線的成本、Serverless 為何特別容易耗盡連線
  • Supavisor 是什麼:Supabase 自研的 Postgres 連線池器,如何在多客戶端間分配少數連線
  • 兩種池化模式:transaction mode vs session mode 的差異、各自的埠與適用場景
  • 實務設定:Direct/Session/Transaction 三種連線字串怎麼寫、Serverless 該怎麼配、prepared statements 的坑怎麼繞

核心概念

為什麼需要連線池

先理解一件事:Postgres 的連線很「貴」。 每建立一條連線,Postgres 就會 fork 出一個獨立的後端行程,配置專屬記憶體。因此資料庫有一個連線數上限(依方案與機器規格而定,小方案可能只有幾十到上百條)。當同時開啟的連線超過上限,新的連線就會被拒絕,你會看到經典的 remaining connection slots are reserved 或 sorry, too many clients already 錯誤。

問題在傳統的長駐後端(一台 VM、一個常開的容器)不明顯——它啟動時建立一個固定大小的連線池(例如 10 條),之後所有請求共用這幾條,連線數穩定可控。但 Serverless/Edge 環境完全不同:

  • 每個請求可能觸發一個全新的函式實例,各自初始化、各自開連線
  • 流量尖峰時可能同時有數百個實例在跑,如果每個都開一條 Direct 連線,瞬間就爆掉連線上限
  • 函式實例生命週期短、來去頻繁,連線建立/關閉的成本被放大

這就是連線池要解決的核心矛盾:用戶端想要「很多條連線」,但資料庫只能給「少數幾條」。 連線池坐在中間,對外假裝有海量連線可用,對內只維持少數實體連線,把用戶端的請求聰明地排到這幾條實體連線上輪流跑。

Supavisor:Supabase 的連線池器

Supavisor 是 Supabase 自行開發的雲原生 Postgres 連線池器(Connection Pooler),你可以把它想成「Postgres 連線的負載平衡器」。它接受來自成千上萬客戶端的連線,並在背後智慧地把這些請求分配到有限的實體 Postgres 連線上。傳統上這個角色由 PgBouncer 擔任,Supavisor 則是為多租戶、雲端規模設計的替代方案,行為上與 PgBouncer 相容(例如同樣支援 transaction 與 session 兩種池化模式)。

Supavisor 提供兩種池化模式,這是本篇最核心的觀念:

模式連線何時借出/歸還支援 prepared statements典型場景
Session Mode(工作階段模式)整個 session(連線)期間都綁定同一條實體連線✅ 支援需要 IPv4 的長駐後端、需要 session 狀態或 prepared statements
Transaction Mode(交易模式)只在「一個交易進行中」借出,交易結束立刻歸還❌ 不支援Serverless、Edge Functions、高並發

理解這兩種模式的差別,只要抓住「連線綁定的時間長短」:

  • Session Mode 一綁就綁一整個連線期間。你連上來、做很多事、直到斷線,這條實體連線都是你的。因為狀態(包含 prepared statements、session 變數)全程保留,行為最接近直連,但每個客戶端都獨佔一條實體連線,池化的節省效果較有限。
  • Transaction Mode 只綁一個交易。你開始一個交易(BEGIN)到結束(COMMIT/ROLLBACK)之間才佔用一條實體連線,交易一結束連線馬上還回池裡給別人用。因此少數幾條實體連線就能服務大量並發客戶端,重用率最高——這正是 Serverless 需要的。代價是:交易之間你可能被分到不同的實體連線,任何「跨交易保留在連線上的狀態」都會失效,這就是它不支援 prepared statements 的根本原因。

Direct Connection vs Pooled

除了兩種池化模式,還有一個「完全不經過池化」的選項:Direct Connection(直接連線)。三者一起看:

連線方式是否經過 Supavisor埠(以官方為準)適用場景
Direct Connection否,直連資料庫本體通常 5432migration、pg_dump 備份、有 IPv6 的長駐後端
Session Pooler是,session 模式通常 5432(pooler 主機)IPv4 環境的長駐後端、需要 prepared statements
Transaction Pooler是,transaction 模式通常 6543(pooler 主機)Serverless、Edge、高並發

這裡有一個很多人踩過的坑:IPv4 / IPv6。Direct Connection 的主機在許多方案下只提供 IPv6 位址,如果你的執行環境(某些雲端函式平台、老舊網路)不支援 IPv6,直連就會失敗。這時候即使你只是要一條長駐連線,也得改走 Session Pooler——因為 pooler 主機同時提供 IPv4,等於幫你繞過 IPv6 的限制。所以「選 Session Pooler」有時不是為了池化的省連線效果,純粹是為了 IPv4 相容性。

一句話收束核心概念:跑 migration/備份用 Direct;長駐後端有 IPv6 用 Direct、只有 IPv4 用 Session Pooler;Serverless/Edge 一律用 Transaction Pooler。 記住這條,大部分連線抉擇都不會錯。

實作範例

以下所有連線字串都以佔位符呈現(主機、專案參照、密碼請換成你自己的)。取得真實字串的正確方式:到 Supabase Dashboard 右上角點 Connect,裡面會直接給你 Direct、Session Pooler、Transaction Pooler 三組完整字串與正確的埠——一律以那裡顯示的為準,不要硬記埠號。

三種連線字串長什麼樣

連線字串的通用格式是 postgresql://[使用者]:[密碼]@[主機]:[埠]/[資料庫]。三種連線方式的差別就在主機、埠、以及使用者名稱格式:

# 1) Direct Connection —— 直連資料庫本體,埠通常 5432
#    主機是 db.[project-ref].supabase.co;使用者就是 postgres
postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres

# 2) Session Pooler —— 走 pooler 主機,埠通常 5432
#    使用者要帶專案參照:postgres.[project-ref]
postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres

# 3) Transaction Pooler —— 走 pooler 主機,埠通常 6543(Serverless 用這個)
postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres

# 建議一律加上 SSL 確保連線加密
postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres?sslmode=require

注意 pooler 的兩組(Session/Transaction)使用者名稱都是 postgres.[PROJECT-REF] 的格式,且主機相同、只差在埠。埠號會依你的方案與區域而定,請以 Dashboard 的 Connect 頁面為準(以官方為準)。

用 Direct Connection 跑 migration 與備份

需要「完整連線能力」的操作——遷移、備份、大量匯入——用 Direct Connection 最合適,因為這些操作往往需要 prepared statements、暫存或 session 狀態,不適合走 transaction pooler:

# psql 直連
psql "postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"

# pg_dump 備份(自訂格式,方便還原)
pg_dump "postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres" \
  -Fc -f backup.dump

在 Serverless 用 Transaction Pooler(Node.js 範例)

Serverless 的正確做法有兩個重點:一、走 Transaction Pooler 的連線字串;二、在模組層級(而非每次請求)建立連線池,讓實例存活期間重用連線。 以 node-postgres(pg)為例:

// db.js —— 在模組層級建立 Pool,實例存活期間重用,而非每次請求都新建
import { Pool } from 'pg'

const pool = new Pool({
  // 這裡放 Transaction Pooler 的連線字串(埠 6543)
  connectionString: process.env.DATABASE_URL,
  max: 5, // Serverless 環境刻意用「小」pool,避免眾多實例合計爆掉連線
})

export default pool
// handler.js —— 每次請求向 pool 借一條、用完歸還
import pool from './db.js'

export async function handler(req) {
  const client = await pool.connect()
  try {
    const { rows } = await client.query('select id, title from posts limit 20')
    return rows
  } finally {
    client.release() // 一定要歸還,否則連線會洩漏
  }
}

反面教材是「每次請求都 new Client() 再 connect()」——那等於每個請求各開一條新連線,Serverless 一放大就把上限打爆。務必在模組層級建立 Pool。

用 Prisma 走 Transaction Pooler:停用 prepared statements

Prisma 預設會用 prepared statements,走 Transaction Pooler 時必須在連線字串加上 pgbouncer=true 來停用它,否則會報錯:

# .env
# Transaction Pooler(埠 6543)+ pgbouncer=true 停用 prepared statements
DATABASE_URL="postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1"

# migration 另外走 Direct Connection(Prisma 用 directUrl 指定)
DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
// schema.prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL") // 應用查詢走 Transaction Pooler
  directUrl = env("DIRECT_URL")   // prisma migrate 走 Direct Connection
}

這個「查詢走 pooler、migration 走 direct」的雙字串配置,是 Prisma + Supabase 的標準做法,記起來能省下大量除錯時間。

查看目前連線數

想知道自己離連線上限還有多遠,可以在 SQL Editor 查 pg_stat_activity:

-- 目前總連線數
select count(*) from pg_stat_activity;

-- 依連線狀態分類(idle 太多通常代表連線沒被好好回收)
select state, count(*)
from pg_stat_activity
group by state
order by count(*) desc;

idle 連線過多,往往是「借了不還」或 pool 開太大的徵兆,正是連線耗盡的前兆。

常見錯誤與最佳實踐

坑一:Serverless 用 Direct Connection,流量一大就爆連線。 這是最常見、也最痛的坑。Serverless 每個實例各開一條 Direct 連線,尖峰時數百個實例同時湧入,瞬間打爆 Postgres 的連線上限,網站回報 too many clients。正確做法:Serverless/Edge/Next.js API Routes 一律走 Transaction Pooler(埠通常 6543),並在模組層級建立小 pool(max 設小一點),讓少數實體連線被高效重用。

坑二:Transaction Mode 用 prepared statements,報莫名其妙的錯。 在 Transaction Mode 下,連線是「一個交易借一條、結束就還」,下一個交易可能被分到別條實體連線,先前 PREPARE 的語句在那條連線上不存在,於是報錯。正確做法:走 Transaction Pooler 就要停用 prepared statements——Prisma 在連線字串加 pgbouncer=true;node-postgres 等驅動則停用其 prepared statement 快取。需要 prepared statements 或長連線狀態時,改走 Session Mode 或 Direct Connection。

坑三:埠用錯——把 6543 當 5432 或反過來。 Transaction 與 Session Pooler 主機相同、只差在埠,很容易貼錯。埠貼錯會讓你「以為在用 transaction pooler,其實走的是 session」,池化行為與 prepared statements 支援全變樣,症狀非常難查。正確做法:不要憑記憶填埠,直接複製 Dashboard Connect 對話框裡對應那一組的完整字串(以官方為準),確保埠、主機、使用者名稱一次到位。

坑四:IPv6 直連在不支援 IPv6 的環境失敗。 Direct Connection 主機在許多方案下只給 IPv6,若執行環境不支援 IPv6,直連就是連不上。正確做法:長駐後端但環境只有 IPv4 時,改走 Session Pooler(pooler 主機提供 IPv4),或依方案購買 IPv4 add-on 讓 Direct 可用。

坑五:pool 開太大或「借了不還」。 把連線池 max 設得很大,或忘了 client.release()/沒關閉連線,會讓 idle 連線堆積,一樣會撞上上限。正確做法:Serverless 環境刻意把 pool 設小;每次借出連線一定在 finally 裡 release();用 pg_stat_activity 定期檢查 idle 連線是否異常堆積。

最佳實踐總結:

  • Serverless/Edge → Transaction Pooler(埠通常 6543),並在模組層級建小 pool、重用連線
  • 長駐後端 → 有 IPv6 用 Direct、只有 IPv4 用 Session Pooler
  • migration/pg_dump 備份 → Direct Connection(需完整連線能力)
  • 走 Transaction Pooler 一律停用 prepared statements(Prisma 加 pgbouncer=true)
  • 埠與連線字串一律以 Dashboard 的 Connect 頁面為準,不要硬記
  • 每次借出連線務必歸還,並用 pg_stat_activity 監控 idle 連線

小結

這篇是 Supabase 系列教學的第 008 篇,也是整個「資料庫段」的收尾。承接上一篇《Database Functions 與 Triggers》——我們讓資料庫在資料變更瞬間自動運轉,這篇則解決「應用長大後連線成為瓶頸」的問題:

  • 為何需要連線池:Postgres 連線很貴、有上限,Serverless 每個實例各開連線特別容易耗盡
  • Supavisor 是什麼:Supabase 自研的連線池器,在多客戶端間智慧分配少數實體連線
  • 兩種模式:session mode(綁整個連線期、支援 prepared statements)vs transaction mode(一交易借一條、重用率最高、不支援 prepared statements)
  • 三種連線:Direct(migration/備份/IPv6 後端)、Session Pooler(IPv4 長駐後端)、Transaction Pooler(Serverless/Edge)
  • 常見坑:Serverless 用 Direct 爆連線、Transaction Mode 用 prepared statements 出錯、埠貼錯、IPv6 相容性、連線借了不還

至此,我們把 Supabase 底層的 Postgres 完整走了一遍:從資料表設計、關聯與索引、Extensions、Functions 與 Triggers,到這篇的連線管理。你已經掌握「資料怎麼存、怎麼算、怎麼連」。

但還有一塊最關鍵的拼圖沒補上:安全。前面我們多次提到 RLS(Row Level Security,行級安全性)——它是 Supabase 讓你「安全地把資料庫直接開放給前端查詢」的基石,也是 security definer 之所以危險、觸發器之所以能自動建 profile 的背後那道牆。下一篇《Row Level Security 入門》,我們正式進入 Supabase 的安全模型:什麼是 RLS、enable row level security 之後為什麼「全部資料都不見了」、using 與 with check 的差別,以及如何寫出第一條讓「使用者只看得到自己資料」的政策。資料庫段到此收尾,安全段正式開始。下一篇見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →