合并具有多个参考的用户并计算他们的集体资产

Bjø*_*hen 12 sql arrays postgresql graph-theory aggregate-functions

有一组用户。一个人可以拥有多个用户,但ref1ref2可能相似,因此可以将用户链接在一起。ref1ref2不重叠,则 中ref1不存在 中的一个值ref2

一个用户可以拥有多种资产。我想“合并”具有一个或多个相似参考的用户,然后计算他们总共拥有多少资产。用户表中可能缺少条目,在这种情况下,我只想将所有者传播到 ref2 并设置 asset_count 和 asset_ids。

下面是一个示例架构来说明:

示例资产

SELECT * FROM assets;
Run Code Online (Sandbox Code Playgroud)
ID 姓名 所有者
1 #1 A
2 #2
3 #3 C
4 #4 A
5 #5 C
6 #6 d
7 #7 e
8 #8 d
9 #9 A
10 #10 A
11 #11 z

用户示例

SELECT * FROM users;
Run Code Online (Sandbox Code Playgroud)
ID 用户名 参考1 参考2
1 波波 A d
2 托托 e
3 莫莫 C d
4 洛洛 A F
5 波波 C F

我最终想要得到什么

SELECT * FROM results;
Run Code Online (Sandbox Code Playgroud)
id 用户名 参考文献1 参考文献2 资产ID 资产数量
1,3,4,5 波波、莫莫、洛洛、波波 一个,c d,f 1,3,4,5,6,8,9,10 8
2 托托 e 2,7 2
z 11 1

我尝试过不同的方法,但这就是我目前所拥有的:

我最近的

SELECT
  ARRAY_AGG(DISTINCT u.id) AS ids,
  ARRAY_AGG(DISTINCT u.username) AS usernames,
  ARRAY_AGG(DISTINCT u.ref1) AS refs1,
  ARRAY_AGG(DISTINCT u.ref2) AS refs2,
  COUNT(DISTINCT a.id) AS asset_count
FROM assets a
JOIN users u ON a.owner = u.ref1 OR a.owner = u.ref2
GROUP BY a.owner
ORDER BY MIN(a.id);
Run Code Online (Sandbox Code Playgroud)
id 用户名 参考文献1 参考文献2 资产数量
1,4 波波、洛洛 A d,f 4
2 托托 e 1
3,5 莫莫、波波 C d,f 2
1,3 波波、莫莫 一个,c d 2
2 托托 e 1

如果我在 ids 上合并上面的表,我几乎得到我想要的结果(没有用户表中丢失的条目)。合并可以轻松地在代码中完成,但随后我无法分页等。如果可能的话,我想在数据库层中进行此操作。

我想要一个问题的解决方案,或者一个很好的解释为什么它不可能(带有示例)。

请查看我的DB Fiddle

GMB*_*GMB 5

这个问题有两个不同的部分:

  • 第一个是如何生成具有共同参考的用户组
  • 第二部分是如何分配集团内的资产,同时考虑到孤儿资产

第 1 部分:图行走问题

基于共同参考来识别用户集群读起来就像一个图形行走问题。这在 SQL 中是一项复杂的任务,并且需要递归查询。该模式是逆透视用户的引用来生成节点,然后识别边(具有共同引用的节点),最后遍历图形(不循环)以生成组。

在 Postgres 中,数组可以方便地聚合节点:

with recursive 
    nodes as (
        select u.id, r.ref
        from users u 
        cross join lateral ( values (u.ref1), (u.ref2) ) r(ref)
    ),
    edges as (
        select distinct n1.id as id1, n2.id as id2
        from nodes n1 
        inner join nodes n2 on n1.ref = n2.ref
    ),
    rcte as (
        select id1, id2, array[id1] as visited from edges where id1 = id2
        union all
        select r.id1, e.id2, r.visited || e.id2
        from rcte r
        inner join edges e on e.id1 = r.id2
        where e.id2 <> all(r.visited) 
    ),
    groups as (
        select id1 as id, array_agg(distinct id2 order by id2) as ids
        from rcte
        group by id1
    )
select * from groups order by id
Run Code Online (Sandbox Code Playgroud)
ID id
1 {1,3,4,5}
2 {2}
3 {1,3,4,5}
4 {1,3,4,5}
5 {1,3,4,5}

第 2 部分:left join和聚合

现在我们已经确定了这些组,我们可以检查资产。由于您希望结果中包含所有资产,因此我们从assets表开​​始,然后将用户和组带入left joins。我们仍然可以使用group by用户组,将所有孤立资产放在同一个组中(组所在的位置null) - 这正是我们想要的。

最后一步是数组聚合;孤儿资产所有者的“传播”ref2可以用表达式来处理case

with recursive 
    nodes  as (...),
    edges  as (...),
    rcte   as (...),
    groups as (...)
select g.ids,
    array_agg(distinct u.username) as usernames,
    array_agg(distinct u.ref1) as refs1,
    case when g.ids is null then array_agg(distinct a.owner) else array_agg(distinct u.ref2) end as refs2,
    array_agg(distinct a.id) as asset_ids,
    count(distinct a.id) as asset_count
from assets a
left join users u on a.owner in (u.ref1, u.ref2)
left join groups g on g.id = u.id
group by g.ids
Run Code Online (Sandbox Code Playgroud)
id 用户名 参考文献1 参考文献2 资产ID 资产数量
{1,3,4,5} {波波,洛洛,莫莫,波波} {a,c} {d,f} {1,3,4,5,6,8,9,10} 8
{2} {托托} {b} {e} {2,7} 2
无效的 {无效的} {无效的} {z} {11} 1

DB Fiddlede 上的演示


附加组件:图形行走性能

在具有大量边缘的网络上,性能会受到影响,特别是如果图中只有很少的集群,每个集群都包含许多用户。针对这种情况我们可以尝试优化查询;这个想法是通过在每次迭代中聚合每个用户的所有路径来尝试限制需要行走的路径数量。

此查询通过了包含 200 个用户的测试用例,这些用户都属于同一集群(而第一个查询耗尽了 DB Fiddle 资源):

with recursive 
    nodes as (
        select u.id, r.ref
        from users u 
        cross join lateral ( values (u.ref1), (u.ref2) ) r(ref)
    ),
    edges as (
        select distinct n1.id as id1, n2.id as id2
        from nodes n1 
        inner join nodes n2 on n1.ref = n2.ref
    ),
    rcte as (
        select id1 as id, array[id1] as visited from edges where id1 = id2
        union all
        select id, array_agg(distinct visited) as visited
        from (
            select r.id, v.visited
            from rcte r
            inner join edges e on e.id1 = any(r.visited)
            cross join lateral unnest(r.visited || e.id2) v(visited)
            where e.id2 <> all(r.visited)
        ) as x
        group by id
    ),
    groups as (
        select distinct on (id) id, visited as ids 
        from rcte 
        order by id, array_length(visited, 1) desc 
    )
select * from groups g order by id
Run Code Online (Sandbox Code Playgroud)