row_number()能用于训练/测试集拆分,因其可为每行稳定、可复现地生成唯一稠密序号;需显式order by(如id或created_at)确保一致性,再结合mod取模(如%10)或ntile分桶实现比例拆分,并须用cte或子查询一次性标记以保证互斥。

ROW_NUMBER() 为什么能用来做训练/测试集拆分
因为 ROW_NUMBER() 能给每一行稳定、可复现地打上唯一序号,配合取模或范围筛选,就能把数据按比例切开。它不依赖数据本身分布,也不需要随机种子(但要注意:默认无 ORDER BY 时结果不可靠)。
- 必须显式写
ORDER BY,否则不同执行可能生成不同序号,导致训练集“每次都不一样” - 用
ORDER BY RAND()看似随机,但在 MySQL 8.0+ 和 PostgreSQL 中性能差、不可复现;SQL Server 的NEWID()同理 - 更稳妥的做法是选一个业务上稳定且有区分度的字段排序,比如
id、created_at,再结合取模或窗口截断
用 MOD 取模实现近似 8:2 拆分(推荐初用)
这是最轻量、兼容性最好的方式,适用于大多数 OLAP 场景。核心思路:按某字段排序后编号,再对编号取模,把余数为 0~7 的归训练集,8~9 归测试集。
SELECT *,
ROW_NUMBER() OVER (ORDER BY id) % 10 AS split_mod
FROM your_table;
- 后续加
WHERE split_mod 得训练集,<code>WHERE split_mod IN (8, 9)得测试集 - 如果表里
id有空缺或不连续,不影响比例稳定性——因为ROW_NUMBER()生成的是稠密序号 - 注意:MySQL 5.7 不支持直接在 WHERE 中引用窗口函数别名,得套一层子查询或 CTE
用 NTILE(10) 做等频分桶(适合倾斜数据)
当原始数据按时间或 ID 分布不均(比如最近一周数据暴增),单纯用 MOD 可能导致测试集集中在某个时间段。NTILE(10) 会尽量让每组行数相等,再挑其中两组作测试集更鲁棒。
SELECT *,
NTILE(10) OVER (ORDER BY created_at) AS bucket
FROM your_table;
-
NTILE不保证绝对均匀(总行数不能被 10 整除时,前面几组多一行),但比MOD对偏斜更友好 - 选
bucket IN (1, 2)还是bucket IN (9, 10)取决于你是否想让测试集覆盖首尾分布——通常建议避开两端,选中间两组(如 4 和 5)以减少时序泄露风险 - PostgreSQL 和 SQL Server 支持,但 SQLite 不支持
NTILE
训练集和测试集必须严格互斥且无 overlap
最容易被忽略的一点:拆分逻辑必须作用于同一份基础数据快照。如果两次查询分别执行 ROW_NUMBER(),又没加 WITH TIES 或事务隔离,可能因并发写入导致某条记录同时出现在两个集合里。
- 务必用 CTE 或子查询一次性算出所有标记列,再分别 SELECT
- 例如:先
WITH labeled AS (SELECT *, ROW_NUMBER() OVER (...) % 10 AS mod_val FROM t),再SELECT * FROM labeled WHERE mod_val 和 <code>SELECT * FROM labeled WHERE mod_val >= 8 - 如果底层表正在被 ETL 写入,记得加
FOR SHARE(PostgreSQL)或指定事务隔离级别,否则可能读到部分更新状态










