PAYATHON 2026

SQLite 如何用递归 CTE 找到降序表值与递增公式的交点?

数据小陈

可以先用 ROW_NUMBER() 按 pc.c 降序编号,再让递归 CTE 每轮连接下一个编号。递归状态保存当前公式值和停止标记,从而按伪代码逐行比较。给定数据的返回值是 3。

可用的递归 CTE

在已有的 pc 表和示例数据上执行:

WITH RECURSIVE
ordered AS (
    SELECT
        c,
        ROW_NUMBER() OVER (ORDER BY c DESC) AS rn
    FROM pc
),
meet_in_middle(rn, plus_one, done) AS (
    VALUES (0, 0, 0)

    UNION ALL

    SELECT
        p.rn,
        CASE
            WHEN m.plus_one > p.c THEN m.plus_one
            ELSE m.plus_one + 1
        END,
        CASE
            WHEN m.plus_one > p.c THEN 1
            ELSE 0
        END
    FROM meet_in_middle AS m
    JOIN ordered AS p
        ON p.rn = m.rn + 1
    WHERE m.done = 0
)
SELECT plus_one
FROM meet_in_middle
ORDER BY rn DESC
LIMIT 1;

ordered 为每个表值分配从 1 开始的编号。递归初始状态 (0, 0, 0) 表示尚未处理任何行,公式值为 0,停止条件尚未满足。

每轮通过 p.rn = m.rn + 1 读取下一行,并执行比较:

  • 如果 m.plus_one > p.c,保留当前公式值,将 done 设为 1。
  • 如果条件不成立,公式值加 1,保持 done = 0。

满足条件的行仍会写入 CTE 结果,保存要返回的值。下一轮由于 m.done = 0 不再成立,递归结束。最后按 rn 降序取一行,得到最终状态中的 plus_one。

示例数据如何得到 3

比较使用的是处理当前行之前的公式值:

行号表值 c比较前的 plus_one比较结果处理后的 plus_onedone
1800 > 8 为假10
2611 > 6 为假20
3422 > 4 为假30
4233 > 2 为真31

处理表值 2 时停止,返回 3,不会再处理表值 0。

原查询缺少“下一行”的定位方式

原查询中的:

FROM pc, meet_in_middle

会将递归产生的行与 pc 中的行组合,没有指定每轮只取降序排列后的下一行。即使创建了 pc(c DESC) 索引,也不能据此把查询当作会自动记住读取位置的循环。

下面的条件则是对候选行进行过滤:

WHERE meet_in_middle.plus_one > cit

它没有表达“条件不满足时继续读取下一行”的流程。因此需要用编号定位下一行,再用递归状态决定是否继续。

原来的最大整数初始值也不再需要。起始行号 0 就能表示“尚未读取表中的任何行”。

边界情况与注意事项

如果始终没有出现 plus_one > c,递归会处理完所有行。每行都会让公式值增加 1,因此最终返回表的行数,与伪代码末尾的 return formula_value 一致。

如果 pc 为空,递归部分不会产生新行,初始状态仍然存在,因此返回 0。如果第一行就满足条件,例如降序后的最大值为负数,也直接返回 0。

比较条件保留为严格大于 >。两者相等时仍然继续,不能改成 >=。

ROW_NUMBER() 会为重复的 c 分配不同编号,因此重复值仍按多行参与计算。相同值之间的顺序不会影响这个问题的结果。

该写法要求 SQLite 支持窗口函数。窗口函数放在独立的非递归 CTE ordered 中,递归部分负责连接和状态更新。递归提前结束并不代表排序或编号也只处理了前几行;数据量较大时,需要结合实际执行计划评估成本。

备注:内容仅供参考。