2025年10月14日·阅读约1分钟

无代码后端的多租户 SaaS 数据模型选项

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

无代码后端的多租户 SaaS 数据模型选项

问题:在不影响性能的情况下保持租户隔离

多租户意味着一个软件产品为多个客户(租户)服务,每个租户只能看到自己的数据。难点在于要始终如一地做到这一点:不仅是某个界面,而是每个 API 调用、管理面板、导出和后台任务。

你的数据模型比大多数团队预期的更影响日常运营。它决定了权限、报表、随增长的查询速度以及一个“小”错误可能造成的风险。少了一个过滤条件就可能泄露数据;隔离做得太激进,报表和支持会变得麻烦。

常见的三种多租户 SaaS 数据模型是:

  • 单个数据库,所有表在每行包含 tenant_id
  • 在一个数据库内为每个租户使用独立 schema
  • 为每个租户使用独立数据库

即便你在无代码后端可视化构建,权衡仍然相同。像 AppMaster 这样的工具会从你的设计生成真实的后端代码和数据库结构,所以早期的建模决策会很快反映到生产行为和性能中。

想象一个工单工具。如果每行工单都有 tenant_id,查询“所有未关闭工单”很简单,但你必须在每处严格检查租户。如果每个租户有自己的 schema 或数据库,默认隔离更强,但跨租户报表(例如“所有客户的平均关闭时间”)就更耗工夫。

目标是实现你可以信任的隔离,同时不为报表、支持和扩展增加不必要的摩擦。

快速决策法:4 个问题缩小选择范围

不要从数据库理论开始。先考虑产品如何被使用,以及每周需要怎样运维。

通常能让答案显而易见的四个问题

  1. 数据有多敏感?是否受严格合规约束? 医疗、金融或受限客户合同通常倾向更强的隔离(独立 schema 或独立数据库),这能降低风险并简化审计。

  2. 你是否经常需要跨租户报表? 如果经常需要“所有客户”的指标(使用量、收入、性能),单数据库加 tenant_id 通常最简单。独立数据库会让这件事变得困难,需要到处查询并合并结果。

  3. 租户之间会有多大差异? 如果租户需要自定义字段、自定义工作流或独特集成,独立 schema 或数据库可以降低变更互相影响的概率。如果大多数租户结构相同,tenant_id 更清爽。

  4. 你的团队实际上能运维什么? 更强的隔离通常意味着更多工作:更多备份、更多迁移、更多环节需要管理,也有更多隐藏故障的地方。

一个实用的做法是对最优的两个选项做原型,并测试真正的痛点:权限规则、报表查询,以及模型演进时变更的推广方式。

方案一:单数据库,所有行带有 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 模式。
Start Building

使用 tenant_id 的单库方案,报表通常比较直观。全局仪表盘(MRR、注册、使用量)可以通过一条跨所有人的查询得到,而租户级报表就是加上过滤条件的相同查询。

代价是随时间而来的性能问题。随着表增长,一个繁忙的租户可能成为“嘈杂邻居”,产生大量行、触发更多写入,并使得常见查询变慢,尤其是在数据库不得不扫描大量数据时。

索引能保持该模型的健康。大多数租户范围的读取应该使用以 tenant_id 为首列的索引,这样数据库可以直接定位到该租户的数据切片。

