LAG・LEAD関数で前日比・前月比を出す方法(MySQL 8.0)

データ分析のグラフが表示された画面 SQL

売上レポートで「昨日と比べてどれだけ増えたか」を出したい場面はよくあります。以前は自己結合やサブクエリで1日前の行を探していました。MySQL 8.0からは、ウィンドウ関数のLAGとLEADを使うと前後の行の値を1行で取り出せます。この記事では、LAG・LEADの基本構文から、前日比・前月比・増減率の計算、店舗ごとの比較までを順に説明します。後半では、データの抜けた日に比較がずれる問題やゼロ除算など、実務でつまずきやすい点と対処法もまとめました。ウィンドウ関数そのものが初めての方は、ウィンドウ関数入門の記事から読むと理解しやすくなります。

このシナリオで考える

2つの店舗(shibuya と shinjuku)の日別売上を記録したテーブルを使います。渋谷店は10月4日が休業日で、行がありません。この「抜け」は後半の落とし穴の説明で使います。

CREATE TABLE daily_sales (
  sale_date DATE        NOT NULL,
  store     VARCHAR(20) NOT NULL,
  amount    INT         NOT NULL,
  PRIMARY KEY (store, sale_date)
);

INSERT INTO daily_sales (sale_date, store, amount) VALUES
  ('2026-10-01', 'shibuya', 120000),
  ('2026-10-02', 'shibuya',  96000),
  ('2026-10-03', 'shibuya', 132000),
  ('2026-10-05', 'shibuya', 150000),  -- 10/04は休業で行なし
  ('2026-10-01', 'shinjuku', 80000),
  ('2026-10-02', 'shinjuku', 88000),
  ('2026-10-03', 'shinjuku', 66000);

LAG・LEADの基本構文

LAGは「前の行」、LEADは「後ろの行」の値を返す関数です。どちらも同じ形で書けます。

LAG(式 [, N [, 既定値]])  OVER (PARTITION BY ... ORDER BY ...)
LEAD(式 [, N [, 既定値]]) OVER (PARTITION BY ... ORDER BY ...)
引数 意味 省略したとき
式 取り出したい列や計算式 省略不可
N 何行前(後ろ)を見るか 1
既定値 該当する行がないときに返す値 NULL

まずは渋谷店だけで、前日の売上と翌日の売上を並べてみます。

SELECT
  sale_date,
  amount,
  LAG(amount)  OVER (ORDER BY sale_date) AS prev_amount,
  LEAD(amount) OVER (ORDER BY sale_date) AS next_amount
FROM daily_sales
WHERE store = 'shibuya'
ORDER BY sale_date;
+------------+--------+-------------+-------------+
| sale_date  | amount | prev_amount | next_amount |
+------------+--------+-------------+-------------+
| 2026-10-01 | 120000 |        NULL |       96000 |
| 2026-10-02 |  96000 |      120000 |      132000 |
| 2026-10-03 | 132000 |       96000 |      150000 |
| 2026-10-05 | 150000 |      132000 |        NULL |
+------------+--------+-------------+-------------+

最初の行には前の行がないので、prev_amount はNULLになります。同じ理由で、最後の行の next_amount もNULLです。OVER句の ORDER BY は「どの順番で前後を決めるか」を指定する部分です。書かなくてもエラーにはなりません。ただし並び順が保証されず、結果が実行のたびに変わるおそれがあります。LAG・LEADでは必ず書くと覚えてください。

前日比と増減率を計算する

LAGで取った値は普通の列と同じように計算に使えます。差額と増減率(%)を出してみましょう。同じOVER句を何度も書く場合は、WINDOW句で名前を付けるとすっきりします。

SELECT
  sale_date,
  amount,
  amount - LAG(amount) OVER w AS diff,
  ROUND(
    (amount - LAG(amount) OVER w) * 100
      / NULLIF(LAG(amount) OVER w, 0),
    1
  ) AS rate_pct
FROM daily_sales
WHERE store = 'shibuya'
WINDOW w AS (ORDER BY sale_date)
ORDER BY sale_date;
+------------+--------+--------+----------+
| sale_date  | amount | diff   | rate_pct |
+------------+--------+--------+----------+
| 2026-10-01 | 120000 |   NULL |     NULL |
| 2026-10-02 |  96000 | -24000 |    -20.0 |
| 2026-10-03 | 132000 |  36000 |     37.5 |
| 2026-10-05 | 150000 |  18000 |     13.6 |
+------------+--------+--------+----------+

増減率の分母を NULLIF(…, 0) で包んでいるのがポイントです。前日の売上が0だと割り算ができません。NULLIFで0をNULLに変えておくと、結果は安全にNULLになります。

既定値を指定する場合

3番目の引数を使うと、前の行がないときの値を決められます。たとえば LAG(amount, 1, 0) とすると、初日の前日売上が0として扱われます。ただし差額が当日の売上そのものになり、増減率も計算できなくなります。比較の基準がないことを正しく表すには、NULLのままにしておく方が無難でしょう。

PARTITION BYで店舗ごとに比べる

店舗が複数あるときは PARTITION BY を付けます。店舗ごとに区切った中で前後の行を探すため、別の店舗の値が混ざりません。

