正确做法必须显式按租户字段切片:ROW_NUMBER() OVER (PARTITION BY tenant_id ORDER BY created_at, id),其中PARTITION BY tenant_id是硬性前提,确保每个租户内部独立计数,ORDER BY需含确定性字段防编号波动。

ROW_NUMBER() 必须配合 PARTITION BY 才能实现租户隔离编号
不加 PARTITION BY tenant_id 的 ROW_NUMBER() 会把全表当做一个序列,所有租户的记录混在一起编号,完全失去多租户语义。真正起作用的是分区逻辑,不是排序本身。
典型错误写法:ROW_NUMBER() OVER (ORDER BY created_at) —— 这会在整个表维度生成 1,2,3,…,A 租户的第 3 条记录可能被编为 17,B 租户的第 1 条可能是 18。
正确做法必须显式按租户字段切片:
ROW_NUMBER() OVER (PARTITION BY tenant_id ORDER BY created_at, id)
-
PARTITION BY tenant_id是硬性前提,确保每个租户内部独立计数 -
ORDER BY子句需包含确定性排序字段(如created_at+id),避免因排序不稳定导致同一行在不同查询中编号变化 - 如果业务要求“每租户从 1001 开始编号”,
ROW_NUMBER()本身不支持偏移,得用1000 + ROW_NUMBER()...手动叠加
WHERE 条件必须放在子查询或 CTE 外层,不能塞进窗口函数里
窗口函数无法感知 WHERE 过滤后的结果集——它总是在原始数据集上计算。若先过滤再编号,必须用嵌套结构,否则编号序号会“跳空”。
比如只查 tenant_id = 't-202' 的最近 5 条,错误写法:
SELECT *, ROW_NUMBER() OVER (PARTITION BY tenant_id ORDER BY created_at) AS rn FROM orders WHERE tenant_id = 't-202' AND status = 'paid'
这看似合理,但实际没问题;真正危险的是下面这种:
SELECT *, ROW_NUMBER() OVER (PARTITION BY tenant_id ORDER BY created_at) AS rn FROM orders WHERE status = 'paid'
此时 rn 是对所有租户的已支付订单统一编号,t-202 的第一条可能得到 rn=42,而非 1。
- 安全模式:用 CTE 或子查询先限定租户+业务状态,再在其结果上开窗
- 性能注意:大表慎用
PARTITION BY tenant_id后再WHERE tenant_id = ?,数据库未必能下推过滤,可能全分区扫描 - PostgreSQL 和 SQL Server 通常能优化该场景,MySQL 8.0+ 表现尚可,旧版 MySQL 不支持窗口函数,直接绕过
编号值不可直接用于业务主键或唯一约束
ROW_NUMBER() 是查询时动态计算的虚拟列,不持久、不索引、不保证并发一致性。把它当“订单号”存到业务字段里,等于埋雷。
常见误用场景:
- 在 INSERT 中用
ROW_NUMBER() OVER (...)生成“新编号”,但并发插入时会算出相同值(因为没锁表) - 把
rn当作display_id写入表,后续更新排序字段后,显示编号错乱 - 依赖
rn = 1查最新记录,但未加FOR UPDATE或事务隔离,读到脏数据
真正稳妥的做法是:编号仅用于展示或分页临时排序;业务侧需要稳定编号,应使用单独的序列(如 PostgreSQL SEQUENCE)、UUID+租户前缀,或应用层分配器。
MySQL 8.0 以下版本没有 ROW_NUMBER(),替代方案很受限
如果还在用 MySQL 5.7 或更早,别试图用变量模拟 ROW_NUMBER() 实现租户编号——在 JOIN、GROUP BY、ORDER BY 复杂组合下,用户变量行为不可靠,官方文档已明确标注“不保证执行顺序”。
可行但笨重的替代路径:
- 用自关联 COUNT:对每条记录,统计同租户中“排序值更小”的记录数,性能 O(n²),万级数据就卡死
- 迁移到 MySQL 8.0+(强烈建议),或换用支持标准窗口函数的数据库
- 把编号逻辑提到应用层:查出租户数据后,在代码里按排序规则分组编号(适合数据量不大、实时性要求不高的后台任务)
特别注意:即使升级到 MySQL 8.0,也要确认 sql_mode 没开启 ONLY_FULL_GROUP_BY 之类影响窗口函数解析的选项。
租户编号真正的难点不在语法,而在于“编号是否要全局连续”“是否允许删除后空缺”“跨库分片时如何协调”——这些决定了你该用数据库窗口函数、应用层序列,还是混合方案。别让 ROW_NUMBER() 成为默认答案。











