tenant_id字段必须加普通索引,否则查询性能断崖式下跌;需建index tenant_id,常按时间查则优先建(tenant_id, created_at)联合索引,软删除场景下顺序应为(tenant_id, deleted_at, created_at)。

tenant_id 字段必须加普通索引,否则查询性能会断崖式下跌——这不是优化建议,是底线。
tenant_id 索引为什么不能省?
常见错误现象:单租户查 50ms,10 个租户并发查就飙到 2s+,EXPLAIN 显示全表扫描。
原因很直接:所有带 WHERE tenant_id = ? 或 JOIN ... ON t.tenant_id = u.tenant_id 的查询,没索引就只能扫全表。
-
tenant_id必须建普通索引(INDEX tenant_id),不是唯一索引——除非业务上它真就唯一 - 如果常按时间范围查(比如「某租户最近 7 天订单」),优先建联合索引:
INDEX tenant_id_created_at (tenant_id, created_at) - 若用软删除(
deleted_at)参与过滤,也得塞进联合索引,否则索引失效;同理,created_at和deleted_at同时存在时,顺序很重要:(tenant_id, deleted_at, created_at)比(tenant_id, created_at, deleted_at)更适配withTrashed()场景
全局作用域里读 tenant_id,从哪取才不翻车?
可靠的前提只有一个:请求生命周期内租户上下文必须唯一、且每次请求开始就重置。
- 中间件里解析子域名 / 请求头 / JWT payload,把租户实例存进
request()->attributes或自定义TenantManager单例 - 全局作用域里只读这个运行时上下文,别从 session 或 auth 用户里硬取——session 可能跨租户复用,auth 用户未必绑定租户
- FPM 进程复用时,上一个请求的
tenant_id会污染下一个;Laravel Octane 下更敏感,必须在每次请求入口显式重置
with() 关联加载为啥不自动继承租户过滤?
因为 Eloquent 的 with() 是独立子查询,不感知父模型的全局作用域。
-
User::with('orders')->get()→ 订单查的是全量,不是当前租户的 - 正确写法:
User::with(['orders' => fn ($q) => $q->where('tenant_id', tenant()->id)])->get() - 更稳妥的做法是给
Order模型也加全局作用域,并确保它的tenant()辅助函数返回当前上下文(不是缓存值、不是 auth 用户字段) - 关系定义里加约束(如
belongsTo的foreignKey+ownerKey)没用,它只管外键匹配,不管租户隔离
队列任务里租户上下文怎么传?
队列任务脱离 HTTP 生命周期,tenant()->id 在执行时大概率是 null 或错的。
- 必须显式传参:
ProcessInvoice::dispatch($invoiceId, tenant()->id) - 任务
__construct()里保存$this->tenantId,handle()里用它重初始化上下文(例如调用Tenant::find($this->tenantId)->makeCurrent()) - 别依赖闭包序列化或静态变量,Laravel 默认队列驱动(database/redis)不保证闭包环境还原
- 如果用了
execute()方法(如$tenant->execute(fn () => User::create(...))),它内部会帮你做上下文切换,但前提是该租户对象可序列化且未被修改过状态
最易被忽略的一点:事件监听器(created、updated)和模型观察者(creating、saving)同样没有自动租户上下文——它们跑在模型生命周期里,但不绑定请求,必须手动补全 tenant_id 或触发重初始化。











