直接用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 <p>注意三点:<br>• 必须用 <code>ORDER BY RANDOM()</code>(PostgreSQL)或 <code>ORDER BY NEWID()</code>(SQL Server)保证打散,不能用固定字段<br>• <code>NTILE</code> 不保证每组行数绝对相等(总数不能被 n 整除时,前几组多 1 行),但误差可控<br>• MySQL 8.0+ 支持 <code>NTILE</code>,MySQL 5.7 及更早版本不支持,得换思路</p><h3>MySQL 5.7 兼容写法:用 <code>RAND()</code> 过滤更直接</h3><p>不需要窗口函数,直接在 <code>WHERE</code> 中用 <code>RAND()</code> 生成 [0,1) 区间随机数,和目标比例比较:</p><pre class="brush:php;toolbar:false;">SELECT * FROM your_table WHERE RAND() <p>这个语句每行独立判断,理论上接近 15% 概率被选中。但要注意:<br>• <code>RAND()</code> 在 MySQL 中**每行调用一次**,行为符合预期<br>• 如果加了 <code>LIMIT</code>,可能提前截断,导致实际比例偏差大,慎用<br>• 在有 <code>JOIN</code> 或子查询的复杂语句里,<code>RAND()</code> 可能被多次求值,建议包裹成派生表隔离</p><h3>抽样结果不稳定?那是没理解 <code>RAND()</code> 的执行时机</h3><p>在同一个查询中多次引用 <code>RAND()</code>(比如 <code>SELECT RAND(), RAND()</code>),会得到不同值;但在 <code>WHERE</code> 和 <code>SELECT</code> 中混用,可能导致意外过滤。更隐蔽的问题是:如果语句带 <code>GROUP BY</code> 或聚合,MySQL 可能将 <code>RAND()</code> 提升到外部作用域,造成整组共用一个随机值——结果就不是行级抽样了。稳妥做法是显式用子查询固化随机性:</p><pre class="brush:php;toolbar:false;">SELECT * FROM ( SELECT *, RAND() AS r FROM your_table ) t WHERE r <p>真正需要稳定可复现抽样的场景(比如 AB 实验),不能依赖 <code>RAND()</code>,得用哈希 + 取模,例如 <code>ABS(HASH(id)) % 100 ,但这就超出 <code>ROW_NUMBER</code> 范畴了。</code></p>










