Warshall的算法用存储过程实现

时间:2013-06-13 20:33:03

标签: mysql stored-procedures query-optimization floyd-warshall transitive-closure

我在MySQL存储过程中实现了Warshall的算法。不幸的是,该过程需要很长时间才能完成。我是编写存储过程的初学者,你知道我能做什么,让它更快吗?

简要说明:我正在尝试计算邻接列表的传递闭包。我想知道,哪些节点是连接的(直接在一条边上,或间接在多条边上)。例如:

Input:  (1, 2), (2, 3), (3, 4)
Output: (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)

以下图片说明了图表:


来自维基共享资源的图片: https://en.wikipedia.org/wiki/File:Transitive-closure.svg

您可以在SQL Fiddle或此处查看代码:

# "Warshall's algorithm" to calculate the transitive closure
# (1) For k = 1 to n
# (2)   For i = 1 to n
# (3)     If d[i,k] = 1
# (4)       For j = 1 to n
# (5)         If d[k,j] = 1 : d[i,j] = 1
create procedure closure()
begin
    drop table if exists adjMatrix;
    drop table if exists idArray;
    create temporary table adjMatrix (idFrom int not null, idTo int not null,
                                      primary key (idFrom, idTo));
    create temporary table idArray (id int);
    insert into adjMatrix select parent_id, id
                          from article where parent_id is not null;
    insert into idArray select id from article;
    blockk: begin
        declare k, fink int;
        declare ck cursor for select id from idArray;
        declare continue handler for not found set fink = 1;
        open ck;
        loopk: loop
            fetch ck into k;
            if fink = 1 then
                leave loopk;
            end if;
            blocki: begin
                declare i, fini int;
                declare ci cursor for select id from idArray;
                declare continue handler for not found set fini = 1;
                -- select k from dual;
                open ci;
                loopi: loop
                    fetch ci into i;
                    if fini = 1 then
                        leave loopi;
                    end if;
                    blockj: begin
                        if exists (select * from adjMatrix where idFrom=i and idTo=k)
                        then
                            blockx: begin
                                declare j, finj int;
                                declare cj cursor for select id from idArray;
                                declare continue handler for not found set finj = 1;
                                open cj;
                                loopj: loop
                                    fetch cj into j;
                                    if finj = 1 then
                                        leave loopj;
                                    end if;
                                    if exists (select * from adjMatrix
                                               where idFrom=k and idTo=j) then
                                        insert into adjMatrix values (i, j);
                                    end if;
                                end loop loopj;
                                close cj;
                            end blockx;
                        end if;
                    end blockj;
                end loop loopi;
                close ci;
                -- select idFrom, idTo from adjMatrix order by idFrom, idTo;
            end blocki;
        end loop loopk;
        close ck;
    end blockk;
    insert into adjMatrix select id, id from article where parent_id is null;
    select idFrom, idTo from adjMatrix order by idFrom, idTo;
    drop temporary table adjMatrix;
    drop temporary table idArray;
end//

在包含1466条记录的表上运行该过程需要45秒才能在我的计算机上运行:

mysql> call closure;
+--------+------+
| idFrom | idTo |
+--------+------+
|      1 |    1 |
|      1 |    2 |
|      1 |    3 |
|      1 |    4 |
|      1 |    5 |
|      2 |    3 |
|      2 |    4 |
|      2 |    5 |
|      3 |    4 |
|      3 |    5 |
|      4 |    5 |
~        ~      ~
|   1587 | 1587 |
|   1588 | 1588 |
|   1589 | 1589 |
+--------+------+
1472 rows in set (45.58 sec)

1 个答案:

答案 0 :(得分:1)

警告:由于我不熟悉mysql,我已将问题“转换”为MSSQL,因此您需要做一些努力将其翻译回来=)

我猜测事情变慢的原因是因为SQL并不适合这种操作;它不“喜欢”分支和循环以及所有这些东西。它的作用A LOT将数据从一个表匹配到另一个表,最好是大堆。 (想想RDBMS中的R)

因此,为了加快存储过程,您可以切换到更适合这种编码方式的不同编程语言;或者你可以将问题转化为更适合SQL的东西。当然后者更有趣! =)

考虑到这个问题,我想出了这个:

CREATE TABLE #result (idFrom int not null, idTo int not null, primary key (idFrom, idTo));

INSERT INTO #result
SELECT parent_id, id
  FROM article 
 WHERE parent_id is not null;

 WHILE @@ROWCOUNT > 0
    BEGIN
        INSERT INTO #result 
        SELECT DISTINCT f.idFrom, t.idTo
          FROM #result f
          JOIN #result t
            ON t.idFrom = f.idTo
         WHERE NOT EXISTS ( SELECT *
                              FROM #result old
                             WHERE old.idFrom = f.idFrom
                               AND old.idTo = t.idTo )
    END

SELECT * FROM #result ORDER BY idFrom, idTo

同样,这是TSQL(MSSQL使用的SQL方言),但我猜测将它转换为mysql(??)应该非常简单。

它的作用是:

  • 创建一个临时表#result与你的adjMatrix表几乎完全相同
  • 从源表中加载“直接链接”
  • 通过将一个记录idTo与另一个记录idFrom匹配来插入所有“二级组合”;确保它在表格中找不到所述组合并确保所述列表仅具有唯一组合(不同)
  • 如果添加了新记录(以及组合),请查看我们是否可以添加“下一个”图层。

一旦找不到新内容,请返回结果

例如:给定输入(1,2),(2,3),(3,4)

首先用(1,2),(2,3),(3,4)填充#result 然后进入循环:

iteration1将匹配以下记录:

  • (1,2) - (2,3)=> (1,3)
  • (2,3) - (3,4)=> (2,4)

并将其添加到#result,我们因此找到(1,2),(2,3),(3,4),(1,3),(2,4)

iteration2将匹配以下记录:

  • (1,2) - (2,3)=> (1,3)但由于WHERE NOT EXISTS()
  • ,它将被淘汰
  • (1,2) - (2,4)=> (1,4)
  • (2,3) - (3,4)=> (2,4)但由于WHERE NOT EXISTS()
  • ,它将被淘汰
  • (1,3) - (3,4)=> (1,4)

然后DISTINCT表示列表,只剩下(1,4)的一个实例并将被添加 找到(1,2),(2,3),(3,4),(1,3),(2,4),(1,4)

iteration4将匹配以下记录:

  • (1,2) - (2,3)=> (1,3)但由于WHERE NOT EXISTS()
  • ,它将被淘汰
  • (1,2) - (2,4)=> (1,4)但由于WHERE NOT EXISTS()
  • ,它将被淘汰
  • (2,3) - (3,4)=> (2,4)但由于WHERE NOT EXISTS()
  • ,它将被淘汰
  • (1,3) - (3,4)=> (1,4)但由于WHERE NOT EXISTS()
  • ,它将被淘汰

随着所有新记录的消除,我们最终得到一个零行数,因此跳出循环。

我已经尝试了使用SqlFiddle输入的算法,结果几乎是瞬间但我最终输出的是15条记录而不是16条。看来你的代码还包括一条(1,1)记录有点让我感到惊讶?!

无论如何,希望这会有所帮助。使用SQL一段时间之后,您将自动学会对数据块进行操作,而不是使用“按值计算”,这可能会使任何RDBMS陷入困境。它通常适用于一小组数据,但是一旦输入更多数据,性能就会向南移动。编写良好的基于​​集合的SQL几乎不会注意到处理100或100000条记录之间的区别。 (可以这么说=)