【SQL】前月のデータを取得する完全ガイド|前月初〜月末の正確な計算・DATETIME落とし穴・前月比・N ヶ月前・RDBMS別構文まで解説

「先月の売上を集計したい」「前月と今月を比較してレポートを出したい」——SQL で前月のデータを取得することは実務で毎月発生する操作です。

一見シンプルに見えますが、RDBMS によって関数名が違う・DATETIME 型の列では月末の境界値を間違えると最終日のデータが抜ける・前月比を出すには GROUP BY と LAG 関数の組み合わせが必要——といった落とし穴が多くあります。

この記事では前月初〜前月末を正確に計算する方法から、DATETIME 型の注意点、月別集計、LAG 関数を使った前月比、N ヶ月前の汎用パターンまで体系的に解説します。

月・四半期・年の 範囲指定全般(BETWEEN の閉区間・会計年度・テーブル結合との組み合わせ)は【SQL】日付の範囲指定完全ガイドで詳しく解説しています。この記事は 前月(先月)の絞り込みと前月比計算 に特化した内容です。
サンプルデータ(以降の例で使用・現在日時は 2024-04-08 と仮定)
-- sales テーブル(売上)
-- id | shop_id | amount | category | sold_at
--  1 |       1 |  12000 | food     | 2024-02-05 09:30:00
--  2 |       1 |   3500 | drink    | 2024-02-14 15:45:00
--  3 |       2 |  85000 | food     | 2024-02-20 11:00:00
--  4 |       1 |   2800 | food     | 2024-03-05 08:00:00
--  5 |       2 |  45000 | drink    | 2024-03-10 17:30:00
--  6 |       1 |   9500 | food     | 2024-03-28 10:00:00
--  7 |       2 |  32000 | food     | 2024-04-01 14:00:00
--  8 |       1 |   7800 | drink    | 2024-04-15 09:00:00
--  9 |       2 |  55000 | food     | 2024-04-30 16:30:00
スポンサーリンク

前月初〜前月末を計算する基本パターン

「前月のデータ」を取得するには、前月 1 日と前月末日を動的に計算して範囲指定します。固定値('2024-03-01' など)を書いてしまうと毎月 SQL を書き直す必要があるため、現在日時から自動計算するのが実務では必須です。

MySQL PostgreSQL SQL Server Oracle
前月初日(1日) DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01') DATE_TRUNC('month', NOW() - INTERVAL '1 month') DATEADD(MONTH, DATEDIFF(MONTH,0,GETDATE())-1, 0) TRUNC(ADD_MONTHS(SYSDATE,-1),'MM')
前月末日 LAST_DAY(NOW() - INTERVAL 1 MONTH) DATE_TRUNC('month', NOW()) - INTERVAL '1 day' DATEADD(MONTH, DATEDIFF(MONTH,0,GETDATE()), 0) - 1 TRUNC(SYSDATE,'MM') - 1
今月初日 DATE_FORMAT(NOW(), '%Y-%m-01') DATE_TRUNC('month', NOW()) DATEADD(MONTH, DATEDIFF(MONTH,0,GETDATE()), 0) TRUNC(SYSDATE,'MM')
前月のデータを取得(MySQL)
-- 基本パターン(DATE 型列に BETWEEN)
SELECT *
FROM sales
WHERE DATE(sold_at) BETWEEN DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
                        AND LAST_DAY(NOW() - INTERVAL 1 MONTH);
-- 現在が 2024-04-08 → 2024-03-01 ~ 2024-03-31 を取得

-- DATETIME 型列には推奨: 翌月初未満(< 今月初)で指定
SELECT *
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
  AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01');
-- 2024-03-01 00:00:00 以上 2024-04-01 00:00:00 未満
-- → 2024-03-31 23:59:59 のデータも確実に含む
前月のデータを取得(PostgreSQL)
-- PostgreSQL: DATE_TRUNC を使って前月初〜今月初未満
SELECT *
FROM sales
WHERE sold_at >= DATE_TRUNC('month', NOW() - INTERVAL '1 month')
  AND sold_at <  DATE_TRUNC('month', NOW());
-- 2024-03-01 00:00:00 以上 2024-04-01 00:00:00 未満

