見出し画像

【PostgreSQL】レコードが存在しない日付を補完する。

APIサーバを拵えてると、データが存在しなかった日付に0を入れて返したいときがある。そんな時の備忘録

環境

psql (PostgreSQL) 12.0

下準備

今回は説明用に以下のテーブルとレコードを使用する。

-- 使用金額テーブル
CREATE TABLE money_record
(id INTEGER NOT NULL, -- 主キー
purchase_date TIMESTAMP, --記録日時(yyyy/MM/dd HH:mm:ss)
using_amount INTEGER NOT NULL, -- 使用金額
PRIMARY KEY (id)
);
-- サンプルレコード
INSERT INTO money_record values( 1, '2021-01-31', 100);
INSERT INTO money_record values( 2, '2021-02-02', 210);
INSERT INTO money_record values( 3, '2021-02-02',  50);
INSERT INTO money_record values( 4, '2021-02-03',  90);
INSERT INTO money_record values( 5, '2021-02-04',  10);
INSERT INTO money_record values( 6, '2021-02-04', 180);
INSERT INTO money_record values( 7, '2021-02-06',  70);
INSERT INTO money_record values( 8, '2021-02-07', 200);
INSERT INTO money_record values( 9, '2021-02-07',   5);
INSERT INTO money_record values(10, '2021-02-07',  25);
COMMIT;

一週間分のレコードを日別に集計

色々やり方はあるが、シンプルな感じでSQLを作る。

-- 2021-02-01 から 2021-02-07 の使用金額を日別に集計
SELECT
   purchase_date
   , SUM(using_amount) AS 使用金額 
FROM
   money_record 
WHERE
   purchase_date BETWEEN '2021-02-01' AND '2021-02-07' 
GROUP BY
   purchase_date
ORDER BY 
   purchase_date ASC;

実行結果はこう

画像1

しかし、これだと「2021/02/01」と「2021/02/05」のレコードが歯抜けになっている。
【レコードが存在しない = 使用金額が0】
であったことを明確に表現したい場合は、ひと手間加える必要がある。

日付マスタを作成

結論から述べると
前項の導出表と、指定範囲分の日付マスタの論理和を取れば良い。

ここでミソとなる部分が日付マスタを作成するSQLである。
作成には「generate_series関数」を使用する。

画像2

そんでもって大体こんな雰囲気のSQLが出来る。

-- 2021-02-01 から 2021-02-07 の日付マスタを作成する
SELECT
   generate_series
   , 0 as value 
FROM
   generate_series( 
       '2021-02-01' ::Date
       , '2021-02-07' ::Date
       , '1 day'
   );
   

実行結果

画像3

集合表を作成

先程の2つの導出表をUNION ALL句で結合する。

SELECT
   purchase_date AS 日付
   , SUM(using_amount) AS 使用金額 
FROM
   money_record 
WHERE
   purchase_date BETWEEN '2021-02-01' AND '2021-02-07' 
GROUP BY
   purchase_date 
UNION ALL 
SELECT
   generate_series AS 日付
   , 0 as 使用金額 
FROM
   generate_series( 
       '2021-02-01' ::Date
       , '2021-02-07' ::Date
       , '1 day'
   );

実行結果

画像4

後は日付でグループ化するだけでOK
最終的に以下のSQLになる。

SELECT
   日付
   , SUM(使用金額) AS 使用金額 
FROM
   ( 
       SELECT
           purchase_date AS 日付
           , SUM(using_amount) AS 使用金額 
       FROM
           money_record 
       WHERE
           purchase_date BETWEEN '2021-02-01' AND '2021-02-07' 
       GROUP BY
           purchase_date 
       UNION ALL 
       SELECT
           generate_series AS 日付
           , 0 as 使用金額 
       FROM
           generate_series( 
               '2021-02-01' ::Date
               , '2021-02-07' ::Date
               , '1 day'
           )
   ) tr 
GROUP BY
   tr.日付 
ORDER BY
   tr.日付 ASC;

実行結果

画像5

無事、「2021/02/01」と「2021/02/05」のレコードが表示されました!

generate_series関数ですが、Postgres特有の関数らしくOracleやMySQLだと別の手段で日付マスタを作成する必要がありそうです。

PostgreSQL 9.3.2文書 より
参考: https://www.postgresql.jp/document/9.3/html/functions-srf.html

この記事が気に入ったらサポートをしてみませんか?