【问题标题】:postgresql: merge rows keeping some information, without loopspostgresql:合并行保留一些信息,没有循环
【发布时间】:2015-08-05 12:48:00
【问题描述】:

我有一个每个用户的通话列表,有时以分钟分隔。用户可以在这些电话中购买或不购买东西。 当用户在最后一次通话后 45 分钟内拨打电话时,我需要考虑与第一次通话是同一个电话。

我需要得到最终的通话次数(合计通话间隔不到 45 分钟) 以及每位用户购买商品的电话次数。

例如,我有一个这样的列表:

buyer       timestamp              bougth_flag        
tom         20150201 9:15                 1
anna        20150201 9:25                 0
tom         20150201 10:15                0
tom         20150201 10:45                1
tom         20150201 10:48                1
anna        20150201 11:50                0
tom         20150201 11:52                0
anna        20150201 11:54                0

决赛桌是:

buyer       time_started        calls              articles_bought        
tom         20150201 9:15          1                        1
anna        20150201 9:25          1                        0
tom         20150201 10:15         3                        2
anna        20150201 10:50         2                        0
tom         20150201 11:52         1                        0

因此,我需要合并相隔不到 45 分钟的行,并且仍然按用户分开。 使用循环很容易做到这一点,但我正在使用的 postgresql 中没有循环或函数/过程。 关于如何做到这一点的任何想法?

谢谢

【问题讨论】:

  • 我使用的 postgresql 中没有循环或函数/过程。呃。请具体。您在使用 Amazon Redshift 吗?如果是这样,这样说。请指定您的 PostgreSQL 版本 (SELECT version())
  • “列表”是什么意思?它是 postgres 中的表吗?
  • 是的,Amazon Redshift 版本 8.0.2。是的,列表就是表格
  • 如果第 1 电话在下午 1:00 打进来,在下午 1:45 打电话给 #2,在下午 1:46 打电话给 #3。即使 #3 和 #1 相隔超过 45 分钟,是否应该合并 3 个调用?
  • 嗨,它们确实应该合并。最后一次通话重新激活该通话。

标签: postgresql amazon-redshift


【解决方案1】:

由于您事先不知道“电话”将持续多长时间(您可能会在一整天内每 30 分钟接到某个买家的电话 - 请参阅对问题的评论),您只能通过递归来解决这个问题CTE。 (请注意,我将您的列 'timestamp' 更改为 'ts'。切勿将关键字用作表或列名。)

WITH conversations AS (
  WITH RECURSIVE calls AS (
    SELECT buyer, ts, bought_flag, row_number() OVER (ORDER BY ts) AS conversation, 1::int AS calls
    FROM (
      SELECT buyer, ts, lag(ts) OVER (PARTITION BY buyer ORDER BY ts) AS lag, bought_flag
      FROM list) sub
    WHERE lag IS NULL OR ts - lag > interval '45 minutes'
    UNION ALL
    SELECT l.buyer, l.ts, l.bought_flag, c.conversation, c.calls + 1
    FROM list l
    JOIN calls c ON c.buyer = l.buyer AND l.ts > c.ts
    WHERE l.ts - c.ts < interval '45 minutes'
  )
  SELECT buyer, ts, bought_flag, conversation, max(calls) AS calls
  FROM calls
  GROUP BY buyer, ts, bought_flag, conversation
  order by conversation, ts
)
SELECT buyer, min(ts) AS time_started, max(calls) AS calls, sum(bought_flag) AS articles_bought
FROM conversations
GROUP BY buyer, conversation
ORDER BY time_started

几句解释:

  • 内部递归 CTE 的起始项有一个子查询,它从表中获取每次调用的基本数据,以及上一次调用的时间。内部 CTE 起始项中的主查询仅保留没有先前调用 (lag IS NULL) 或先前调用超过 45 分钟的那些行。因此,这些是我在这里称之为“对话”的最初呼叫。对话获得一列和一个 id,它只是查询中的行号,另一列用于跟踪对话“呼叫”中的呼叫次数。
  • 在递归术语中,同一会话中的连续呼叫被添加,“呼叫”计数器增加。
  • 当呼叫非常接近时(例如 10:45 和 10:15 之后的 10:48),那么后面的呼叫可能会被多次包含,这些重复的呼叫 (10:48) 通过选择每次对话的顺序中最早的呼叫。
  • 最后,在主查询中,“bought_flag”列是针对每个买家的每次对话求和的。

