为什么 Supabase 需要两个数据库 URL:连接池详解
如果你把 Next.js 应用(使用 Prisma)部署到 Vercel,并连接 Supabase(或 Neon),你可能见过类似配置:
DATABASE_URL="postgresql://...@pooler.supabase.com:6543/postgres?pgbouncer=true"
DIRECT_URL="postgresql://...@pooler.supabase.com:5432/postgres"
两个 URL?不同端口?pgbouncer=true 是什么?下面拆解一下。
问题:连接数上限
PostgreSQL 对并发连接数有硬性限制。Supabase 的免费层大约是 60 个连接。听起来不少,对吧?
关键在于:serverless 函数会在每次请求时新建数据库连接。
当 Vercel 函数启动时会连接 Postgres,结束后连接可能还会停留一段时间。流量一上来,就会看到:
Error: too many connections for role "postgres"
即使只有 60 个连接,在流量高峰期也可能几秒内耗尽。
解决方案:连接池
这就是 PgBouncer 的用武之地。它是一个轻量级连接池,位于应用与 PostgreSQL 之间。
没有连接池
┌──────────────┐
│ Request 1 │──────┐
├──────────────┤ │
│ Request 2 │──────┼────▶ PostgreSQL (60 connection limit)
├──────────────┤ │
│ Request 3 │──────┘
└──────────────┘
Each request = 1 connection
60 requests = database at capacity
使用 PgBouncer
┌──────────────┐ ┌───────────┐
│ Request 1 │──────┤ │
├──────────────┤ │ PgBouncer │────▶ PostgreSQL
│ Request 2 │──────┤ (pooler) │ (60 connections)
├──────────────┤ │ │
│ Request 3 │──────┤ │
└──────────────┘ └───────────┘
1000s of requests share 60 connections
PgBouncer 会维护一组数据库连接池,并在请求之间智能复用。你的应用可以同时服务上千并发用户,而数据库实际连接数仍然很少。
为什么需要两个 URL?
现在来到最容易困惑的部分:为什么既要 DATABASE_URL 又要 DIRECT_URL?
DATABASE_URL(端口 6543)
它通过 PgBouncer 连接,使用事务级连接池模式:
- 连接在请求之间共享
- 事务开始时分配连接,结束后释放
- 对高并发应用非常高效
用于: 所有运行时查询(SELECT、INSERT、UPDATE、DELETE)
DIRECT_URL(端口 5432)
它直接连到 PostgreSQL,绕过连接池:
- 每个连接专属一个客户端
- 支持所有 PostgreSQL 特性
- 效率低一些,但某些操作必须使用
用于: 数据库迁移和结构变更
为什么迁移必须用直连?
PgBouncer 的事务池模式有一些限制,不支持:
- 预编译语句(prepared statements)
- 咨询锁(advisory locks)
- 会话级设置(会话内持久的 SET 命令)
Prisma 的迁移依赖咨询锁来防止并发迁移:
-- Prisma 内部会这样做
SELECT pg_advisory_lock(72707369); -- 防止竞态的锁
-- ... 执行迁移 ...
SELECT pg_advisory_unlock(72707369);
但在 PgBouncer 下,你的“会话”可能在每条查询时都换了连接,所以这些锁不会生效。这就是迁移必须走直连的原因。
pgbouncer=true 参数
当你在连接字符串里加上 ?pgbouncer=true,你是在告诉 Prisma:
“我通过连接池连接,请按连接池模式调整行为。”
Prisma 会:
- 禁用预编译语句 —— PgBouncer 的事务池模式不支持
- 调整连接处理方式 —— 适配连接池约束
如果缺少这个参数,可能会出现类似错误:
prepared statement "s0" already exists
拼起来看整体
完整的 Prisma 配置如下:
// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // 运行时查询走连接池
directUrl = env("DIRECT_URL") // 迁移走直连
}
以及环境变量:
# 运行时查询(通过 PgBouncer)
DATABASE_URL="postgresql://user:pass@pooler.supabase.com:6543/postgres?pgbouncer=true"
# 迁移(直连 PostgreSQL)
DIRECT_URL="postgresql://user:pass@pooler.supabase.com:5432/postgres"
实际运行流程
开发阶段
npx prisma migrate dev
Prisma 使用 DIRECT_URL 来:
- 获取咨询锁
- 应用结构变更
- 释放锁
部署阶段(Vercel)
npx prisma migrate deploy && npx prisma generate && npm run build
prisma migrate deploy→ 使用DIRECT_URL(端口 5432)prisma generate→ 生成客户端(不需要 DB 连接)- 应用运行时 → 使用
DATABASE_URL(端口 6543)
运行时
每个 API 路由、服务端组件或 server action 都会使用连接池:
// 这里会自动使用 DATABASE_URL
const users = await prisma.user.findMany();
快速对照
| 方面 | DATABASE_URL | DIRECT_URL |
|---|---|---|
| 端口 | 6543 | 5432 |
| 经过 | PgBouncer | 直连 Postgres |
| 连接共享 | 是(连接池) | 否(专属) |
| 预编译语句 | 禁用 | 启用 |
| 咨询锁 | 不支持 | 支持 |
| 用途 | 应用查询 | 迁移 |
| 需要参数 | ?pgbouncer=true | 无 |
常见坑
1. 忘记在 schema.prisma 里写 directUrl
// 错误 - 迁移会失败
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// 正确
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
2. 缺少 pgbouncer 参数
# 错误 - 会触发 prepared statement 报错
DATABASE_URL="postgresql://...@pooler.supabase.com:6543/postgres"
# 正确
DATABASE_URL="postgresql://...@pooler.supabase.com:6543/postgres?pgbouncer=true"
3. 用错端口
- 6543 = 连接池(PgBouncer)
- 5432 = 直连(PostgreSQL)
端口搞反会直接连不上或出现各种奇怪错误。
总结
在 serverless 部署里,连接池不是“可有可无”,而是必需品。不然应用只要有点流量就会崩。
两个 URL 的配置一开始看起来很怪,但它优雅地解决了两个现实问题:
- DATABASE_URL(连接池)高效处理大量并发请求
- DIRECT_URL(直连)确保迁移安全、可控
理解了这个架构背后的原因,配置就会变得理所当然。
这篇文章写于我搭建 Next.js + Prisma + Supabase 项目、纳闷为什么要两个数据库 URL 的时候。