如何选择使用WITH RECURSIVE子句

时间:2013-09-06 14:18:29

标签: sql postgresql recursive-query

我用谷歌搜索并阅读了一些文章 this postgreSQL manual pagethis blog page 并尝试以适度的成功自己进行查询(其中一部分挂起,而其他部分工作正常且速度快), 但到目前为止,我不能完全了解这个魔法是如何工作的。

任何人都可以给出非常明确的解释,说明这样的查询语义和执行过程, 更好地基于典型样本,如因子计算或来自(id,parent_id,name)表的完整树扩展?

为了提出良好的with recursive查询,应该知道哪些基本指导和典型错误?

1 个答案:

答案 0 :(得分:41)

首先,让我们尝试简化和澄清manual page上给出的算法描述。为了简化它,现在只考虑union all子句中的with recursive(以及之后的union):

WITH RECURSIVE pseudo-entity-name(column-names) AS (
    Initial-SELECT
UNION ALL
    Recursive-SELECT using pseudo-entity-name
)
Outer-SELECT using pseudo-entity-name

为了澄清它,让我们用伪代码描述查询执行过程:

working-recordset = result of Initial-SELECT

append working-recordset to empty outer-recordset

while( working-recordset is not empty ) begin

    new working-recordset = result of Recursive-SELECT 
        taking previous working-recordset as pseudo-entity-name

    append working-recordset to outer-recordset

end

overall-result = result of Outer-SELECT 
    taking outer-recordset as pseudo-entity-name

甚至更短 - 数据库引擎执行初始选择,将结果行作为工作集。然后它在工作集上重复执行递归选择,每次用获得的查询结果替换工作集的内容。当递归选择返回空集时,此过程结束。并且首先通过初始选择然后通过递归选择给出的所有结果行被收集并且被馈送到外部选择,这结果成为整体查询结果。

此查询正在计算 factorial 为3:

WITH RECURSIVE factorial(F,n) AS (
    SELECT 1 F, 3 n
UNION ALL
    SELECT F*n F, n-1 n from factorial where n>1
)
SELECT F from factorial where n=1

初始选择SELECT 1 F, 3 n给出初始值:3表示参数,1表示函数值。
递归选择SELECT F*n F, n-1 n from factorial where n>1表示每次我们需要将最后一个函数值乘以最后一个参数值并递减参数值。
数据库引擎执行它:

首先,它执行initail select,它给出了工作记录集的初始状态:

F | n
--+--
1 | 3

然后它使用递归查询转换工作记录集并获得其第二个状态:

F | n
--+--
3 | 2

然后是第三个州:

F | n
--+--
6 | 1

在第三个状态中,在递归选择中没有跟随n>1条件的行,所以工作集是循环退出。

外部记录集现在包含所有行,由初始和递归选择返回:

F | n
--+--
1 | 3
3 | 2
6 | 1

外部选择过滤掉外部记录集的所有中间结果,仅显示最终的因子值,这将成为整体查询结果:

F 
--
6

现在让我们考虑一下表forest(id,parent_id,name)

id | parent_id | name
---+-----------+-----------------
1  |           | item 1
2  | 1         | subitem 1.1
3  | 1         | subitem 1.2
4  | 1         | subitem 1.3
5  | 3         | subsubitem 1.2.1
6  |           | item 2
7  | 6         | subitem 2.1
8  |           | item 3

' 展开完整树'这意味着在计算它们的级别和(可能)路径时,以人类可读的深度优先顺序对树项进行排序。在不使用WITH RECURSIVE子句(或Oracle CONNECT BY子句,PostgreSQL不支持)的情况下,两个任务(正确排序和计算级别或路径)都无法在一个(甚至任何常数)SELECT中解决。但是这个递归查询完成了这项工作(好吧,差不多了,请参阅下面的注释):

WITH RECURSIVE fulltree(id,parent_id,level,name,path) AS (
    SELECT id, parent_id, 1 as level, name, name||'' as path from forest where parent_id is null
UNION ALL
    SELECT t.id, t.parent_id, ft.level+1 as level, t.name, ft.path||' / '||t.name as path
    from forest t, fulltree ft where t.parent_id = ft.id
)
SELECT * from fulltree order by path

数据库引擎执行它:

首先,它执行initail select,它从forest表中提供所有最高级别的项目(根):

