Рекурсивный CTE PostgreSQL, соединяющий несколько идентификаторов с дополнительной логикой для других полей - PullRequest
0 голосов
/ 04 декабря 2018

В моей базе данных PostgreSQL у меня есть столбец идентификатора, который показывает каждый входящий уникальный список. У меня также есть столбец connected_lead_id, который показывает, связаны ли аккаунты друг с другом (т. Е. Муж и жена, родители и дети, группа друзей)., группа инвесторов и т. д.).

Когда мы подсчитываем количество идентификаторов, созданных за период времени, мы хотим видеть количество уникальных «групп» connected_ids за период.Другими словами, мы не хотели бы считать пару мужа и жены, мы хотели бы считать только одну, поскольку они действительно являются одним лидером.

Мы хотим иметь возможность создать представление, которое имеет только«первый» идентификатор, основанный на дате «create_at», а затем в конце содержит дополнительные столбцы для «connected_lead_id_1», «connected_lead_id_2», «connected_lead_id_3» и т. д.

Мы хотим добавить дополнительную логику, чтобымы берем источник "первого" идентификатора, если он не равен нулю, затем берем источник "второго" connected_lead_id, если он не равен нулю и так далее.Наконец, мы хотим взять самую раннюю дату on_boarded_date из группы connected_lead_id.

id    |    created_at      | connected_lead_id | on_boarded_date | source     |
  2   | 9/24/15 23:00      |        8          |                 |
  4   |  9/25/15 23:00     |        7          |                 |event
  7   |  9/26/15 23:00     |        4          |                 |
  8   |  9/26/15 23:00     |        2          |                 |referral
  11  |  9/26/15 23:00     |       336         |   7/1/17        |online
  142 |  4/27/16 23:00     |       336         |                 |
  336 |  7/4/16 23:00      |        11         |   9/20/18       |referral

Конечная цель:

id    |    created_at      | on_boarded_date | source     |  
  2   | 9/24/15 23:00      |                 | referral   |
  4   |  9/25/15 23:00     |                 | event      |
  11  |  9/26/15 23:00     |   7/1/17        | online     |

В идеале у нас также должно быть число дополнительных столбцов в концепоказать каждый connected_lead_id, который прикреплен к базовому идентификатору.

Спасибо за помощь!

Ответы [ 2 ]

0 голосов
/ 05 декабря 2018

Хорошо, лучшее, что я могу придумать на данный момент, - это сначала создать максимальные группы связанных идентификаторов, а затем присоединиться к таблице потенциальных клиентов, чтобы получить оставшиеся данные (см. SQL Fiddle * 1002.* для настройки, полных запросов и результатов).

Чтобы получить максимальные группы, вы можете использовать рекурсивное выражение общей таблицы, чтобы сначала увеличить группы, а затем запрос, чтобы отфильтровать результаты CTE до максимального значения.groups:

with recursive cte(grp) as (
select case when l.connected_lead_id is null then array[l.id] 
            else array[l.id, l.connected_lead_id]
       end      from leads l
union all
select grp || l.id
  from leads l
  join cte
    on l.connected_lead_id = any(grp)
   and not l.id = any(grp)
)
select * from cte c1

Выше CTE выводит несколько похожих групп, а также промежуточные группы.Предикат запроса ниже сокращает немаксимальные группы и ограничивает результаты только одной перестановкой каждой возможной группы:

 where not exists (select 1 from cte c2
                   where c1.grp && c2.grp
                     and ((not c1.grp @> c2.grp)
                       or (c2.grp < c1.grp
                      and c1.grp @> c2.grp
                      and c1.grp <@ c2.grp)));

Результаты :

|        grp |
|------------|
|        2,8 |
|        4,7 |
|         14 |
| 11,336,142 |
|      12,13 |

Затем присоедините последний запрос, приведенный выше, к своей таблице потенциальных клиентов и используйте оконные функции для получения значений оставшихся столбцов, а также отдельный оператор для сокращения его до конечного набора результатов:

with recursive cte(grp) as (
...
)
select distinct 
       first_value(l.id) over (partition by grp order by l.created_at) id
     , first_value(l.created_at) over (partition by grp order by l.created_at) create_at
     , first_value(l.on_boarded_date) over (partition by grp order by l.created_at) on_boarded_date
     , first_value(l.source) over (partition by grp 
                                   order by case when l.source is null then 2 else 1 end
                                   , l.created_at) source
     , grp CONNECTED_IDS
  from cte c1
  join leads l
    on l.id = any(grp)
 where not exists (select 1 from cte c2
                   where c1.grp && c2.grp
                     and ((not c1.grp @> c2.grp)
                       or (c2.grp < c1.grp
                      and c1.grp @> c2.grp
                      and c1.grp <@ c2.grp)));

Результаты :

| id |            create_at | on_boarded_date |   source | connected_ids |
|----|----------------------|-----------------|----------|---------------|
|  2 | 2015-09-24T23:00:00Z |          (null) | referral |           2,8 |
|  4 | 2015-09-25T23:00:00Z |          (null) |    event |           4,7 |
| 11 | 2015-09-26T23:00:00Z |      2017-07-01 |   online |    11,336,142 |
| 12 | 2015-09-26T23:00:00Z |      2017-07-01 |    event |         12,13 |
| 14 | 2015-09-26T23:00:00Z |          (null) |   (null) |            14 |
0 голосов
/ 05 декабря 2018

