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)
| 引数 | 型 | 説明 |
|---|---|---|
start | bigint / timestamptz | 開始値(この値を含む) |
stop | bigint / timestamptz | 終了値(この値を含む) |
step | bigint / 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 でほぼ解決します。
コメント