加一个字段是下午的活,补十二万行归属是两个月。
一、加一个字段的那个下午
周岚以为这是一个下午的活:给用户表加一个 workspace_id,默认值指向一个自动创建的“我的工作区”。
写到第三张表时她停住了。文档、分享链接、API 密钥、Webhook、账单记录、审计日志、模板、导出任务,八张业务表都直接挂在 user_id 上,没有任何一张知道“归属”是什么。
更麻烦的是老数据。三百四十四个账号里,一百个个人档持有者、一百个团队席位、一百四十四个免费与试用账号;谁是哪个工作区的成员,历史记录里没有答案。
以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。
加字段是分钟级,补归属是周级。 数据库不会拒绝一个带默认值的列,但默认值会把所有历史数据塞进同一个错误的位置。
归属不是外键,是语义。 user_id 回答“谁创建的”,workspace_id 回答“它属于谁”;两者在个人产品里恰好重合,在团队产品里必须分开。
二、四张核心表:谁连人,谁连组织
先把人和组织之间的路径画出来,再谈字段。
| 表 | 一行代表什么 | 主键 | 必须有的约束 |
|---|---|---|---|
| organizations | 一个付费主体,可能被收购或更名 | org_id | 软删除;名称不要求全平台唯一 |
| workspaces | 一个隔离空间,成员与资源在此共享 | workspace_id | 必须属于一个组织;slug 在组织内唯一 |
| memberships | 一个人在一个工作区里的身份 | (workspace_id, user_id) | 角色只存这里;删除即撤销访问 |
| invitations | 一次尚未接受的邀请 | invite_id | 邮箱加工作区唯一;必须有状态与过期时间 |
membership 是唯一同时连人和组织的表。 它一边指向 user,一边指向 workspace,workspace 再指向 organization;去掉它,人和组织之间就没有合法路径。
角色挂在 membership 上,不挂在 user 上。 同一个人在两个工作区可以是管理员和只读;角色写在 user 上,这个人在第二个工作区就只能是同一种身份。
邀请是成员关系的前置状态,不是一张日志表。 它需要过期时间、接受次数与撤销状态;否则一个九十天前的链接仍然能把陌生人放进工作区。
外键只是底线,约束才是模型。 组织软删除后工作区不能继续写入、成员关系删除后邀请必须失效,这些规则要写在数据库或统一的写入口里。
不要给 memberships 加自增主键就完事。 复合主键能天然阻止同一个人在同一空间出现两条角色记录;用自增主键就必须再加唯一索引。
三、八张业务表:哪些属于组织,哪些属于工作区
归属键不是按表名猜的,是按“谁共享、谁付钱”判的。
| 业务表 | 归属键 | 属于谁 | 判据 |
|---|---|---|---|
| documents | workspace_id | 工作区 | 内容在团队内共享,跨区不可见 |
| share_links | workspace_id | 工作区 | 链接的可见范围由工作区决定 |
| api_keys | workspace_id | 工作区 | 密钥代表一个空间的调用身份 |
| webhooks | workspace_id | 工作区 | 回调只关心本空间的资源 |
| audit_logs | org_id | 组织 | 跨工作区的合规证据要能一次导出 |
| billing_records | org_id | 组织 | 账单合并到付费主体 |
| templates | org_id | 组织 | 品牌模板被多个工作区复用 |
| export_jobs | workspace_id | 工作区 | 导出范围不越过一个空间 |
判据一:谁付钱,谁拥有账单与审计。 账单、发票、审计日志属于组织,因为它们回答的是“这家公司花了多少、谁动过”。
判据二:谁共享,谁拥有内容。 文档、链接、密钥、Webhook 属于工作区,因为它们的可见范围就是一个空间的边界。
判据三:跨工作区复用的配置放组织层。 模板、品牌与默认策略放组织;一旦放错,第二个工作区就要复制一份。
归属键要一次定完,不要边做边加。 八张表分三次改,就会有三种默认值口径;迁移脚本的复杂度不是线性增长。
八张表里五张归工作区、三张归组织。 这个比例不是巧合:内容与调用身份天然按空间隔离,钱与合规证据天然按付费主体合并。
四、四步迁移:先补默认值,再切换读路径
不停机的迁移不是一次大爆炸,是四次可回滚的推进。
| 步骤 | 动作 | 观察指标 | 回滚方式 |
|---|---|---|---|
| 一、回填 | 给个人档建默认工作区,八张表写入归属 | 空归属行数降到零 | 字段可空,停止写入 |
| 二、双写 | 新旧字段同时写,读仍走 user_id | 两列不一致率 | 关闭双写开关 |
| 三、切读 | 按 workspace_id 查询,user_id 降为创建者 | 错误率与慢查询 | 按工作区灰度切回 |
| 四、清理 | 删除归属语义,保留创建者字段 | 一周无回滚需求 | 从备份恢复 |
现有工作区 24 = 20 个团队各 1 个 + 4 个团队多开 1 个
个人档持有者 100 → 各补 1 个默认工作区
迁移后工作区 = 24 + 100 = 124
免费与试用账号 144 → 首次创建内容时惰性创建,不预填
8 张业务表合计 = 124000 行
回填速率 = 2000 行/分钟
回填耗时 = 124000 ÷ 2000 = 62 分钟
双写 7 天,写放大 2 倍
按日均写入 4800 行估算,7 天 = 33600 行
额外写入 = 33600 行,行数不变、写次数翻倍
回滚窗口 = 7 天
核对 = 20 + 4 = 24,其中团队多开 4 个
62 分钟是回填时间,不是迁移时间。 真正的成本在验证:八张表逐张比对归属,任何一张漏掉都会在切换后变成越权访问。
双写的七天是观察期,不是保险期。 它的价值在于让你看到不一致率降到零,而不是让你多一个后悔的借口。
读路径切换要能按工作区灰度。 先切四个多开工作区的团队,再切单工作区团队,最后切个人;出问题时影响面可控。
旧字段不要立刻删。 user_id 保留为 created_by,回答“谁创建的”;归属语义交给 workspace_id,两个问题不要挤在一个列里。
二十四个工作区里有四个是团队多开的,这四个最容易出错。 多开的空间往往有交叉成员,迁移时要逐个确认成员关系,而不是批量套默认值。
五、行动清单与复算验收
在写迁移脚本之前,先用下面八项把模型和回滚条件钉住。
- 列出所有直接挂在 user_id 上的业务表,标出哪些其实是归属
- 给 organizations、workspaces、memberships、invitations 画出主键与外键
- 确认角色字段只在 memberships 上,user 表里没有残留
- 给 invitations 加过期时间、状态与邮箱加工作区的唯一约束
- 用十二万四千行、每分钟两千行算出回填窗口,并预留两倍验证时间
- 为四个多开工作区的团队单独列出成员核对表
- 准备读路径开关,能按工作区灰度切换
- 写下回滚条件:不一致率超过多少、错误率超过多少就停
验收标准:你能在不停机的情况下给旧数据补上归属,并说清每一张表属于组织还是工作区;说不清,说明迁移还停在加字段的阶段。
使用边界与实操验收
本文讨论的是数据模型与迁移顺序,不涉及具体数据库选型或权限框架。
三层不是三张表。 organizations、workspaces、memberships 是三个概念,落到 schema 可能是四张表,也可能多一张组织成员表;关键是归属路径唯一。
不要把组织层当成权限层。 组织回答“属于哪家公司”,权限回答“这个人能做什么”;两者混在一起,第二个工作区就会继承错误的角色。
行级安全不是迁移的替代品。 它能在读的时候兜底,但不能替你补历史数据的归属;先有正确的列,再谈策略。
本文不覆盖邀请流程。 邀请的过期、重发、接受与撤销在下一篇展开;这里只保证表结构能支撑它。
迁移顺序比工具重要。 同一套四步,用脚本还是用平台都能做;顺序错了,工具只会让错误更快发生。
关键参考与核验范围
核验日期:2026-10-25。以下链接用于核对上下文边界、行级隔离与约束设计的一般做法,本文未逐条引用其内容。
- Martin Fowler:Bounded Context :用于核对“先划边界,再定模型”的原则。
- PostgreSQL:Row Security Policies :用于核对行级隔离与策略表达式的能力边界。
- PostgreSQL:Constraints :用于核对唯一约束、外键与复合主键的写法。
- Auth0:Multi-Tenant Applications :用于核对外部身份服务如何组织租户与成员。
引用口径。 本文只借用“归属先于授权”这一一般原则;三百四十四个账号、十二万四千行、62 分钟与七天全部是教学假设,与上述材料无关。
核验范围。 核验的是“四张核心表 + 八个归属键 + 四步迁移”这套结构是否仍可操作,不是比对某个数据库或身份服务的功能。
如何自行复核。 在测试库复制一份生产数据,按四步走一遍,记录每一步的空归属行数与不一致率;如果第三步切读后出现任何一条越权记录,就回到第二步重新核对归属键。
系列导航
- 上一篇:什么时候必须有「组织」:五个信号与三种模型
- 下一篇:邀请与成员生命周期:从发出邀请到移除成员
- 回到目录:一个人的多租户:组织、邀请、角色与数据隔离
- 相关阅读:Figma、Notion、Slack 与 Shopify:B2B 应用市场的 OAuth、审核与分成
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。