2016-06-28 113 views
1

我正在对单个表messages执行队列分析。我需要计算创建消息的用户的保留率(day_0),还会在第二天,第二天等(day_1,day_2等)中创建消息。使用pgsql/activerecord进行队列分析

我以前在做ruby迭代中的大部分处理后查询。现在我有更大的表格来处理。它的速度太慢,内存密集的红宝石,所以我需要卸载到DB的繁重工作。我也尝试了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; 

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

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

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

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

回答

1

堆分析有很好的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_id

我认为如果您有created_atuser_id(合在一起)的索引,这应该会表现的很好。或者如果你的日子总是在同一时刻开始(比如午夜UTC),那么你可以使用一个功能指数,只有日期(不是时间戳)和user_id,然后用简单的那一天代替所有的范围条件。这将表现得更好。

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

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

+0

我可能应该指定,但'user_id'实际上并不是另一个表的关键。这只是一个唯一的字符串标识符。 – mnort9

+0

只是好奇,为什么没有外键呢? –

+0

该字段在我的数据库中实际上并不是'user_id',我只是将其用作此帖的示例。可能是我的一个不好的例子b/c它看起来像一个外键 – mnort9