我在 PostgreSQL 中有以下递归函数。但我想知道它是如何工作的
- 执行顺序(先按每个根节点深入层次结构,或先列出所有根节点,然后转到下一层。)
- 它是如何从这个函数中分离出来的。
谁能帮我解释一下?
CREATE OR REPLACE FUNCTION build_hierarchey(location_id int) RETURNS SETOF jsonb
AS $BODY$
BEGIN
RETURN QUERY
SELECT
CASE WHEN COUNT(x) > 0
THEN ((to_jsonb(t) || jsonb_build_object('Children', jsonb_agg(f.x)))
ELSE to_jsonb(t)
END
FROM "Locations" t
LEFT JOIN build_hierarchey(t."Id") AS f(x) ON true
WHERE t."ParentLocationId" = location_id OR (location_id IS null AND t."ParentLocationId" IS null)
GROUP BY t."Id", t."Name";
END;
$BODY$ LANGUAGE 'plpgsql'
这就是我使用它的方式。
select jsonb_agg(build_hierarchey) from build_hierarchey(null::int)
结果为 json (hirarchey)
[
{
"Id": 11,
"Name": "Zone-C",
"Children": [
{
"Id": 23,
"Name": "01-C",
"CategoryId": null
},
{
"Id": 20,
"Name": "01-A",
"CategoryId": null
}
],
"CategoryId": null
},
{
"Id": 19,
"Name": "Zone-K",
"CategoryId": null
},
{
"Id": 1,
"Name": "ccc",
"Children": [
{
"Id": 3,
"Name": "01-A",
"CategoryId": null
},
{
"Id": 5,
"Name": "01-C",
"Children": [
{
"Id": 8,
"Name": "01-C-03",
"CategoryId": null
},
{
"Id": 7,
"Name": "01-C-02",
"CategoryId": null
},
{
"Id": 6,
"Name": "01-C-01",
"CategoryId": null
}
],
"CategoryId": null
},
{
"Id": 4,
"Name": "01-B",
"CategoryId": null
}
],
"CategoryId": null
},
{
"Id": 18,
"Name": "Zone-J",
"CategoryId": null
},
{
"Id": 2,
"Name": "Zone-B",
"Children": [
{
"Id": 10,
"Name": "02-A",
"CategoryId": null
},
{
"Id": 9,
"Name": "01-A",
"CategoryId": null
}
],
"CategoryId": null
},
{
"Id": 16,
"Name": "Zone-H",
"CategoryId": null
},
{
"Id": 15,
"Name": "Zone-G",
"CategoryId": null
},
{
"Id": 14,
"Name": "Zone-F",
"CategoryId": null
},
{
"Id": 17,
"Name": "Zone-I",
"CategoryId": null
},
{
"Id": 22,
"Name": "Zone-AA",
"CategoryId": null
}
]
这是“位置”表。 (自引用)
SELECT "Id", "Name", "ParentLocationId" FROM "Locations"
ID Name ParentLocationId
1 "ccc"
2 "Zone-B"
3 "01-A" 1
4 "01-B" 1
5 "01-C" 1
6 "01-C-01" 5
7 "01-C-02" 5
8 "01-C-03" 5
9 "01-A" 2
10 "02-A" 2
11 "Zone-C"
14 "Zone-F"
15 "Zone-G"
最佳答案
与许多递归函数一样,我们可以概念上将其分解为几个步骤。
首先,我们有基本情况 - 当给定 null
作为输入时,查询选择所有没有父级的 locations
:
SELECT
...
FROM "Locations" t
WHERE location_id IS null AND t."ParentLocationId" IS null
接下来,我们有重复的情况 - 当给定一个 ID 作为输入时,查询选择所有具有该 ID 作为其父级的 locations
:
SELECT
...
FROM "Locations" t
WHERE t."ParentLocationId" = location_id
然后我们添加递归 - 当返回上述任一列表时,也再次调用该函数,找到每个 ID:
SELECT
...
FROM "Locations" t
LEFT JOIN build_hierarchey(t."Id") AS f(x) ON true
存在 ON true
子句是因为必须有一些条件,但要连接的实际行由传入函数的 t."Id"
决定.它需要是 LEFT JOIN
,因为该函数有时不会生成任何行;例如使用您的示例数据 build_hierarchey(3)
将不返回任何行,但我们不希望从 build_hierarchey(1)
的结果中排除位置 3。
现在我们需要将当前查询的结果(t
)与递归函数调用的结果(f
)结合起来:
SELECT
CASE WHEN COUNT(x) > 0
THEN ((to_jsonb(t) || jsonb_build_object('Children', jsonb_agg(f.x)))
ELSE to_jsonb(t)
END
FROM ... as t
LEFT JOIN ... as f(x)
GROUP BY t."Id", t."Name";
同样,这必须处理函数未返回任何行的情况。剩下的只是格式化:将每组结果扁平化为一个 JSON 对象,并将它们组合成所需的结构。
现在,回答您的问题:
the sequence of execution (it goes deep hierarchy by each root node first or list all root node first then go to next level.)
在 SQL 中,谈论执行顺序 很少有用,因为 DBMS 不会一次生成一行并吐出结果,它会尝试尽可能高效地生成请求的数据尽可能。据我所知,DBMS 可以自由选择是否将某一级别的所有行都加载到内存中,并在每个行上运行该函数;或者函数是否在遇到每一行时深度优先运行。
一般来说,唯一能保证的就是结果会按照查询请求的顺序出来。在这种情况下,没有 ORDER BY
子句,因此每个级别的实际结果顺序也完全未定义 - DBMS 将在运行时选择最方便的顺序查询。
how it breaks from this function.
这个函数实际上并不能保证退出;它的退出条件依赖于您的数据不包含循环的假设 - 也就是说,它最终将到达一个没有“子节点”的节点(即,一个从未出现在 ParentLocationId
列中的 ID)。
为了演示一个循环,请考虑以下数据:
ID Name ParentLocationId
1 "A" 3
2 "B" 1
3 "C" 2
对 build_hierarchey(1)
的调用将按如下方式进行:
- 获取父节点为 1 的节点:找到 ID=2
- 获取父节点 2 的节点:找到 ID=3
- 获取父节点为 3 的节点:找到 ID=1
- 获取父节点 1 的节点:无限循环
这可能在应用程序的其他地方被阻止了,但我们还有一个额外的保护:通过调用 build_hierarchey(null)
我们永远不会遇到这样的循环。我们可以证明如下:
- 每个节点要么只有一个父节点,要么没有父节点
- 一个循环由跟随父节点和到达同一个节点组成
- 要使一个节点成为循环的一部分,它必须有一个父节点
- 因此以
null
为父节点的节点不是循环的一部分 - 如果一个节点在一个循环中,它的父节点必须在同一个循环中(循环 A-B-C-A 可以扩展到 A-B-C-A-B,依此类推)
- 因此,循环中的节点不能有其父级为
null
的父级,因为该父级不能成为循环的一部分 - 因此,从父节点为
null
的节点开始(build_hierarchey(null)
所做的)永远不会达到循环,即使表中存在循环
关于Postgresql递归函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55087629/