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_at 和user_id(一起)上有索引,我认为这应该会很好。或者,如果您的日子总是从同一时刻开始(比如 UTC 午夜),那么您可以使用仅包含日期(不是时间戳)和 user_id 的功能索引,然后将所有范围条件替换为当天。这样会更好。
哦,还有:您的查询(和我的)总是只返回一行,这似乎很可疑。我想知道这是否真的是您想要的,或者这是否只是为您的问题简化事情的意外。如果您希望每个起始日有一行,那么您可以将您的 day 列放回去,按它分组,删除我的 WHERE 条件,并根据之前的 m 表而不是 t.s 进行所有连接。