MySQLで『グループごとに最新1件だけ』を取得 ~ 8.0以降で使えるウィンドウ関数でシンプルに書く ~
はじめに
MySQL で SQL を書いていて「何かしらでグループ化して、その中で最新の 1 件だけ欲しい」みたいなパターンに何度か直面しました。
具体的に浮かぶユースケースとしては履歴を INSERT して記録する設計において、以下のようなケースがあるかと思います。
- 各ユーザーの最新ログイン履歴
- 各注文の最新ステータス
- 各記事の最新編集内容
これらのやりたいことは共通していて「グループごとに最新 1 件を取る」ことでした。
なので今回はこのとき書く SQL を備忘録がてら書いておこうというのが趣旨です。
準備: 記事の更新履歴テーブル
今回は例として、ブログ記事の更新履歴テーブルを題材にします。
やりたいことは「記事ごとに最新の編集履歴だけを取得したい」です。
CREATE TABLE article_histories (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
article_id INT NOT NULL, -- 記事を識別するID
version INT NOT NULL, -- バージョン番号
content TEXT, -- 記事内容
updated_at DATETIME(6) NOT NULL -- 更新日時
);
サンプルデータ
INSERT INTO article_histories (article_id, version, content, updated_at) VALUES
(1, 1, '初回投稿', '2025-10-01 09:00:00.000000'),
(1, 2, 'タイトル修正', '2025-10-05 12:30:00.000000'),
(2, 1, '初回投稿', '2025-10-03 08:00:00.000000'),
(2, 2, '本文追記', '2025-10-06 18:20:10.000000'),
(3, 1, '初回投稿', '2025-10-02 10:00:00.000000');
方法 1: ROW_NUMBER()で最新 1 件を取る(MySQL 8 以降)
MySQL 8 以降では、ウィンドウ関数を使えば一発です。
SELECT article_id, version, content, updated_at
FROM (
SELECT
t.*,
ROW_NUMBER() OVER (PARTITION BY article_id ORDER BY updated_at DESC, id DESC) AS rn
FROM article_histories AS t
) AS ranked
WHERE rn = 1
ORDER BY article_id;
+------------+---------+--------------------+----------------------------+
| article_id | version | content | updated_at |
+------------+---------+--------------------+----------------------------+
| 1 | 2 | タイトル修正 | 2025-10-05 12:30:00.000000 |
| 2 | 2 | 本文追記 | 2025-10-06 18:20:10.000000 |
| 3 | 1 | 初回投稿 | 2025-10-02 10:00:00.000000 |
+------------+---------+--------------------+----------------------------+
3 rows in set (0.004 sec)
ポイント
-
PARTITION BY article_idで記事ごとにグループ化。 -
ORDER BY updated_at DESCで新しい順に並べ、行番号を振る。 -
WHERE rn = 1で各グループの 1 番目=最新行のみ抽出。
時間が被ったときように用に id DESC を追加しておくと一応安心です。
(余談)サブクエリではなくwith句を使う
上のサブクエリを使った書き方は少しネストが深くなるので、MySQL 8以降なら with句(CTE(Common Table Expression))を使って読みやすく書くこともできます。
WITH ranked AS (
SELECT
t.*,
ROW_NUMBER() OVER (
PARTITION BY article_id
ORDER BY updated_at DESC, id DESC
) AS rn
FROM article_histories AS t
)
SELECT article_id, version, content, updated_at
FROM ranked
WHERE rn = 1
ORDER BY article_id;
履歴系やログ系クエリはどうしても複雑になりがちなので、CTE は積極的に使った方がチーム開発でも読みやすさが担保できる印象です。
方法 2: MAX() と JOIN で書く(一般的な SQL でのやり方)
MySQL 5 系などウィンドウ関数が使えない環境では、
MAX() サブクエリと JOIN を組み合わせます。
SELECT t.*
FROM article_histories AS t
JOIN (
SELECT article_id, MAX(updated_at) AS max_time
FROM article_histories
GROUP BY article_id
) AS latest
ON t.article_id = latest.article_id
AND t.updated_at = latest.max_time;
+----+------------+---------+--------------------+----------------------------+
| id | article_id | version | content | updated_at |
+----+------------+---------+--------------------+----------------------------+
| 2 | 1 | 2 | タイトル修正 | 2025-10-05 12:30:00.000000 |
| 4 | 2 | 2 | 本文追記 | 2025-10-06 18:20:10.000000 |
| 5 | 3 | 1 | 初回投稿 | 2025-10-02 10:00:00.000000 |
+----+------------+---------+--------------------+----------------------------+
3 rows in set (0.002 sec)
この方法でも同様に「各 article_id の最新 1 件」を取得できます。
ただし、同時刻に複数レコードがあると重複して返るので注意が必要です。
まとめ
| 方法 | 特徴 | MySQL 対応 |
|---|---|---|
ROW_NUMBER() |
シンプル・拡張性高い | 8.0 以降 |
MAX()+JOIN
|
SQL的に通常の方法。 | 5 系〜 |
「グループごとに最新の 1 件を取る」これはログ、履歴、ステータス管理など様々な場面で登場する SQL パターンかと思います。
MySQL 8 以降を使えるなら、まずは ROW_NUMBER() で書くのが非常に便利です。
きっとクエリがすっきり見通し良くなるでしょう。
Discussion