【问题标题】:How to write a "recursive update"?如何编写“递归更新”?
【发布时间】:2015-02-26 14:43:26
【问题描述】:

我有某种树存储在表中。它有 2 个关键列 idparent_id。还有一些抽象数据,例如namemtime。可以说这是一个文件系统。
我可以从特定的id 中选择所有孩子或所有父母。 (如this answer 中所述)

问题是如何更新(或删除)这样的子树?
例如,我想更新某个节点及其所有子节点(或节点及其所有父节点到根节点)的修改时间。或者删除这个带有子节点的节点。就性能而言,最好的方法是什么?这个表可以是非常大的 100M+ 或记录。

DDL

create table test (
  id int not null primary key,
  parent_id int not null,
  name varchar(100),
  mtime timestamp default current_timestamp
);
insert into test(id, parent_id, name) values(1, 0, "row1");
insert into test(id, parent_id, name) values(2, 1, "row2");
insert into test(id, parent_id, name) values(3, 2, "row3");
insert into test(id, parent_id, name) values(4, 2, "row4");
insert into test(id, parent_id, name) values(5, 4, "row5");
insert into test(id, parent_id, name) values(6, 4, "row6");
insert into test(id, parent_id, name) values(7, 6, "row7");

是什么造就了我们这棵树:

row1
|
row2--row4--row5
|     |
row3  row6
      |
      row7

--- 更新 Try1 ---

按照弗拉基米尔的建议尝试过:

create procedure upd_test (start_id integer)
as
begin
    WITH RECURSIVE
    CTE (id)
    AS
    (
        SELECT T.id
        FROM test AS T
        WHERE T.id = :start_id

        UNION ALL

        SELECT T.id
        FROM
            test AS T
            INNER JOIN CTE ON CTE.id = T.parent_id
    )
    UPDATE test
        SET mtime = '2001-02-03 10:11:12'
        WHERE id IN (SELECT id FROM CTE);
end

得到:

Invalid token.
Dynamic SQL Error.
SQL error code = -104.
Token unknown - line 19, column 5.
UPDATE.

【问题讨论】:

  • 您能否将sqlfiddle 与架构和一些示例数据拼凑在一起?
  • 添加了架构和数据。 sqlfiddle 不支持火鸟
  • 传统 RDB 不太擅长存储递归数据。如果您真的关心性能,请考虑将一些面向图形的 DB 与 RDB 一起使用。您可以将其用作“索引”。 Neo4j 不错。
  • 你建议什么数据库?请记住,它应该是免费软件,甚至是更好的开源软件。

标签: sql firebird


【解决方案1】:

确保您在idparent_id 上都有索引,这样才能有效地工作。

阅读有关 Firebird 的文档后 (http://www.firebirdsql.org/file/documentation/reference_manuals/reference_material/html/langrefupd25-select.html#langrefupd25-select-cte)

  • 最大递归深度为1024(所以,你需要检查它是否足够你的数据)
  • 用括号括起来时,CTE 构造可用作 SELECT 语句中的子查询,也可用于 UPDATE、MERGE 等。

更新

我已经在 Windows 7 64bit 上安装了最新的 Firebird 2.5.3 来测试语法。

基于上述,将某个节点的时间戳(例如,ID = 4)及其所有子节点的时间戳更新为某个值(例如,2001-02-03 10:11:12)的查询如下所示:

UPDATE TEST SET 
MTIME = '2001-02-03 10:11:12'
WHERE ID IN
(
    WITH RECURSIVE
    CTE (id)
    AS
    (
        SELECT T.id
        FROM test AS T
        WHERE T.id = 4

        UNION ALL

        SELECT T.id
        FROM
            test AS T
            INNER JOIN CTE ON CTE.id = T.parent_id
    )
    SELECT id FROM CTE
);

我检查并按预期工作(ID 为 4、5、6、7 的行已更新)。

删除

同样的做法,即:

DELETE FROM TEST
WHERE ID IN
(
    WITH RECURSIVE
    CTE (id)
    AS
    (
        SELECT T.id
        FROM test AS T
        WHERE T.id = 4

        UNION ALL

        SELECT T.id
        FROM
            test AS T
            INNER JOIN CTE ON CTE.id = T.parent_id
    )
    SELECT id FROM CTE
);

运行时没有语法错误,但它只删除了 一个id = 4。我会称之为错误。

删除临时表

以下工作正常。提前创建global temporary table。 临时的只是表中的数据,而不是表本身,所以必须提前创建,并且会保留在数据库中。默认情况下,此类临时表中的数据将在事务结束时被清理。

CREATE GLOBAL TEMPORARY TABLE ToDelete
(id int not null primary key);

将递归 CTE 的结果插入到临时表中,然后使用它从主表中删除找到的 ID。确保这两个语句在同一个事务中运行。

INSERT INTO ToDelete
WITH RECURSIVE
CTE (id)
AS
(
    SELECT T.id
    FROM test AS T
    WHERE T.id = 4

    UNION ALL

    SELECT T.id
    FROM
        test AS T
        INNER JOIN CTE ON CTE.id = T.parent_id
)
SELECT id FROM CTE
;

DELETE FROM TEST
WHERE ID IN (SELECT ID FROM ToDelete)
;

我检查了,这按预期工作(ID 为 4、5、6、7 的行已被删除)。

【讨论】:

  • 无法让它工作。用代码和错误更新了问题。
  • 我找到了 Firebird 2.5.3 的正确语法。查看更新的答案。
  • 非常感谢。现在它完美无缺。与使用临时表相比,效率不能说太多。稍后我会研究这个,当我们有更大的 BD 时,我会在这里写。
  • @Dimanoid,不客气。性能很难预测。您应该生成大量数据并测试不同变体在您的系统和硬件上的表现。至少有两种明显不同的情况。 1)浅树多,即更新的行数少 2)深树少,即更新的行数多。索引有助于查找要更新的行,但更新索引本身需要时间。
