直接用ROW_NUMBER()配合取模(如WHERE rn % 10 = 1)做比例抽样行不通,因其结果严重依赖ORDER BY字段的分布:若按自增主键排序,则抽中开头连续段;若按倾斜字段(如城市)排序,则某些组全中、某些全无,实为伪周期采样而非真正比例抽样。

用 ROW_NUMBER() 做比例抽样为什么行不通
直接对全表加 ROW_NUMBER() OVER (ORDER BY ...) 再用 % 取余过滤,比如 WHERE rn % 10 = 1 模拟 10% 抽样,看似简单,但结果严重依赖 ORDER BY 字段的分布。如果按主键排序,而主键是自增 ID,那抽出来的就是开头连续一段,根本不是随机抽样;如果按某个倾斜字段(如城市名)排序,某些组可能全被抽中、某些完全落空。这不是比例抽样,是伪周期采样。
NTILE() 是更靠谱的替代方案
NTILE(n) 会把结果集**大致均分**为 n 组,每组编号 1 到 n,适合做粗粒度比例控制。比如要抽 15%,可设 NTILE(100),再取 ntile :
SELECT * FROM ( SELECT *, NTILE(100) OVER (ORDER BY RANDOM()) AS ntile FROM your_table ) t WHERE ntile <= 15;
注意三点:
• 必须用 ORDER BY RANDOM()(PostgreSQL)或 ORDER BY NEWID()(SQL Server)保证打散,不能用固定字段
• NTILE 不保证每组行数绝对相等(总数不能被 n 整除时,前几组多 1 行),但误差可控
• MySQL 8.0+ 支持 NTILE,MySQL 5.7 及更早版本不支持,得换思路
MySQL 5.7 兼容写法:用 RAND() 过滤更直接
不需要窗口函数,直接在 WHERE 中用 RAND() 生成 [0,1) 区间随机数,和目标比例比较:
SELECT * FROM your_table WHERE RAND() < 0.15;
这个语句每行独立判断,理论上接近 15% 概率被选中。但要注意:
• RAND() 在 MySQL 中**每行调用一次**,行为符合预期
• 如果加了 LIMIT,可能提前截断,导致实际比例偏差大,慎用
• 在有 JOIN 或子查询的复杂语句里,RAND() 可能被多次求值,建议包裹成派生表隔离
抽样结果不稳定?那是没理解 RAND() 的执行时机
在同一个查询中多次引用 RAND()(比如 SELECT RAND(), RAND()),会得到不同值;但在 WHERE 和 SELECT 中混用,可能导致意外过滤。更隐蔽的问题是:如果语句带 GROUP BY 或聚合,MySQL 可能将 RAND() 提升到外部作用域,造成整组共用一个随机值——结果就不是行级抽样了。稳妥做法是显式用子查询固化随机性:
SELECT * FROM ( SELECT *, RAND() AS r FROM your_table ) t WHERE r < 0.15;
真正需要稳定可复现抽样的场景(比如 AB 实验),不能依赖 RAND(),得用哈希 + 取模,例如 ABS(HASH(id)) % 100 ,但这就超出 <code>ROW_NUMBER 范畴了。

