id | parent_id | level | name             | path
---+-----------+-------+------------------+----------------------------------------
1  |           | 1     | item 1           | item 1
8  |           | 1     | item 3           | item 3
6  |           | 1     | item 2           | item 2

然后,它执行递归选择,它给出forest表中的所有第二级项:

id | parent_id | level | name             | path
---+-----------+-------+------------------+----------------------------------------
2  | 1         | 2     | subitem 1.1      | item 1 / subitem 1.1
3  | 1         | 2     | subitem 1.2      | item 1 / subitem 1.2
4  | 1         | 2     | subitem 1.3      | item 1 / subitem 1.3
7  | 6         | 2     | subitem 2.1      | item 2 / subitem 2.1

然后,它再次执行递归选择,检索3d级别项目:

id | parent_id | level | name             | path
---+-----------+-------+------------------+----------------------------------------
5  | 3         | 3     | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1

现在它再次执行递归选择,尝试检索第4级项目,但没有它们,所以循环退出。

外部SELECT设置正确的人类可读行顺序,在路径列上排序:

id | parent_id | level | name             | path
---+-----------+-------+------------------+----------------------------------------
1  |           | 1     | item 1           | item 1
2  | 1         | 2     | subitem 1.1      | item 1 / subitem 1.1
3  | 1         | 2     | subitem 1.2      | item 1 / subitem 1.2
5  | 3         | 3     | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1
4  | 1         | 2     | subitem 1.3      | item 1 / subitem 1.3
6  |           | 1     | item 2           | item 2
7  | 6         | 2     | subitem 2.1      | item 2 / subitem 2.1
8  |           | 1     | item 3           | item 3

注意:只有在项目名称中没有标点字符排序 - /之前,结果行顺序才会保持正确。如果我们在Item 2中重命名Item 1 *,它将会破坏排在Item 1及其后代之间的行顺序。 更稳定的解决方案是使用制表符(E'\t')作为查询中的路径分隔符(稍后可以用更可读的路径分隔符替换:在外部选择中,在替换为人或等之前)。制表符分隔的路径将保留正确的顺序,直到项目名称中有制表符或控制字符 - 可以轻松检查和排除,而不会丢失可用性。

修改上一个查询以扩展任意子树非常简单 - 您只需要用parent_id is null替换条件perent_id=1(例如)。请注意,此查询变体将返回相对于 Item 1的所有级别和路径。

现在关于典型错误。特定于递归查询的最显着的典型错误是在递归选择中定义不良停止条件,这导致无限循环。

例如,如果我们在上面的factorial示例中省略where n>1条件,则递归select的执行永远不会给出空集(因为我们没有条件来过滤掉单行)并且循环将无限地继续。

这是你的一些查询挂起的最可能的原因(另一个非特定但仍然可能的原因是选择非常无效,在有限但非常长的时间内执行)。

据我所知,目前没有太多RECURSIVE特定的查询 guidlines 。但我想建议(相当明显)逐步递归查询构建过程。

  • 分别构建和调试您的初始选择。

  • 用带有RECURSIVE结构的脚手架包裹它
    并开始构建和调试递归选择。

推荐的刮擦构造如下:

WITH RECURSIVE rec( <Your column names> ) AS (
    <Your ready and working initial SELECT>
UNION ALL
    <Recursive SELECT that you are debugging now>
)
SELECT * from rec limit 1000

这个最简单的外部选择将输出整个外部记录集,正如我们所知,它包含初始选择的所有输出行,并且recusrive的每次执行都以原始输出顺序循环选择 - 就像上面的示例一样! limit 1000部分将防止悬挂,将其替换为超大输出,您可以在其中看到错过的停止点。

  • 调试初始和递归后,选择构建并调试外部选择。

现在最后要提到的是 - 在union子句中使用 union all 而不是with recursive的区别。它引入了行唯一性约束,它在我们的执行伪代码中产生两个额外的行:

working-recordset = result of Initial-SELECT

discard duplicate rows from working-recordset /*union-specific*/

append working-recordset to empty outer-recordset

while( working-recordset is not empty ) begin

    new working-recordset = result of Recursive-SELECT 
        taking previous working-recordset as pseudo-entity-name

    discard duplicate rows and rows that have duplicates in outer-recordset 
        from working-recordset /*union-specific*/

    append working-recordset to outer-recordset

end

overall-result = result of Outer-SELECT 
    taking outer-recordset as pseudo-entity-name