递归CTE PostgreSQL连接多个ID和其他字段的附加逻辑

时间:2018-12-04 17:44:51

标签: postgresql common-table-expression recursive-query

在我的PostgreSQL数据库中,我有一个id列,显示进入的每个唯一线索。我还有一个connected_lead_id列,显示帐户是否彼此关联(即丈夫和妻子,父母和子女,朋友组) ,一组投资者等)。

当我们计算一个时间段内创建的ID的数量时,我们想查看一个时间段内connectd_id的唯一“组”的数量。换句话说,我们不想同时计算一对夫妻,我们只想计算一对,因为他们确实是一个领先者。

我们希望能够创建一个仅基于“ created_at”日期具有“第一个” ID的视图,然后在末尾包含“ connected_lead_id_1”,“ connected_lead_id_2”,“ connected_lead_id_3”等其他列。

我们要添加其他逻辑,以便我们采用“第一个” ID的源(除非为空),然后采用“第二个” connected_lead_id的源,除非其为空,依此类推。最后,我们想从connected_lead_id组中获取最早的on_boarded_date。

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     |

理想情况下,我们还会在末尾有i个额外的列,以显示附加到基本ID的每个connected_lead_id。

感谢您的帮助!

2 个答案:

答案 0 :(得分:1)

demo:db<>fiddle

主要思想-草图:

  1. 遍历有序集。获取所有id中从未见过的所有connected_lead_id cli )。这些是您进行递归的起点。 问题是您的电话号码142以前没有见过,但由于其期限,它与11属于同一组。因此,最好获得看不见的ID的片段。使用这些值,在递归部分中稍后更容易计算组的ID。由于存在循环,因此需要一个函数/存储过程。

  2. 递归部分:第一步是获取起始cli的id。使用created_at时间戳计算第一个推荐ID。之后,可以在cli上进行简单的树递归。

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. 将所有引用相同cli的id分组
  2. 遍历id数组。如果以前没有看到数组的元素,则将引用的cli添加到输出变量(_new_values)中。在这两种情况下,都将id和cli添加到存储所有尚未看到的id(_seen_values)的变量中
  3. 发出clis。

到目前为止,结果为{8, 7, 336}(相当于ID {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数组扩展为每个id的每一行。
  2. Creating a window:窗口函数按clis将所有值分组,并按created_at时间戳对窗口排序。在您的示例中,除分组的11142以外,所有值都在各自的窗口中。
  3. 这是一个帮助变量,以后可以获取最新行。
  4. first_value()给出有序窗口框架的第一个值。假设142的时间戳created_at较小,则结果为142。但这还是11
  5. 需要一个变量来保存已访问过的ID。没有这些信息,将创建无限循环:2-8-2-8-2-8-2-8-...
  6. 获取窗口的最小日期(此处相同:如果142的日期比11短,将是结果)。

现在计算递归的起始查询。以下描述了递归部分:

  1. 将表(原始函数结果)与先前的递归结果进行连接。第二个条件是我上面提到的无限循环的停止。
  2. 将当前访问的ID附加到受访变量中。
  3. 如果当前的on_boarded_date较早,则采用该方法。
  4. COALESCE给出第一个NOT NULL值。因此,第一个NOT NULL source在整个递归过程中都是安全的

在给出所有递归步骤的结果的递归之后,我们只想筛选出每个起始ID的最深访问。

  1. DISTINCT ON (id)给出第一个出现id的行。要获取最后一个,整个集合按depth变量降序排列。

答案 1 :(得分:1)

好吧,目前我能提出的最好的办法是,首先建立最大数量的相关ID组,然后再加入到潜在客户表中以获取其余数据(有关详细信息,请参见此SQL Fiddle设置,完整的查询和结果)。

要获取最大组,您可以使用递归公用表表达式来首先扩展组,然后执行查询以将CTE结果过滤为最大组:

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

Results

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

接下来,将上面的最终查询返回到Leads表,并使用窗口函数获取剩余的列值,并使用不同的运算符将其修剪到最终结果集:

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

Results

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