2016-06-28 4 views
1

私は単一のテーブルmessagesでコホート分析を行っています。私はメッセージを作成したユーザー(day_0)の保持率を計算し、翌日、翌日などにメッセージを作成する必要があります(day_1、day_2など)。pgsql/activerecordを使用したコホート分析

私は以前、ルビ反復で事後処理のほとんどを行っていました。今私は扱う大きなテーブルを持っています。それはルビーでは遅すぎるとメモリが集中するので、私は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; 

5日前にメッセージを作成したユーザーの数を返します。今、私は、今日まで、翌日、翌日など、メッセージを作成したユーザーの数を調べる必要があります。

これと同じ分析を異なる基準日に実行する必要があります。だから5日の代わりに、それは基本日として4日前に分析を開始します。

これは1つのクエリで実行できますか?

編集:messages.user_idは、実際には別のテーブルのキーではありません。これは単なる一意の識別子(文字列)なので、このクエリに結合する他のテーブルはありません。

答えて

1

ヒープアナリティクスはかなり類似した何かのための素晴らしいblog post about lateral joinsを持っています。それはあなたにいくつかのアイデアを与えるかもしれない。あなたの状況は実際よりも簡単ですので、あなたのソリューションも簡単です。

最初にいくつかのメモ。 day出力は、入力と常に同じであるため、出力が必要ないようです。第2に、毎日別の出力列が必要な場合(または結果が配列に蓄積されることはあまり望ましくないようです)、可変日数が必要な場合は、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を使用しています。あなたが好きではない場合はオプションです。もちろん、Railsでは:start_timeの代わりに?を使用してクエリをパラメータ化することもできます。それをテストするための

は右user_id Sが含まれているかどうので、あなたが決めることができ、各COUNT(...)array_agg(...)とを交換すると便利です。

インデックスがcreated_atuser_id(一緒にある)の場合、これはうまくいくと思います。または、あなたの日が常に同じ瞬間(UTCの深夜)で始まる場合は、日付(タイムスタンプではない)とuser_idの機能インデックスを使用して、すべての範囲条件をその日だけに置き換えることができます。それはさらに優れたパフォーマンスを発揮します。

また、あなたのクエリ(と私の)は常に1つの行を返します。これはかなり疑わしいようです。私はそれが本当にあなたが望むものか、それがあなたの質問のために物事を単純化する事故であるのだろうかと思います。開始日に1行が必要な場合は、day列をグループ化してグループ化し、WHERE条件を削除し、t.sの代わりに前のmテーブルに基づいてすべての結合を実行できます。

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

フィールドは実際に私のdbの 'user_id'ではなく、私はこのポストの例として使っています。おそらく私の悪い例ですが、外国のキーのように見えます。 – mnort9

関連する問題