递归 CTE PostgreSQL 将多个 ID 与其他字段的附加逻辑连接起来
Recursive CTE PostgreSQL Connecting Multiple IDs with Additional Logic for Other Fields
在我的 PostgreSQL 数据库中,我有一个 id 列,显示每个唯一的线索。我还有一个 connected_lead_id 列,显示帐户是否相互关联(即丈夫和妻子,parents 和 children、朋友群、投资者群等)。
当我们统计一个时间段内创建的id数量时,我们希望看到一个时间段内connected_ids的unique "groups"的数量。换句话说,我们不想把夫妻俩都算进去,我们只想算一个,因为他们真的是一个线索。
我们希望能够创建一个视图,该视图仅具有基于 "created_at" 日期的 "first" ID,然后在末尾包含 "connected_lead_id_1"、"connected_lead_id_2"、"connected_lead_id_3" 等
我们想添加额外的逻辑,以便我们获取 "first" id 的源,除非它为 null,然后获取 "second" connected_lead_id 的源,除非它为 null等等。最后,我们想从 connected_lead_id 组中取出最早的 on_boarded_date。
id | created_at | connected_lead_id | on_boarded_date | source |
2 | 9/24/15 23:00 | 8 | |
4 | 9/25/15 23:00 | 7 | |event
7 | 9/26/15 23:00 | 4 | |
8 | 9/26/15 23:00 | 2 | |referral
11 | 9/26/15 23:00 | 336 | 7/1/17 |online
142 | 4/27/16 23:00 | 336 | |
336 | 7/4/16 23:00 | 11 | 9/20/18 |referral
最终目标:
id | created_at | on_boarded_date | source |
2 | 9/24/15 23:00 | | referral |
4 | 9/25/15 23:00 | | event |
11 | 9/26/15 23:00 | 7/1/17 | online |
理想情况下,我们还会在末尾有 i 个额外的列来显示附加到基本 ID 的每个 connected_lead_id。
感谢您的帮助!
主要思想-草图:
循环遍历有序集。获取所有 id
s,之前在任何 connected_lead_id
(cli) 中都没有见过。这些是递归的起点。
问题是您的号码 142
以前从未见过,但由于其 cli 而与 11
属于同一组。因此,最好获取看不见的 ID 的 clis。使用这些值,稍后在递归部分计算组的 id 会更简单。由于循环,需要一个 function/stored 过程。
递归部分:第一步是获取起始clis的id。使用 created_at
时间戳计算第一个引用 ID。之后就可以在clis上做一个简单的树递归了。
1.函数:
CREATE OR REPLACE FUNCTION filter_groups() RETURNS int[] AS $$
DECLARE
_seen_values int[];
_new_values int[];
_temprow record;
BEGIN
FOR _temprow IN
-- 1:
SELECT array_agg(id ORDER BY created_at) as ids, connected_lead_id FROM groups GROUP BY connected_lead_id ORDER BY MIN(created_at)
LOOP
-- 2:
IF array_length(_seen_values, 1) IS NULL
OR (_temprow.ids || _temprow.connected_lead_id) && _seen_values = FALSE THEN
_new_values := _new_values || _temprow.connected_lead_id;
END IF;
_seen_values := _seen_values || _temprow.ids;
_seen_values := _seen_values || _temprow.connected_lead_id;
END LOOP;
RETURN _new_values;
END;
$$ LANGUAGE plpgsql;
- 将引用相同 cli 的所有 ID 分组
- 遍历 id 数组。如果之前没有看到数组的元素,请将引用的 cli 添加到输出变量 (
_new_values
)。在这两种情况下,将 id 和 cli 添加到存储所有已见 id 的变量中 (_seen_values
)
- 分发clis。
目前的结果是{8, 7, 336}
(相当于ids{2,4,11,142}
!)
2。递归:
-- 1:
WITH RECURSIVE start_points AS (
SELECT unnest(filter_groups()) as ids
),
filtered_groups AS (
-- 3:
SELECT DISTINCT
1 as depth, -- 3
first_value(id) OVER w as id, -- 4
ARRAY[(MIN(id) OVER w)] as visited, -- 5
MIN(created_at) OVER w as created_at,
connected_lead_id,
MIN(on_boarded_date) OVER w as on_boarded_date -- 6,
first_value(source) OVER w as source
FROM groups
WHERE connected_lead_id IN (SELECT ids FROM start_points)
-- 2:
WINDOW w AS (PARTITION BY connected_lead_id ORDER BY created_at)
UNION
SELECT
fg.depth + 1,
fg.id,
array_append(fg.visited, g.id), -- 8
LEAST(fg.created_at, g.created_at),
g.connected_lead_id,
LEAST(fg.on_boarded_date, g.on_boarded_date), -- 9
COALESCE(fg.source, g.source) -- 10
FROM groups g
JOIN filtered_groups fg
-- 7
ON fg.connected_lead_id = g.id AND NOT (g.id = ANY(visited))
)
SELECT DISTINCT ON (id) -- 11
id, created_at,on_boarded_date, source
FROM filtered_groups
ORDER BY id, depth DESC;
WITH
部分给出函数的结果。 unnest()
将 id 数组扩展到每个 id 的每一行。
- Creating a window:window 函数按其 clis 对所有值进行分组,并按
created_at
时间戳对 window 进行排序。在您的示例中,所有值都在它们自己的 window 中,除了分组的 11
和 142
。
- 这是稍后获取最新行的帮助变量。
first_value()
给出有序 window 帧的第一个值。假设 142
有一个较小的 created_at 时间戳,结果应该是 142
。但它仍然是 11
。
- 需要一个变量来保存访问过哪个id。如果没有此信息,将创建一个无限循环:
2-8-2-8-2-8-2-8-...
- 取 window 的最小日期(这里也是一样:如果
142
的日期比 11
小,这就是结果)。
现在计算递归的起始查询。下面介绍递归部分:
- 将table(原函数结果)与之前的递归结果相结合。第二个条件就是我上面说的死循环停止。
- 将当前访问的id附加到访问的变量中。
- 如果当前
on_boarded_date
早于它。
COALESCE
给出第一个 NOT NULL
值。所以第一个 NOT NULL
source
在整个递归过程中都是安全的
在给出所有递归步骤的结果的递归之后,我们只想过滤掉每个起始 ID 的最深访问。
DISTINCT ON (id)
给出第一次出现 id 的行。为了获得最后一个,整个集合由 depth
变量降序排列。
好的,目前我能想出的最好方法是首先构建最大的相关 ID 组,然后返回到您的 table 潜在客户以获得其余数据(请参阅此 SQL Fiddle 用于设置、完整查询和结果)。
要获得最大组,您可以使用递归通用 table 表达式首先增加组,然后通过查询将 CTE 结果过滤到最大组:
with recursive cte(grp) as (
select case when l.connected_lead_id is null then array[l.id]
else array[l.id, l.connected_lead_id]
end from leads l
union all
select grp || l.id
from leads l
join cte
on l.connected_lead_id = any(grp)
and not l.id = any(grp)
)
select * from cte c1
上面的CTE输出了几个类似的组以及中间组。下面的查询谓词删除了非最大组,并将结果限制为每个可能组的一个排列:
where not exists (select 1 from cte c2
where c1.grp && c2.grp
and ((not c1.grp @> c2.grp)
or (c2.grp < c1.grp
and c1.grp @> c2.grp
and c1.grp <@ c2.grp)));
| grp |
|------------|
| 2,8 |
| 4,7 |
| 14 |
| 11,336,142 |
| 12,13 |
接下来将上面的最终查询连接回您的潜在客户 table 并使用 window 函数获取剩余的列值,并使用 distinct 运算符将其修剪为最终结果集:
with recursive cte(grp) as (
...
)
select distinct
first_value(l.id) over (partition by grp order by l.created_at) id
, first_value(l.created_at) over (partition by grp order by l.created_at) create_at
, first_value(l.on_boarded_date) over (partition by grp order by l.created_at) on_boarded_date
, first_value(l.source) over (partition by grp
order by case when l.source is null then 2 else 1 end
, l.created_at) source
, grp CONNECTED_IDS
from cte c1
join leads l
on l.id = any(grp)
where not exists (select 1 from cte c2
where c1.grp && c2.grp
and ((not c1.grp @> c2.grp)
or (c2.grp < c1.grp
and c1.grp @> c2.grp
and c1.grp <@ c2.grp)));
| id | create_at | on_boarded_date | source | connected_ids |
|----|----------------------|-----------------|----------|---------------|
| 2 | 2015-09-24T23:00:00Z | (null) | referral | 2,8 |
| 4 | 2015-09-25T23:00:00Z | (null) | event | 4,7 |
| 11 | 2015-09-26T23:00:00Z | 2017-07-01 | online | 11,336,142 |
| 12 | 2015-09-26T23:00:00Z | 2017-07-01 | event | 12,13 |
| 14 | 2015-09-26T23:00:00Z | (null) | (null) | 14 |
在我的 PostgreSQL 数据库中,我有一个 id 列,显示每个唯一的线索。我还有一个 connected_lead_id 列,显示帐户是否相互关联(即丈夫和妻子,parents 和 children、朋友群、投资者群等)。
当我们统计一个时间段内创建的id数量时,我们希望看到一个时间段内connected_ids的unique "groups"的数量。换句话说,我们不想把夫妻俩都算进去,我们只想算一个,因为他们真的是一个线索。
我们希望能够创建一个视图,该视图仅具有基于 "created_at" 日期的 "first" ID,然后在末尾包含 "connected_lead_id_1"、"connected_lead_id_2"、"connected_lead_id_3" 等
我们想添加额外的逻辑,以便我们获取 "first" id 的源,除非它为 null,然后获取 "second" connected_lead_id 的源,除非它为 null等等。最后,我们想从 connected_lead_id 组中取出最早的 on_boarded_date。
id | created_at | connected_lead_id | on_boarded_date | source |
2 | 9/24/15 23:00 | 8 | |
4 | 9/25/15 23:00 | 7 | |event
7 | 9/26/15 23:00 | 4 | |
8 | 9/26/15 23:00 | 2 | |referral
11 | 9/26/15 23:00 | 336 | 7/1/17 |online
142 | 4/27/16 23:00 | 336 | |
336 | 7/4/16 23:00 | 11 | 9/20/18 |referral
最终目标:
id | created_at | on_boarded_date | source |
2 | 9/24/15 23:00 | | referral |
4 | 9/25/15 23:00 | | event |
11 | 9/26/15 23:00 | 7/1/17 | online |
理想情况下,我们还会在末尾有 i 个额外的列来显示附加到基本 ID 的每个 connected_lead_id。
感谢您的帮助!
主要思想-草图:
循环遍历有序集。获取所有
id
s,之前在任何connected_lead_id
(cli) 中都没有见过。这些是递归的起点。 问题是您的号码142
以前从未见过,但由于其 cli 而与11
属于同一组。因此,最好获取看不见的 ID 的 clis。使用这些值,稍后在递归部分计算组的 id 会更简单。由于循环,需要一个 function/stored 过程。递归部分:第一步是获取起始clis的id。使用
created_at
时间戳计算第一个引用 ID。之后就可以在clis上做一个简单的树递归了。
1.函数:
CREATE OR REPLACE FUNCTION filter_groups() RETURNS int[] AS $$
DECLARE
_seen_values int[];
_new_values int[];
_temprow record;
BEGIN
FOR _temprow IN
-- 1:
SELECT array_agg(id ORDER BY created_at) as ids, connected_lead_id FROM groups GROUP BY connected_lead_id ORDER BY MIN(created_at)
LOOP
-- 2:
IF array_length(_seen_values, 1) IS NULL
OR (_temprow.ids || _temprow.connected_lead_id) && _seen_values = FALSE THEN
_new_values := _new_values || _temprow.connected_lead_id;
END IF;
_seen_values := _seen_values || _temprow.ids;
_seen_values := _seen_values || _temprow.connected_lead_id;
END LOOP;
RETURN _new_values;
END;
$$ LANGUAGE plpgsql;
- 将引用相同 cli 的所有 ID 分组
- 遍历 id 数组。如果之前没有看到数组的元素,请将引用的 cli 添加到输出变量 (
_new_values
)。在这两种情况下,将 id 和 cli 添加到存储所有已见 id 的变量中 (_seen_values
) - 分发clis。
目前的结果是{8, 7, 336}
(相当于ids{2,4,11,142}
!)
2。递归:
-- 1:
WITH RECURSIVE start_points AS (
SELECT unnest(filter_groups()) as ids
),
filtered_groups AS (
-- 3:
SELECT DISTINCT
1 as depth, -- 3
first_value(id) OVER w as id, -- 4
ARRAY[(MIN(id) OVER w)] as visited, -- 5
MIN(created_at) OVER w as created_at,
connected_lead_id,
MIN(on_boarded_date) OVER w as on_boarded_date -- 6,
first_value(source) OVER w as source
FROM groups
WHERE connected_lead_id IN (SELECT ids FROM start_points)
-- 2:
WINDOW w AS (PARTITION BY connected_lead_id ORDER BY created_at)
UNION
SELECT
fg.depth + 1,
fg.id,
array_append(fg.visited, g.id), -- 8
LEAST(fg.created_at, g.created_at),
g.connected_lead_id,
LEAST(fg.on_boarded_date, g.on_boarded_date), -- 9
COALESCE(fg.source, g.source) -- 10
FROM groups g
JOIN filtered_groups fg
-- 7
ON fg.connected_lead_id = g.id AND NOT (g.id = ANY(visited))
)
SELECT DISTINCT ON (id) -- 11
id, created_at,on_boarded_date, source
FROM filtered_groups
ORDER BY id, depth DESC;
WITH
部分给出函数的结果。unnest()
将 id 数组扩展到每个 id 的每一行。- Creating a window:window 函数按其 clis 对所有值进行分组,并按
created_at
时间戳对 window 进行排序。在您的示例中,所有值都在它们自己的 window 中,除了分组的11
和142
。 - 这是稍后获取最新行的帮助变量。
first_value()
给出有序 window 帧的第一个值。假设142
有一个较小的 created_at 时间戳,结果应该是142
。但它仍然是11
。- 需要一个变量来保存访问过哪个id。如果没有此信息,将创建一个无限循环:
2-8-2-8-2-8-2-8-...
- 取 window 的最小日期(这里也是一样:如果
142
的日期比11
小,这就是结果)。
现在计算递归的起始查询。下面介绍递归部分:
- 将table(原函数结果)与之前的递归结果相结合。第二个条件就是我上面说的死循环停止。
- 将当前访问的id附加到访问的变量中。
- 如果当前
on_boarded_date
早于它。 COALESCE
给出第一个NOT NULL
值。所以第一个NOT NULL
source
在整个递归过程中都是安全的
在给出所有递归步骤的结果的递归之后,我们只想过滤掉每个起始 ID 的最深访问。
DISTINCT ON (id)
给出第一次出现 id 的行。为了获得最后一个,整个集合由depth
变量降序排列。
好的,目前我能想出的最好方法是首先构建最大的相关 ID 组,然后返回到您的 table 潜在客户以获得其余数据(请参阅此 SQL Fiddle 用于设置、完整查询和结果)。
要获得最大组,您可以使用递归通用 table 表达式首先增加组,然后通过查询将 CTE 结果过滤到最大组:
with recursive cte(grp) as (
select case when l.connected_lead_id is null then array[l.id]
else array[l.id, l.connected_lead_id]
end from leads l
union all
select grp || l.id
from leads l
join cte
on l.connected_lead_id = any(grp)
and not l.id = any(grp)
)
select * from cte c1
上面的CTE输出了几个类似的组以及中间组。下面的查询谓词删除了非最大组,并将结果限制为每个可能组的一个排列:
where not exists (select 1 from cte c2
where c1.grp && c2.grp
and ((not c1.grp @> c2.grp)
or (c2.grp < c1.grp
and c1.grp @> c2.grp
and c1.grp <@ c2.grp)));
| grp |
|------------|
| 2,8 |
| 4,7 |
| 14 |
| 11,336,142 |
| 12,13 |
接下来将上面的最终查询连接回您的潜在客户 table 并使用 window 函数获取剩余的列值,并使用 distinct 运算符将其修剪为最终结果集:
with recursive cte(grp) as (
...
)
select distinct
first_value(l.id) over (partition by grp order by l.created_at) id
, first_value(l.created_at) over (partition by grp order by l.created_at) create_at
, first_value(l.on_boarded_date) over (partition by grp order by l.created_at) on_boarded_date
, first_value(l.source) over (partition by grp
order by case when l.source is null then 2 else 1 end
, l.created_at) source
, grp CONNECTED_IDS
from cte c1
join leads l
on l.id = any(grp)
where not exists (select 1 from cte c2
where c1.grp && c2.grp
and ((not c1.grp @> c2.grp)
or (c2.grp < c1.grp
and c1.grp @> c2.grp
and c1.grp <@ c2.grp)));
| id | create_at | on_boarded_date | source | connected_ids |
|----|----------------------|-----------------|----------|---------------|
| 2 | 2015-09-24T23:00:00Z | (null) | referral | 2,8 |
| 4 | 2015-09-25T23:00:00Z | (null) | event | 4,7 |
| 11 | 2015-09-26T23:00:00Z | 2017-07-01 | online | 11,336,142 |
| 12 | 2015-09-26T23:00:00Z | 2017-07-01 | event | 12,13 |
| 14 | 2015-09-26T23:00:00Z | (null) | (null) | 14 |