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_one | done |
|---|---|---|---|---|---|
| 1 | 8 | 0 | 0 > 8 为假 | 1 | 0 |
| 2 | 6 | 1 | 1 > 6 为假 | 2 | 0 |
| 3 | 4 | 2 | 2 > 4 为假 | 3 | 0 |
| 4 | 2 | 3 | 3 > 2 为真 | 3 | 1 |
处理表值 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 中,递归部分负责连接和状态更新。递归提前结束并不代表排序或编号也只处理了前几行;数据量较大时,需要结合实际执行计划评估成本。
备注:内容仅供参考。
