【问题标题】:How to count all the connected nodes (rows) in a graph on Postgres?如何计算 Postgres 图表中所有连接的节点(行)?
【发布时间】:2017-08-12 00:13:21
【问题描述】:

我的表有account_iddevice_id。一个account_id 可以有多个device_ids,反之亦然。我正在尝试计算每个连接的多对多关系的深度。

例如:

account_id | device_id
1 | 10
1 | 11
1 | 12
2 | 10
3 | 11
3 | 13
3 | 14
4 | 15
5 | 15
6 | 16

如何构建一个查询,知道将帐户 1-3 组合在一起,将帐户 4-5 组合在一起,然后单独保留 6?账户 1-3 的所有 7 个条目都应该组合在一起,因为它们在某个时候都触及了相同的 account_id 或 device_id。我正在尝试将它们组合在一起并输出计数。

帐户 1 在设备的 10、11、12 上使用。这些设备也使用其他帐户,因此我们希望将它们包含在组中。他们使用了额外的账户 2 和 3。但账户 3 被另外 2 台设备进一步使用,因此我们也将它们包括在内。组的扩展会引入任何其他也“接触”组中已有帐户或设备的帐户或设备。

如下图所示:

【问题讨论】:

  • 我不禁觉得我们需要更多信息来帮助您。请进一步详细说明这些组是如何派生的。
  • @SandPiper 很抱歉造成混乱。 account_id 1 看到了 device_ids 10、11、12。device_id 10 和 11 也有其​​他帐户看到了它们。 account_id 2 也看到了device_id 10 和account_id 3 看到了device_id 11。由于account_id 3 已被“触摸”,我们还需要包括它所看到的所有其他device_ids,所以@987654337 @ 13 和 15。
  • 有趣的问题。肯定会投票给一个优雅的答案。我的 5 美分:可能 EXTENSION intarray 与它的“数组交集”+“WITH RECURSIVE”方法一起可以使它整洁。
  • 建议将标题更改为:“在图中查找(连接)集群”(可能有更好的数学措辞)

标签: sql postgresql graph connected-components


【解决方案1】:

您可以使用递归 cte:

with recursive t(account_id, device_id) as (
       select 1, 10 union all
       select 1, 11 union all
       select 1, 12 union all
       select 2, 10 union all
       select 3, 11 union all
       select 3, 13 union all
       select 3, 14 union all
       select 4, 15 union all
       select 5, 15 union all
       select 6, 16
     ),
     a as (
      select distinct t.account_id as a, t2.account_id as a2
      from t join
           t t2
           on t2.device_id = t.device_id and t.account_id >= t2.account_id
     ),
     cte as (
      select a.a, a.a2 as mina
      from a
      union all
      select a.a, cte.a
      from cte join
           a
           on a.a2 = cte.a and a.a > cte.a
     )
select grp, array_agg(a)
from (select a, min(mina) as grp
      from cte
      group by a
     ) a
group by grp;

Here 是一个 SQL Fiddle。

【讨论】:

  • 谢谢@GordonLinoff。似乎这种方法可以根据需要很好地聚合组,但是当我尝试进行计数时,我无法获得第 1 组的 7 行。您对如何从分组中获得准确计数有任何想法吗? ?我将继续研究这个答案。
【解决方案2】:

您可以GROUP BY device_id,然后将 account_id 聚合到 Postgres 数组中。这是一个示例查询,虽然我不确定您的实际表名是什么。

SELECT
  device_id,
  array_agg(account_id) as account_ids
FROM account_device --or whatever your table name is
GROUP BY device_id;

结果 - 希望它是您想要的:

16 | {6}
15 | {4,5}
13 | {3}
11 | {1,3}
14 | {3}
12 | {1}
10 | {1,2}

【讨论】:

  • 感谢您的回复。我认为这让我成功了一半。那么请注意设备 10 和 11 是如何共享 account_id 1 的?因为它是共享的,所以我想将它们组合在一起。基本上我想以某种方式获得一个像 device_ids {10,11,12,13,14} | 这样的组{1,2,3}
  • 啊,我明白了!是的,这将是一个更复杂的查询,但仍然非常可行。正如其他答案之一所暗示的那样,递归 CTE 适用于涉及“图遍历”的此类事情。这里有更全面的资源slideshare.net/quipo/rdbms-in-the-social-networks-age/…
【解决方案3】:
-- \i tmp.sql