SELECT
  store,
  sale_date,
  amount,
  LAG(amount) OVER (
    PARTITION BY store
    ORDER BY sale_date
  ) AS prev_amount
FROM daily_sales
ORDER BY store, sale_date;
+----------+------------+--------+-------------+
| store    | sale_date  | amount | prev_amount |
+----------+------------+--------+-------------+
| shibuya  | 2026-10-01 | 120000 |        NULL |
| shibuya  | 2026-10-02 |  96000 |      120000 |
| shibuya  | 2026-10-03 | 132000 |       96000 |
| shibuya  | 2026-10-05 | 150000 |      132000 |
| shinjuku | 2026-10-01 |  80000 |        NULL |
| shinjuku | 2026-10-02 |  88000 |       80000 |
| shinjuku | 2026-10-03 |  66000 |       88000 |
+----------+------------+--------+-------------+

新宿店の初日が、渋谷店の最終日ではなくNULLになっている点に注目してください。PARTITION BYを忘れると、店舗の境目で別店舗の売上と比べてしまいます。

前月比・前年同月比を出す

月単位で比べるときは、先に月ごとに集計してからLAGを使います。CTE(WITH句)で集計を分けると読みやすくなります。

WITH monthly AS (
  SELECT
    DATE_FORMAT(sale_date, '%Y-%m') AS ym,
    SUM(amount) AS total
  FROM daily_sales
  GROUP BY ym
)
SELECT
  ym,
  total,
  LAG(total) OVER (ORDER BY ym)     AS prev_month,
  LAG(total, 12) OVER (ORDER BY ym) AS same_month_last_year
FROM monthly
ORDER BY ym;

Nに12を渡すと、12行前、つまり前年同月の値が取れます。同じ考え方で、日別データに LAG(amount, 7) を使えば前週の同じ曜日と比べられます。なお MySQLでは LAG(SUM(amount)) OVER (ORDER BY ym) のように、集約関数をそのまま中に書くこともできます。

比較したいもの データの単位 書き方
前日比 日別 LAG(amount) OVER (ORDER BY 日付)
前週同曜日比 日別 LAG(amount, 7) OVER (ORDER BY 日付)
前月比 月別 LAG(total) OVER (ORDER BY 年月)
前年同月比 月別 LAG(total, 12) OVER (ORDER BY 年月)
翌日との差 日別 LEAD(amount) OVER (ORDER BY 日付)

よくある落とし穴

1. 行が抜けると「前日」ではなくなる

LAGが見るのは「1日前」ではなく「1行前」です。渋谷店の10月5日は、休業日の4日を飛ばして3日と比べられていました。LAG(amount, 7) で前週比を出す場合も、途中に抜けがあると別の日と比べてしまいます。対策は2つあります。

  1. カレンダー用のテーブルとLEFT JOINして、抜けた日を0件の行として補う
  2. 前の行の日付も取り出して、ちょうど1日前のときだけ比較する

2つ目の方法は次のように書けます。

SELECT
  sale_date,
  amount,
  CASE
    WHEN DATEDIFF(sale_date, LAG(sale_date) OVER w) = 1
    THEN amount - LAG(amount) OVER w
  END AS diff_from_yesterday
FROM daily_sales
WHERE store = 'shibuya'
WINDOW w AS (ORDER BY sale_date);
+------------+--------+---------------------+
| sale_date  | amount | diff_from_yesterday |
+------------+--------+---------------------+
| 2026-10-01 | 120000 |                NULL |
| 2026-10-02 |  96000 |              -24000 |
| 2026-10-03 | 132000 |               36000 |
| 2026-10-05 | 150000 |                NULL |
+------------+--------+---------------------+

2. WHEREで絞ると前の行が消える

ウィンドウ関数はWHEREで絞り込んだ後の行に対して計算されます。WHERE sale_date >= ‘2026-10-02’ と書くと、10月2日の前日売上はNULLになります。期間を絞りたいときは、全期間でLAGを計算したCTEやサブクエリを作り、その外側で絞り込みましょう。WHERE句にLAGを直接書けないため、結果で絞るときもこの形になります。

3. その他の注意点

  • ORDER BYの値が同じ行が複数あると、どちらが「前」になるかは決まりません。並び順が一意になる列まで指定します
  • 前の行の値がNULLの場合も、LAGはそのままNULLを返します。既定値が使われるのは「前の行がない」ときだけです
  • MySQLでは IGNORE NULLS(NULLを飛ばして前の値を探す指定)は使えず、書くとエラーになります
  • Nには負の数を指定できません。後ろの行を見たいときはLEADを使います

まとめ

LAGは前の行、LEADは後ろの行の値を取り出す関数です。差額や増減率は、取り出した値と当日の値を計算するだけで出せます。店舗などのグループごとに比べるなら PARTITION BY、前週・前年と比べるなら Nの指定を使います。ただしLAGが見るのはあくまで「1行前」です。抜けた日がないか、WHEREで比較元を消していないかを確かめてから集計に使ってください。

参考リンク

アイキャッチ画像: Photo by 2H Media on Unsplash

タイトルとURLをコピーしました