Postgres: объединение учетных записей в одну личность по общему адресу электронной почты - PullRequest
0 голосов
/ 03 ноября 2018

Я создаю каталог пользователей, где:

  • каждый пользователь может иметь учетную запись в одной или нескольких внешних службах, а
  • каждая из этих учетных записей может иметь один или несколько адресов электронной почты.

Что я хочу знать, как я могу объединить эти учетные записи в одну личность через общие адреса электронной почты?

Например, скажем, у меня есть две службы, 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. Вот анимированный визуал:

Animated visual

Самый близкий ответ, который я смог найти, это этот , и я подозреваю, что решение - это рекурсивный 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 голосов
/ 03 ноября 2018

demo1: дБ <> скрипка , demo2: дБ <> скрипка

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:

СОЕДИНЕНИЕ обеих таблиц на одних и тех же адресах электронной почты для получения точки соприкосновения. Идентификаторы с одинаковыми идентификаторами будут объединены в один массив:

|   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 с сгенерированными кластерами уникальных идентификаторов. Так что нам нужны все они только один раз. Это работа последнего DISTINCT. Итак, CTE clustered приводит к

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

Теперь мы знаем, какие идентификаторы объединены, и должны поделиться своими данными. Теперь мы объединяем кластеризованные ids с исходными таблицами. Поскольку мы сделали это в CTE combined, мы можем повторно использовать эту часть (вот почему, кстати, она передается на аутсорсинг в один CTE: нам больше не нужно еще одно соединение обеих таблиц на этом этапе). Оператор JOIN <@ говорит: JOIN, если массив «точки соприкосновения» combined является подгруппой кластера идентификаторов clustered. Это дает в:

| 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 |

Теперь мы можем группировать адреса электронной почты, используя кластерные идентификаторы (крайний правый столбец).

array_agg агрегирует письма одного столбца, array_cat объединяет почтовые массивы обоих столбцов в один большой почтовый массив.

Поскольку существуют столбцы, в которых адрес электронной почты равен NULL, мы можем отфильтровать эти значения перед кластеризацией с помощью предложения FILTER (WHERE...).

Результат на данный момент:

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

Теперь мы группируем все адреса электронной почты для одного идентификатора. Мы должны генерировать новые уникальные идентификаторы. Для этого предназначена оконная функция 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 |
0 голосов
/ 03 ноября 2018

ОК, при условии, что у вас есть только две «службы», и, предполагая, что для начала вы не слишком озабочены тем, как лучше всего представить новый ключ (я использовал текст как самый простой для использования), тогда, пожалуйста, попробуйте запрос ниже. Это работает для меня на 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
;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...