-- DATE_TRUNC の確認
SELECT
    DATE_TRUNC('month', NOW() - INTERVAL '1 month') AS prev_month_start,
    DATE_TRUNC('month', NOW()) AS this_month_start;
-- prev_month_start: 2024-03-01 00:00:00
-- this_month_start: 2024-04-01 00:00:00
前月のデータを取得(SQL Server)
-- SQL Server: DATEADD + DATEDIFF で月初を計算
DECLARE @prev_start DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) - 1, 0);
DECLARE @this_start DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);

SELECT *
FROM sales
WHERE sold_at >= @prev_start
  AND sold_at <  @this_start;

-- 変数なし(インライン)
SELECT *
FROM sales
WHERE sold_at >= DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) - 1, 0)
  AND sold_at <  DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);
前月のデータを取得(Oracle)
-- Oracle: ADD_MONTHS + TRUNC で月初を計算
SELECT *
FROM sales
WHERE sold_at >= TRUNC(ADD_MONTHS(SYSDATE, -1), 'MM')
  AND sold_at <  TRUNC(SYSDATE, 'MM');
-- TRUNC(SYSDATE, 'MM') = 今月初日の 00:00:00

-- Oracle の LAST_DAY(前月末日 = 今月初日 - 1)
SELECT
    TRUNC(ADD_MONTHS(SYSDATE, -1), 'MM') AS prev_month_start,
    TRUNC(SYSDATE, 'MM') - 1             AS prev_month_end
FROM dual;

DATETIME 型の落とし穴:月末データが抜ける問題

DATETIME 型の列(時刻あり)に BETWEEN '2024-03-01' AND '2024-03-31' を使うと、'2024-03-31 00:00:00' より後のレコード(例: 09:30:00)が取得されません。

DATETIME 型での前月取得(NG vs OK)
-- 現在日時: 2024-04-08 とする

-- NG①: LAST_DAY で DATE 型の末日を使う → 時刻が 00:00:00 扱いになる
SELECT *
FROM sales
WHERE sold_at BETWEEN DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
                  AND LAST_DAY(NOW() - INTERVAL 1 MONTH);
-- LAST_DAY = '2024-03-31'(= '2024-03-31 00:00:00')
-- → 2024-03-31 08:00:00 の id=4 が取れない

-- NG②: YEAR + MONTH 関数(インデックスが効かない)
SELECT *
FROM sales
WHERE YEAR(sold_at) = YEAR(NOW() - INTERVAL 1 MONTH)
  AND MONTH(sold_at) = MONTH(NOW() - INTERVAL 1 MONTH);
-- 動作するが sold_at 列に関数を適用するためインデックスが使われない

-- ★ OK: 今月初未満で指定(DATETIME 列に最も安全・インデックスも効く)
SELECT *
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
  AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01');
-- 2024-03-01 00:00:00 ≦ sold_at < 2024-04-01 00:00:00
-- → 2024-03-31 の全時刻のデータを確実に取得

-- ★ 1月でも対応: DATE_FORMAT は月をまたいでも正しく動く
-- 現在が 2024-02-15 → prev = 2024-01-01, this = 2024-02-01 ✓
-- 現在が 2024-01-05 → prev = 2023-12-01, this = 2024-01-01 ✓(年またぎも OK)
1月の落とし穴(年またぎ):
NOW() - INTERVAL 1 MONTH を使う限り、1月から前月を計算すると自動的に昨年 12 月になります(MySQL・PostgreSQL とも同様)。ただし YEAR(NOW()) - 1MONTH = 12 を手動で組み合わせる書き方は年またぎを自分で処理しなければならないため、バグを生みやすいです。DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01') のようにINTERVAL を使って計算する方が安全です。

前月のデータを集計する(SUM / COUNT / GROUP BY)

前月の売上合計・件数・平均を集計するパターンです。

前月の基本集計(MySQL)
-- 前月の売上合計・件数・平均
SELECT
    COUNT(*)          AS orders_count,
    SUM(amount)       AS total_amount,
    AVG(amount)       AS avg_amount,
    MAX(amount)       AS max_amount
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
  AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01');

