我有一种存储在表格中的树。它有2个关键列id
和parent_id
。以及一些抽象数据,例如name
和mtime
。让我们说这是一个文件系统。
我可以从特定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.
答案 0 :(得分:2)
确保您在id
和parent_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)
正如你所说,你能够得到你想要的所有身份。
所以一个解决方案是:
选择要更新的所有ID并将其存储到#tmpid表
更新/删除
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
答案 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' );
您可以对从根开始并更改其所有子项的函数使用相同的方法。您可以再次使用相同的方法来创建删除过程。