可用mod配合行号实现固定步长抽样:先用row_number()生成有序序号,再对序号mod n取余为0的行;须显式order by保证结果可复现,各数据库row_number()支持度不同。

用 MOD 配合行号实现固定步长抽样最直接
SQL 没有原生的“每 N 行取 1 行”语法,但几乎所有主流数据库(PostgreSQL、MySQL 8.0+、SQL Server、Oracle、SQLite)都支持用行号与 MOD 运算组合实现。核心思路是:先生成连续序号,再对序号取模,保留余数为某固定值的行。
关键点在于行号生成方式因数据库而异,且必须注意 ORDER BY —— 没有显式排序时,行号顺序不确定,抽样结果不可复现。
- PostgreSQL / SQL Server / Oracle:用
ROW_NUMBER() OVER (ORDER BY ...) - MySQL 8.0+:同样支持
ROW_NUMBER();5.7 及更早版本只能靠变量模拟(不推荐,易出错) - SQLite:从 3.25.0 起支持
ROW_NUMBER();旧版需用(SELECT COUNT(*) FROM t AS t2 WHERE t2.id 类子查询(性能差,慎用)
PostgreSQL 示例:每 10 行取第 1 行(步长=10,起始偏移=0)
假设表叫 sales,想按时间顺序每 10 条记录取最早那条:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY created_at, id) AS rn FROM sales ) t WHERE MOD(t.rn, 10) = 1;
注意:MOD(rn, 10) = 1 表示取第 1、11、21… 行(即自然计数下“每 10 行的第 1 行”)。若想取第 0、10、20… 行(从 0 开始计数),得写成 MOD(rn - 1, 10) = 0 或直接 MOD(rn, 10) = 0 并接受第 10 行是第一个样本——这取决于你对“步长起点”的定义。
- 步长为
N时,MOD(rn, N) = k中的k取值范围是1到N(ROW_NUMBER()从 1 开始) - 若用
RANK()或DENSE_RANK()替代,遇到并列值会跳号,导致实际步长不均,务必避免 - 大表上加
ORDER BY可能触发全表排序,影响性能;如有合适索引(如created_at上的索引),可显著加速
MySQL 8.0+ 兼容写法及常见翻车点
MySQL 8.0+ 语法与 PostgreSQL 基本一致,但有两个高频坑:
-
MOD函数在 MySQL 中对负数返回负余数(如MOD(-1, 5) = -1),而行号不会为负,所以安全;但若误用表达式导致负值,结果会意外漏行 - 别名作用域问题:MySQL 不允许在
WHERE中直接引用SELECT里的别名rn,必须套一层子查询(如上面 PostgreSQL 示例那样),不能写成SELECT ..., ROW_NUMBER() ... AS rn WHERE MOD(rn, 10) = 1 - 如果表无主键或唯一排序依据,
ORDER BY用id或PRIMARY KEY是底线;仅用ORDER BY RAND()会破坏步长逻辑,变成随机抽样
替代方案:用 OFFSET + LIMIT 循环查(仅适合小规模预估)
当只要少量样本(比如总共就取 5 个点),且能接受应用层配合时,可用分页方式模拟步长:
SELECT * FROM sales ORDER BY id LIMIT 1 OFFSET 0; SELECT * FROM sales ORDER BY id LIMIT 1 OFFSET 9; SELECT * FROM sales ORDER BY id LIMIT 1 OFFSET 19;
这种方式绕开了窗口函数,兼容性极好(连 MySQL 5.6 都支持),但缺点明显:
- 每次查询都要扫描前
OFFSET行,第 100 个样本就要跳过 999 行,I/O 和 CPU 成倍增长 - 无法单条 SQL 完成,需业务代码循环拼 SQL 或用存储过程
- 若数据在查询间隙发生增删,
OFFSET对应的物理行可能漂移,样本位置失真
真正需要稳定、高效、单语句的固定步长抽样,ROW_NUMBER() + MOD 是目前最通用也最可控的路径。唯一要死守的底线是:永远显式 ORDER BY,永远验证 MOD 的余数起始值是否符合你的采样意图。