-- 前月をカテゴリ別に集計
SELECT
    category,
    COUNT(*)    AS orders_count,
    SUM(amount) AS total_amount
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
  AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01')
GROUP BY category
ORDER BY total_amount DESC;
今月と前月を一度に集計して比較(CASE WHEN)
-- 1 つのクエリで今月分と前月分を集計
SELECT
    SUM(CASE WHEN sold_at >= DATE_FORMAT(NOW(), '%Y-%m-01')
              AND sold_at <  NOW()
             THEN amount ELSE 0 END) AS this_month_total,
    SUM(CASE WHEN sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
              AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01')
             THEN amount ELSE 0 END) AS prev_month_total
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01');

-- 前月比(%)を計算
SELECT
    SUM(CASE WHEN sold_at >= DATE_FORMAT(NOW(), '%Y-%m-01') THEN amount ELSE 0 END) AS this_month,
    SUM(CASE WHEN sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
              AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01')
             THEN amount ELSE 0 END) AS prev_month,
    ROUND(
        SUM(CASE WHEN sold_at >= DATE_FORMAT(NOW(), '%Y-%m-01') THEN amount ELSE 0 END)
        / NULLIF(
            SUM(CASE WHEN sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
                      AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01')
                     THEN amount ELSE 0 END)
          , 0) * 100, 1
    ) AS growth_pct
FROM sales;

月別集計と LAG 関数で前月比を計算する

複数月のデータを集計し、前月との差・比率を一覧で出すにはGROUP BY + ウィンドウ関数 LAG を組み合わせます。LAG 関数は直前の行の値を参照できるため、「前月比」の計算に最適です。

LAG 関数で前月比を計算(MySQL 8.0+ / PostgreSQL)
-- ① まず月別集計 CTE を作成
WITH monthly AS (
    SELECT
        DATE_FORMAT(sold_at, '%Y-%m') AS year_month,
        SUM(amount)                    AS total_amount,
        COUNT(*)                       AS orders_count
    FROM sales
    WHERE sold_at >= '2024-01-01'   -- 集計期間の開始
    GROUP BY DATE_FORMAT(sold_at, '%Y-%m')
)
-- ② LAG で前月の値を取得し比較
SELECT
    year_month,
    total_amount,
    orders_count,
    LAG(total_amount) OVER (ORDER BY year_month) AS prev_month_total,
    total_amount - LAG(total_amount) OVER (ORDER BY year_month) AS diff,
    ROUND(
        (total_amount - LAG(total_amount) OVER (ORDER BY year_month))
        / NULLIF(LAG(total_amount) OVER (ORDER BY year_month), 0) * 100,
        1
    ) AS growth_pct
FROM monthly
ORDER BY year_month;

-- 結果例(2024-04-08時点)
-- year_month | total_amount | prev_month_total | diff   | growth_pct
-- 2024-02    |       100500 |             NULL |   NULL |       NULL
-- 2024-03    |        57300 |           100500 | -43200 |      -43.0
-- 2024-04    |        94800 |            57300 |  37500 |       65.4
ショップ別に前月比を計算(PARTITION BY)
-- shop_id ごとに独立した前月比
WITH monthly AS (
    SELECT
        shop_id,
        DATE_FORMAT(sold_at, '%Y-%m') AS year_month,
        SUM(amount)                    AS total_amount
    FROM sales
    WHERE sold_at >= '2024-01-01'
    GROUP BY shop_id, DATE_FORMAT(sold_at, '%Y-%m')
)
SELECT
    shop_id,
    year_month,
    total_amount,
    LAG(total_amount) OVER (PARTITION BY shop_id ORDER BY year_month) AS prev_month,
    ROUND(
        (total_amount - LAG(total_amount) OVER (PARTITION BY shop_id ORDER BY year_month))
        / NULLIF(LAG(total_amount) OVER (PARTITION BY shop_id ORDER BY year_month), 0) * 100,
        1
    ) AS growth_pct
