我在单个表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
实际上并不是其他表的键。它只是一个唯一标识符(字符串),因此没有其他表可以与此查询结合使用。
答案 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_id
和user_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