コールセンターでのユーザーコールに関する情報を格納するテーブルがあります。このテーブルには、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
を使用できません。
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_raw
のNULL
値がこれらのダミー行)。
...
Erwin Brandstetter が answer で正しく指摘しているように、現時点(Postgres 9.5以降)では、PostgresのRANGE
句にはSQL Serverと同様の制限があります。 ドキュメントによると :
valuePRECEDINGおよびvalueFOLLOWINGのケースは現在、ROWSモードでのみ許可されています。
したがって、上記のRANGE
を使用したこのメソッドは、Postgres 9.5を使用していても機能しません。
上記のSQL Serverの質問で概説されているアプローチを使用できます。たとえば、データを日次合計にグループ化し、欠落している日の行を追加し、SUM
とCOUNT
を使用して移動OVER
とROWS
を計算してから、移動を計算します平均。
これらの線に沿った何か:
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なしのインラインサブクエリで記述できます。
さまざまなバリアントのパフォーマンスを実際のデータで確認する価値があります。
大きな賞金は現在受け入れられている答えを模範的に見せますが、私はいくつかの詳細に完全に満足しているわけではありません。したがって、私はこの答えを追加しました。
これを簡単にするために、実際のテーブル定義を提供しておく必要があります。
サンプルデータから判断すると、call_dt_tm
はタイプtimestamp with time zone
(timestamptz
)です。一致する日付はタイムゾーンに依存するため、列call_dt_key
は機能的にcompletelyに依存しません。しかし、それを定義する場合(オフセットだけではなく、DSTに注意してください!)、日付はtimestamptz
から簡単かつ確実に導出でき、は保存されません冗長。これを正しく行うには、次のような式を使用します。
(call_dt_tm AT TIME ZONE 'Asia/Hong_Kong')::date -- use your time zone name
詳細:
MATERIALIZED VIEW
を追加して、使いやすいように派生日付列を付けることができます...
この質問のために、私はあなたの与えられた表に固執します。
質問と回答の両方のカウント41日要件ごとの40ではなく。下限と上限が含まれているため、(かなり一般的な)1つずれたエラーが発生します。
その結果、下の2行で異なる結果が得られます。
date
、interval
、timestamp
interval
からdate
を減算すると、timestamp
が生成されます(call_dt_key - INTERVAL '41 day'
のように)。このクエリの目的では、integer
を減算して別のdate
(call_dt_key - 41
など)を生成する方が効率的です。
RANGE
句では不可能@ Vladimir Postgres 9.5のウィンドウ関数のフレーム定義にRANGE
句を使用した解決策が提案されました(現在は修正されています)。
実際、この点で Postgres 9.4 と 9.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_sum
とday_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
これを頻繁に実行する場合、繰り返し計算を回避するために、シバン全体をMVにラップします。
ウィンドウ関数とフレーム句ROWS BETWEEN ....
を使用したソリューションも可能です。しかし、例のデータは、範囲内のほとんどの日の値がない(島よりもギャップが多い)ことを示しているため、これより速くなることはありません。関連: