从MySQL表中获取交替的偶数记录,通常意味着先对结果集按照某个字段排序,然后生成连续的行号,再筛选行号为偶数的那些行。它跟直接判断某个字段是否为偶数不是一回事,这是两个完全不同的需求。比如自增主键id的值为2、4、6,可能碰巧是偶数,但删除过数据后,第2条记录的id可能已经是3。如果不先明确需求,查询结果很容易出现偏差。

先厘清需求:行序偶数与字段值偶数
在关系型数据库里,表本身没有默认的行号概念,SELECT返回的结果集本质上是一个集合,除非使用ORDER BY,否则数据库不保证每次返回的顺序一致。因此,所谓交替的偶数记录,必须先规定一个排序依据。常见的做法是以自增主键、创建时间或者业务字段作为排序键,然后为每一行生成一个从1开始的连续序号,再取序号能被2整除的行。
这一点之所以重要,是因为很多初学者直接用WHERE id % 2 = 0来取偶数记录。这种写法判断的是id字段的数值是否为偶数,而不是行在排序结果中的位置是否为偶数。当id连续且从1开始时,二者结果相同;但只要发生过删除、回滚或者手动指定了不连续的主键值,两条SQL的结果就会完全不同。要获取交替的偶数行,正确思路是先排序、再编号、最后筛选。
还有一个容易混淆的地方是物理存储顺序。即使你按插入顺序写入数据,InnoDB也可能因为B+树索引的页分裂、更新和回收导致磁盘上的物理顺序与插入顺序不一致。不要依赖任何没有ORDER BY的查询来模拟行号,否则结果既不稳定也不可预测。
MySQL 8.0:使用ROW_NUMBER()窗口函数
MySQL从8.0版本开始支持窗口函数,其中ROW_NUMBER()非常适合处理行号生成。它会按照OVER子句中指定的排序规则,为结果集的每一行分配一个唯一的连续整数。拿到行号之后,只需要在外层查询中保留行号对2取模等于0的记录,就能得到交替的偶数行。
下面是一个基于订单表的示例。假设orders表包含id、customer_name和amount字段,我们希望按id升序排列,取出第2、第4、第6条等偶数行。可以先使用公用表表达式生成带行号的临时结果集,然后再筛选。
WITH numbered AS (
SELECT
id,
customer_name,
amount,
ROW_NUMBER() OVER (ORDER BY id) AS rn
FROM orders
)
SELECT id, customer_name, amount
FROM numbered
WHERE rn % 2 = 0
ORDER BY id;
这段SQL的执行逻辑非常清晰:内层查询先根据id排序并生成从1开始的连续行号rn,外层查询只保留rn为偶数的行。由于rn是排序后的序号,即使id存在空洞,例如id为1、3、7、8,最终取出的也是排序后的第2条和第4条记录,而不是id值本身为偶数的记录。
如果不喜欢CTE语法,也可以使用派生表来实现同样的效果。窗口函数只能在SELECT列表或ORDER BY中使用,不能直接写在WHERE条件里,因此必须把带行号的结果包装成子查询或CTE,再在外层过滤。这是很多开发者在第一次接触窗口函数时容易犯的错误。
SELECT id, customer_name, amount
FROM (
SELECT
id,
customer_name,
amount,
ROW_NUMBER() OVER (ORDER BY id) AS rn
FROM orders
) AS numbered
WHERE rn % 2 = 0
ORDER BY id;
使用ROW_NUMBER()的好处是语法标准、意图明确,而且优化器能够较好地处理窗口排序。如果排序列上有索引,性能通常可以接受;如果数据量很大,窗口排序本身会产生一定开销,但相比使用变量模拟行号的写法,可读性和稳定性明显更优。
MySQL 5.7:使用用户变量模拟行号
在MySQL 8.0之前,没有窗口函数可用,常见做法是借助用户变量在查询过程中累加计数。用户变量的赋值发生在结果集被依次处理时,因此可以利用它模拟行号生成。需要注意的是,变量赋值和ORDER BY同时使用时,MySQL 5.7的求值顺序并不严格保证,建议先在内层子查询中完成排序,再在外层对排序好的结果集进行变量累加。
下面仍然以orders表为例,按id升序排列,并筛选出偶数行。为了安全地初始化行号,可以在子查询中通过交叉连接设置初始值,避免依赖SET语句在查询前单独执行。
SELECT id, customer_name, amount
FROM (
SELECT
t.id,
t.customer_name,
t.amount,
(@row_number := @row_number + 1) AS rn
FROM (
SELECT id, customer_name, amount
FROM orders
ORDER BY id
) AS t
CROSS JOIN (SELECT @row_number := 0) AS init
) AS numbered
WHERE rn % 2 = 0;
这段代码的最内层子查询先把原始数据按id排好序,然后通过CROSS JOIN (SELECT @row_number := 0) AS init初始化变量,每处理一行时变量加1并输出为rn。外层再根据rn的奇偶性进行过滤。由于排序已经在最内层完成,变量累加的顺序与最终所需的行号顺序保持一致,结果更加可靠。
如果要过滤的是奇数行,只需要把WHERE rn % 2 = 0改成WHERE rn % 2 = 1。如果想从第3条开始每隔一行取一条,也就是偏移后再交替,还可以把筛选条件写成WHERE rn % 2 = 1 AND rn >= 3,或者使用WHERE (rn - 3) % 2 = 0。这种基于行号的筛选非常灵活,但要注意行号从1开始,计算边界时不要漏掉或重复。
用户变量方案在MySQL 8.0中仍然能运行,但官方文档已经明确说明,变量赋值表达式的求值顺序并未完全保证,未来版本可能调整。因此如果你的数据库是8.0及以上,优先使用窗口函数;只有在维护老版本系统或需要兼容特定环境时,才推荐使用变量方案。
性能与稳定性注意事项
无论使用窗口函数还是用户变量,排序都是整个查询的核心成本。为了让行号生成稳定且高效,最好在ORDER BY的字段上建立合适的索引。例如示例中按id排序,主键索引天然有序,可以直接利用;如果按创建时间排序,则需要在创建时间列上建立索引。没有索引时,MySQL需要先对符合条件的记录做文件排序,数据量一大,查询延迟会明显上升。
还有一个细节是NULL值的排序。如果排序字段允许NULL,行号生成虽然不会失败,但NULL在升序中的位置可能不符合业务预期。MySQL默认将NULL视为最小值,升序时NULL会排在最前面。若业务要求NULL排在最后,可以在ORDER BY中使用ORDER BY COALESCE(sort_col, 999999999)或者ORDER BY sort_col IS NULL, sort_col来调整。否则取出的偶数行可能包含你并不想要的NULL值记录。
另外,当表中存在并发写入时,两次执行同一查询得到的结果可能不同。这并非MySQL的问题,而是因为数据集合本身发生了变化。如果业务场景要求可重复读取一组稳定的偶数记录,建议在事务中使用合适的隔离级别,或者基于某个不变的时间快照列进行过滤。对于报表类查询,也可以在夜间低峰期将需要的数据预先落入汇总表,再基于汇总表做行号筛选,避免长时间锁表或占用过多资源。
总结起来,获取交替的偶数记录并不复杂,核心步骤始终是先排序、再编号、后过滤。MySQL 8.0优先选择ROW_NUMBER(),老版本则可以用用户变量模拟。无论采用哪种方式,都不要把物理存储顺序当作默认排序,也不要把字段值奇偶与行序奇偶混为一谈。理解了这两点,就能稳定地拿到正确的偶数行数据。
MySQL偶数记录ROW_NUMBER修改时间:2026-08-23 16:30:06