【讨论】:

    【解决方案2】:

    最大的问题是您需要每 45 分钟对结果进行分组,这很棘手。这个查询是一个很好的起点,但它并不完全正确。它应该可以帮助您开始:

    SELECT a.buyer,
           MIN(a.timestamp),
           COUNT(a),
           COUNT(b),
           SUM(a.bougth_flag),
           SUM(b.bougth_flag)
    FROM calls a
    LEFT JOIN calls b ON (a.buyer = b.buyer
                          AND a.timestamp != b.timestamp
                          AND a.timestamp < b.timestamp
                          AND a.timestamp + '45 minutes'::INTERVAL > b.timestamp)
    GROUP BY a.buyer,
             DATE_TRUNC('hour', a.timestamp) ;
    

    结果:

    ┌───────┬─────────────────────┬───────┬───────┬─────┬─────┐
    │ buyer │         min         │ count │ count │ sum │ sum │
    ├───────┼─────────────────────┼───────┼───────┼─────┼─────┤
    │ tom   │ 2015-02-01 11:52:00 │     1 │     0 │   0 │   Ø │
    │ anna  │ 2015-02-01 11:50:00 │     2 │     1 │   0 │   0 │
    │ anna  │ 2015-02-01 09:25:00 │     1 │     0 │   0 │   Ø │
    │ tom   │ 2015-02-01 09:15:00 │     1 │     0 │   1 │   Ø │
    │ tom   │ 2015-02-01 10:15:00 │     4 │     3 │   2 │   3 │
    └───────┴─────────────────────┴───────┴───────┴─────┴─────┘
    

    【讨论】:

    • 您好,沃尔夫,谢谢您的回答。这确实是一个不错的起点。我认为挑战来自 FuzzyTree 提到的要求:如果呼叫 #1 在下午 1:00 进入,在下午 1:45 呼叫 #2 并在下午 1:46 呼叫 #3,则应合并三个呼叫
    【解决方案3】:

    感谢 Patrick 关于原始版本的通知。 您在这里肯定需要 WINDOW 函数,但 CTE 在这里是可选的。

    with start_points as(
      select tmp.*,
      --calculate distance between start points
      (lead(ts) OVER w)-ts AS start_point_lead from( select t.*, ts - (lag(ts) OVER w) AS lag from test t window w as (PARTITION BY buyer ORDER BY ts)
      ) tmp where lag is null or lag>interval '45 minutes' 
            window w as (PARTITION BY buyer ORDER BY ts) order by ts
     )
     select s.buyer, s.ts, count(*), sum(t.bougth_flag) from start_points s join test t 
     on t.buyer=s.buyer and (t.ts-s.ts<s.start_point_lead or s.start_point_lead is null)and t.ts>=s.ts
    group by s.buyer, s.ts order by s.ts
    

    【讨论】:

    • " 但不是在 cmets 中给出的附加条件" 你的意思是评论:“如果呼叫 #1 在下午 1:00 进来怎么办,.....?”
    • 是的,这将是 cmets 中唯一的附加条件。
    • 感谢您对初始版本的看法是正确的。我编辑了它。如您所见,可以结合滞后/超前功能,不要使用 WITH RECURSIVE。 PS 我在提交我的第一个答案和时间戳之前就看到了你的答案-> ts 是最后一分钟的更改。
    猜你喜欢
    • 1970-01-01
    • 2011-08-16
    • 2020-07-11
    • 1970-01-01
    • 2019-11-15
    • 1970-01-01
    • 1970-01-01
    • 2011-05-19
    • 1970-01-01
    相关资源
    最近更新 更多