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 的报表与性能

将数据设计转成代码
以可视化方式设计表、约束和索引,并生成可投入生产的后端代码。
Model Data

使用 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 的报表与性能

创建租户安全的门户
构建内部工具和客户门户,随着扩展保持租户安全。
Try AppMaster

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

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

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

常见模式包括:

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

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

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

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

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

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

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

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

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

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

独立数据库的报表与性能

统一租户范围
为每个端点和管理界面创建并复用安全的 tenant_id 模式。
Start Building

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

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

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

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

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

集中权限逻辑
使用可视化业务逻辑在每次读写时强制执行租户检查。
Generate Code

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

常见泄露来源是忘记在某个表上加租户字段,尤其是像 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。
准备就绪后,您可以选择合适的订阅。

开始吧