FROM monthly
ORDER BY shop_id, year_month;
-- PARTITION BY shop_id により各ショップの先頭月の prev_month は NULL になる
LAG 関数の構文と第2・第3引数:
LAG(値, N, デフォルト値) OVER (PARTITION BY ... ORDER BY ...)
第2引数 N は何行前を参照するか(省略すると 1 行前)。
第3引数はその行が存在しない場合の代替値(省略すると NULL)。
LAG(total_amount, 1, 0) と書けば最初の月の前月値を 0 として扱えます。
LAG 関数の詳細は前年データの取得と前年比計算も参照してください。

YEAR / MONTH 関数による前月絞り込みとインデックスへの影響

YEAR() / MONTH() 関数を列に適用する書き方はシンプルですが、インデックスが使われなくなる(非 SARGable)ため、大量データでは避けるべきです。

YEAR / MONTH 関数の書き方と代替
-- ===== YEAR / MONTH 関数(シンプルだがインデックス非使用になりやすい)=====
SELECT *
FROM sales
WHERE YEAR(sold_at) = YEAR(NOW() - INTERVAL 1 MONTH)
  AND MONTH(sold_at) = MONTH(NOW() - INTERVAL 1 MONTH);
-- → sold_at 列に関数を適用するため EXPLAIN で type=ALL(フルスキャン)になりやすい

-- ===== SARGable な書き方(インデックスのレンジスキャンを使える)=====
SELECT *
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
  AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01');
-- → EXPLAIN で type=range になりインデックスが使われる

-- ===== EXPLAIN で確認 =====
EXPLAIN SELECT *
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
  AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01');
-- type: range, key: idx_sold_at → インデックス使用 ✓
書き方 インデックス 年またぎ対応 DATETIME 月末
YEAR(col)=N AND MONTH(col)=M 使われない(非 SARGable) 〇(YEAR=2023・MONTH=12 で指定) 〇(関数適用なので時刻を考慮しない)
BETWEEN 月初 AND LAST_DAY() 使われる(range) △(LAST_DAY は 00:00:00 扱い → 月末時刻が抜ける)
>= 前月初 AND < 今月初 使われる(range) 〇(全時刻を確実に含む)

N ヶ月前の汎用パターン

前月(1 ヶ月前)だけでなく、2 ヶ月前・3 ヶ月前など任意の月のデータを取得する汎用パターンです。

N ヶ月前のデータを取得(MySQL)
-- N を変えるだけでどの月でも対応可能
SET @n = 1;  -- 1 = 前月、2 = 前々月、3 = 3 ヶ月前...

SELECT *
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL @n MONTH, '%Y-%m-01')
  AND sold_at <  DATE_FORMAT(NOW() - INTERVAL (@n - 1) MONTH, '%Y-%m-01');
-- @n=1: 2024-03-01 〜 2024-03-31 (前月)
-- @n=2: 2024-02-01 〜 2024-02-29 (前々月)
-- @n=3: 2024-01-01 〜 2024-01-31 (3 ヶ月前)

-- 直近 3 ヶ月(当月含まず、3 ヶ月分)
SELECT *
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL 3 MONTH, '%Y-%m-01')
  AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01');
-- 2024-01-01 〜 2024-03-31 のデータ
N ヶ月前(PostgreSQL / SQL Server / Oracle)
-- ===== PostgreSQL =====
-- 前月
WHERE sold_at >= DATE_TRUNC('month', NOW() - INTERVAL '1 month')
  AND sold_at <  DATE_TRUNC('month', NOW());

-- N ヶ月前(N=2 の場合)
WHERE sold_at >= DATE_TRUNC('month', NOW() - INTERVAL '2 months')
  AND sold_at <  DATE_TRUNC('month', NOW() - INTERVAL '1 month');

-- ===== SQL Server =====
-- 前月
WHERE sold_at >= DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) - 1, 0)
  AND sold_at <  DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);

-- N ヶ月前(N=2 の場合)
WHERE sold_at >= DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) - 2, 0)
  AND sold_at <  DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) - 1, 0);

-- ===== Oracle =====
-- 前月
WHERE sold_at >= TRUNC(ADD_MONTHS(SYSDATE, -1), 'MM')
  AND sold_at <  TRUNC(SYSDATE, 'MM');

