使用 Supabase 进行 Schema 设计:分区与规范化
Source: Dev.to
第 6 天 – 使用 Supabase 的模式设计
PostgreSQL 支持 schemas(模式),它们充当用于分组表的命名空间。
典型的使用场景包括:
- 多租户 – 为每个客户创建单独的 schema,实现完整的数据隔离。
- 访问控制 – 对每个 schema 设置权限。
- 扩展隔离 – 将扩展(例如
pgvector)放在专用的 schema 中。
在许多项目中,所有表都位于默认的 public schema:
-- 默认是 public schema
SELECT * FROM public.users;
-- 创建一个单独的 schema
CREATE SCHEMA app_auth;
SELECT * FROM app_auth.users;
单一 public schema 的问题
- 随着表数量增长,可视性下降。
- 难以判断某个表属于哪个功能。
- 权限管理变得复杂。
为了解决这些问题,我按功能拆分了 schema,并添加了 app_ 前缀。
app_ 前缀
Supabase 保留了系统 schema,如 auth、storage 和 public。
通过在自定义 schema 前加上 app_(即 application 的缩写),可以:
- 与 Supabase 系统 schema 明显区分。
- 在 pgAdmin 等工具中按字母顺序首先出现,便于快速定位。
Supabase 系统 schemas
├── auth # Supabase 认证
├── storage # Supabase 存储
├── public # 默认
应用 schemas
├── app_auth # 自定义认证
├── app_billing # 计费
├── app_content # 内容管理
├── app_admin # 管理功能
├── app_ai # AI 功能
├── app_social # 社交功能
├── app_system # 系统日志等
我完全避免使用 Supabase 的系统 schemas,所有内容都放在自定义 schemas 中,以降低供应商锁定。对于小型服务来说,维护大量 schema 的开销可能不值得,但随着 SaaS 的成长,这种做法的收益会逐渐显现。
当前 Schema 概览
| Schema | 负责范围 | 示例表 |
|---|---|---|
app_admin | 管理功能 | tenants、teams、members |
app_ai | AI 功能 | embeddings、search_vectors |
app_auth | 认证 | users、sessions、accounts |
app_billing | 计费 | subscriptions、payment_history |
app_content | 内容管理 | contents、pages、tables |
app_social | 社交功能 | bookmarks、comments、reactions |
app_system | 系统日志 | activity_logs、system_logs |
分区标准
- 功能内聚 – 将相关表放在一起(例如
app_auth)。 - 变更频率 – 将频繁变更的表单独隔离。
- 权限边界 – 将仅管理员可见的数据 (
app_admin) 与普通用户数据 (app_content) 分离。
使用 Drizzle ORM 定义 Schemas
// database/app_auth/schema.ts
import { pgSchema } from 'drizzle-orm/pg-core';
export const appAuth = pgSchema('app_auth');
// database/app_auth/users.ts
import { appAuth } from './schema';
import { text, timestamp, uuid } from 'drizzle-orm/pg-core';
export const users = appAuth.table('users', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
email: text('email').notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
与 schemas 对应的目录结构
src/database/
├── app_auth/
│ ├── schema.ts # Schema 定义
│ ├── users.ts # 表定义
│ ├── sessions.ts
│ └── index.ts # 导出
├── app_billing/
│ ├── schema.ts
│ ├── subscriptions.ts
│ └── index.ts
├── index.ts # 汇总导出
└── relations.ts # 关系定义
规范化与反规范化
即使是个人项目,基本的规范化(1NF‑3NF)也非常有价值:
- 1NF – 消除重复组;使用关联表(junction table)代替 CSV 字段。
- 2NF – 去除部分依赖;把仅依赖复合键一部分的列拆分出来。
- 3NF – 去除传递依赖;仅在需要时存储派生值。
实用技巧
- 关联表 – 处理多对多关系(例如
content_tags)。 - 复合主键 – 对多租户表使用
(tenant_id, id)。 - 反规范化 – 为读密集型查询存储聚合计数(如书签数量)。
行级安全 (RLS)
RLS 提供每行的访问控制。虽然我目前在应用层实现权限控制,但分区的 schema 布局有助于以后编写 RLS 策略,因为每个 schema 可以拥有独立的策略集合。
在 Supabase 中使用自定义 Schemas
添加自定义 schemas 后,需要配置三件事,否则会出现 “table not found” 错误。
1. 在 Supabase 仪表盘中公开 schemas
Project Settings → Data API → Exposed schemas → 添加你的 schemas(例如 public, app_admin, app_ai, …)。
2. 在 DATABASE_URL 中包含 schemas
# .env.local
DATABASE_URL=postgresql://user:password@host:5432/postgres?schema=public,app_admin,app_ai,app_auth,app_billing,app_content,app_social,app_system
3. 配置 Drizzle ORM 的 schemaFilter
// drizzle.config.ts
import type { Config } from 'drizzle-orm';
export default {
schema: [
'./src/database/app_admin/index.ts',
'./src/database/app_auth/index.ts',
'./src/database/app_billing/index.ts',
// …
],
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
// 多 schema 支持
schemaFilter: [
'public',
'app_admin',
'app_ai',
'app_auth',
'app_billing',
'app_content',
'app_social',
'app_system',
],
} satisfies Config;
收获
有效之处
- 通过基于功能的 schemas 实现清晰的职责划分。
- 目录结构与 schema 名称保持一致,提升可读性。
- 基础规范化结合有针对性的反规范化,兼顾性能。
注意事项
- 过多的 schemas 会增加复杂度。
- 别忘了在 Supabase 设置中公开自定义 schemas。
- 即使是小项目,也应提前规划好 schema 设计,以便未来扩展。
接下来
明天的文章将讨论 数据库 ID 设计——在 UUID、CUID2、顺序 ID 等之间的取舍。
本系列相关帖子
- 12/5 – Git 分支策略:面向独立开发者的实用工作流
- 12/7 – 数据库 ID 设计:在 UUID、CUID2、顺序 ID 等之间的取舍