2016-06-28 9 views
1

Ich führe eine Kohortenanalyse auf einer einzigen Tabelle messages. Ich muss die Aufbewahrungsraten von Benutzern berechnen, die eine Nachricht erstellt haben (Tag_0), habe am nächsten Tag, am nächsten Tag usw. eine Nachricht erstellt (Tag_1, Tag_2 usw.).Kohortenanalyse mit pgsql/activerecord

Ich habe zuvor die meisten der Verarbeitung nach Abfrage in Ruby Iterationen. Jetzt habe ich größere Tische zu bewältigen. Es ist viel zu langsam und speicherintensiv in Ruby, also muss ich das schwere Heben zur DB abladen. Ich habe auch das cohort_me Juwel versucht und schlechte Leistung erfahren.

Ich habe nicht viel Erfahrung mit SQL w/out activerecord. Hier ist, was ich bisher:

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; 

Dies gibt die Anzahl der Nutzer, die vor Nachrichten 5 Tage erstellt. Jetzt muss ich die Anzahl der Benutzer ermitteln, die am folgenden Tag, am nächsten Tag usw. Nachrichten bis zum aktuellen Tag erstellt haben.

Ich muss dieselbe Analyse an verschiedenen Basistagen durchführen. Also next statt 5 Tage, es beginnt die Analyse vor 4 Tagen als Basistag.

Kann dies mit einer Abfrage durchgeführt werden?

EDIT:messages.user_id ist eigentlich kein Schlüssel zu einer anderen Tabelle. Es ist einfach ein eindeutiger Bezeichner (String), so dass keine anderen Tabellen mit dieser Abfrage verknüpft werden müssen.

Antwort

1

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.

0

Aufgrund des Fehlens eines Fremdschlüssels würde ich versuchen, die Meldungen zuerst in Bereiche zu setzen. Siehe diesen Beitrag: In SQL, how can you “group by” in ranges? zwischen den Zeiten verwenden. Check if a time is between two times (time DataType) und dann GROUP BY messages.user_id

+0

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

+0

Nur neugierig, warum nicht einen fremden Schlüssel dazu haben? –

+0

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