-- N ヶ月前(N=2 の場合)
WHERE sold_at >= TRUNC(ADD_MONTHS(SYSDATE, -2), 'MM')
  AND sold_at <  TRUNC(ADD_MONTHS(SYSDATE, -1), 'MM');

前月の特定日・特定期間を取得するパターン

前月全体ではなく「前月の第 1 週」「前月 15 日以前」など、前月内の一部を取得するパターンです。

前月内の特定期間を取得(MySQL)
-- 前月の 1 日〜15 日
SELECT *
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
  AND sold_at <  DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-16');
-- '%Y-%m-16' で翌日(16日 00:00:00 未満)を指定することで 15 日の全時刻を含む

-- 前月の最終週(最終 7 日間)
SELECT *
FROM sales
WHERE sold_at >= LAST_DAY(NOW() - INTERVAL 1 MONTH) - INTERVAL 6 DAY
  AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01');
-- LAST_DAY - 6 = 月末から 7 日前(月末の週)

-- 前月の月曜日始まりの週ごとに集計
SELECT
    DATE_FORMAT(sold_at, '%Y-%u') AS week,
    MIN(DATE(sold_at)) AS week_start,
    COUNT(*) AS cnt,
    SUM(amount) AS total
FROM sales
WHERE sold_at >= DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
  AND sold_at <  DATE_FORMAT(NOW(), '%Y-%m-01')
GROUP BY DATE_FORMAT(sold_at, '%Y-%u')
ORDER BY week;

アプリケーションからのパラメータ渡しパターン

レポート画面など「対象月をユーザーが選べる」ケースでは、SQL の中で動的に計算するよりアプリ側で月初・月末を計算して渡す方が保守しやすいです。

アプリ側で月初・翌月初を計算して渡す(Python 例)
-- Python で前月の開始日・終了日を計算
from datetime import date
from dateutil.relativedelta import relativedelta  # pip install python-dateutil

today = date.today()  # 2024-04-08

# 前月初日
prev_start = (today.replace(day=1) - relativedelta(months=1))  # 2024-03-01
# 今月初日(= 前月の翌月初 = 前月の「<」に渡す値)
this_start = today.replace(day=1)  # 2024-04-01

import pymysql
conn = pymysql.connect(host='localhost', db='mydb')
cur = conn.cursor()

# プレースホルダーで安全に渡す(SQL インジェクション対策)
cur.execute("""
    SELECT category, SUM(amount) AS total
    FROM sales
    WHERE sold_at >= %s
      AND sold_at <  %s
    GROUP BY category
""", (prev_start, this_start))

for row in cur.fetchall():
    print(row)

-- PHP(PDO)での例
$prevStart = (new DateTime('first day of last month'))->format('Y-m-d');
$thisStart = (new DateTime('first day of this month'))->format('Y-m-d');

$stmt = $pdo->prepare(
    "SELECT category, SUM(amount) FROM sales
     WHERE sold_at >= :start AND sold_at < :end
     GROUP BY category"
);
$stmt->execute([':start' => $prevStart, ':end' => $thisStart]);
アプリ側で計算する場合の注意点:

  • 月初日の計算: Python の date.replace(day=1) が最もシンプル
  • 「翌月初」は relativedelta(months=1) を使うと月末日問題(2月28/29日など)を自動処理できる
  • SQL に渡す終了値は「前月末日」より「今月初日(以上を除く)」が安全
  • プレースホルダー(%s / :name)を必ず使う(SQL インジェクション対策)

前月のデータが 0 件の月も表示する(LEFT JOIN)

集計したとき特定の月にデータが 1 件もない場合、GROUP BY では行が出ません。「0 件の月も 0 として表示したい」ケースには、月を生成するテーブルや CTE と LEFT JOIN を使います。

月カレンダーテーブルと LEFT JOIN で 0 件月を表示(MySQL)
-- 直近 12 ヶ月分の月一覧を生成して LEFT JOIN
WITH months AS (
    SELECT DATE_FORMAT(
        DATE_SUB(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL n MONTH), '%Y-%m'
    ) AS year_month
    FROM (
        SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3
        UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7
        UNION SELECT 8 UNION SELECT 9 UNION SELECT 10 UNION SELECT 11
    ) t
)
SELECT
    m.year_month,
    COALESCE(SUM(s.amount), 0) AS total_amount,
    COUNT(s.id)                AS orders_count
