web-dev-qa-db-ja.com

フィールドへの40日間の移動平均wtを計算する

コールセンターでのユーザーコールに関する情報を格納するテーブルがあります。このテーブルには、call_id、通話が行われた日付、通話の実際の日時、通話タイプ、通話に関連付けられたスコアが含まれます。

私の要件は、通話日に関するスコアの40日移動平均を計算することです。 40日は、呼び出し日の前日から開始する必要があります。過去40日間に通話がない場合は、移動平均が計算されている通話日の行を含める必要があります。

以下はサンプルデータです。

select * from test_aes;

出力:

call_id | call_dt_key | call_type_id |       call_dt_tm       | aes_raw
   1    | 2016-01-01  | CT1          | 2016-01-01 00:00:10-08 |      10
   2    | 2016-01-01  | CT1          | 2016-01-01 00:00:20-08 |      20
   3    | 2016-01-01  | CT1          | 2016-01-01 00:00:30-08 |      10
   4    | 2016-01-01  | CT1          | 2016-01-01 00:00:40-08 |      20
   5    | 2016-01-01  | CT1          | 2016-01-01 00:00:50-08 |      10
   6    | 2016-01-01  | CT1          | 2016-01-01 00:01:00-08 |      20
   7    | 2016-01-01  | CT1          | 2016-01-01 00:02:00-08 |      10
   8    | 2016-01-01  | CT1          | 2016-01-01 00:03:00-08 |      20
   9    | 2016-01-01  | CT1          | 2016-01-01 00:04:00-08 |      10
  10    | 2016-01-01  | CT1          | 2016-01-01 00:05:00-08 |      20
  11    | 2016-01-05  | CT1          | 2016-01-05 00:00:10-08 |      10
  12    | 2016-01-05  | CT1          | 2016-01-05 00:00:20-08 |      10
  13    | 2016-01-05  | CT1          | 2016-01-05 00:00:30-08 |      20
  14    | 2016-01-05  | CT1          | 2016-01-05 00:00:40-08 |      20
  15    | 2016-01-05  | CT1          | 2016-01-05 00:00:50-08 |      20
  16    | 2016-01-10  | CT1          | 2016-01-10 00:00:10-08 |      10
  17    | 2016-01-10  | CT1          | 2016-01-10 00:00:20-08 |      20
  18    | 2016-01-15  | CT1          | 2016-01-15 00:00:10-08 |      10
  19    | 2016-01-15  | CT1          | 2016-01-15 00:00:20-08 |      20
  20    | 2016-01-15  | CT1          | 2016-01-15 00:00:30-08 |      20
  21    | 2016-01-16  | CT1          | 2016-01-16 00:00:10-08 |      20
  22    | 2016-01-16  | CT1          | 2016-01-16 00:00:20-08 |      10
  23    | 2016-01-16  | CT1          | 2016-01-16 00:00:30-08 |      20
  24    | 2016-01-20  | CT1          | 2016-01-20 00:00:10-08 |      20
  25    | 2016-01-20  | CT1          | 2016-01-20 00:00:20-08 |      10
  26    | 2016-01-21  | CT1          | 2016-01-21 00:00:10-08 |      10
  27    | 2016-01-21  | CT1          | 2016-01-21 00:00:20-08 |      20
  28    | 2016-01-31  | CT1          | 2016-01-31 00:00:10-08 |      10
  29    | 2016-01-31  | CT1          | 2016-01-31 00:00:20-08 |      20
  30    | 2016-02-01  | CT1          | 2016-02-01 00:00:10-08 |      10
  31    | 2016-02-01  | CT1          | 2016-02-01 00:00:20-08 |      20
  32    | 2016-02-10  | CT1          | 2016-02-10 00:00:10-08 |      10
  33    | 2016-02-10  | CT1          | 2016-02-10 00:00:20-08 |      20
  34    | 2016-02-15  | CT1          | 2016-02-15 00:00:15-08 |      10
  35    | 2016-02-15  | CT1          | 2016-02-15 00:00:20-08 |      20
  36    | 2016-02-26  | CT1          | 2016-02-26 00:00:15-08 |      10
  37    | 2016-02-26  | CT1          | 2016-02-26 00:00:20-08 |      20
  38    | 2016-03-04  | CT1          | 2016-03-04 00:00:15-08 |      10
  39    | 2016-03-04  | CT1          | 2016-03-04 00:00:20-08 |      20
  40    | 2016-03-18  | CT1          | 2016-03-18 00:00:15-07 |      10
  41    | 2016-03-18  | CT1          | 2016-03-18 00:00:20-07 |      20