demo: db <> fiddle

Основная идея - эскиз:

  1. Цикл по заказанному набору.Получите все id с, которые никогда не видели ни в одном connected_lead_id ( cli ).Это ваши отправные точки для рекурсии. Проблема в том, что ваш номер 142 не был виден ранее, но находится в той же группе, что и 11, из-за его значения .Так что было бы лучше получить клис невидимых идентификаторов.С этими значениями намного проще вычислить идентификаторы групп позже в части рекурсии.Из-за цикла необходима функция / хранимая процедура.

  2. Рекурсивная часть: Первый шаг - получить идентификаторы исходного клиса.Вычисление первого ссылочного идентификатора с использованием отметки времени created_at.После этого можно выполнить простую рекурсию дерева по клису.

1.Функция:

CREATE OR REPLACE FUNCTION filter_groups() RETURNS int[] AS $$
DECLARE
    _seen_values int[];
    _new_values int[];
    _temprow record;
BEGIN
    FOR _temprow IN
        -- 1:
        SELECT array_agg(id ORDER BY created_at) as ids, connected_lead_id FROM groups GROUP BY connected_lead_id ORDER BY MIN(created_at)
    LOOP
        -- 2:
        IF array_length(_seen_values, 1) IS NULL 
            OR (_temprow.ids || _temprow.connected_lead_id) && _seen_values = FALSE THEN

            _new_values := _new_values || _temprow.connected_lead_id;
        END IF;

        _seen_values := _seen_values || _temprow.ids;
        _seen_values := _seen_values || _temprow.connected_lead_id;
    END LOOP;

    RETURN _new_values;
END;
$$ LANGUAGE plpgsql;
  1. Группировка всех идентификаторов, которые относятся к одному и тому же кли
  2. Цикл по массивам идентификаторов.Если элемент массива не был виден ранее, добавьте указанную переменную cli к выходной переменной (_new_values).В обоих случаях добавьте идентификаторы и cli в переменную, в которой хранятся все пока не просмотренные идентификаторы (_seen_values)
  3. Выдайте клис.

Результат пока {8, 7, 336} (что эквивалентно идентификаторам {2,4,11,142}!)

2.Рекурсия:

-- 1:
WITH RECURSIVE start_points AS (
    SELECT unnest(filter_groups()) as ids
),
filtered_groups AS (
    -- 3:
    SELECT DISTINCT
       1 as depth, -- 3
       first_value(id) OVER w as id, -- 4
       ARRAY[(MIN(id) OVER w)] as visited, -- 5
       MIN(created_at) OVER w as created_at,
       connected_lead_id,
       MIN(on_boarded_date) OVER w as on_boarded_date -- 6,
       first_value(source) OVER w as source
    FROM groups 
    WHERE connected_lead_id IN (SELECT ids FROM start_points)
    -- 2:
    WINDOW w AS (PARTITION BY connected_lead_id ORDER BY created_at)

    UNION

    SELECT
        fg.depth + 1,
        fg.id,
        array_append(fg.visited, g.id), -- 8
        LEAST(fg.created_at, g.created_at), 
        g.connected_lead_id, 
        LEAST(fg.on_boarded_date, g.on_boarded_date), -- 9
        COALESCE(fg.source, g.source) -- 10
    FROM groups g
    JOIN filtered_groups fg
    -- 7
    ON fg.connected_lead_id = g.id AND NOT (g.id = ANY(visited))

)
SELECT DISTINCT ON (id) -- 11
    id, created_at,on_boarded_date, source 
FROM filtered_groups 
ORDER BY id, depth DESC;
  1. Часть WITH выдает результаты функции.unnest() расширяет массив id в каждую строку для каждого идентификатора.
  2. Создание окна : оконная функция группирует все значения по их клису и упорядочивает окно по отметке времени created_at.В вашем примере все значения находятся в своем собственном окне, за исключением 11 и 142, которые сгруппированы.
  3. Это переменная справки для получения последних строк позже.
  4. first_value()дает первое значение упорядоченной оконной рамы.Предполагая, что 142 имеет меньшую временную метку create_at, результат будет 142.Но это все же 11.
  5. Требуется переменная, чтобы сохранить, какой идентификатор был посещен.Без этой информации будет создан бесконечный цикл: 2-8-2-8-2-8-2-8-...
  6. Минимальная дата окна берется (то же самое здесь: если 142 будет иметь дату меньше, чем 11, это будет результат).

Теперь начальный запрос рекурсии вычисляется.Ниже описана рекурсивная часть:

Соединение таблицы (исходные результаты функции) с предыдущим результатом рекурсии.Второе условие - остановка бесконечного цикла, о котором я упоминал выше. Добавление текущего посещенного идентификатора в переменную посещения. Если текущий on_boarded_date раньше, он берется. COALESCE дает первое значение NOT NULL.Таким образом, первый NOT NULL source сохраняется на протяжении всей рекурсии

После рекурсии, которая дает результат всех шагов рекурсии, мы хотим отфильтровать только самые глубокие посещения каждого начального идентификатора.

DISTINCT ON (id) выдает строку с первым появлением идентификатора.Чтобы получить последний, весь набор упорядочен по убыванию переменной depth.
...