FROM months m
LEFT JOIN sales s
  ON DATE_FORMAT(s.sold_at, '%Y-%m') = m.year_month
GROUP BY m.year_month
ORDER BY m.year_month;

-- PostgreSQL: generate_series で月一覧を生成
SELECT
    TO_CHAR(months.d, 'YYYY-MM') AS year_month,
    COALESCE(SUM(s.amount), 0)   AS total_amount
FROM generate_series(
    DATE_TRUNC('month', NOW()) - INTERVAL '11 months',
    DATE_TRUNC('month', NOW()),
    '1 month'
) AS months(d)
LEFT JOIN sales s
  ON s.sold_at >= months.d
 AND s.sold_at <  months.d + INTERVAL '1 month'
GROUP BY months.d
ORDER BY months.d;

よくある質問(FAQ)

Q1月に前月(12月)のデータを取得しようとすると年をまたぎますが、正しく動きますか?
ANOW() - INTERVAL 1 MONTHADD_MONTHS(SYSDATE, -1) は年をまたいだ計算を自動的に正しく行います。2024 年 1 月に実行すれば 2023 年 12 月を返します。ただし MONTH(NOW()) - 1 のように手動で月を計算すると 0 になってしまうためこの書き方は使わないでください。
Q前月のデータが重複して取得されることがありますが、なぜですか?
A前月の絞り込みを複数の条件で指定したり、JOIN を含む場合に重複が発生することがあります。DISTINCT や GROUP BY を追加するか、条件の重複を確認してください。また BETWEEN と >= AND < を混在させると境界値の解釈がずれる場合があります。統一して >= 前月初 AND < 今月初 を使うことをお勧めします。
Q前月の日付を固定値で書いた方がシンプルですが問題ありますか?
A固定値(例: '2024-03-01')で書くと毎月 SQL を書き直す必要があり、書き忘れると間違った月のデータが返るバグになります。本番環境のレポートや定期バッチでは 動的に計算する方式を必ず使ってください。
Q前月末日の 23:59:59 以降のデータが取れないことがありますが、どう対処しますか?
ALAST_DAY()'2024-03-31' を BETWEEN の終端に使うと'2024-03-31 00:00:00' として解釈され、当日の時刻を持つデータが取れません。解決策は sold_at < DATE_FORMAT(NOW(), '%Y-%m-01')(今月初未満)を使うことです。詳細は日付の比較完全ガイドの DATETIME 落とし穴の節を参照してください。
Q前月比ではなく前月差(金額の差)を出したいのですが?
Atoday_amount - LAG(amount) OVER (ORDER BY year_month) で前月との差額を計算できます。差額がプラスなら増加、マイナスなら減少です。差額と比率(%)を両方表示するには、上記の LAG セクションのクエリで diff(差)と growth_pct(比率)を両方 SELECT に含めてください。

まとめ

前月のデータを正確に取得するための要点をまとめます。

ポイント 推奨パターン
基本の絞り込み sold_at >= 前月初 AND sold_at < 今月初(DATETIME 列に最安全)
前月初(MySQL) DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01')
前月初(PostgreSQL) DATE_TRUNC('month', NOW() - INTERVAL '1 month')
今月初(共通) MySQL: DATE_FORMAT(NOW(), '%Y-%m-01') / PG: DATE_TRUNC('month', NOW())
月末が抜ける問題 LAST_DAY / 月末日 を BETWEEN 終端に使わず < 今月初 にする
年またぎ(1月→12月) INTERVAL 1 MONTH の引き算で自動処理される(手動計算は NG)
インデックス活用 列に YEAR()/MONTH() を適用しない → 範囲比較(SARGable)を維持する
前月比計算 GROUP BY 月別集計 → LAG(total) OVER (ORDER BY year_month)
N ヶ月前 INTERVAL N MONTH を変えるだけで汎用化できる

日付の比較全般は日付の比較完全ガイド、月・四半期・年の範囲指定パターンは日付の範囲指定完全ガイド、前年比の計算は前年データの取得と前年比計算も参照してください。