PostgreSQL の generate_series 完全ガイド ― 基本から実務応用まで

目次

1. generate_series とは何か

generate_series は PostgreSQL に組み込まれた 集合返却関数(Set-Returning Function / SRF) です。
指定した範囲の値を 行として 返してくれます。

たとえば「1〜5 の整数を行で欲しい」と思ったとき、こう書けます。

SELECT * FROM generate_series(1, 5);

結果:

generate_series
1
2
3
4
5

たったこれだけで連番の行が得られます。

どんなときに使うのか

SQL は本来「存在するデータを検索する」言語です。
しかし実務では 「欠損しているデータ」「まだ起きていないイベント」 も軸として扱いたい場面が頻繁にあります。

例:先月の日別売上レポートを作りたいが、売上がなかった日はテーブルに行がないため集計結果に現れない。

こういうとき generate_series でカレンダー軸を作り、売上データを LEFT JOIN すると 「売上ゼロの日も含めた完全なレポート」 が作れます。これが最大の使いどころです。


2. 基本構文とシグネチャ

PostgreSQL 公式が定義するシグネチャは以下の 3 種類です。

-- ① 整数(bigint)
generate_series(start, stop)
generate_series(start, stop, step)

-- ② タイムスタンプ(WITH / WITHOUT TIME ZONE)
generate_series(start, stop, step_interval)
引数説明
startbigint / timestamptz開始値(この値を含む)
stopbigint / timestamptz終了値(この値を含む)
stepbigint / intervalステップ幅。省略時は 1

ポイント: stop含まれます(inclusive)。
generate_series(1, 5) は 1, 2, 3, 4, 5 まで生成します。


3. 整数シリーズ ― 連番を自在に作る

3.1 基本の連番

-- 1 から 10
SELECT generate_series(1, 10);
-- → 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

-- 0 から始める
SELECT generate_series(0, 9);
-- → 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

3.2 ステップを変える

-- 偶数だけ(2 刻み)
SELECT generate_series(0, 20, 2);
-- → 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20

-- 5刻み
SELECT generate_series(0, 100, 5);

-- 逆順はステップをマイナスにする
SELECT generate_series(10, 1, -1);
-- → 10, 9, 8, 7, 6, 5, 4, 3, 2, 1

注意: 逆順の場合は step を必ず負の値にしてください。
generate_series(10, 1) のようにステップ省略で逆順を期待しても、行が 1 件も返りません

3.3 列に別名をつける

generate_series をそのまま使うとカラム名が generate_series になります。
AS で分かりやすい名前をつけましょう。

-- シンプルな別名
SELECT generate_series(1, 5) AS id;

-- FROM 句に書く方法(推奨)
SELECT n FROM generate_series(1, 5) AS t(n);

FROM 句に書く方が意図が明確で、他のテーブルと組み合わせるときも安全です。


4. タイムスタンプシリーズ ― 日付・時間軸を作る

4.1 日付を 1 日刻みで生成

SELECT generate_series(
    '2024-01-01'::date,
    '2024-01-31'::date,
    '1 day'::interval
)::date AS day;

末尾の ::date で日付型にキャストしています。省略すると timestamp 型のまま返ります。

4.2 さまざまな interval

-- 1時間刻み
SELECT generate_series(
    '2024-01-01 00:00'::timestamp,
    '2024-01-01 23:00'::timestamp,
    '1 hour'::interval
) AS hour_slot;

-- 15分刻み
SELECT generate_series(
    '2024-01-01 09:00'::timestamp,
    '2024-01-01 18:00'::timestamp,
    '15 minutes'::interval
) AS slot;

-- 1ヶ月刻み(月末を正しく扱える)
SELECT generate_series(
    '2024-01-01'::date,
    '2024-12-01'::date,
    '1 month'::interval
)::date AS month_start;

interval には '1 day''1 hour''15 minutes''1 month' など日本語感覚に近い文字列が使えます。


5. 他のテーブルと JOIN して使う

generate_series は FROM 句に置けるため、通常のテーブルと同じように JOIN できます。
これが実務上の最大の武器です。

-- カレンダー軸を生成して LEFT JOIN
SELECT
    d.day::date,
    COALESCE(s.amount, 0) AS sales
FROM generate_series('2024-01-01', '2024-01-31', '1 day'::interval) AS d(day)
LEFT JOIN daily_sales s ON s.sale_date = d.day::date
ORDER BY d.day;

