我正在建立用户目录,其中:
我想知道的是,如何通过公共电子邮件地址将这些帐户汇总为单个身份?
例如,假设我有两个服务,A
和B
。对于每种服务,我都有一张表格,该表格将一个帐户与一个或多个电子邮件地址相关联。
因此,如果服务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
。这是动画的视觉效果:
我能找到的最接近答案是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
答案 0 :(得分:1)
demo1:db<>fiddle,demo2: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}
)
然后unnest
个ids
数组可作为过滤的基础。结束于:
| 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
;