【解决方案2】:

正如你所说,你可以得到你想要的所有 ID。

所以一种解决方案是:

  1. 选择所有要更新的ID并存储到#tmpid表中

  2. 更新/删除

    UPDATE t
    SET t.mtime = GETDATE()
    FROM dbo.TreeTable t INNER JOIN #tmpid i ON t.id = i.id
    
    DELETE t FROM dbo.TreeTable t INNER JOIN #tmpid i ON t.id =i.id
    

但是:未经测试!请检查您的数据量是否可以...

为了达到最佳性能,总是需要有有意义的索引:

CREATE UNIQUE CLUSTERED INDEX idx_treetable_id
  ON dbo.TreeTable(id);
CREATE UNIQUE INDEX idx_treetable_unique
    ON dbo.TreeTable(id,parent_id)
CREATE NONCLUSTERED INDEX idx_parent_id
  ON dbo.TreeTable(parent_id);
GO

【讨论】:

  • 不幸的是,您的答案适用于 SQL Server,不适用于 Firebird;类似的事情可以在 Firebird 中完成,但需要一个全局临时表,并使用 MERGE 语句,因为 Firebird 不支持 UPDATE 中的连接。
  • @MarkRotteveel,我找到了UPDATE 的正确语法(使用 Firebird 2.5.3 测试)。看我的回答。
【解决方案3】:

这不完全是您的解决方案,但您应该首先尝试创建一个递归 SELECT 语句。当您可以选择您希望的所有行时,您只需将其更改为 UPDATE 或 DELETE。

这是一个递归 SELECT 的示例。为了能够对其进行全面测试,我需要大量数据,所以我想最好让您自己尝试一下。应该是这样的。

With Recurs AS
( 
select id, name, parent_id
from test
union
select id, name, parent_id
from Recurs r
where id=r.parent_id
)
select *
from Recurs
order by id
option (maxrecursion 0)

【讨论】:

  • 请先阅读问题。我说我知道如何选择我需要的东西并提供了链接。
【解决方案4】:

如果您确定树的深度小于 1024,在 MYSQL 中您可以使用以下过程:

DELIMITER $$ 

CREATE PROCEDURE upd_test( IN start_id INT, IN str_date CHAR(20) )
BEGIN

    DECLARE next_id INT DEFAULT NULL ;

    UPDATE  test
    SET     mtime = str_date
    WHERE   id    = start_id;

    SELECT parent_id FROM test WHERE id = start_id INTO next_id;

    IF next_id > 0 
    THEN 
        CALL upd_test( next_id, str_date ); 
    END IF;

END$$

DELIMITER ;

接下来,将递归深度设置为 1024(这显然是 Vladimir 提到的 Firebird 支持的最大值)并运行您的程序。

set max_sp_recursion_depth = 1024;

使用您给我们的示例,您可以使用给定日期时间的过程来更新节点 6、4、2 和 1:

CALL upd_test( 6, '2001-02-03 10:11:12' );

您可以对从根开始并更改其所有子级的函数使用相同的方法。并且您可以再次使用相同的方法来创建删除过程。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-12-12
    • 1970-01-01
    • 2019-10-03
    • 2013-04-29
    • 1970-01-01
    • 1970-01-01
    • 2021-01-18
    • 1970-01-01
    相关资源
    最近更新 更多