特定の月の各行に基づいて、特定のclient_idの後続の12か月の売上の合計を計算する必要があります。
これは、クライアントごとの月ごとの総売上高の初期テーブルです(特定のクライアント511656A75
でここでフィルタリングされています):
CREATE TEMP TABLE foo AS
SELECT idclient, month_transac, sales
FROM ( VALUES
( '511656A75', '2010-06-01', 68.57 ),
( '511656A75', '2010-07-01', 88.63 ),
( '511656A75', '2010-08-01', 94.91 ),
( '511656A75', '2010-09-01', 70.66 ),
( '511656A75', '2010-10-01', 28.84 ),
( '511656A75', '2015-10-01', 85.00 ),
( '511656A75', '2015-12-01', 114.42 ),
( '511656A75', '2016-01-01', 137.08 ),
( '511656A75', '2016-03-01', 172.92 ),
( '511656A75', '2016-04-01', 125.00 ),
( '511656A75', '2016-05-01', 127.08 ),
( '511656A75', '2016-06-01', 104.17 ),
( '511656A75', '2016-07-01', 98.22 ),
( '511656A75', '2016-08-01', 37.08 ),
( '511656A75', '2016-10-01', 108.33 ),
( '511656A75', '2016-11-01', 104.17 ),
( '511656A75', '2017-01-01', 201.67 )
) AS t(idclient, month_transac, sales);
一部の月には売上がない(行がない)ため、WINDOW
関数を使用できないことに注意してください(たとえば、前の12行)。
同様の問題に対してこの素晴らしい答えを使用します( ローリング合計/カウント/日付間隔の平均 )私はこのクエリを実行しました:
SELECT t1.idclient
, t1.month_transac
, t1.sales
, SUM(t2.sales) as sales_ttm
FROM temp_sales_sample_month_aggr t1
LEFT JOIN temp_sales_sample_month_aggr t2 USING (idclient)
WHERE
t1.idclient = '511656A75' -- for example only
AND t2.month_transac >= (t1.month_transac - interval '12 months')
AND t2.month_transac < t1.month_transac
GROUP BY 1, 2, 3
ORDER BY 2
;
結果はOKです:sales_ttm
は、行の月の売上を除いた後の12か月の売上の合計です(つまり、最後の行Jan 2017はすべての2016年の売上を合計します)。
idclient | month_transac | sales | sales_ttm
-----------+---------------+--------+---------
511656A75 | 2010-07-01 | 88.63 | 68.57
511656A75 | 2010-08-01 | 94.91 | 157.20
[...]
511656A75 | 2015-12-01 | 114.42 | 824.83
511656A75 | 2016-01-01 | 137.08 | 892.17
511656A75 | 2016-03-01 | 172.92 | 752.75
511656A75 | 2016-04-01 | 125.00 | 925.67
511656A75 | 2016-05-01 | 127.08 | 1028.17
511656A75 | 2016-06-01 | 104.17 | 1155.25
511656A75 | 2016-07-01 | 98.22 | 1073.59
511656A75 | 2016-08-01 | 37.08 | 1171.81
511656A75 | 2016-10-01 | 108.33 | 1000.97
511656A75 | 2016-11-01 | 104.17 | 1024.30
511656A75 | 2017-01-01 | 201.67 | 1014.05
問題は、最初の月(ここでは2010年6月-最初の表の最初の行の値を参照)が結果セットに含まれていないことです。これは、過去の売り上げがないため、LEFT JOINに行がないためです。
予想/希望:
idclient | month_transac | sales | sales_ttm
-----------+---------------+--------+---------
511656A75 | 2010-06-01 | 68.57 | 0.00
511656A75 | 2010-07-01 | 88.63 | 68.57
511656A75 | 2010-08-01 | 94.91 | 157.20
511656A75 | 2010-09-01 | 70.66 | 252.11
[...]
行の売上高を(t2.month_transac <= t1.month_transac
を付けて加算してから減算する)こともできますが、よりエレガントな方法を見つけることができると思います。
私はLATERAL
結合も使用しようとしました(Erwinが彼のanwserで提案したように(「範囲条件で自己結合を実行する方がPostgres 9.1にはLATERAL結合がないため、なおさら効率的です。 ")ですが、エラーが発生しただけなので、その動作を把握できていません。
WINDOW
関数を除外する必要があることを確認しますか?t1
からすべての行を取得する方法はありますか?LATERAL
は役に立ちますか?PostgreSQL 9.6.2、Windows 10またはUbuntu 16.04を使用
したがって、これまでに3つの可能な解決策があります。結果のテーブルが同一であるかどうかを確認したところ、どちらがより良いパフォーマンスかを確認してみましょう(同じです)。すべてのクライアントの1%のサンプルからの結果のテーブルであることを認識し、270k行のテーブルでテストを実行
LEFT JOIN
およびGROUP BY
これは、質問の推奨クエリの修正バージョンです。つまり、現在の月を合計に含め、すべての行を表示するために合計から月の値を差し引きます。
SELECT t1.idclient
, t1.month_transac
, t1.sales
, SUM(t2.sales) - t1.sales as sales_ttm
FROM temp_sales_sample_month_aggr t1
LEFT JOIN temp_sales_sample_month_aggr t2 USING (idclient)
WHERE
t2.month_transac >= (t1.month_transac - interval '12 months') AND
t2.month_transac <= t1.month_transac
GROUP BY 1, 2, 3
ORDER BY 2
;
クエリのパフォーマンス:
Planning time: 3.615 ms
Execution time: 1315.636 ms
SELECT
t1.idclient
, t1.month_transac
, t1.sales
, (SELECT
coalesce(SUM(t2.sales), 0)
FROM
temp_sales_sample_month_aggr t2
WHERE
t2.idclient = t1.idclient
AND t2.month_transac >= (t1.month_transac - interval '12 months')
AND t2.month_transac < t1.month_transac
) AS sales_ttm
FROM
temp_sales_sample_month_aggr t1
GROUP BY
t1.idclient, t1.month_transac, t1.sales
ORDER BY
t1.month_transac ;
クエリのパフォーマンス:
Planning time: 0.350 ms
Execution time: 3163.354 ms
サブクエリで処理する行がもっとあると思います
LEFT JOIN LATERAL
アプローチようやくうまく動作しました。
SELECT t1.idclient
, t1.month_transac
, t1.sales
, COALESCE(lat.sales_ttm, 0.0)
FROM temp_sales_sample_month_aggr t1
LEFT JOIN LATERAL (
SELECT SUM(t2.sales) as sales_ttm
FROM temp_sales_sample_month_aggr t2
WHERE
t1.idclient = t2.idclient AND
t2.month_transac >= (t1.month_transac - interval '12 months') AND
t2.month_transac < t1.month_transac
) lat ON TRUE
ORDER BY 2
;
クエリのパフォーマンス:
Planning time: 0.468 ms
Execution time: 2773.754 ms
したがって、単純なLEFT JOIN
と比較して、LATERALはここでは役に立たないと思います
このようなものが動作するはずです。
-- IN A CTE
-- Grab the idclient, and the monthly range needed
-- We need the range because you can't sum over NULL (yet, afaik).
WITH idclient_month AS (
SELECT idclient, month_transac
FROM (
SELECT idclient, min(month_transac), max(month_transac)
FROM foo
GROUP BY idclient
) AS t
CROSS JOIN LATERAL generate_series(min::date, max::date, '1 month')
AS gs(month_transac)
)
-- If we move this where clause down the rows get filtered /before/ the window function
SELECT *
FROM (
SELECT
idclient,
month_transac,
monthly_sales,
sum(monthly_sales) OVER (
PARTITION BY idclient
ORDER BY month_transac
ROWS 12 PRECEDING
)
- monthly_sales
AS sales_ttm
-- Here, we sum up the sales by idclient, and month
-- We coalesce to 0 so we can use this in a window function
FROM (
SELECT idclient, month_transac, coalesce(sum(sales), 0) AS monthly_sales
FROM foo
RIGHT OUTER JOIN idclient_month
USING (idclient,month_transac)
GROUP BY idclient, month_transac
ORDER BY idclient, month_transac
) AS t
) AS g
WHERE g.monthly_sales > 0;
ここで
CTEでidclientの日付範囲を計算します。
SELECT idclient, month_transac
FROM (
SELECT idclient, min(month_transac), max(month_transac)
FROM foo
GROUP BY idclient
) AS t
CROSS JOIN LATERAL generate_series(min::date, max::date, '1 month')
AS gs(month_transac)
idclient | month_transac
-----------+------------------------
511656A75 | 2010-06-01 00:00:00-05
511656A75 | 2010-07-01 00:00:00-05
511656A75 | 2010-08-01 00:00:00-05
511656A75 | 2010-09-01 00:00:00-05
511656A75 | 2010-10-01 00:00:00-05
511656A75 | 2010-11-01 00:00:00-05
511656A75 | 2010-12-01 00:00:00-06
511656A75 | 2011-01-01 00:00:00-06
[....]
RIGHT OUTER
そのCTEをサンプルデータセットに変換します。 growサンプルデータセットを作成し、必要に応じて、monthly_sales = 0のエントリを作成します。
ROWS 12 PRECEDING
を超えるウィンドウを使用するウィンドウ関数を使用します。それが鍵です。それは過去12か月です。ウィンドウ関数はnullの行を操作できないため、この手順に進む前にそれらを0に設定します。
monthly_sales > 0
の行のみを選択します。計算に利用できるもの(ウィンドウ)とあまり関係がないように、ウィンドウ関数の後にこれを行う必要があります。
出力、
idclient | month_transac | monthly_sales | sales_ttm
-----------+------------------------+---------------+-----------
511656A75 | 2010-06-01 00:00:00-05 | 68.57 | 0.00
511656A75 | 2010-07-01 00:00:00-05 | 88.63 | 68.57
511656A75 | 2010-08-01 00:00:00-05 | 94.91 | 157.20
511656A75 | 2010-09-01 00:00:00-05 | 70.66 | 252.11
511656A75 | 2010-10-01 00:00:00-05 | 28.84 | 322.77
511656A75 | 2015-10-01 00:00:00-05 | 85.00 | 0.00
511656A75 | 2015-12-01 00:00:00-06 | 114.42 | 85.00
511656A75 | 2016-01-01 00:00:00-06 | 137.08 | 199.42
511656A75 | 2016-03-01 00:00:00-06 | 172.92 | 336.50
511656A75 | 2016-04-01 00:00:00-05 | 125.00 | 509.42
511656A75 | 2016-05-01 00:00:00-05 | 127.08 | 634.42
511656A75 | 2016-06-01 00:00:00-05 | 104.17 | 761.50
511656A75 | 2016-07-01 00:00:00-05 | 98.22 | 865.67
511656A75 | 2016-08-01 00:00:00-05 | 37.08 | 963.89
511656A75 | 2016-10-01 00:00:00-05 | 108.33 | 1000.97
511656A75 | 2016-11-01 00:00:00-05 | 104.17 | 1024.30
511656A75 | 2017-01-01 00:00:00-06 | 201.67 | 1014.05
(17 rows)
おそらくWINDOW
関数ほど最適ではないが、PostgreSQLのより多くのバージョンで(そして MySQL のような他のDBでも、わずかな変更で)機能する別の代替案があります。 :
SELECT
t1.idclient
, t1.month_transac::date /* to_char(t1.month_transac::date, 'YYYY-MM-DD') */
, t1.sales
, (SELECT
coalesce(SUM(t2.sales), 0)
FROM
temp_sales_sample_month_aggr t2
WHERE
t2.idclient = t1.idclient
AND t2.month_transac >= (t1.month_transac - interval '12 months')
AND t2.month_transac < t1.month_transac
) AS sales_ttm
FROM
temp_sales_sample_month_aggr t1
GROUP BY
t1.idclient, t1.month_transac, t1.sales
ORDER BY
t1.month_transac ;
これはあなたが得るものです:
| idclient | to_char | sales | sales_ttm |
|-----------|------------|--------|-----------|
| 511656A75 | 2010-06-01 | 68.57 | 0 |
| 511656A75 | 2010-07-01 | 88.63 | 68.57 |
| 511656A75 | 2010-08-01 | 94.91 | 157.2 |
| 511656A75 | 2010-09-01 | 70.66 | 252.11 |
| 511656A75 | 2010-10-01 | 28.84 | 322.77 |
| 511656A75 | 2015-10-01 | 85 | 0 |
| 511656A75 | 2015-12-01 | 114.42 | 85 |
| 511656A75 | 2016-01-01 | 137.08 | 199.42 |
| 511656A75 | 2016-03-01 | 172.92 | 336.5 |
| 511656A75 | 2016-04-01 | 125 | 509.42 |
| 511656A75 | 2016-05-01 | 127.08 | 634.42 |
| 511656A75 | 2016-06-01 | 104.17 | 761.5 |
| 511656A75 | 2016-07-01 | 98.22 | 865.67 |
| 511656A75 | 2016-08-01 | 37.08 | 963.89 |
| 511656A75 | 2016-10-01 | 108.33 | 1000.97 |
| 511656A75 | 2016-11-01 | 104.17 | 1024.3 |
| 511656A75 | 2017-01-01 | 201.67 | 1014.05 |
(私はサンプル入力データを使用しました...これは、後の例と一致していないようです。)
SQLFiddle で確認できます。