Supabase 数据库设计:表结构、关系与 Row Level Security 完全指南
凌晨三点,我盯着 Supabase Dashboard 上那个红色的警告标志——“RLS not enabled”。脑子里闪过一堆问题:用户的文章数据会不会泄露?这个外键关系对不对?多对多关系到底怎么建?
说实话,刚开始用 Supabase 的时候,我踩了太多坑。创建表后忘记启用 RLS,导致任何人都能读取所有数据。外键设计不规范,删了用户数据,文章还在。多对多关系的连接表,我甚至试过用数组存储——彻底的灾难。
折腾了几个月,总算摸清了 Supabase 数据库设计的套路。这篇文章会把这些经验整理出来。
一、表结构设计:PostgreSQL 的命名约定
1.1 命名约定:snake_case 才是正道
PostgreSQL 有个奇怪的脾气:你不加双引号,它就把标识符全转成小写。你加了双引号,它就严格按你写的来。
这意味着什么?用驼峰命名(UserProfile)的话,你得在每处都加上双引号。太麻烦了。
所以 PostgreSQL 社区的约定是:snake_case(下划线分隔),表名复数,列名单数。
-- ✅ 推荐
CREATE TABLE users (
id UUID PRIMARY KEY,
email TEXT UNIQUE,
created_at TIMESTAMPTZ
);
1.2 列类型选择:别被 MySQL 的思维困住
错误 1:用 VARCHAR 而不是 TEXT
在 PostgreSQL 里,TEXT 和 VARCHAR 性能完全一样。区别只是 VARCHAR(n) 有长度限制。除非你确实需要限制长度,否则直接用 TEXT。
错误 2:用 TIMESTAMP 而不是 TIMESTAMPTZ
TIMESTAMP 不存储时区信息。你的服务器在美国,用户在中国,显示时间就会乱套。TIMESTAMPTZ 会自动转换时区。
错误 3:用 SERIAL 而不是 UUID
SERIAL 是自增整数。单机应用没问题,但分布式系统会冲突。UUID 是全局唯一。
二、三种表关系:一对一、一对多、多对多
2.1 一对一:加个 UNIQUE 就行
最常见的场景:用户和档案。
CREATE TABLE profiles (
id UUID PRIMARY KEY,
user_id UUID UNIQUE REFERENCES users(id) ON DELETE CASCADE,
bio TEXT
);
关键点:user_id UUID UNIQUE。UNIQUE 约束确保每个用户只能有一个档案。
2.2 一对多:最普通的外键
作者和书籍。一个作者可以写很多本书。
CREATE TABLE books (
id UUID PRIMARY KEY,
author_id UUID REFERENCES authors(id) ON DELETE CASCADE,
title TEXT
);
查询时,Supabase JS 可以直接嵌套取关联数据。
2.3 多对多:连接表是关键
学生和课程。一个学生可以选多门课,一门课可以有多个学生。
解决方案:创建一个连接表。
CREATE TABLE enrollments (
student_id UUID REFERENCES students(id) ON DELETE CASCADE,
course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
PRIMARY KEY (student_id, course_id)
);
三、Row Level Security:数据库自己当保安
3.1 RLS 的”默认拒绝”哲学
我第一次用 Supabase,创建了个 posts 表,直接用 anon key 从前端查询。结果——所有数据都返回了。吓了一跳。
原来 Supabase 默认不启用 Row Level Security(RLS)。没启用的话,任何人拿到 anon key 就能读写所有数据。
所以第一条铁律:创建表后,立即启用 RLS。
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
启用了 RLS 之后呢?还没完。启用但没策略,等于”拒绝所有访问”。必须创建至少一个策略。
3.2 策略语法:USING 和 WITH CHECK
- USING:过滤现有行(SELECT、UPDATE、DELETE)
- WITH CHECK:验证新行(INSERT、UPDATE)
3.3 四种常见策略模式
模式 1:用户访问自己的数据
CREATE POLICY "Users manage own data"
ON posts FOR ALL
TO authenticated
USING (user_id = auth.uid());
模式 2:公开 + 私有数据混合
已发布的所有人可见,草稿只有作者可见。
模式 3:多租户隔离
团队成员只能访问自己团队的数据。
模式 4:RBAC 角色控制
管理员有特殊权限。
四、RLS 性能优化
4.1 性能杀手:子查询每行执行一次
RLS 策略里的子查询,会在每一行数据上执行一次。10万行数据,策略里有子查询检查团队关系——查询超时 3 分钟。
4.2 优化方案一:添加索引
RLS 策略用到的列,必须加索引。
CREATE INDEX idx_posts_user_id ON posts(user_id);
Supabase 官方测试:无索引 450ms,有索引 45ms。10倍提升。
4.3 优化方案二:SECURITY DEFINER 函数
把子查询封装成函数,只执行一次。
CREATE OR REPLACE FUNCTION user_teams()
RETURNS SETOF UUID
LANGUAGE SQL SECURITY DEFINER STABLE
AS $$ SELECT team_id FROM team_members WHERE user_id = auth.uid(); $$;
五、实战案例
5.1 博客系统:文章、分类、标签
完整实现包括表结构、RLS 策略和索引配置。
5.2 多租户 SaaS:团队协作
团队数据隔离、团队成员访问、管理员权限控制。
总结
核心要点:
- snake_case 命名
- UUID 主键、TEXT 字符、TIMESTAMPTZ 时间
- RLS 必启用
- 索引 + SECURITY DEFINER 函数优化
常见问题
创建表后必须立即启用 RLS 吗?
为什么 PostgreSQL 推荐 snake_case?
RLS 策略为什么不能用 FOR ALL?
如何检查 RLS 状态?
5 分钟阅读 · 发布于: 2026年4月4日 · 修改于: 2026年4月5日

评论
使用 GitHub 账号登录后即可评论