Neon 无服务器 PostgreSQL 完全指南¶
为什么要学 Neon¶
像 Git 一样管理数据库:Neon 支持数据库分支(Branching),就像 Git 分支一样。你可以从生产数据库瞬间创建一个分支用于开发或测试,完全隔离且不影响生产。创建分支是瞬时的(copy-on-write),不复制数据。
真正的 Serverless,按需计费:Neon 的计算节点在没有查询时自动缩容到零,有请求时自动唤醒。你只为实际使用的计算时间和存储空间付费。对于开发环境和低流量应用,成本可以降低 90% 以上。
计算与存储分离架构:Neon 将 PostgreSQL 的计算层和存储层分离。存储层使用自研的 Pageserver,基于 S3 构建,支持时间旅行(Time Travel)查询历史数据。计算节点可以独立扩缩容。
完全兼容 PostgreSQL:Neon 运行的就是标准 PostgreSQL(不是仿制品),支持所有 PG 扩展(pgvector、PostGIS 等)、所有 ORM(Prisma、Drizzle、SQLAlchemy 等)、所有工具。迁移到 Neon 不需要修改代码。
开发者体验极佳:Web 控制台可视化管理、CLI 工具、GitHub 集成(Preview Branches)、连接池内置(PgBouncer)、免费层慷慨(0.5 GB 存储 + 分支)。
核心概念详解¶
Neon 架构(白话解释)¶
传统 PostgreSQL 是把数据和计算绑在同一台服务器上。Neon 把它们拆开了:
- 计算(Compute):运行 PostgreSQL 实例的虚拟机。可以启动、停止、扩缩。没有查询时关机(scale to zero),有查询时自动开机。
- 存储(Storage / Pageserver):专门存数据的层,基于云对象存储(S3)。支持版本化,可以回溯到任意时间点。
- Safekeepers:类似 WAL(Write-Ahead Log)接收者,确保数据持久性。
数据库分支(Branching)¶
| 概念 | Git 类比 | Neon 实现 |
|---|---|---|
| 主分支 | main | 生产数据库 |
| 功能分支 | feature/xxx | 开发/测试数据库 |
| 创建分支 | git checkout -b | 瞬时,copy-on-write |
| 分支数据 | 复制文件 | 共享底层页面,按需复制 |
| 删除分支 | git branch -d | 释放独占数据 |
| 分支时间点 | git checkout commit | 可以从历史时间点创建分支 |
生产分支 (main)
│
├── 开发分支 (dev) → 开发者日常开发
├── PR预览分支 (pr-123) → GitHub PR 自动创建
├── 测试分支 (staging) → QA 团队测试
└── 调试分支 (debug) → 从昨天15:00的数据创建,排查问题
自动扩缩容¶
无请求 ────────────→ 有请求 ────────────→ 高负载 ────────────→ 无请求
(0 CU, 休眠) (唤醒, 0.25 CU) (自动扩到 8 CU) (缩回 0 CU)
| | | |
不计费 ~300ms冷启动 自动扩容 自动休眠
CU (Compute Unit) = 1 vCPU + 4GB RAM。免费层给 0.25 CU。
Neon vs 传统托管 PostgreSQL 对比¶
| 特性 | Neon | AWS RDS | Supabase | PlanetScale |
|---|---|---|---|---|
| 数据库 | PostgreSQL | PostgreSQL/MySQL | PostgreSQL | MySQL (Vitess) |
| Scale to Zero | 支持 | 不支持 | 不支持 | 支持(付费版) |
| 分支 | 支持(核心功能) | 不支持 | 不支持 | 支持 |
| 时间旅行 | 支持 | 需要手动快照 | PITR恢复 | 不支持 |
| 计算存储分离 | 是 | 否 | 否 | 是 |
| 连接池 | 内置 | 需自建 | 内置(Supavisor) | 内置 |
| 冷启动 | ~300ms | N/A(常开) | N/A | ~1-2s |
| 免费层 | 0.5GB + 分支 | 12个月750h | 500MB | 5GB |
| 价格模型 | 按用量 | 按实例时间 | 按实例 | 按行读写 |
| 扩展支持 | 全部PG扩展 | 大部分 | 大部分 | MySQL限制 |
安装与配置¶
注册并创建项目¶
- 访问 https://neon.tech 注册账号
- 创建 Project(选择区域:AWS us-east-1 / eu-central-1 等)
- 获取连接字符串
安装 Neon CLI¶
# macOS
brew install neonctl
# npm 全局安装
npm install -g neonctl
# 验证安装
neonctl --version
# 登录
neonctl auth
CLI 基本操作¶
# 列出项目
neonctl projects list
# 创建新项目
neonctl projects create --name my-project --region-id aws-us-east-1
# 列出分支
neonctl branches list --project-id <project-id>
# 创建分支
neonctl branches create --project-id <project-id> --name dev
# 从特定时间点创建分支(时间旅行)
neonctl branches create --project-id <project-id> --name debug-branch --parent main --timestamp "2024-12-01T15:00:00Z"
# 获取连接字符串
neonctl connection-string --project-id <project-id> --branch-name dev
# 删除分支
neonctl branches delete --project-id <project-id> --branch-name dev
# 执行 SQL
neonctl sql --project-id <project-id> --query "SELECT version();"
连接字符串格式¶
关键参数: - sslmode=require:Neon 要求 SSL 连接 - 端点 ID(ep-xxx-yyy-123456):用于路由到正确的计算节点
连接池配置¶
Neon 内置连接池(基于 PgBouncer),通过在连接字符串中添加 -pooler 使用:
# 直连(适合迁移、长事务)
postgresql://user:pass@ep-xxx.neon.tech/db?sslmode=require
# 连接池(适合无服务器环境,如 Vercel/Cloudflare)
postgresql://user:pass@ep-xxx-pooler.neon.tech/db?sslmode=require
快速上手:5 分钟最小示例¶
Node.js + @neondatabase/serverless¶
.env:
index.js:
import { neon } from '@neondatabase/serverless';
import 'dotenv/config';
const sql = neon(process.env.DATABASE_URL);
async function main() {
// 创建表
await sql`
CREATE TABLE IF NOT EXISTS todos (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
)
`;
// 插入数据
await sql`INSERT INTO todos (title) VALUES ('学习 Neon')`;
await sql`INSERT INTO todos (title) VALUES ('写示例代码')`;
// 查询数据
const todos = await sql`SELECT * FROM todos ORDER BY id`;
console.log('所有待办:', todos);
// 参数化查询
const search = '学习';
const results = await sql`
SELECT * FROM todos WHERE title LIKE ${'%' + search + '%'}
`;
console.log('搜索结果:', results);
// 更新
await sql`UPDATE todos SET completed = true WHERE id = 1`;
// 事务
await sql.transaction([
sql`INSERT INTO todos (title) VALUES ('任务A')`,
sql`INSERT INTO todos (title) VALUES ('任务B')`,
]);
console.log('完成!');
}
main().catch(console.error);
运行:
进阶用法¶
场景一:Prisma 集成¶
prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_DATABASE_URL") // 直连用于迁移
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}
.env:
DATABASE_URL=postgresql://user:pass@ep-xxx-pooler.neon.tech/db?sslmode=require
DIRECT_DATABASE_URL=postgresql://user:pass@ep-xxx.neon.tech/db?sslmode=require
使用 Neon Serverless Driver + Prisma:
import { Pool, neonConfig } from '@neondatabase/serverless';
import { PrismaNeon } from '@prisma/adapter-neon';
import { PrismaClient } from '@prisma/client';
import ws from 'ws';
neonConfig.webSocketConstructor = ws;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaNeon(pool);
const prisma = new PrismaClient({ adapter });
// 正常使用 Prisma
const users = await prisma.user.findMany({
include: { posts: true },
});
场景二:Drizzle ORM 集成¶
src/db/schema.ts:
import { pgTable, serial, text, boolean, timestamp, integer } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false),
authorId: integer('author_id').references(() => users.id),
});
src/db/index.ts:
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
// 使用
const allUsers = await db.select().from(schema.users);
const userWithPosts = await db.query.users.findMany({
with: { posts: true },
});
场景三:GitHub Preview Branches¶
在 Neon 控制台中启用 GitHub 集成后,每个 PR 会自动创建一个数据库分支:
# .github/workflows/preview.yml
name: Preview Environment
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create Neon Branch
id: create-branch
uses: neondatabase/create-branch-action@v5
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: pr-${{ github.event.number }}
api_key: ${{ secrets.NEON_API_KEY }}
- name: Run Migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url_with_pooler }}
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview branch created!\nDB: \`${{ steps.create-branch.outputs.branch_id }}\``
})
场景四:时间旅行查询¶
# 从24小时前创建分支
neonctl branches create \
--project-id <id> \
--name recovery-branch \
--parent main \
--timestamp "2024-12-14T10:00:00Z"
# 连接到恢复分支查看历史数据
psql "$(neonctl connection-string --branch-name recovery-branch)"
-- 在恢复分支中查看历史数据
SELECT * FROM users WHERE deleted_at IS NOT NULL;
-- 恢复误删的数据:从恢复分支导出,导入到主分支
\copy (SELECT * FROM users WHERE id = 42) TO '/tmp/recovered_user.csv' CSV
场景五:Vercel + Next.js 集成¶
# 在 Vercel 中添加 Neon 集成
# Vercel Dashboard → Integrations → Neon
# 或手动设置环境变量
# DATABASE_URL(池化连接,用于运行时)
# DIRECT_DATABASE_URL(直连,用于迁移)
// app/api/users/route.ts (Next.js App Router)
import { neon } from '@neondatabase/serverless';
export async function GET() {
const sql = neon(process.env.DATABASE_URL!);
const users = await sql`SELECT * FROM users LIMIT 10`;
return Response.json(users);
}
export async function POST(request: Request) {
const sql = neon(process.env.DATABASE_URL!);
const { name, email } = await request.json();
const [user] = await sql`
INSERT INTO users (name, email) VALUES (${name}, ${email})
RETURNING *
`;
return Response.json(user, { status: 201 });
}
场景六:pgvector 向量搜索¶
-- 启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 创建带向量列的表
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(1536) -- OpenAI ada-002 维度
);
-- 创建索引
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 插入数据(嵌入向量由应用层生成)
INSERT INTO documents (content, embedding)
VALUES ('Neon 是 Serverless PostgreSQL', '[0.1, 0.2, ...]');
-- 余弦相似度搜索
SELECT id, content, 1 - (embedding <=> '[0.1, 0.2, ...]') AS similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 5;
场景七:自动扩缩容配置¶
# 通过 Neon API 配置自动扩缩
curl -X PATCH \
"https://console.neon.tech/api/v2/projects/<project-id>/endpoints/<endpoint-id>" \
-H "Authorization: Bearer $NEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"endpoint": {
"autoscaling_limit_min_cu": 0.25,
"autoscaling_limit_max_cu": 4,
"suspend_timeout_seconds": 300
}
}'
| 参数 | 说明 | 推荐值 |
|---|---|---|
autoscaling_limit_min_cu | 最小计算单元 | 0 (scale to zero) 或 0.25 |
autoscaling_limit_max_cu | 最大计算单元 | 根据负载,1-8 |
suspend_timeout_seconds | 空闲多久后休眠 | 300 (5分钟) |
场景八:数据库迁移工作流¶
# 1. 从 main 创建迁移分支
neonctl branches create --name migration-test --parent main
# 2. 在迁移分支上运行迁移
DATABASE_URL=$(neonctl connection-string --branch-name migration-test) \
npx prisma migrate deploy
# 3. 验证迁移结果
DATABASE_URL=$(neonctl connection-string --branch-name migration-test) \
npx prisma db seed
# 4. 确认无误后,在 main 上执行
DATABASE_URL=$(neonctl connection-string --branch-name main) \
npx prisma migrate deploy
# 5. 清理迁移分支
neonctl branches delete --branch-name migration-test
常见问题与排错¶
问题一:冷启动延迟¶
症状:第一次查询需要 300ms-2s。
优化: 1. 设置 suspend_timeout_seconds 为较大值(如 600) 2. 使用最低 CU 设为 0.25(不完全缩容到 0) 3. 使用连接池端点减少连接建立时间 4. 设置预热查询(定期 ping)
问题二:连接数限制¶
症状:too many connections 错误。
解决: - 使用池化连接端点(-pooler) - Serverless 环境(Vercel/Cloudflare Workers)必须使用池化连接 - 直连的连接数限制取决于 CU 大小
问题三:SSL 连接错误¶
解决:确保连接字符串包含 sslmode=require。
// Node.js
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: true },
});
问题四:分支存储空间¶
每个分支共享主分支的数据(copy-on-write),只有修改过的页面占用额外空间。但大量写入操作的分支会占用更多存储。
问题五:如何从现有 PostgreSQL 迁移¶
# 1. 从旧数据库导出
pg_dump -h old-host -U user -d dbname -Fc -f backup.dump
# 2. 导入到 Neon
pg_restore -h ep-xxx.neon.tech -U user -d dbname backup.dump
# 或使用 Neon 的导入功能
neonctl import --project-id <id> --source "postgresql://user:pass@old-host/db"
问题六:查询性能调优¶
-- 启用查询统计扩展
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- 查看慢查询
SELECT query, mean_exec_time, calls, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- 分析查询执行计划
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM users WHERE email = 'test@example.com';
参考资源¶
- 官方文档:https://neon.tech/docs
- Neon CLI 文档:https://neon.tech/docs/reference/neon-cli
- GitHub:https://github.com/neondatabase/neon
- Neon Serverless Driver:https://github.com/neondatabase/serverless
- Pricing:https://neon.tech/pricing
- Neon + Prisma 指南:https://neon.tech/docs/guides/prisma
- Neon + Drizzle 指南:https://neon.tech/docs/guides/drizzle
- Neon + Vercel 指南:https://neon.tech/docs/guides/vercel
- Neon API 参考:https://api-docs.neon.tech/reference
- Neon 社区论坛:https://community.neon.tech/