使用pgsql / activerecord进行队列分析

时间:2016-06-28 14:57:08

标签: sql ruby-on-rails postgresql activerecord

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

我之前在ruby迭代中执行了大部分处理后查询。现在我有更大的表来处理。它在红宝石中太慢而且内存密集,所以我需要将繁重的工作卸载到数据库中。我也尝试过cohort_me宝石并且表现不佳。

我没有太多使用SQL w / out activerecord的经验。这就是我到目前为止所拥有的:

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;

这将返回五天前创建邮件的用户数。现在我需要找到第二天,之后一天创建消息的那些用户的数量等等,直到当天。

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

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

编辑: messages.user_id实际上并不是其他表的键。它只是一个唯一标识符(字符串),因此没有其他表可以与此查询结合使用。

2 个答案:

答案 0 :(得分:1)

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

首先说几句。您似乎不需要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'); 输出,因为它总是等于您的输入。第二,无论如何,你每天都需要一个单独的输出列(或者在数组中累积结果,这似乎不太理想),所以如果你想要一个可变的天数,你将不得不动态地构建SQL这一点。

为了测试,我制作了一张桌子,并给了它几行:

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

我在这里使用:start_time CTE只是为了避免一次又一次地重复?。如果你不喜欢它是可选的。在Rails中也很自然地使用:start_time代替COUNT(...)来参数化查询。

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

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

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

答案 1 :(得分:0)

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