返回博客2026年1月7日

为什么 Supabase 需要两个数据库 URL:连接池详解

数据库PostgreSQLPrismaSupabaseVercel

如果你把 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 会:

  1. 禁用预编译语句 —— PgBouncer 的事务池模式不支持
  2. 调整连接处理方式 —— 适配连接池约束

如果缺少这个参数,可能会出现类似错误:

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 来:

  1. 获取咨询锁
  2. 应用结构变更
  3. 释放锁

部署阶段(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_URLDIRECT_URL
端口65435432
经过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 的时候。


往期回顾

准备开始了吗?

先简单说明目标,我会给出最合适的沟通方式。