租户ID是否加索引取决于查询模式:高频按tenant_id查询则必须加索引,否则宜用组合索引(tenant_id放最左位);误建单列索引会拖慢写入并增加磁盘占用,UUID型tenant_id需设前缀索引。

租户ID字段加不加索引,取决于查询模式
如果大部分查询都带 tenant_id 条件(比如 SELECT * FROM orders WHERE tenant_id = 123 AND status = 'paid'),那必须给 tenant_id 加索引;否则全表扫描会拖垮性能。但若只有管理后台偶尔按租户查,而日常查询全是主键或业务唯一键(如 order_no),那单独建 tenant_id 索引反而浪费写入开销和空间。
更常见也更高效的做法是组合索引:把 tenant_id 放在联合索引最左位,例如 INDEX idx_tenant_status (tenant_id, status)。这样既能支持租户隔离查询,又避免额外索引膨胀。
容易踩的坑:
- 误以为“所有带 tenant_id 的表都该加单列索引”,结果导致 INSERT/UPDATE 变慢、磁盘占用翻倍
- 用 UUID 做
tenant_id且未设前缀索引,在长字符串上建完整索引,严重拖慢索引构建和内存命中率
共享表 vs 分库分表:从连接数和运维成本倒推选型
单库共享表(所有租户数据存在同一张 users 表,靠 tenant_id 隔离)适合中小规模场景:租户数
一旦连接数逼近 MySQL 默认的 max_connections(通常 151),或某租户突发流量打满 IOPS,就该考虑分库——不是因为“数据量大”,而是因为资源争抢不可控。分库后每个租户独占连接池、慢查询不会波及其他租户,但代价是跨库 JOIN 消失、分布式事务难做、备份脚本得循环执行。
实操建议:
- 先用共享表 + 强制 SQL 审计(如
WHERE tenant_id = ?缺失时拒绝执行),跑 3–6 个月观察 QPS、慢查分布、连接峰值 - 分库别直接手写路由逻辑,优先试
ShardingSphere-JDBC或MyCat,它们能透明拦截 SQL 并改写tenant_id到库名 - 绝不混合使用:同一业务模块不能一部分租户走分库、一部分走共享表,否则权限、归档、灰度发布全乱套
硬删除还是软删除?看审计合规和历史数据依赖
加 is_deleted 字段并默认为 0 是常见做法,但问题在于:它让所有业务 SQL 都得补 AND is_deleted = 0,漏写一条就泄露已删数据。更糟的是,当租户要求“彻底清除 GDPR 数据”时,软删除反而成了障碍。
真正可落地的方案是分层处理:
- 用户主动删的数据 → 软删除(保留 30 天,供误操作恢复)
- 租户退订后整库清理 → 真删,配合
mysqldump --where="tenant_id != 456"快速导出留存库 - 财务类表(如
invoices)一律禁用删除,只允许状态变更为canceled,因为账务必须留痕
注意 is_deleted 字段类型别用 TINYINT 存 0/1,改用 ENUM('active', 'deleted', 'archived'),既防非法值,又让 SQL 更自解释。
时间字段用 UTC 还是本地时区?关键看跨时区租户是否共用数据
如果租户分布在不同时区(比如 SaaS 工具服务全球客户),所有时间字段(created_at、updated_at)必须统一存 UTC,应用层按租户配置的时区做转换。否则会出现“上海租户看到的订单时间比纽约租户早 12 小时”这种逻辑错乱。
但若所有租户都在同一国家、甚至同一省份(比如政务系统只服务某市下辖区县),存本地时间(如 Asia/Shanghai)反而更直观,省去每次 SELECT 都要 CONVERT_TZ() 的开销。
容易被忽略的细节:
-
TIMESTAMP类型自动转 UTC 存储,但DATETIME不转——混用会导致部分字段有时区偏移、部分没有 - MySQL 8.0+ 支持
TIMESTAMP WITH TIME ZONE语法,但实际仍是存 UTC,只是语法糖,别指望它自动处理显示时区 - 租户自行设置的“偏好时区”应存为字符串(如
"Europe/Berlin"),而非偏移量(如"+02:00"),因为夏令时规则会变
租户隔离不是加个字段就完事,真正的难点藏在查询路径、连接生命周期、删与存的语义边界里——这些地方一松动,隔离就变成幻觉。


















