「注文日時は日付と時刻まで保存されているのに、月ごとの件数を数えたい」。PostgreSQLでは`date_trunc('month', 日時)`で日時を月初へそろえ、`GROUP BY`で同じ月の行をまとめられます。この記事では、テーブルを作らず試せるサンプルSQLと、その結果の読み方を説明します。
結論:date_truncで月初にそろえてからGROUP BYする
`date_trunc`は日付時刻を指定した単位に切り捨てるPostgreSQLの関数です。`month`を指定すると、同じ年・同じ月の日時はその月の1日0時0分にそろいます。
SELECT date_trunc('month', TIMESTAMP '2026-02-23 14:35:20');
-- 想定結果: 2026-02-01 00:00:00
月別集計では、このそろえた月初値を`GROUP BY`のキーにします。`EXTRACT(MONTH FROM …)`で月の数字だけを取り出す場合と違い、`date_trunc`なら2025年1月と2026年1月を誤って同じグループにするのを避けやすくなります。
用語解説:GROUP BY
同じ値を持つ行をまとめ、COUNTやSUMなどの集計関数で件数・合計を計算するSQLの指定です。
準備なしで動かせる月別集計SQL
以下はPostgreSQLのSQL入力画面(`psql`やSQLエディター)で実行できます。`WITH orders AS (…)`はこのクエリだけで使える仮のデータを用意する構文なので、実テーブルを作成したり更新したりしません。
WITH orders (ordered_at, amount) AS (
VALUES
(TIMESTAMP '2026-01-12 10:00:00', 1200),
(TIMESTAMP '2026-01-28 18:30:00', 800),
(TIMESTAMP '2026-02-02 09:15:00', 1500)
)
SELECT
date_trunc('month', ordered_at)::date AS month_start,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY date_trunc('month', ordered_at)::date
ORDER BY month_start;
想定結果は1月が2件・合計2000、2月が1件・合計1500です。SQLを実行できる環境があれば、返った結果と照合してください。
| month_start | order_count | total_amount |
|---|---|---|
| 2026-01-01 | 2 | 2000 |
| 2026-02-01 | 1 | 1500 |
`::date`はPostgreSQLの型変換の書き方です。今回は月初の日時のうち日付だけを見やすく表示しています。集計キーにも同じ変換式を指定し、何でグループ化したかを明確にしました。
クエリを1行ずつ読む:どこで月をまとめている?
- `VALUES`で注文日時と金額の3行を用意する。
- `date_trunc`で各日時を2026-01-01または2026-02-01にそろえる。
- `GROUP BY`で同じ月初日の行をまとめる。
- `COUNT(*)`で行数を、`SUM(amount)`で金額合計を計算する。
- `ORDER BY`で古い月から表示する。
まずは合計を外して`COUNT(*)`だけにしてみると、どの行が同じグループになるか理解しやすくなります。次にサンプルの1件を3月へ変え、3か月分の結果が返るか確認してください。
(GROUP BYそのものに慣れていない場合については『GROUP BY使い方完全解説|SQLエラー・WHERE HAVINGの違いは?』をご参照ください)
月別集計がずれるときの確認点
最初に、日時列の型が`timestamp without time zone`か`timestamp with time zone`(`timestamptz`)かを確かめましょう。前者は入力した日時の数字をそのまま扱います。後者の月境界は、セッションのタイムゾーン設定に影響されます。
例えばUTCでは1月31日23時30分でも、日本時間では2月1日8時30分です。日本時間の売上集計をしたいなら、どのタイムゾーンの月を数えるかを決める必要があります。
-- paid_at が timestamptz 型の場合の考え方
SELECT
date_trunc(
'month',
TIMESTAMPTZ '2026-01-31 23:30:00+00'
AT TIME ZONE 'Asia/Tokyo'
)::date AS month_in_japan;
-- 想定結果: 2026-02-01
上の例では`AT TIME ZONE`で日本時間の時刻表現へ変えてから月初にそろえています。実テーブルの列型・保存方式に応じた変換が必要で、時刻が最初から日本時間の`timestamp`列なら二重変換を避けます。
- 1月・2月の境界にある1件をサンプルに入れ、どちらの月に入ったか確認する。
- `COUNT(*)`と`SUM(amount)`を混同しない。金額がNULLの場合のSUMの扱いは別途確認する。
- `GROUP BY`の式とSELECTの式が整合しているか確認する。
- 画面表示の`to_char`で文字列にする前に、日時の月境界が正しいか確かめる。
よくある質問と次の練習
date_truncはMySQLでもそのまま使えますか?
この記事はPostgreSQL向けです。MySQLには同じ関数名が標準で存在しないため、日付関数を製品ごとに選んでください。
月ごとに数字の1〜12だけでGROUP BYしてもよいですか?
複数年のデータを扱う場合は年を区別しないと別の年の同月を混ぜます。月初の日付でまとめるか、年と月の両方を集計キーにします。
(SQLの日付関数を他のデータベースと比較したい場合については『SQL日付関数の完全ガイド|現場で使えるテンプレ・失敗回避法も解説』をご参照ください)
次は実データを変更せず、`WHERE ordered_at >= … AND ordered_at < …`を使った期間抽出を組み合わせてみましょう。集計する単位と集計の対象期間を分けて考えると、月別レポートのSQLを読みやすくできます。