最可靠方式是MOD(ROW_NUMBER() OVER (ORDER BY stable_col), 2),必须配合窗口函数生成连续序号,排序字段需稳定(如created_at, id),不可直接对id取模或省略ORDER BY。

MOD函数在SQL中怎么判断奇偶行
直接用 MOD(row_number() OVER (ORDER BY ...), 2) 是最可靠的方式。注意不能对原始主键或ID直接取模——如果ID是自增但中间有删除,就会漏桶;也不能依赖物理顺序(如没有 ORDER BY 的 ROW_NUMBER()),结果不可复现。
常见错误现象:MOD(id, 2) 在分页或删数据后颜色错乱;MOD(1, 2) 写成 MOD(2, 1) 导致全为 0;没加 OVER() 报错 “window function requires OVER clause”。
- 必须配合
ROW_NUMBER()或类似窗口函数生成连续序号 - 排序字段要明确且稳定(比如
created_at, id),否则同页两次查询可能行序不同 - 返回值是 1(奇数行)或 0(偶数行),不是布尔值,别直接当条件写
WHERE MOD(...) = TRUE
用MOD做批量分桶(比如每5行一组)
分桶本质是整除截断,MOD(n, 5) 只能返回余数(0~4),真正分组得用 FLOOR((n-1)/5) 或 (n-1) / 5(整数除法,部分数据库如 PostgreSQL、SQL Server 支持;MySQL 需显式 FLOOR)。
使用场景:导出时按每100条切一个文件;报表里把用户按注册顺序每50人分一运营组;ETL 中控制并发批次大小。
- 起始偏移很重要:从第1行开始分桶就用
(row_num - 1) / bucket_size;想让第1–5行属于桶1(而非桶0),再加1 - PostgreSQL 中
SELECT ..., (ROW_NUMBER() OVER (...) - 1) / 5 AS bucket_id可直接整除 - MySQL 8.0+ 同样支持整数除法,但 5.7 必须写
FLOOR((ROW_NUMBER() OVER (...) - 1) / 5) - Oracle 需用
FLOOR,且注意ROWNUM不能在WHERE后直接用,得套一层子查询
不同数据库对MOD和整除的兼容性差异
MOD 函数名基本统一,但行为细节有坑:SQL Server 用 % 运算符(col % 2),不支持 MOD() 函数;BigQuery 中 MOD 对负数返回负余数(MOD(-1, 3) = -1),而 PostgreSQL 返回正余数(2),影响分桶边界。
- 安全写法:统一用
MOD(ABS(col), N)避免负数干扰(如果业务允许忽略符号) - SQL Server 必须用
%,且ROW_NUMBER()后不能直接跟%,需括号包裹:(ROW_NUMBER() OVER (...)) % 2 - SQLite 的
MOD实际是%运算符别名,不支持函数调用语法MOD(x,y),会报错 - ClickHouse 用
modulo函数,不是MOD
为什么不能只靠ORDER BY + LIMIT做分桶
单纯用 LIMIT 10 OFFSET 20 做分页式分桶,无法保证跨批次逻辑一致——OFFSET 跳过的行如果被并发删改,后续桶内容就偏移了。而基于 ROW_NUMBER() + MOD 或整除的方案,只要排序规则固定,每次重跑结果完全可重现。
性能上,窗口函数确实比简单查询略重,但现代数据库(如 PostgreSQL 14+、MySQL 8.0+)对 ROW_NUMBER() OVER (ORDER BY indexed_col) 优化很好,只要排序字段有索引,基本无感。
- 别在没索引的字段上
ORDER BY然后ROW_NUMBER(),会触发全局排序,OOM 风险高 - 如果只是临时着色(前端渲染用),优先考虑把
ROW_NUMBER()下推到应用层计算,减少 SQL 复杂度 - 真正大批量分桶(千万级)时,
FLOOR((ROW_NUMBER()-1)/N)比嵌套CASE WHEN MOD(...) IN (0,1,2...) THEN ...更快也更易维护
实际用的时候,最容易被忽略的是排序字段的稳定性——哪怕加了 ORDER BY created_at,如果该字段存在秒级重复值,ROW_NUMBER() 的分配顺序仍可能因执行计划变化而抖动。补救办法很简单:末尾追加一个唯一字段,比如 ORDER BY created_at, id。











