web-dev-qa-db-ja.com

現在のロー日付の過去12か月の総売上高

特定の月の各行に基づいて、特定の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関数を除外する必要があることを確認しますか?
  • 「シンプルな」LEFY JOINを使用して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

@joanoloアプローチ-サブクエリ

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はここでは役に立たないと思います

5
ant1j

このようなものが動作するはずです。

-- 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;

ここで

  1. 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
     [....]
    
  2. RIGHT OUTERそのCTEをサンプルデータセットに変換します。 growサンプルデータセットを作成し、必要に応じて、monthly_sales = 0のエントリを作成します。

  3. ROWS 12 PRECEDINGを超えるウィンドウを使用するウィンドウ関数を使用します。それが鍵です。それは過去12か月です。ウィンドウ関数はnullの行を操作できないため、この手順に進む前にそれらを0に設定します。

  4. 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)
3
Evan Carroll

おそらく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 で確認できます。

2
joanolo