CREATE TABLE graph(
        account_id integer NOT NULL --references accounts(id)
        , device_id integer not NULL --references(devices(id)
        ,PRIMARY KEY(account_id, device_id)
        );

INSERT INTO graph (account_id, device_id)VALUES
 (1,10) ,(1,11) ,(1,12)
,(2,10)
,(3,11) ,(3,13) ,(3,14)
,(4,15)
,(5,15)
,(6,16)
        ;

-- SELECT* FROM graph ;

        -- Find the (3) group leaders
WITH seed AS (
        SELECT row_number() OVER () AS cluster_id -- give them a number
        , g.account_id
        , g.device_id
        FROM graph g
        WHERE NOT EXISTS (SELECT*
                FROM graph nx
                WHERE (nx.account_id = g.account_id OR nx.device_id = g.device_id)
                AND nx.ctid < g.ctid
                )
        )
-- SELECT * FROM seed;
;

WITH recursive omg AS (
        --use the above CTE in a sub-CTE
        WITH seed AS (
                SELECT row_number()OVER () AS cluster_id
                , g.account_id
                , g.device_id
                , g.ctid AS wtf --we need an (ordered!) canonical id for the tuples
                                -- ,just to identify and exclude them
                FROM graph g
                WHERE NOT EXISTS (SELECT*
                        FROM graph nx
                        WHERE (nx.account_id = g.account_id OR nx.device_id = g.device_id) AND nx.ctid < g.ctid
                        )
                )
        SELECT s.cluster_id
                , s.account_id
                , s.device_id
                , s.wtf
        FROM seed s
        UNION ALL
        SELECT o.cluster_id
        , g.account_id
        , g.device_id
        , g.ctid AS wtf
        FROM omg o
        JOIN graph g ON (g.account_id = o.account_id OR g.device_id = o.device_id)
                         -- AND (g.account_id > o.account_id OR g.device_id > o.device_id)
                         AND g.ctid > o.wtf
                -- we still need to exclude duplicates here
                -- (which could occur if there are cycles in the graph)
                -- , this could be done using an array
        )
SELECT *
FROM omg
ORDER BY cluster_id, account_id,device_id
        ;

结果:


DROP SCHEMA
CREATE SCHEMA
SET
CREATE TABLE
INSERT 0 10
 cluster_id | account_id | device_id 
------------+------------+-----------
          1 |          1 |        10
          2 |          4 |        15
          3 |          6 |        16
(3 rows)

 cluster_id | account_id | device_id |  wtf   
------------+------------+-----------+--------
          1 |          1 |        10 | (0,1)
          1 |          1 |        11 | (0,2)
          1 |          1 |        12 | (0,3)
          1 |          1 |        12 | (0,3)
          1 |          2 |        10 | (0,4)
          1 |          3 |        11 | (0,5)
          1 |          3 |        13 | (0,6)
          1 |          3 |        14 | (0,7)
          1 |          3 |        14 | (0,7)
          2 |          4 |        15 | (0,8)
          2 |          5 |        15 | (0,9)
          3 |          6 |        16 | (0,10)
(12 rows)

较新的版本(我在表格中添加了一个 Id 列)


        -- for convenience :set of all adjacent nodes.
CREATE TEMP VIEW pair AS
        SELECT one.id AS one
                , two.id AS two
        FROM graph one
        JOIN graph two ON (one.account_id = two.account_id OR one.device_id = two.device_id)
                AND one.id <> two.id
        ;

WITH RECURSIVE flood AS (
        SELECT g.id, g.id AS parent_id
        , 0 AS lev
        , ARRAY[g.id]AS arr
        FROM graph g
        UNION ALL
        SELECT c.id, p.parent_id AS parent_id
        , 1+p.lev AS lev
        , p.arr || ARRAY[c.id] AS arr
        FROM graph c
        JOIN flood p ON EXISTS (
                        SELECT * FROM pair WHERE p.id = pair.one AND c.id = pair.two)
                AND p.parent_id < c.id
                AND NOT p.arr @> ARRAY[c.id]    -- avoid cycles/loops
        )
SELECT g.*, a.parent_id
        , dense_rank() over (ORDER by a.parent_id)AS group_id
FROM graph g
JOIN (SELECT id, MIN(parent_id) AS parent_id
        FROM flood
        GROUP BY id
        ) a
ON g.id = a.id
ORDER BY a.parent_id, g.id
        ;

新结果:


CREATE VIEW
 id | account_id | device_id | parent_id | group_id 
----+------------+-----------+-----------+----------
  1 |          1 |        10 |         1 |        1
  2 |          1 |        11 |         1 |        1
  3 |          1 |        12 |         1 |        1
  4 |          2 |        10 |         1 |        1
  5 |          3 |        11 |         1 |        1
  6 |          3 |        13 |         1 |        1
  7 |          3 |        14 |         1 |        1
  8 |          4 |        15 |         8 |        2
  9 |          5 |        15 |         8 |        2
 10 |          6 |        16 |        10 |        3
(10 rows)

【讨论】:

  • 同样的问题,试试这个输入 (1, 100), (1, 101), (2, 101), (2, 102), (3, 102), (3, 103), (4, 103), (4, 104), (5, 104), (5, 105) - 这是一组,而不是四组。
  • 是的,那是因为我的g.ctid &gt; o.wtf 有点过于严格(种子也是如此)。真的需要一个数组/集合在两个(所有四个)方向上增长和重复数据删除。 [更好的方法可能只是简单的 iterative union-find 之类的东西]
猜你喜欢
  • 2017-03-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多