Postgres:通过公用电子邮件地址将帐户汇总为一个身份

时间:2018-11-03 03:00:36

标签: sql postgresql graph

我正在建立用户目录,其中:

  • 每个用户可以在一个或多个外部服务上拥有一个帐户,并且
  • 每个帐户可以有一个或多个电子邮件地址。

我想知道的是,如何通过公共电子邮件地址将这些帐户汇总为单个身份?

例如,假设我有两个服务,AB。对于每种服务,我都有一张表格,该表格将一个帐户与一个或多个电子邮件地址相关联。

因此,如果服务A具有以下帐户电子邮件地址:

account_id | email_address
-----------|--------------
1          | a@foo.com
1          | b@foo.com
2          | c@foo.com

和服务B具有以下帐户电子邮件地址:

account_id | email_address
-----------|--------------
3          | a@foo.com
3          | a@bar.com
4          | d@foo.com

我想创建一个表,将这些帐户的电子邮件地址汇总为一个用户身份:

user_id | email_address
--------|--------------
X       | a@foo.com
X       | b@foo.com
X       | a@bar.com
Y       | c@foo.com
Z       | d@foo.com

如您所见,服务1的帐户A和服务2的帐户B已被合并到一个普通用户X中,通用电子邮件地址a@foo.com。这是动画的视觉效果:

Animated visual

我能找到的最接近答案是this one,我怀疑该解决方案是递归CTE,但是鉴于输入和引擎不同,我很难实现它。

说明:我正在寻找一种解决方案,可以处理任意数量的服务,因此输入表可能会更好:

service_id | account_id | email_address
-----------|------------|--------------
A          | 1          | a@foo.com
A          | 1          | b@foo.com
A          | 2          | c@foo.com
B          | 3          | a@foo.com
B          | 3          | a@bar.com
B          | 4          | d@foo.com

2 个答案:

答案 0 :(得分:1)

demo1:db<>fiddledemo2:db<>fiddle

WITH combined AS (
    SELECT
        a.email as a_email,
        b.email as b_email,
        array_remove(ARRAY[a.id, b.id], NULL) as ids
    FROM 
        a
    FULL OUTER JOIN b ON (a.email = b.email)
), clustered AS (
    SELECT DISTINCT
        ids
    FROM (
        SELECT DISTINCT ON (unnest_ids) 
            *, 
            unnest(ids) as unnest_ids 
        FROM combined
        ORDER BY unnest_ids, array_length(ids, 1) DESC
    ) s
)
SELECT DISTINCT
    new_id, 
    unnest(array_cat) as email
FROM (
    SELECT
        array_cat(
            array_agg(a_email) FILTER (WHERE a_email IS NOT NULL), 
            array_agg(b_email) FILTER (WHERE b_email IS NOT NULL)
        ), 
        row_number() OVER () as new_id
    FROM combined co
    JOIN clustered cl
    ON co.ids <@ cl.ids
    GROUP BY cl.ids
) s

分步说明:

为便于解释,我将使用此数据集。这比您的要复杂一些。它可以更好地说明我的步骤。在您的较小组合中不会出现某些问题。将字符视为电子邮件地址的变量。

表A:

| id | email |
|----|-------|
|  1 |     a |
|  1 |     b |
|  2 |     c |
|  5 |     e |

表B

| id | email |
|----|-------|
|  3 |     a |
|  3 |     d |
|  4 |     e |
|  4 |     f |
|  3 |     b |

CTE combined

将两个表加入同一电子邮件地址以获得联系点。相同ID的ID将在一个数组中串联:

|   a_email |   b_email | ids |
|-----------|-----------|-----|
|    (null) | a@bar.com |   3 |
| a@foo.com | a@foo.com | 1,3 |
| b@foo.com |    (null) |   1 |
| c@foo.com |    (null) |   2 |
|    (null) | d@foo.com |   4 |

CTE clustered(对不起,名字...):

目标是仅在一个数组中准确获取所有元素。在combined中,例如,您可以看到例如元素4的数组:{5,4}{4}

首先按行的ids数组的长度对行进行排序,因为稍后DISTINCT应该采用最长的数组(因为保持接触点{5,4}而不是{4}

然后unnestids数组可作为过滤的基础。结束于:

| a_email | b_email | ids | unnest_ids |
|---------|---------|-----|------------|
|       b |       b | 1,3 |          1 |
|       a |       a | 1,3 |          1 |
|       c |  (null) |   2 |          2 |
|       b |       b | 1,3 |          3 |
|       a |       a | 1,3 |          3 |
|  (null) |       d |   3 |          3 |
|       e |       e | 5,4 |          4 |
|  (null) |       f |   4 |          4 |
|       e |       e | 5,4 |          5 |

DISTINCT ON过滤后

| a_email | b_email | ids | unnest_ids |
|---------|---------|-----|------------|
|       b |       b | 1,3 |          1 |
|       c |  (null) |   2 |          2 |
|       b |       b | 1,3 |          3 |
|       e |       e | 5,4 |          4 |
|       e |       e | 5,4 |          5 |

我们只对ids列中生成的唯一ID簇感兴趣。所以我们只需要一次。这是最后一个DISTINCT的工作。因此CTE clustered的结果是

| ids |
|-----|
|   2 |
| 1,3 |
| 5,4 |

现在,我们知道合并了哪些ID,并应该共享它们的数据。现在,我们将聚簇的ids加入到原始表中。由于我们已经在CTE combined中完成了此操作,因此我们可以重用此部分(这就是为什么将其外包给单个CTE的原因:在此步骤中,我们不再需要两个表的另一个联接)。 JOIN运算符<@说:如果combined的“接触点”数组是clustered的ID簇的子组,则进行JOIN。这样产生:

| a_email | b_email | ids | ids |
|---------|---------|-----|-----|
|       c |  (null) |   2 |   2 |
|       a |       a | 1,3 | 1,3 |
|       b |       b | 1,3 | 1,3 |
|  (null) |       d |   3 | 1,3 |
|       e |       e | 5,4 | 5,4 |
|  (null) |       f |   4 | 5,4 |

现在,我们可以使用群集ID(最右边的列)对电子邮件地址进行分组。

array_agg汇总一列的邮件,array_cat将两列的电子邮件数组合并为一个大电子邮件数组。

由于存在电子邮件为NULL的列,我们可以在使用FILTER (WHERE...)子句进行聚类之前过滤掉这些值。

到目前为止的结果:

| array_cat |
|-----------|
|         c |
| a,b,a,b,d |
|     e,e,f |

现在,我们将所有电子邮件地址归为一个ID。我们必须生成新的唯一ID。这就是window function row_number的目的。它只是向表中添加了行计数:

| array_cat | new_id |
|-----------|--------|
|         c |      1 |
| a,b,a,b,d |      2 |
|     e,e,f |      3 |

最后一步是unnest数组以获取每个电子邮件地址的一行。由于数组中仍然有一些重复项,因此我们也可以在此步骤中使用DISTINCT消除它们:

| new_id | email |
|--------|-------|
|      1 |     c |
|      2 |     a |
|      2 |     b |
|      2 |     d |
|      3 |     e |
|      3 |     f |

答案 1 :(得分:0)

好的,假设您只有两个“服务”,并且假设开始时您并不太在意如何最好地表示新密钥(我使用文字是最容易使用的),那么请尝试下面的查询。这在Postgres 9.6上对我有效:

WITH shared_addr AS 
(
SELECT foo.account_a, foo.account_b, row_number() OVER (ORDER BY foo.account_a) AS shared_id
FROM (
SELECT 
  a.account_id as account_a
, b.account_id as account_b
FROM
service_a a
JOIN
service_b b
ON 
a.email_address = b.email_address
GROUP BY a.account_id, b.account_id
) foo
)
SELECT
bar.account_id,
bar.email_address
FROM
(
SELECT
'A-' || service_a.account_id::text AS account_id,
service_a.email_address
FROM service_a
LEFT OUTER JOIN 
shared_addr
ON
shared_addr.account_a = service_a.account_id
WHERE shared_addr.account_b IS NULL

UNION ALL

SELECT
'B-' ||service_b.account_id::text,
service_b.email_address FROM service_b
LEFT OUTER JOIN 
shared_addr
ON
shared_addr.account_b = service_b.account_id
WHERE shared_addr.account_a IS NULL

UNION ALL

(
SELECT
'shared-' || shared_addr.shared_id::text,
service_b.email_address
FROM service_b
JOIN 
shared_addr
ON
shared_addr.account_b = service_b.account_id

UNION

SELECT
'shared-' || shared_addr.shared_id::text,
service_a.email_address
FROM service_a
JOIN 
shared_addr
ON
shared_addr.account_a = service_a.account_id
)
) bar
;