【问题标题】:Cohort analysis using pgsql/activerecord使用 pgsql/activerecord 进行队列分析
【发布时间】:2016-06-28 14:57:08
【问题描述】:

我正在对单个表 messages 执行同期群分析。我需要计算创建消息的用户的保留率(day_0),还在第二天、后天等(day_1、day_2 等)创建消息。

我以前在 ruby​​ 迭代中进行大部分处理后查询。现在我有更大的桌子要处理。 ruby 太慢且内存密集,所以我需要将繁重的工作卸载到数据库。我也尝试过cohort_me gem,但性能很差。

我对不带 activerecord 的 SQL 没有太多经验。到目前为止,这是我所拥有的:

SELECT 
date_trunc('day', messages.created_at) as day,
count(distinct messages.user_id) as day_5_users
FROM 
messages
WHERE 
messages.created_at >= date_trunc('day', now() - interval '5 days') AND 
messages.created_at < date_trunc('day', now() - interval '4 days')
GROUP BY 1
ORDER BY 1;

这将返回五天前创建消息的用户数。现在我需要找到第二天、后天等创建消息的 THOSE 用户的数量,直到当天。

我需要在不同的基准日执行相同的分析。所以接下来不是 5 天,而是在 4 天前作为基准日开始分析。

这可以通过一个查询来完成吗?

编辑: messages.user_id 实际上并不是另一个表的键。它只是一个唯一标识符(字符串),因此没有其他表要与此查询连接。

【问题讨论】:

    标签: sql ruby-on-rails postgresql activerecord


    【解决方案1】:

    Heap Analytics 有一个很好的 blog post about lateral joins 用于做一些非常相似的事情。它可能会给你一些想法。您的情况实际上比他们的情况更简单,因此您的解决方案也更容易。

    首先要注意几点。您似乎不需要day 输出,因为它总是等于您的输入。其次,无论如何,您每天都需要一个单独的输出列(或将结果累积到一个数组中,这似乎不太理想),因此如果您想要可变天数,则必须动态构建 SQL那个。

    为了测试,我做了一个表格并给了它几行:

    create table messages (user_id integer, created_at timestamp);
    insert into messages values (1, now() - interval '5 days'), (1, now() - interval '4 days'), (1, now() - interval '2 days');
    insert into messages values (2, now() - interval '10 days'), (2, now() - interval '2 days');
    insert into messages values (3, now() - interval '2 days'), (3, now() - interval '1 days');
    insert into messages values (4, now() - interval '5 days');
    

    我认为您可以使用横向连接获得一个非常干净的解决方案,有点像上面的文章:

    \set start_time '''2016-06-23 06:00:00'''
    
    WITH t(s) AS (
      SELECT  :start_time::timestamp
    )
    SELECT  COUNT(DISTINCT m1.user_id) AS day_5_messages,
            COUNT(DISTINCT m2.user_id) AS day_4_messages,
            COUNT(DISTINCT m3.user_id) AS day_3_messages,
            COUNT(DISTINCT m4.user_id) AS day_2_messages,
            COUNT(DISTINCT m5.user_id) AS day_1_messages
    FROM    messages m1
    CROSS JOIN t
    LEFT OUTER JOIN LATERAL (
        SELECT * FROM messages msub
        WHERE msub.user_id = m1.user_id
        AND msub.created_at <@
          tsrange(t.s + interval '1 day',
                  t.s + interval '2 days')
        LIMIT 1
    ) m2
    ON true
    LEFT OUTER JOIN LATERAL (
        SELECT * FROM messages msub
        WHERE msub.user_id = m2.user_id
        AND msub.created_at <@
          tsrange(t.s + interval '2 days',
                  t.s + interval '3 days')
        LIMIT 1
    ) m3
    ON true
    LEFT OUTER JOIN LATERAL (
        SELECT * FROM messages msub
        WHERE msub.user_id = m3.user_id
        AND msub.created_at <@
          tsrange(t.s + interval '3 days',
                  t.s + interval '4 days')
        LIMIT 1
    ) m4
    ON true
    LEFT OUTER JOIN LATERAL (
        SELECT * FROM messages msub
        WHERE msub.user_id = m4.user_id
        AND msub.created_at <@
          tsrange(t.s + interval '4 days',
                  t.s + interval '5 days')
        LIMIT 1
    ) m5
    ON true
    WHERE   m1.created_at <@
      tsrange(t.s,
              t.s + interval '1 day')
    ;
    

    这里我使用t(s) CTE 只是为了避免一次又一次地重复:start_time。如果你不喜欢它是可选的。当然,在 Rails 中,您会使用 ? 而不是 :start_time 来参数化查询。

    对于测试,将每个 COUNT(...) 替换为 array_agg(...) 会很有帮助,这样您就可以决定是否包含正确的 user_ids。

    如果您在created_atuser_id(一起)上有索引,我认为这应该会很好。或者,如果您的日子总是从同一时刻开始(比如 UTC 午夜),那么您可以使用仅包含日期(不是时间戳)和 user_id 的功能索引,然后将所有范围条件替换为当天。这样会更好。

    哦,还有:您的查询(和我的)总是只返回一行,这似乎很可疑。我想知道这是否真的是您想要的,或者这是否只是为您的问题简化事情的意外。如果您希望每个起始日有一行,那么您可以将您的 day 列放回去,按它分组,删除我的 WHERE 条件,并根据之前的 m 表而不是 t.s 进行所有连接。

    【讨论】:

      【解决方案2】:

      基于缺少外键,我会尝试首先将消息放入范围。请参阅此帖子:In SQL, how can you “group by” in ranges? 在时间之间使用。 Check if a time is between two times (time DataType) 然后GROUP BY messages.user_id

      【讨论】:

      • 我可能应该指定,但user_id 实际上并不是另一个表的键。它只是一个唯一的字符串标识符。
      • 只是好奇,为什么没有外键呢?
      • 我的数据库中的字段实际上不是user_id,我只是将其用作本文的示例。可能是我的一个坏例子 b/c 它看起来像一个外键
      • @shane 根据您的反馈,我用另一个想法编辑了我的帖子。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-24
      • 1970-01-01
      相关资源
      最近更新 更多