Heap Analytics hat eine schöne blog post about lateral joins für etwas ziemlich ähnliches. Es könnte Ihnen einige Ideen geben. Ihre Situation ist eigentlich einfacher als ihre, also ist Ihre Lösung auch einfacher.
Zuerst ein paar Notizen. Sie scheinen den day
Ausgang nicht zu benötigen, da er immer Ihren Eingängen entspricht. Zweitens benötigen Sie für jeden Tag eine separate Ausgabespalte (oder akkumulieren Ergebnisse in einem Array, was weniger wünschenswert erscheint). Wenn Sie also eine variable Anzahl von Tagen haben möchten, müssen Sie das SQL dynamisch erstellen Das.
Zum Testen habe ich einen Tisch und gab ihm ein paar Zeilen:
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');
Ich denke, Sie eine sehr saubere Lösung erhalten können seitlich schließt sich mit, die Art wie der Artikel oben:
\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')
;
Hier verwende ich den t(s)
CTE nur um zu vermeiden, immer wieder :start_time
zu wiederholen. Es ist optional, wenn Sie es nicht mögen. Auch natürlich in Rails würden Sie ?
anstelle von :start_time
verwenden, um die Abfrage zu parametrisieren.
Zum Testen ist es hilfreich, jedes COUNT(...)
durch array_agg(...)
zu ersetzen, damit Sie entscheiden können, ob die richtigen user_id
s enthalten sind oder nicht.
Ich denke, das sollte gut funktionieren, wenn Sie einen Index auf created_at
und user_id
(zusammen) haben.Oder wenn Ihre Tage immer im selben Moment beginnen (sagen wir um Mitternacht UTC), dann könnten Sie einen Funktionsindex nur mit dem Datum (nicht Zeitstempel) und user_id
verwenden und dann alle Bereichsbedingungen ersetzen, indem Sie einfach an diesem Tag sind. Das wird noch besser funktionieren.
Oh auch: Ihre Abfrage (und meins) zurück immer nur eine Zeile, die ziemlich verdächtig scheint. Ich frage mich, ob das wirklich das ist, was du willst, oder ob das nur ein Zufall ist, Dinge für deine Frage zu vereinfachen. Wenn Sie eine Zeile pro Starttag wollten, dann könnten Sie Ihre day
Spalte zurücksetzen, gruppieren, entfernen Sie meine WHERE
Bedingung, und führen Sie alle Joins basierend auf der vorherigen m
Tabelle anstelle von t.s
.
Ich sollte wahrscheinlich von angegeben werden, aber 'user_id' ist eigentlich kein Schlüssel für eine andere Tabelle. Es ist nur eine eindeutige Zeichenfolgenkennung. – mnort9
Nur neugierig, warum nicht einen fremden Schlüssel dazu haben? –
Das Feld ist nicht wirklich "user_id" in meiner db, ich habe das nur als Beispiel für diesen Beitrag verwendet. Wahrscheinlich ein schlechtes Beispiel meinerseits b/c sieht aus wie ein Fremdschlüssel – mnort9