【问题标题】:How to cluster rows in a postgresql table that match an input value or match a value from any of the other matching rows?如何对 postgresql 表中匹配输入值或匹配任何其他匹配行的值的行进行聚类?
【发布时间】:2020-11-24 02:42:05
【问题描述】:

我的 postgresql 数据库中有一个如下所示的表

如果集群中的每个联系人与集群中的另一个联系人共享contact_id_a 或contact_id_b 值(或两者),我如何恢复一组联系人?

在上面屏幕截图的示例中,第 1-6 行将在同一个集群中,而第 8 行将不属于任何集群。

如何使用 SQL 查询或结合 Java 代码的 SQL 查询来实现?

对于上下文,此表列出了联系人列表中的所有潜在重复联系人。我们希望向列表所有者显示所有可能重复的联系人,以便用户可以手动管理这些重复。

这是我的起始代码:

DuplicateCandidate firstDuplicate = db.sql("select * from duplicates where list_id = "+list_id+ " and ignore_duplicate is not true").first(DuplicateCandidate);
        String sql = "select * from duplicates where list_id = "+list_id+ "and ignore_duplicate is not true "
                + "and (contact_id_a = ? or contact_id_b = ? or contact_id_a = ? or contact_id_b = ?";
        List<DuplicateCandidate> groupOfDuplicates  = db.sql(sql, firstDuplicate.contact_id_a,firstDuplicate.contact_id_a, firstDuplicate.contact_id_b, firstDuplicate.contact_id_b).results(DuplicateCandidate.class);

这将带回第一行和包含 16247096 或 16247097 的任何其他行,但不会带回与第二个查询结果中的 contact_ids 匹配的其他基本行。

干杯。

【问题讨论】:

  • 如果识别重复是用例,为什么不只是SELECT a, b, c, COUNT(1) AS d FROM table GROUP BY a, b, c HAVING d &gt; 1;其中 a、b 和 c 是您要识别重复项的列?
  • 因为我们不只是搜索完全匹配。我们正在使用评分方案。我们为每一列分配了唯一的权重。我们在 Java 中检查两个联系人之间的相似程度。如果每列中的相似度之和(使用 Levenshtein 距离)乘以相应的列权重低于截止阈值,则我们认为这两个联系人重复并将它们添加到表中。这太复杂了,无法在 SQL 语句中完成。
  • @GNG 。 . . db/SQL fiddle 会很有帮助——或者至少是非图像格式的数据。
  • 这个问题本质上是Generate cluster field for a set of records which match the condition(s) using PostgreSQL 的副本,它有一个类似但不太通用的解决方案。

标签: java sql postgresql cluster-analysis


【解决方案1】:

您可以使用递归 CTE。这会遍历图表,然后为每一行分配图表中的最小标识符。请注意,您的数据没有每一行的唯一标识符,因此首先生成一个:

with recursive d as (
      select row_number() over (order by contact_id_a, contact_id_b) as id, d.*
      from duplicates d
     ),
     cte (id, contact_id_a, contact_id_b, min_id, ids, lev) as (
      select id, contact_id_a, contact_id_b, id as min_id, array[id] as ids, 1 as lev
      from d
      union all
      select d.id, d.contact_id_a, d.contact_id_b, least(d.id, cte.min_id), ids || d.id, lev + 1
      from cte join
           d
           on cte.contact_id_a = d.contact_id_a or cte.contact_id_b = d.contact_id_b
      where d.id <> ALL (cte.ids)
     )
select distinct on (id) cte.*
from cte
order by id, min_id;

min_id 列包含您想要的分组。

Here 是一个说明代码的 dbfiddle。

