如何编写"递归更新"?

时间:2015-02-26 14:43:26

标签: sql firebird

我有一种存储在表格中的树。它有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.

4 个答案:

答案 0 :(得分:2)

确保您在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的行已被删除)。

答案 1 :(得分:1)

正如你所说,你能够得到你想要的所有身份。

所以一个解决方案是:

  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
    
  3. 但是:没有测试过!请检查您的数据量是否正常......

    为了达到最佳效果,必须始终拥有有意义的索引:

    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
    

答案 2 :(得分:0)

这不完全是您的解决方案,但您应该首先尝试创建递归的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)

答案 3 :(得分:0)

如果您确定树的深度小于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' );

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