web-dev-qa-db-ja.com

時系列クエリPostgreSQL 9.5に累積合計を追加する

一定の日付範囲と時間間隔で時系列を提供するクエリを作成し、各時間間隔の収益を表示します。

SELECT
    interval_date,
    coalesce(campaign_revenue,0) AS campaign_revenue,
FROM
    -- generate_series helps fill the empty gaps in the following JOIN
    generate_series(
        $2::timestamp,
        $3::timestamp,
        $4) AS interval_date -- could be '1 day', '1 hour' or '1 minute'.
LEFT OUTER JOIN
    -- This SELECT gets all timeseries rows that have data
    (SELECT
        date_trunc($4, s.created) AS interval,
        SUM(s.revenue) campaign_revenue
    FROM
        sale_event AS s
    WHERE
        s.campaignid = $1 AND s.created BETWEEN $2 AND $3 AND s.event_type = 'session_closed'
    GROUP BY
        interval) results
ON
    (results.interval = interval_date);

クエリは、sale_eventテーブルのすべての行を取得し、作成された日付をある間隔に切り捨て(createdタイムスタンプを時系列の望ましい粒度に合わせます)、この時間間隔でグループ化し、event_typesession_closedである行のrevenue列を合計します。

これは非常にうまく機能し、指定された間隔で収入を得ることができます。結果は次のようになります。

interval_date   |   campaign_revenue
------------------------------------
 2018-08-05     |   0.0
 2018-08-06     |   1.5
 2018-08-07     |   0.0
 2018-08-08     |   0.5
 2018-08-09     |   1.0

提供される範囲が2018-08-05 - 2018-08-09およびinterval = '1 day'の場合。

その日までの収益の合計を結果に追加したいと思います。したがって、2018-08-05の前に10.0の総収益がある場合、結果は次のようになります。

interval_date   |   campaign_revenue   |   total_campaign_revenue
-----------------------------------------------------------------
 2018-08-05     |   0.0                |   10.0
 2018-08-06     |   1.5                |   11.5
 2018-08-07     |   0.0                |   11.5
 2018-08-08     |   0.5                |   12.0
 2018-08-09     |   1.0                |   13.0
2
AlexGordon

私がそれを正しく理解していれば、クエリの外にウィンドウ関数を追加するだけです:

SELECT interval_date, campaign_revenue
     , SUM(campaign_revenue) OVER (ORDER BY interval_date) 
      + (SELECT SUM(revenue) 
         FROM sale_event
         WHERE s.campaignid = $1
           AND s.created < $2
           AND s.event_type = 'session_closed') as total_campaign_revenue
FROM (
    SELECT interval_date
         , coalesce(campaign_revenue,0) AS campaign_revenue
    FROM
        -- generate_series helps fill the empty gaps in the following JOIN
        ...
        interval) results
    ON (results.interval = interval_date)
);

別のオプションは、ウィンドウ関数を直接適用し、campaign_revenueにFILTER句を使用することです

3
Lennart

oneスキャンでは、基になるテーブルから関連するすべての行を読み取る方が高速な場合があります。
そして、同じSELECT内の集約関数に対してウィンドウ関数を実行できます。

これを EXPLAIN (ANALYZE, TIMING OFF) でテストして、どちらが速いかを確認します。

_SELECT interval_ts
     , coalesce(revenue      , 0) AS campaign_revenue
     , coalesce(total_revenue, 0) AS total_campaign_revenue    
FROM   generate_series($2::timestamp, $3::timestamp, $4) AS interval_ts
LEFT   JOIN (
   SELECT date_trunc($4, created) AS interval_ts
        , SUM(revenue)                                              AS revenue
        , SUM(SUM(revenue)) OVER (ORDER BY date_trunc($4, created)) AS total_running
   FROM   sale_event AS s
   WHERE  campaignid = $1
   AND    created <= $3                   -- read all relevant rows in one scan
   AND    event_type = 'session_closed'
   GROUP  BY date_trunc($4, created)
   ) results USING (interval_ts);
_

JOINは、サブクエリの先頭の余剰行を自動的に除外します。

SUM(SUM(revenue)) OVER (ORDER BY date_trunc($4, created))が機能するのは、 マニュアルの引用

デフォルトのフレーミングオプションは_RANGE UNBOUNDED PRECEDING_で、これは_RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW_と同じです。 _ORDER BY_の場合、これはフレームを、パーティションの開始から現在の行の最後の_ORDER BY_ピアまでのすべての行になるように設定します。

まさにあなたが必要とするもの。

関連:

残りの弱点:収益のないインターバルの合計がありません。それが許容できない場合は、この手法を使用して修正できます。

そう:

_SELECT interval_ts, campaign_revenue, total_revenue
     , coalesce(first_value(total_revenue) OVER (PARTITION BY grp ORDER BY interval_ts), 0) AS total_campaign_revenue    
FROM  (
   SELECT interval_ts
        , coalesce(revenue, 0) AS campaign_revenue
        , total_revenue
        , count(total_revenue) OVER (ORDER BY interval_ts) AS grp
   FROM   (
      SELECT interval_ts
           , coalesce(revenue, 0) AS campaign_revenue
           , count(total_revenue) OVER (ORDER BY interval_ts) AS grp
      FROM   generate_series($2::timestamp, $3::timestamp, $4) AS interval_ts
      LEFT   JOIN (
         SELECT date_trunc($4, created) AS interval_ts
              , SUM(revenue) AS revenue
              , SUM(SUM(revenue)) OVER (ORDER BY date_trunc($4, created)) AS total_running
         FROM   sale_event AS s
         WHERE  campaignid = $1
         AND    created <= $3                   -- read all relevant rows in one scan
         AND    event_type = 'session_closed'
         GROUP  BY date_trunc($4, created)
         ) results USING (interval_ts)
      ) sub1
   ) sub2;
_

オーバーヘッドが追加されたため、競合できるかどうかはわかりません。選択範囲が小さく、テーブルが大きい場合でも、そうなる可能性があります。

マイナーノート:

  • 結合条件を括弧で囲む必要はありません。
  • timestampを「日付」と呼ばないでください。それは誤解を招くものです。 _interval_ts_の代わりに_interval_date_を使用します。
  • Postgresで許可されている場合でも、列のエイリアスとしてSQLキーワードintervalを使用したくありません。

  • 同じ列エイリアス_interval_ts_を使用して、より短いUSING構文を許可する-どのかっこが必要か。これは、結合された列_interval_ts_のoneインスタンスのみを外部クエリに公開するため、修飾されていない名前があいまいではありません。

  • 列のエイリアスのASキーワードを省略しないでください(テーブルのエイリアスの場合は問題ありません)。
3