【讨论】:

  • 错误:“cte”处或附近的语法错误第 5 行:递归 cte(contact_id_a、contact_id_b、min_id、ids... ^ SQL 状态:42601 字符:141
  • @GNG 。 . .我修复了一些语法错误并添加了一个 dbfiddle.
【解决方案2】:

像这样的集群是一个迭代过程,其步骤数未知。我从来没有找到可以在递归查询中完成的解决方案。

我已经六年多没有从事 CRM 工作了,但是下面的功能类似于我们过去生成匹配组的方式。逐行执行此操作对于我们的工作量来说表现不够好,并通过宿主语言使用例如Java HashMap()HashSet() 和倒排索引会创建非常混乱的代码。

假设这个架构:

\d contact_info 
                 Table "public.contact_info"
      Column      |  Type   | Collation | Nullable | Default 
------------------+---------+-----------+----------+---------
 contact_id_a     | bigint  |           |          | 
 contact_id_b     | bigint  |           |          | 
 ignore_duplicate | boolean |           |          | false
 list_id          | integer |           |          | 496

select * from contact_info ;
 contact_id_a | contact_id_b | ignore_duplicate | list_id 
--------------+--------------+------------------+---------
     16247096 |     16247097 | f                |     496
     16247096 |     16247098 | f                |     496
     16247096 |     16247099 | f                |     496
     16247097 |     16247098 | f                |     496
     16247097 |     16247099 | f                |     496
     16247098 |     16247099 | f                |     496
     16247094 |     16247095 | f                |     496
(7 rows)

此函数创建两个临时表来保存中间集群,然后在没有更多集群可能时返回结果。

create or replace function cluster_contact() 
  returns table (clust_id bigint, contact_id bigint) 
  language plpgsql as $$
declare 
  last_count bigint := 1;
  this_count bigint := 0;
begin
  create temp table contact_match (clust_id bigint, contact_id bigint) on commit drop;
  create index cm_1 on contact_match (contact_id, clust_id);
  create index cm_2 on contact_match using hash (clust_id);
  create temp table contact_hold (clust_id bigint, contact_id bigint) on commit drop;

  with dedup as (
    select distinct least(ci.contact_id_a) as clust_id,
           greatest(ci.contact_id_b) as contact_id
      from contact_info ci
     where not ci.ignore_duplicate
  )
  insert into contact_match
    select d.clust_id, d.clust_id from dedup d
    union
    select d.clust_id, d.contact_id from dedup d;

  while last_count > this_count loop

    if this_count = 0 then 
      select count(distinct cm.clust_id) into last_count from contact_match cm;
    else 
      last_count := this_count;
    end if;

    with new_cid as (
      select cm.contact_id as clust_id_old,
             min(cm.clust_id) as clust_id_new
        from contact_match cm
       group by cm.contact_id
    )
    update contact_match
       set clust_id = nc.clust_id_new
      from new_cid nc
     where contact_match.clust_id = nc.clust_id_old;

    truncate table contact_hold;
    insert into contact_hold 
      select distinct * from contact_match;
 
    truncate table contact_match;
    insert into contact_match
      select * from contact_hold;

    select count(distinct cm.clust_id) into this_count from contact_match cm;

  end loop;

  return query select * from contact_match order by clust_id, contact_id;
end $$;

我见过开发人员面临的最大心理障碍之一是忽略了将contact_id 与其自身的关系包括在内。这会导致处理不连贯,并且心理模型因左侧和右侧而不必要地复杂化。

select * from cluster_contact();
 clust_id | contact_id 
----------+------------
 16247094 |   16247094
 16247094 |   16247095
 16247096 |   16247096
 16247096 |   16247097
 16247096 |   16247098
 16247096 |   16247099
(6 rows)

如果您需要澄清此解决方案中的任何步骤,或者它不适合您,请发表评论。

另外,知道 Levenshtein 在fuzzystrmatch 中可用,而且效果很好。

如果您希望使用从 1 开始的连续 clust_id,请将函数中的 return query 更改为:

  return query 
    select dense_rank() over (order by cm.clust_id) as clust_id, 
           cm.contact_id 
      from contact_match cm 
     order by clust_id, contact_id;

它会产生:

select * from cluster_contact();
 clust_id | contact_id 
----------+------------
        1 |   16247094
        1 |   16247095
        2 |   16247096
        2 |   16247097
        2 |   16247098
        2 |   16247099
(6 rows)

【讨论】:

  • 这似乎有效。我们如何改变这个结果,使 cluster_id 只是一个增量整数?或者让 cluster_id 成为集群中的最小 contact_id 有什么好处?
  • @GNG 我使用了最低的contact_id,因为它在那里。您可以使用dense_rank() 在末尾重新编号。我会将其添加到我的答案中。
猜你喜欢
  • 2015-03-25
  • 1970-01-01
  • 2014-03-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-02
  • 2020-07-04
相关资源
最近更新 更多