したがって、出力は次のようになります。

 call_dt_key | average_40 
 2016-01-01  | 15.0000 (include rows for 2016-01-01)  
 2016-01-05  | 15.0000 (don't include rows for 2016-01-05)  
 2016-01-10  | 15.3333 (don't include rows for 2016-01-10)  
 2016-01-15  | 15.2941 (don't include rows for 2016-01-15)  
 2016-01-16  | 15.5000 (don't include rows for 2016-01-16)  
 2016-01-20  | 15.6522 (don't include rows for 2016-01-20)  
 2016-01-21  | 15.6000 (don't include rows for 2016-01-21)  
 2016-01-31  | 15.5556 (don't include rows for 2016-01-31)  
 2016-02-01  | 15.5172 (don't include rows for 2016-02-01)  
 2016-02-10  | 15.4839 (start date 2015-12-31 end date 2016-02-09)
 2016-02-15  | 15.6522 (start date 2016-01-05 end date 2016-02-14)   
 2016-02-26  | 15.3333 (start date 2016-01-16 end date 2016-02-25)  
 2016-03-04  | 15.0000 (start date 2016-01-23 end date 2016-03-03)  
 2016-03-18  | 15.0000 (start date 2016-02-06 end date 2016-03-17)  

以下のリンクのスキーマとテストデータ: SQL Fiddle

test_aesには1日に何千もの行があるため、ROWSウィンドウ定義でAVGを使用できません。

6
lpremani

call_type_id列の役割は何であるかという質問からは本当に明確ではありません。あなたが明確にするまで私はそれを無視します。

ウィンドウ関数なし

以下は、ウィンドウ関数をまったく使用しない単純なバリアントです。

(call_dt_key, aes_raw)にインデックスがあることを確認してください。

CTE_Datesは、テーブル内のすべての日付のリストを返し、各日の平均を計算します。このaverage_current_dayは、初日に必要になります。サーバーは何らかの方法でインデックス全体をスキャンするため、そのような平均を計算する方が簡単です。

次に、個別の日ごとに自己結合を使用して、過去40日間の平均を計算します。これにより、最初の日はNULLが返され、メインクエリではaverage_current_dayに置き換えられます。

ここでCTEを使用する必要はありません。クエリを読みやすくするだけです。

WITH
CTE_Dates
AS
(
    SELECT
        call_dt_key
        ,call_dt_key - INTERVAL '41 day' AS dt_from
        ,call_dt_key - INTERVAL '1 day' AS dt_to
        ,AVG(test_aes.aes_raw) AS average_current_day
    FROM test_aes
    GROUP BY call_dt_key
)
SELECT
    CTE_Dates.call_dt_key
    ,COALESCE(prev40.average_40, CTE_Dates.average_current_day) AS average_40
FROM
    CTE_Dates
    LEFT JOIN LATERAL
    (
        SELECT AVG(test_aes.aes_raw) AS average_40
        FROM test_aes
        WHERE
                test_aes.call_dt_key >= CTE_Dates.dt_from
            AND test_aes.call_dt_key <= CTE_Dates.dt_to
    ) AS prev40 ON true
ORDER BY call_dt_key;

結果

|                call_dt_key |         average_40 |
|----------------------------|--------------------|
|  January, 01 2016 00:00:00 |                 15 |
|  January, 05 2016 00:00:00 |                 15 |
|  January, 10 2016 00:00:00 | 15.333333333333334 |
|  January, 15 2016 00:00:00 | 15.294117647058824 |
|  January, 16 2016 00:00:00 |               15.5 |
|  January, 20 2016 00:00:00 | 15.652173913043478 |
|  January, 21 2016 00:00:00 |               15.6 |
|  January, 31 2016 00:00:00 | 15.555555555555555 |
| February, 01 2016 00:00:00 | 15.517241379310345 |
| February, 10 2016 00:00:00 | 15.483870967741936 |
| February, 15 2016 00:00:00 | 15.652173913043478 |
| February, 26 2016 00:00:00 | 15.333333333333334 |
|    March, 04 2016 00:00:00 |                 15 |
|    March, 18 2016 00:00:00 |                 15 |

これが SQL Fiddle です。

推奨インデックスを使用すると、このソリューションは悪くなりすぎないはずです。


同様の質問がありますが、SQL Serverの場合( ウィンドウ関数を使用した日付範囲ローリング合計 )。 Postgresは指定されたサイズのウィンドウで RANGE をサポートしているようですが、現時点ではSQL Serverはサポートしていません。したがって、Postgresのソリューションは少し単純になる可能性があります。

重要な部分は次のとおりです。

AVG(...) OVER (ORDER BY call_dt_key RANGE BETWEEN 41 PRECEDING AND 1 PRECEDING)

これらのウィンドウ関数を使用して移動平均を計算するには、最初に日付のギャップを埋める必要があります。これにより、表には1日あたり少なくとも1つの行が含まれます(aes_rawNULL値がこれらのダミー行)。

...

Erwin Brandstetteranswer で正しく指摘しているように、現時点(Postgres 9.5以降)では、PostgresのRANGE句にはSQL Serverと同様の制限があります。 ドキュメントによると

valuePRECEDINGおよびvalueFOLLOWINGのケースは現在、ROWSモードでのみ許可されています。

したがって、上記のRANGEを使用したこのメソッドは、Postgres 9.5を使用していても機能しません。


ウィンドウ関数の使用

上記のSQL Serverの質問で概説されているアプローチを使用できます。たとえば、データを日次合計にグループ化し、欠落している日の行を追加し、SUMCOUNTを使用して移動OVERROWSを計算してから、移動を計算します平均。

これらの線に沿った何か:

WITH
CTE_Dates
AS
(
    SELECT
        call_dt_key
        ,SUM(test_aes.aes_raw) AS sum_daily
        ,COUNT(*) AS cnt_daily
        ,AVG(test_aes.aes_raw) AS avg_daily
        ,LEAD(call_dt_key) OVER(ORDER BY call_dt_key) - INTERVAL '1 day' AS next_date
    FROM test_aes
    GROUP BY call_dt_key
)
,CTE_AllDates
AS
(
    SELECT
        CASE WHEN call_dt_key = dt THEN call_dt_key ELSE NULL END AS final_dt
        ,avg_daily
        ,SUM(CASE WHEN call_dt_key = dt THEN sum_daily ELSE NULL END) 
            OVER (ORDER BY dt ROWS BETWEEN 41 PRECEDING AND 1 PRECEDING)
        /SUM(CASE WHEN call_dt_key = dt THEN cnt_daily ELSE NULL END) 
            OVER (ORDER BY dt ROWS BETWEEN 41 PRECEDING AND 1 PRECEDING) AS avg_40
    FROM
        CTE_Dates
        INNER JOIN LATERAL
            generate_series(call_dt_key, COALESCE(next_date, call_dt_key), '1 day') 
            AS all_dates(dt) ON true
)
SELECT
    final_dt
    ,COALESCE(avg_40, avg_daily) AS final_avg
FROM CTE_AllDates
WHERE final_dt IS NOT NULL
ORDER BY final_dt;

結果は最初のバリアントと同じです。 SQL Fiddle を参照してください。

繰り返しますが、これはCTEなしのインラインサブクエリで記述できます。

さまざまなバリアントのパフォーマンスを実際のデータで確認する価値があります。

7

大きな賞金は現在受け入れられている答えを模範的に見せますが、私はいくつかの詳細に完全に満足しているわけではありません。したがって、私はこの答えを追加しました。

テーブル定義

これを簡単にするために、実際のテーブル定義を提供しておく必要があります。

サンプルデータから判断すると、call_dt_tmはタイプtimestamp with time zonetimestamptz)です。一致する日付はタイムゾーンに依存するため、列call_dt_keyは機能的にcompletelyに依存しません。しかし、それを定義する場合(オフセットだけではなく、DSTに注意してください!)、日付はtimestamptzから簡単かつ確実に導出でき、は保存されません冗長。これを正しく行うには、次のような式を使用します。

(call_dt_tm AT TIME ZONE 'Asia/Hong_Kong')::date  -- use your time zone name

詳細:

MATERIALIZED VIEW を追加して、使いやすいように派生日付列を付けることができます...

この質問のために、私はあなたの与えられた表に固執します。

40日

質問と回答の両方のカウント41日要件ごとの40ではなく。下限と上限が含まれているため、(かなり一般的な)1つずれたエラーが発生します。

その結果、下の2行で異なる結果が得られます。

dateintervaltimestamp

intervalからdateを減算すると、timestampが生成されます(call_dt_key - INTERVAL '41 day'のように)。このクエリの目的では、integerを減算して別のdatecall_dt_key - 41など)を生成する方が効率的です。

RANGE句では不可能

@ Vladimir Postgres 9.5のウィンドウ関数のフレーム定義にRANGE句を使用した解決策が提案されました(現在は修正されています)。

実際、この点で Postgres 9.49.5 の間で何も変更されておらず、マニュアルのテキストも変更されていません。ウィンドウ関数のフレーム定義ではRANGE UNBOUNDED PRECEDINGおよびRANGE UNBOUNDED FOLLOWINGのみが許可されます-valuesは使用できません。

回答

もちろん、CTEを使用して、毎日の合計/カウント/平均を即座に計算できます。しかし、あなたのテーブル...

ユーザーの通話に関する情報をコールセンターに保存します

この種の情報は後で変更されません。したがって、マテリアライズドビューで毎日の集計を1回計算し、それを基に構築します。

CREATE MATERIALIZED VIEW mv_test_aes AS
SELECT call_dt_key       AS day
     , sum(aes_raw)::int AS day_sum
     , count(*)::int     AS day_ct
FROM   test_aes
WHERE  call_dt_key < (now() AT TIME ZONE 'Asia/Hong_Kong')::date  -- see above
GROUP  BY call_dt_key
ORDER  BY call_dt_key;

現在の日付は常にありませんが、それは機能です。日が終わる前に結果は正しくないでしょう。

クエリを実行する前に、MVを1日1回1回更新する必要があります。または、最新の日がありません。

とにかくテーブル全体が読み込まれるため、基になるテーブルのインデックスは必要ありません

CREATE INDEX test_aes_day_val ON test_aes (call_dt_key, aes_raw);

よりスマートなマテリアライズドビューを手動で作成し、標準のMVですべてを再作成するのではなく、新しい日を追加するだけです。しかし、それは質問の範囲を超えています...

ただし、MVのインデックスを強くお勧めします。

CREATE INDEX foo ON mv_test_aes (day, day_sum, day_ct);

インデックスのみのスキャンを期待してday_sumday_ctのみを追加しました。クエリにそれらが表示されない場合は、インデックスに列は必要ありません。

SELECT t.day
     , round(COALESCE(sum(t1.day_sum) * 1.0 / sum(t1.day_ct)  -- * 1.0 to avoid int division
                         , t.day_sum  * 1.0 /      t.day_ct), 4) AS avg_40days
FROM   mv_test_aes t
LEFT   JOIN mv_test_aes t1 ON t1.day <  t.day
                          AND t1.day >= t.day - 40  -- not 41
GROUP  BY t.day, t.day_sum, t.day_ct
ORDER  BY t.day;

結果:

日| avg_40days 
 ----------- + ------------ 
 2016-01-01 | 15.0000 
 2016-01-05 | 15.0000 
 2016-01-10 | 15.3333 
 2016-01-15 | 15.2941 
 2016-01-16 | 15.5000 
 2016-01-20 | 15.6522 
 2016-01-21 | 15.6000 
 2016-01-31 | 15.5556 
 2016-02-01 | 15.5172 
 2016-02-10 | 15.4839 
2016-02-15 | 15.5556-正しい結果
 2016-02-26 | 15.0000
 2016-03-04 | 15.0000 
 2016-03-18 | 15.0000

SQLフィドル。

これを頻繁に実行する場合、繰り返し計算を回避するために、シバン全体をMVにラップします。

ウィンドウ関数とフレーム句ROWS BETWEEN ....を使用したソリューションも可能です。しかし、例のデータは、範囲内のほとんどの日の値がない(島よりもギャップが多い)ことを示しているため、これより速くなることはありません。関連:

3