このクエリは「1/1〜1/31 の全日付」を軸として持ち、売上データが存在する日は金額を、存在しない日は 0 を返します。

COALESCE(値, 0) は「NULL なら 0 を返す」関数です。LEFT JOIN で一致しなかった行は NULL になるため、これで 0 埋めできます。


6. 実務応用例 3 選

基本を押さえたところで、実際の業務でよく出てくるパターンを 3 つ紹介します。


6.1 月次レポートの “穴埋め”

課題: 売上がない月が集計から消えてしまう。

売上テーブルに行が存在しない月は GROUP BY しても結果に現れません。
generate_series で月の軸を先に作り、LEFT JOIN することで解決します。

-- orders テーブル: order_date, amount
WITH months AS (
    SELECT generate_series(
        date_trunc('month', '2024-01-01'::date),
        date_trunc('month', '2024-12-01'::date),
        '1 month'::interval
    )::date AS month_start
),
monthly_sales AS (
    SELECT
        date_trunc('month', order_date)::date AS month_start,
        SUM(amount) AS total
    FROM orders
    WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31'
    GROUP BY 1
)
SELECT
    m.month_start,
    to_char(m.month_start, 'YYYY年MM月') AS month_label,
    COALESCE(ms.total, 0) AS total_sales
FROM months m
LEFT JOIN monthly_sales ms USING (month_start)
ORDER BY m.month_start;

ポイント:

  • date_trunc('month', ...) で月の初日に正規化してから JOIN のキーとして使う
  • COALESCE(ms.total, 0) で売上のない月を 0 として表示

6.2 稼働日カレンダーの生成

課題: 土日を除いた営業日のリストが欲しい。

generate_series で全日付を生成してから WHERE で土日を除外するだけです。

SELECT day::date AS business_day
FROM generate_series(
    '2024-04-01'::date,
    '2024-04-30'::date,
    '1 day'::interval
) AS t(day)
WHERE EXTRACT(DOW FROM day) NOT IN (0, 6)  -- 0=日曜, 6=土曜
ORDER BY day;

EXTRACT(DOW FROM ...) は曜日を数値で返します(0=日〜6=土)。

祝日テーブルが別途あれば、さらに除外できます:

WHERE EXTRACT(DOW FROM day) NOT IN (0, 6)
  AND day::date NOT IN (SELECT holiday_date FROM national_holidays)

このように 「全日付を生成してからフィルタする」 発想が generate_series の典型的な使い方です。


6.3 サンプルデータの大量生成

課題: 開発・テスト用のデータを素早く大量に作りたい。

generate_series で連番を作り、それをベースにデータを組み立てます。

-- users テーブルに 10,000 件のダミーデータを挿入
INSERT INTO users (name, email, created_at)
SELECT
    'user_' || n                                                  AS name,
    'user_' || n || '@example.com'                               AS email,
    NOW() - (random() * 365)::int * '1 day'::interval            AS created_at
FROM generate_series(1, 10000) AS t(n);

random() と組み合わせることで、連番でありながらランダム感のある日付データを生成できます。

カテゴリをランダムに割り当てる例:

INSERT INTO products (name, category, price)
SELECT
    '商品_' || n,
    (ARRAY['食品', '家電', '衣類', '書籍', 'スポーツ'])[ceil(random() * 5)::int],
    (random() * 10000 + 100)::numeric(10, 2)
FROM generate_series(1, 1000) AS t(n);

本番環境を汚さずに動作確認できるデータをすぐ用意できるのは、開発効率の面で非常に助かります。


7. まとめ

generate_series は「存在するデータを検索する」という SQL の枠を超えて、軸となるデータそのものを作り出す関数です。

ユースケース使うパターン
連続する数値・日付軸を作る基本の generate_series(start, stop, step)
欠損データを 0 埋めするLEFT JOIN + COALESCE
条件でフィルタした日付リストgenerate_series + WHERE
テストデータを一括生成するINSERT INTO ... SELECT ... FROM generate_series(...)

まず試すなら 月次・日次の集計で穴埋め が最も効果を実感しやすいでしょう。
「データがない日でも 0 として出力したい」という悩みは generate_series + LEFT JOIN でほぼ解決します。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ITベンダーでSEとして働いている駆け出しエンジニア。
プログラミング未経験で入社したため、周りに追いつこうと奮闘中。

コメント

コメントする

目次