【问题标题】:Renumber table rows with a recursive statement使用递归语句重新编号表行
【发布时间】:2019-11-11 20:50:22
【问题描述】:

为了了解递归的行为(在 SQLite 中),我尝试了以下语句来使用递归语句对表的行重新编号:

让我们创建一个示例表,

CREATE TABLE tb
    (x TEXT(1) PRIMARY KEY);

INSERT INTO tb
    VALUES ('a'), ('b'), ('c');

并重新编号从 2 开始的行

SELECT tb.x as x, tb.rowid + 1 as idx from tb; 
/* yields expected:
a|2
b|3
c|4
*/

尝试对递归 WITH(忽略 ROWID)执行相同操作,会导致分歧 — 在这里,我添加了 LIMIT 6 以防止分歧:

WITH RECURSIVE
newtb AS (
   SELECT tb.x, 2 AS idx FROM tb
   UNION ALL
   SELECT tb.x, newtb.idx + 1
      FROM tb, newtb
      LIMIT 6 -- only to prevent divergence!
)
SELECT * FROM newtb;
/* yields indefinitely:
a|2
b|2
c|2
a|3
b|3
c|3
...
*/

为什么递归在到达表tb的末尾时没有停止?可以预防吗?

请注意,该问题可以重新表述为如何在 SQLite 中生成以下过程伪代码的结果(不用多说):

tb := {'a', 'b', 'c'};
num := {1, 2, 3};
result := {};  # initialize an empty table

for i in {1, ..., length(tb)}  # assume index starts from 1
    append tuple(num[i], tb[i]) to result;
end for

# result will be {(1, 'a'), (2, 'b'), (3, 'c')}

这相当于 Python 等语言中的zip 操作。

根据@CPerkins 的提示,可以通过window functions(对于SQLite >= 3.25)非常优雅地实现这一目标;例如,

SELECT (row_number() OVER (ORDER BY x)) + 2 AS newId, x FROM tb;

【问题讨论】:

  • 第一个问题是没有关键字的连接——只有一个表的列表——产生两个源表的笛卡尔积。换句话说,FROM tb, newtb 是您在所有记录中获得重复组合的原因。这不仅在递归语句中是正确的,而且是应用任何此类连接的方式。
  • 接下来,如果你没有 LIMIT 子句,那么你需要一个 WHERE 子句来真正停止递归。这就是为什么如果没有 LIMIT 6,您将获得无限数量的行。
  • 当递归部分不返回任何行时,递归 cte 停止;你的总是每次都会返回一个。
  • rowid 视为连续行号是错误的,因为前者并不总是连续的。
  • 窗口函数更适合重新编号行。

标签: sqlite infinite-loop recursive-query


【解决方案1】:

为什么递归在到达表 tb 的末尾时没有停止?

因为它就是这样设计的,而且非常有用。它与大多数具有某种递归形式的语言几乎没有什么不同,并且通常是解决某些编程问题(例如遍历目录树)的高效且有效的方法。

  • 大多数计算机编程语言通过允许一个 函数从自己的代码中调用自己。一些功能性 编程语言不定义任何循环结构,而是依赖 仅在递归上重复调用代码。可计算性理论 证明这些仅递归的语言是图灵完备的;他们 与图灵完全命令式一样具有计算能力 语言,这意味着它们可以解决与 命令式语言,即使没有迭代控制结构,例如 while 和 for.Recursion (computer science)

如果您使用 LIMIT (SELECT count() FROM tb) 而不是 LIMIT 6,则递归将根据表中的行数停止。

但是,如果您要重新编号(通过在 rowid 上加 1),那么您会看到更像:-

WITH RECURSIVE 
    cte(idx,newidx) AS (
        SELECT (SELECT max(rowid) FROM tb),(SELECT max(rowid) FROM tb) +1
        UNION ALL
        SELECT 
        idx-1, newidx-1 FROM cte
        WHERE idx > 0
    )
SELECT (SELECT x FROM tb WHERE tb.rowid = cte.idx) AS x, newidx, idx AS original FROM cte WHERE x IS NOT NULL;

这将(假设 tb 有带有 a、b 和 c 的行 .... X、Y 和 Z 并且行 d-w 已被删除)导致:-

SQlite 的推理是:-

递归公用表表达式提供了执行以下操作的能力 树和图的分层或递归查询,一种能力 这在 SQL 语言中是不可用的。 SQL As Understood By SQLite - WITH clause

这可以避免吗?

是的,你不能使用递归,因为可能有替代方案,但与其他语言中的递归一样,如果你确实使用递归,那么你必须有一些方法来检测递归何时应该完成。使用 WHERE 或 LIMIT 子句有助于实现这一点。

【讨论】:

  • 如果表tb 没有ROWID 会怎样?我正在寻找一种简单的(递归)策略来重新编号行而不使用ROWID
  • @AlQuemist 除了很少使用的WITHOUT ROWID tablesVIRTUAL tables,每个表都有一个rowid column。您要重新编号的其他数字是什么?表格只有一个没有数值的列?
  • 我添加了一条注释以进一步阐明我的目标。
猜你喜欢
  • 1970-01-01
  • 2013-07-06
  • 1970-01-01
  • 2013-04-25
  • 2015-10-08
  • 1970-01-01
  • 2015-12-22
  • 1970-01-01
  • 2014-08-25
相关资源
最近更新 更多