一个好的基线:

  • 添加以 tenant_id 为首列的复合索引(例如 tenant_id + created_attenant_id + statustenant_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 的报表与性能

发布一个安全的 SaaS 后端
构建一个把角色、权限和租户上下文内置到流程中的安全 SaaS 后端。
Create App

独立 schema 在默认情况下比纯 tenant_id 过滤提供更强的隔离,因为表在物理上是分开的,并且可以对 schema 设置权限。

当大多数报表是按租户查询时,这种方式很理想。查询变得简单,因为你只读取某个租户的表,不必在共享表上反复过滤。这个模型也支持需要额外表或自定义列的“特殊”租户,而不会迫使所有人都承担这些字段。

跨租户汇总报表是 schema 的短板。你要么需要一个能查询多个 schema 的报表层,要么在共享 schema 中维护汇总表。

常见模式包括:

  • 按租户的仪表盘只查询该租户的 schema
  • 带有每晚汇总的中央分析 schema,从每个租户抽取数据
  • 将租户数据导出为适合数据仓库的格式的导出作业

就租户级工作负载而言,性能通常不错。每个租户的索引更小,一个 schema 中的大量写入不太可能影响其他 schema。但代价是运维开销:为新租户开设 schema、运行迁移,并在模型变更时保证所有 schema 对齐。

当你希望比 tenant_id 更严格隔离但又不想管理大量数据库,或预期每租户会高度定制时,schema 是不错的选择。

方案三:每租户独立数据库

为每个租户使用独立数据库时,每个客户都有自己的数据库(或在同一服务器上的独立数据库)。这是隔离最强的一种方式:某个租户的数据损坏、配置错误或高负载很难波及到其他租户。

它适合受监管的环境(医疗、金融、政府)或期望严格隔离、定制保留策略或专属性能的企业客户。

入驻流程变成了一个预配工作流。当新租户注册时,系统需要创建或克隆数据库、应用基础 schema(表、索引、约束)、安全地创建并存储凭据,并将 API 请求路由到正确的数据库。

如果你用 AppMaster 构建,关键设计点是把租户目录放在哪(用于映射租户到数据库连接),以及如何保证每个请求使用正确的连接。

升级和迁移是主要的权衡点。schema 变更不再是“执行一次”,而是“为每个租户执行一次”。这增加了运维工作和风险,因此团队常用版本化 schema,并把迁移作为受控作业按租户跟踪进度。

好处是掌控:你可以先迁移大租户,观察性能,然后逐步推广变更。

独立数据库的报表与性能

快速原型化多租户
在几分钟内建模租户并观察数据布局在真实 API 中的表现。
Try AppMaster

独立数据库最容易理解。意外的跨租户读取不太可能发生,权限错误的波及范围通常仅限单个租户。

性能也是优势。Tenant A 的重查询、大批量导入或失控报表不会拖慢 Tenant B。这在防止“嘈杂邻居”问题上非常有效,并允许按租户调整资源。

代价体现在报表上。跨租户的全局分析最困难,因为数据被物理拆分。实践中常见的做法包括把关键事件或表复制到中央报表数据库、将事件发送到仓库式数据集、在租户数量较少时运行每租户报表并聚合结果,或把产品指标与客户数据分离。

运维成本是另一大因素。更多数据库意味着更多备份、升级、监控和故障响应。你也更容易遇到连接数限制,因为每个租户可能需要独立的连接池。

常见错误:导致数据泄露或后期痛点的做法

测试通过再部署
当隔离和报表查询通过测试后,从原型推进到部署。
Deploy App

大多数多租户问题不是“大设计”失败,而是小疏忽累积成安全漏洞、报表混乱和昂贵的修复。多租户的可行性在于把租户隔离当作一种习惯,而不是事后才加的功能。

常见泄露来源是忘记在某个表上加租户字段,尤其是像 user_rolesinvoice_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 中建立两个租户的种子数据。当这些测试在现实数据量下通过时,你就可以更有信心地做出决定。

示例场景:从首批客户到规模化

及早测试隔离性
在决定 schema 前,设置两个租户并运行破坏性测试。
Build Prototype

一家 20 人的公司要发布客户门户:发票、工单和一个简单仪表盘。他们预计第一个月有 10 个租户,并计划在一年内增长到 1000 个。

早期最简单的模型通常是单数据库且所有存客户数据的表包含 tenant_id。构建速度快、报表容易、避免重复设置。

在 10 个租户时,最大风险不是性能,而是权限。遗漏一个过滤(例如“列出发票”查询忘记了 tenant_id)就会泄露数据。团队应在一个一致的位置强制租户检查(共享业务逻辑或可复用 API 模式),并把租户作用域视为不可妥协。

当从 10 个增长到 1000 个租户时,需求会变化。报表压力增大、支持要求“导出某租户的全部数据”,一些大租户会主导流量并拖慢共享表。

一个实用的升级路径通常如下:

  1. 保持相同的应用逻辑和权限规则,但把高流量租户迁移到独立 schema。
  2. 对于最大或有合规要求的租户,把它们迁移到独立数据库。
  3. 保持一个共享的报表层来读取所有租户,并在非高峰期安排重负载报表。

选择能在当下保证数据安全的最简单模型,然后为“少数超大租户”问题规划迁移路径,而不是一开始就为其优化。

下一步:在无代码后端中选择模型并做原型

根据你首先要保护的目标选择:数据隔离、运维简易性,或租户级扩展能力。从构建一个小原型并用权限与报表场景尝试破坏它来获得信心。

简单入门指南:

  • 若大多数租户较小且需要简单的跨租户报表,从单数据库并在每行使用 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 放在与你常用筛选匹配的索引最前面,这样数据库能直接跳到某个租户的数据切片。常见基线是索引 (tenant_id, created_at) 用于时间视图,另外常用的仪表盘筛选可加 (tenant_id, status)(tenant_id, user_id)。同时把唯一性约束设为按租户作用(例如每租户唯一的邮箱)。

按租户使用独立 schema 我能获得和失去什么?

独立 schema 将表放在不同命名空间,减少意外跨租户关联的机会,并可在 schema 级别设置权限。主要缺点是迁移:每个 schema 都需要相同的变更,随着租户数增长这会成为流程问题。它是介于 tenant_id 与独立数据库之间的折中方案,适合想要比 tenant_id 更严格隔离但又希望只管理一个数据库的场景。

什么时候每租户独立数据库才值得?

当你需要尽可能缩小故障波及面时(性能激增、配置错误或数据损坏),每租户独立数据库是最值得的。代价是运维开销更大:每个租户都会增加配额、备份、监控和迁移的工作量。你还需要一个可靠的租户目录和请求路由机制,确保每次 API 调用都使用正确的数据库连接。

如果数据按 schema 或数据库分散,如何做“全租户”报表?

使用单数据库并 tenant_id 时,全局报表最简单:只要去掉租户过滤即可。对于 schema 或独立数据库,常见做法是把关键事件或汇总定期复制到共享的报表存储。保持简单规则:产品级指标进入汇总或报表层,而租户数据保持隔离。

让支持人员访问租户账号时最安全的做法是什么?

在支持工具中明确显示租户上下文,并要求在查看记录前执行显式的租户切换。若使用冒充(impersonation),记录谁在何时访问了什么,并限制其时效。避免接受只有记录 ID 而没有租户上下文的支持流程,因为这类设计会导致“发票 1001”之类的问题变成真实泄露。

如何在不把模型弄得混乱的情况下支持租户定制?

当租户需要不同字段或工作流时,schema 或独立数据库可以降低一个租户的改动影响到其他租户的风险。如果大多数租户相似,保持单一共享模型并用 tenant_id 管理差异,或通过功能开关与可选字段实现定制,是更简单的做法。关键是避免那种“几乎全局”的表,它们在共享与租户特定含义之间模糊不清。

如何在像 AppMaster 这样的无代码后端中安全实现多租户?

及早定义租户边界:确认认证后把租户上下文存在哪,并确保每次读写都使用它。在 AppMaster 中,通常是在 Business Process 逻辑中从已认证用户设置 tenant_id,然后再进行创建或查询操作,防止端点忘记这一点。把这作为一个可复用模式在所有地方应用,而不是每个界面单独实现。

在确定多租户模型前我应该跑哪些测试?

创建两个租户,填入相似数据,然后仅修改记录 ID 来尝试突破隔离(读、改、删)。确认后台任务、导入和导出仍然写入正确的租户范围,因为这些路径容易被忽视。还要针对真实样本数据跑一次租户级报表和一次全局管理报表,确认性能与访问规则在增长时仍然成立。

容易上手
创造一些 惊人的东西

使用免费计划试用 AppMaster。
准备就绪后,您可以选择合适的订阅。

开始吧