无代码后端的多租户 SaaS 数据模型选项
多租户 SaaS 的数据模型选择会影响安全性、报表和性能。比较 tenant_id、独立 schema 与独立数据库的权衡。

问题:在不影响性能的情况下保持租户隔离
多租户意味着一个软件产品为多个客户(租户)服务,每个租户只能看到自己的数据。难点在于要始终如一地做到这一点:不仅是某个界面,而是每个 API 调用、管理面板、导出和后台任务。
你的数据模型比大多数团队预期的更影响日常运营。它决定了权限、报表、随增长的查询速度以及一个“小”错误可能造成的风险。少了一个过滤条件就可能泄露数据;隔离做得太激进,报表和支持会变得麻烦。
常见的三种多租户 SaaS 数据模型是:
- 单个数据库,所有表在每行包含
tenant_id - 在一个数据库内为每个租户使用独立 schema
- 为每个租户使用独立数据库
即便你在无代码后端可视化构建,权衡仍然相同。像 AppMaster 这样的工具会从你的设计生成真实的后端代码和数据库结构,所以早期的建模决策会很快反映到生产行为和性能中。
想象一个工单工具。如果每行工单都有 tenant_id,查询“所有未关闭工单”很简单,但你必须在每处严格检查租户。如果每个租户有自己的 schema 或数据库,默认隔离更强,但跨租户报表(例如“所有客户的平均关闭时间”)就更耗工夫。
目标是实现你可以信任的隔离,同时不为报表、支持和扩展增加不必要的摩擦。
快速决策法:4 个问题缩小选择范围
不要从数据库理论开始。先考虑产品如何被使用,以及每周需要怎样运维。
通常能让答案显而易见的四个问题
-
数据有多敏感?是否受严格合规约束? 医疗、金融或受限客户合同通常倾向更强的隔离(独立 schema 或独立数据库),这能降低风险并简化审计。
-
你是否经常需要跨租户报表? 如果经常需要“所有客户”的指标(使用量、收入、性能),单数据库加
tenant_id通常最简单。独立数据库会让这件事变得困难,需要到处查询并合并结果。 -
租户之间会有多大差异? 如果租户需要自定义字段、自定义工作流或独特集成,独立 schema 或数据库可以降低变更互相影响的概率。如果大多数租户结构相同,
tenant_id更清爽。 -
你的团队实际上能运维什么? 更强的隔离通常意味着更多工作:更多备份、更多迁移、更多环节需要管理,也有更多隐藏故障的地方。
一个实用的做法是对最优的两个选项做原型,并测试真正的痛点:权限规则、报表查询,以及模型演进时变更的推广方式。
方案一:单数据库,所有行带有 tenant_id
这是最常见的设置:所有客户共享同一套表,每条租户拥有的记录都带有 tenant_id。运维简单,因为只运行一个数据库和一套迁移脚本。
规则很严格:如果某行属于某个租户,它必须包含 tenant_id,并且每个查询都要基于它过滤。租户拥有的表通常包括用户、角色、项目、工单、发票、消息、文件元数据,以及连接租户数据的关联表。
为减少泄露,把 tenant_id 视为不可妥协:
- 在租户拥有的表上把
tenant_id设为必填(NOT NULL) - 添加以
tenant_id为首列的索引(例如tenant_id, created_at) - 唯一性规则包含
tenant_id(例如每租户唯一的邮箱) - 在每个 API 和业务流程中传递
tenant_id,不要仅依赖 UI 表单 - 在后端强制执行,而不仅是客户端过滤
在 PostgreSQL 中,行级安全(row-level security)策略可以作为强有力的安全网,尤其适合动态生成查询的场景。
参考数据通常分为两类:无 tenant_id 的共享表(例如国家列表),以及带 tenant_id 的租户作用域目录(例如自定义标签或流水线)。
如果你用 AppMaster 构建,一个简单的习惯能防止大多数事故:在 Business Process 逻辑中于任何创建或读取之前从已认证用户的租户设置 tenant_id,并始终保持这一模式。
权限影响:每种方法会带来哪些变化
权限是多租户成败的关键。你选择的数据布局会改变如何存储用户、如何限定查询范围,以及如何避免管理界面中的“糟糕一瞬间”。
在单库并在每行带有 tenant_id 的情况下,团队通常使用一张共享的 Users 表,把每个用户关联到租户及一个或多个角色。核心规则依然是:每次读写都必须包含租户范围,即便是看似“小”的表如设置、标签或日志也不例外。
采用独立 schema 时,通常保留共享的身份层(登录、密码、多因素认证),而租户数据放在每个租户的 schema 中。权限在某种程度上变成路由问题:应用必须在业务逻辑运行前指向正确的 schema。
使用独立数据库时,隔离最强,但权限逻辑更多地转移到基础设施层面:选择正确的数据库连接、管理凭据,以及处理“全局”员工账户。
在所有三种方案中,一些通用模式能稳定降低跨租户风险:
- 将
tenant_id放入会话或认证令牌的声明(claims)并视为必需 - 在一个中心位置(中间件或共享的 Business Process)集中化租户检查,而不是在各处散布
- 在管理工具中明确显示租户上下文并要求显式切换
- 对支持访问使用冒充并记录审计日志
在 AppMaster 中,这通常意味着在认证后把租户上下文存储起来,并在 API 端点和 Business Processes 中重复使用它,从而让每次查询都有上下文。一个支持人员只有在应用设置租户上下文后才应该看到订单,而不是因为 UI 刚好做了过滤。
单库 + tenant_id 的报表与性能
使用 tenant_id 的单库方案,报表通常比较直观。全局仪表盘(MRR、注册、使用量)可以通过一条跨所有人的查询得到,而租户级报表就是加上过滤条件的相同查询。
代价是随时间而来的性能问题。随着表增长,一个繁忙的租户可能成为“嘈杂邻居”,产生大量行、触发更多写入,并使得常见查询变慢,尤其是在数据库不得不扫描大量数据时。
索引能保持该模型的健康。大多数租户范围的读取应该使用以 tenant_id 为首列的索引,这样数据库可以直接定位到该租户的数据切片。
一个好的基线:
- 添加以
tenant_id为首列的复合索引(例如tenant_id + created_at、tenant_id + status、tenant_id + user_id) - 只有在需要跨租户查询时才保留真正的全局索引
- 关注那些“忘记”包含
tenant_id的连接和过滤,它们会导致慢扫描
保留策略与删除也需要规划,因为某个租户的大量历史数据会膨胀整张表。如果租户有不同的保留政策,考虑按租户的软删除加定期归档,或把旧行移到按 tenant_id 键控的归档表。
方案二:按租户独立 schema
采用独立 schema 时,你仍然使用一个 PostgreSQL 数据库,但每个租户有自己的 schema(例如 tenant_42)。该 schema 内的表仅属于该租户。感觉上像是给每个客户一个“迷你数据库”,但不需要管理多个数据库的开销。
一种常见设置是在共享 schema 中放共享服务,而租户数据放在租户 schema。划分通常基于什么必须在所有客户间共享,什么则绝对不能混合。
典型划分:
- 共享 schema:tenants 表、套餐(plans)、计费记录、功能开关、审计设置
- 租户 schema:业务表,比如订单、工单、库存、项目、自定义字段
- 两边都可能包含(取决于产品):用户与角色,尤其是当用户可访问多个租户时
该模型减少了跨租户关联的风险,因为表位于不同命名空间。它也便于备份或恢复单个租户:只针对某个 schema 操作即可。
迁移是让团队惊讶的点。当你新增表或列时,必须对每个租户 schema 应用相同的更改。10 个租户时还好;1000 个租户时需要流程:跟踪 schema 版本、分批运行迁移,并在失败时安全回滚以避免一个损坏的租户阻塞其他租户。
像认证和计费等共享服务通常放在租户 schema 之外。一个实用模式是共享认证(一个 user 表配合租户成员表)和共享计费(Stripe 客户 ID、发票),而租户 schema 存储租户拥有的业务数据。
如果使用 AppMaster,请及早规划 Data Designer 模型如何映射到共享与租户 schema,并保持全局服务稳定,以便租户 schema 能在不破坏登录或付款的情况下演进。
独立 schema 的报表与性能
独立 schema 在默认情况下比纯 tenant_id 过滤提供更强的隔离,因为表在物理上是分开的,并且可以对 schema 设置权限。
当大多数报表是按租户查询时,这种方式很理想。查询变得简单,因为你只读取某个租户的表,不必在共享表上反复过滤。这个模型也支持需要额外表或自定义列的“特殊”租户,而不会迫使所有人都承担这些字段。
跨租户汇总报表是 schema 的短板。你要么需要一个能查询多个 schema 的报表层,要么在共享 schema 中维护汇总表。
常见模式包括:
- 按租户的仪表盘只查询该租户的 schema
- 带有每晚汇总的中央分析 schema,从每个租户抽取数据
- 将租户数据导出为适合数据仓库的格式的导出作业
就租户级工作负载而言,性能通常不错。每个租户的索引更小,一个 schema 中的大量写入不太可能影响其他 schema。但代价是运维开销:为新租户开设 schema、运行迁移,并在模型变更时保证所有 schema 对齐。
当你希望比 tenant_id 更严格隔离但又不想管理大量数据库,或预期每租户会高度定制时,schema 是不错的选择。
方案三:每租户独立数据库
为每个租户使用独立数据库时,每个客户都有自己的数据库(或在同一服务器上的独立数据库)。这是隔离最强的一种方式:某个租户的数据损坏、配置错误或高负载很难波及到其他租户。
它适合受监管的环境(医疗、金融、政府)或期望严格隔离、定制保留策略或专属性能的企业客户。
入驻流程变成了一个预配工作流。当新租户注册时,系统需要创建或克隆数据库、应用基础 schema(表、索引、约束)、安全地创建并存储凭据,并将 API 请求路由到正确的数据库。
如果你用 AppMaster 构建,关键设计点是把租户目录放在哪(用于映射租户到数据库连接),以及如何保证每个请求使用正确的连接。
升级和迁移是主要的权衡点。schema 变更不再是“执行一次”,而是“为每个租户执行一次”。这增加了运维工作和风险,因此团队常用版本化 schema,并把迁移作为受控作业按租户跟踪进度。
好处是掌控:你可以先迁移大租户,观察性能,然后逐步推广变更。
独立数据库的报表与性能
独立数据库最容易理解。意外的跨租户读取不太可能发生,权限错误的波及范围通常仅限单个租户。
性能也是优势。Tenant A 的重查询、大批量导入或失控报表不会拖慢 Tenant B。这在防止“嘈杂邻居”问题上非常有效,并允许按租户调整资源。
代价体现在报表上。跨租户的全局分析最困难,因为数据被物理拆分。实践中常见的做法包括把关键事件或表复制到中央报表数据库、将事件发送到仓库式数据集、在租户数量较少时运行每租户报表并聚合结果,或把产品指标与客户数据分离。
运维成本是另一大因素。更多数据库意味着更多备份、升级、监控和故障响应。你也更容易遇到连接数限制,因为每个租户可能需要独立的连接池。
常见错误:导致数据泄露或后期痛点的做法
大多数多租户问题不是“大设计”失败,而是小疏忽累积成安全漏洞、报表混乱和昂贵的修复。多租户的可行性在于把租户隔离当作一种习惯,而不是事后才加的功能。
常见泄露来源是忘记在某个表上加租户字段,尤其是像 user_roles、invoice_items 或标签这样的关联表。一切看起来正常,直到某个报表或搜索通过该表做连接,把别的租户的行拉出来。
另一个常见问题是管理面板绕过租户过滤。通常起始于“只是给支持用一下”,然后被复用。无代码工具并没有改变这一风险:每个读取租户数据的查询、业务流程和端点都需要相同的租户范围。
ID 也会坑你。如果跨租户共享人类可读的 ID(例如 order_number = 1001)并假设它们是全局唯一,支持工具和集成会混淆记录。保持租户作用域的标识与内部主键分离,并在查找时包含租户上下文。
最后,团队低估了随规模增长的迁移与备份复杂度。十个租户时很容易的事,到了 1000 个就变得慢且高风险。
防止大多数痛点的快速检查:
- 在每个表(包含关联表)明确标注租户所有权
- 采用统一的租户范围模式并在各处复用
- 确保报表和导出在非全局场景下不能在没有租户范围的情况下运行
- 避免在 API 与支持工具中使用模糊租户归属的标识
- 提前练习恢复和迁移步骤,而不是等到规模增长后再做
示例:支持人员搜索“invoice 1001”并拉出错误租户的记录,原因是查找跳过了租户范围。这是个小 bug,却能造成大影响。
在最终确定前的快速清单
在锁定多租户数据模型前,运行几项测试。目标是尽早发现数据泄露并确认所选方案在表变大时仍可用。
一天内能做的快速检查
- 隔离性证明: 创建两个租户(A 和 B),加入相似记录,然后验证每次读写都限定在活动租户范围。不要只依赖 UI 过滤。
- 权限绕过测试: 以租户 A 的用户登录,仅修改记录 ID 尝试打开、编辑或删除租户 B 的记录。如果任何操作成功,视为发布阻塞项。
- 写路径安全: 确认新记录始终获得正确的租户值(或进入正确的 schema/database),包括后台任务、导入或自动化场景。
- 报表试验: 确认能做租户级报表与“全租户”报表(供内部员工使用),并明确谁能看到全局视图。
- 性能检查: 现在就制定索引策略(特别是
(tenant_id, created_at)和其他常用过滤),并故意测量至少一个慢查询以知道“坏”的表现是什么样。
为使报表测试更具体,挑两个你知道会用到的问题(一个租户范围、一个全局)并在样本数据上运行它们。
-- Tenant-only: last 30 days, one tenant
SELECT count(*)
FROM tickets
WHERE tenant_id = :tenant_id
AND created_at >= now() - interval '30 days';
-- Global (admin): compare tenants
SELECT tenant_id, count(*)
FROM tickets
WHERE created_at >= now() - interval '30 days'
GROUP BY tenant_id;
如果你在 AppMaster 中做原型,把这些检查内置到 Business Process 流程(读、写、删),并在 Data Designer 中建立两个租户的种子数据。当这些测试在现实数据量下通过时,你就可以更有信心地做出决定。
示例场景:从首批客户到规模化
一家 20 人的公司要发布客户门户:发票、工单和一个简单仪表盘。他们预计第一个月有 10 个租户,并计划在一年内增长到 1000 个。
早期最简单的模型通常是单数据库且所有存客户数据的表包含 tenant_id。构建速度快、报表容易、避免重复设置。
在 10 个租户时,最大风险不是性能,而是权限。遗漏一个过滤(例如“列出发票”查询忘记了 tenant_id)就会泄露数据。团队应在一个一致的位置强制租户检查(共享业务逻辑或可复用 API 模式),并把租户作用域视为不可妥协。
当从 10 个增长到 1000 个租户时,需求会变化。报表压力增大、支持要求“导出某租户的全部数据”,一些大租户会主导流量并拖慢共享表。
一个实用的升级路径通常如下:
- 保持相同的应用逻辑和权限规则,但把高流量租户迁移到独立 schema。
- 对于最大或有合规要求的租户,把它们迁移到独立数据库。
- 保持一个共享的报表层来读取所有租户,并在非高峰期安排重负载报表。
选择能在当下保证数据安全的最简单模型,然后为“少数超大租户”问题规划迁移路径,而不是一开始就为其优化。
下一步:在无代码后端中选择模型并做原型
根据你首先要保护的目标选择:数据隔离、运维简易性,或租户级扩展能力。从构建一个小原型并用权限与报表场景尝试破坏它来获得信心。
简单入门指南:
- 若大多数租户较小且需要简单的跨租户报表,从单数据库并在每行使用
tenant_id开始。 - 若需要更强的隔离但仍希望只管理一个数据库,可考虑按租户独立 schema。
- 若租户要求硬隔离(合规、专属备份、嘈杂邻居风险),考虑每租户独立数据库。
在构建前,用明文写下租户边界:定义角色(owner、admin、agent、viewer)、各自权限,以及“全局”数据的含义(套餐、模板、审计日志)。决定报表如何工作:仅租户级,还是内部员工可以看“全租户”。
如果你使用 AppMaster,可以快速原型这些模式:在 Data Designer 中建模表(包含 tenant_id、唯一约束和查询依赖的索引),然后在 Business Process Editor 中强制规则,确保每次读写都有租户范围。如果需要参考平台,AppMaster 在 appmaster.io 上可用。
一个实用的最终测试:创建两个租户(A 和 B),加入相似的用户和订单,针对两者运行相同流程。尝试为租户 A 导出报表,然后故意把租户 B 的 ID 传入相同端点。当这些尝试每次都失败且关键报表在现实数据量下仍然快速运行时,你的原型才算“足够安全”。
常见问题
如果你想要最简单的运维和频繁的跨租户分析,默认从单数据库并在每个租户拥有的表上使用 tenant_id 开始。需要更强隔离或每租户定制而又不想运行多个数据库时,可考虑按租户独立 schema。当合规或企业需求要求硬隔离与单租户性能控制时,选择每租户独立数据库。
在后端把租户范围视为强制要求,而不是 UI 过滤。让 tenant_id 在租户拥有的表中为必填,并且始终从已认证用户的上下文中派生,而不是信任客户端输入。若栈允许,可加上 PostgreSQL 的行级安全作为保险,并编写测试尝试仅修改 ID 访问另一个租户的记录。
把 tenant_id 放在与你常用筛选匹配的索引最前面,这样数据库能直接跳到某个租户的数据切片。常见基线是索引 (tenant_id, created_at) 用于时间视图,另外常用的仪表盘筛选可加 (tenant_id, status) 或 (tenant_id, user_id)。同时把唯一性约束设为按租户作用(例如每租户唯一的邮箱)。
独立 schema 将表放在不同命名空间,减少意外跨租户关联的机会,并可在 schema 级别设置权限。主要缺点是迁移:每个 schema 都需要相同的变更,随着租户数增长这会成为流程问题。它是介于 tenant_id 与独立数据库之间的折中方案,适合想要比 tenant_id 更严格隔离但又希望只管理一个数据库的场景。
当你需要尽可能缩小故障波及面时(性能激增、配置错误或数据损坏),每租户独立数据库是最值得的。代价是运维开销更大:每个租户都会增加配额、备份、监控和迁移的工作量。你还需要一个可靠的租户目录和请求路由机制,确保每次 API 调用都使用正确的数据库连接。
使用单数据库并 tenant_id 时,全局报表最简单:只要去掉租户过滤即可。对于 schema 或独立数据库,常见做法是把关键事件或汇总定期复制到共享的报表存储。保持简单规则:产品级指标进入汇总或报表层,而租户数据保持隔离。
在支持工具中明确显示租户上下文,并要求在查看记录前执行显式的租户切换。若使用冒充(impersonation),记录谁在何时访问了什么,并限制其时效。避免接受只有记录 ID 而没有租户上下文的支持流程,因为这类设计会导致“发票 1001”之类的问题变成真实泄露。
当租户需要不同字段或工作流时,schema 或独立数据库可以降低一个租户的改动影响到其他租户的风险。如果大多数租户相似,保持单一共享模型并用 tenant_id 管理差异,或通过功能开关与可选字段实现定制,是更简单的做法。关键是避免那种“几乎全局”的表,它们在共享与租户特定含义之间模糊不清。
及早定义租户边界:确认认证后把租户上下文存在哪,并确保每次读写都使用它。在 AppMaster 中,通常是在 Business Process 逻辑中从已认证用户设置 tenant_id,然后再进行创建或查询操作,防止端点忘记这一点。把这作为一个可复用模式在所有地方应用,而不是每个界面单独实现。
创建两个租户,填入相似数据,然后仅修改记录 ID 来尝试突破隔离(读、改、删)。确认后台任务、导入和导出仍然写入正确的租户范围,因为这些路径容易被忽视。还要针对真实样本数据跑一次租户级报表和一次全局管理报表,确认性能与访问规则在增长时仍然成立。


