ROW_NUMBERで最新レコードだけ取得するSQL実践ガイド
この記事でわかること(結論→全体像)
ROW_NUMBERを使うと、最新レコードだけを安全に1行へ絞り込めます。
「最新の1件だけ欲しい」という要望はよくありますが、実は「最新の定義」を先に決めないと迷子になります。
最新の定義は、作成日時なのか更新日時なのか、あるいは状態遷移の日時なのかで変わります。
まずは「順位を付けて1位を取る」という考え方を押さえると、応用まで迷いません。
順位付けができれば、最新だけでなく「2番目に新しい」や「上位N件」も同じ発想で作れます。
上位N件を作る場合も、rn <= Nにするだけなので、形として覚えやすいです。
この記事では、単純な最新1件からグループ別最新、DBMS差、安定性と性能の注意点、よくある疑問の答えまでまとめて扱います。
実務でありがちな「結果がたまに変わる」「遅くて回らない」も、原因と対策が分かるようにします。
途中で「MAXで良いのでは」と感じやすいポイントも、具体例で違いが分かるようにします。
SQL例はそのまま写して試せる形にしつつ、結果がブレないための並び順(tie-break)も必ず入れます。
実務では同時刻のデータが珍しくないため、tie-breakは「お守り」ではなく必須の設計要素です。
同時刻が起きるのは、バッチ投入や一括更新、外部連携などがあるシステムで特に多いです。
MySQLは8.0以降でウィンドウ関数が使える点を前提にし、5.7以前向けの代替案も最低限だけ触れます。
移植性を重視するなら、特定DBだけの便利構文より、ROW_NUMBERの共通パターンを軸にするのが安全です。
チーム内でSQLの読みやすさを重視する場合も、共通パターンの方がレビューが速くなります。
結論(最短の型)は、サブクエリでROW_NUMBERを付けて、外側でrn=1に絞るだけです。
最短の型でも、ORDER BYを一意にする列を必ず足すことが安定性の鍵です。
「一意にできない列しかない」なら、要件の再確認かテーブル設計の見直しが必要です。
- SELECT *
- FROM (
- SELECT
- t.*,
- ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC) AS rn
- FROM t
- ) x
- WHERE rn = 1;
上の型が読めれば、PARTITION BYを足すだけで「グループごとの最新」に拡張できます。
さらにWHEREで対象範囲を絞る位置まで理解できると、性能面でも失敗しにくくなります。
SQLで最新レコードだけ取得する基本形
「最新だけ欲しい」の多くは、テーブルの行を並べ替えて先頭を1件取る問題です。
ただし、単純にORDER BYしてLIMIT 1するだけだと、グループ別には対応できず、同時刻の行があると結果がブレることがあります。
また、LIMIT 1は「全体で1件」しか取れないため、顧客別や商品別の最新取得には向きません。
LIMIT 1は手早い反面、要件が少し複雑になるとすぐに書き直しが必要になります。
ROW_NUMBERは「並び順に従って連番を振る」ため、どの行が1位かを明示的に決められます。
「どの列で並べたか」がSQLに残るので、あとから読んだ人が判断根拠を追いやすいのも利点です。
このセクションでは、まず単一テーブルから最新1件を取る最小構成を作ります。
最小構成の時点で、安定性のためのtie-breakもセットにして覚えます。
また、絞り込み条件をどこに置くかも最初に押さえ、後半の性能の話へ繋げます。
前提データ(サンプルテーブルと期待結果)
例として、注文テーブルordersから「全体で最新の注文1行」を取りたいケースを想定します。
ordersにはid、customer_id、created_at、status、total_amountが入っているとします。
created_atが新しいほど最新で、同一時刻があった場合はidが大きい方を最新として扱うルールにします。
このルールは「最新が2つある」状態を避けるための、実務上の決め事です。
このルールを決めると、結果が「いつ実行しても同じ」になり、検証もしやすくなります。
同時刻が起きない前提にせず、起きたときの扱いまで最初から書いておくと事故が減ります。
さらに「キャンセルを除外する」「確定のみ対象にする」など、対象の定義もここで整理します。
基本構文(ROW_NUMBER→rn=1)
ROW_NUMBERはOVER句の中で並び順を指定し、その順に1,2,3…と番号を振ります。
この番号は「計算された列」なので、元テーブルに列を追加しなくても使えます。
最初に番号を振る対象を決めるため、一般にはサブクエリ(またはCTE)でrn列を作ります。
次に外側でrn=1に絞ると、最新の1行だけが残ります。
- SELECT
- id,
- customer_id,
- created_at,
- status,
- total_amount
- FROM (
- SELECT
- o.*,
- ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC) AS rn
- FROM orders o
- ) x
- WHERE rn = 1;
この形は「順序を決めて順位を付けてから、1位だけ残す」と読むと理解が安定します。
ORDER BY created_at DESCだけにすると、同時刻の行があると1位が揺れる可能性があるため、id DESCのような追加キー(tie-break)を入れるのが基本です。
tie-breakは「必ず一意になる列」を選ぶのがコツで、主キーや連番があるならそれを使います。
もし主キーがUUIDでも、比較可能で一意ならtie-breakとして使えます。
最新の定義が「更新日時updated_at」なのか「作成日時created_at」なのかは、業務ルールで必ず統一します。
集計や監査で使う場合は、どちらを採用したかをコメントや仕様書にも残しておくと安心です。
「削除済みを除外する」などの条件がある場合は、順位付けの前にWHEREで絞り込むのが原則です。
先に絞ることで、意図どおりの集合から最新を取れて、計算量も減りやすくなります。
- SELECT *
- FROM (
- SELECT
- o.*,
- ROW_NUMBER() OVER (ORDER BY updated_at DESC, id DESC) AS rn
- FROM orders o
- WHERE o.status <> ‘CANCELLED’
- ) x
- WHERE rn = 1;
条件が複数ある場合も、基本は「内側で対象を作り、外側でrn=1」に統一すると見通しが良くなります。
この形に慣れると、次の「グループごとに最新」もほぼ同じ書き方になります。
グループごとに最新レコードを取得する方法
実務では「顧客ごと」「商品ごと」「拠点ごと」など、グループ単位で最新行が欲しい場面が多いです。
たとえば「顧客ごとの最新注文」「商品ごとの最新入荷」「店舗ごとの最新売上」などが典型です。
このときはPARTITION BYでグループを区切り、各グループ内でROW_NUMBERを振ります。
外側でrn=1に絞る点は同じなので、基本形の理解がそのまま活きます。
グループ別は行数が増えやすいので、絞り込みの位置とインデックスが特に効いてきます。
グループ別のSQLは、返る行数も増えるので、実行計画の差が体感で分かりやすいです。
顧客ごとの最新注文を取得する例(PARTITION BY)
顧客ごとに最新の注文を1行ずつ取りたいなら、customer_idでパーティションを切ります。
- SELECT
- id,
- customer_id,
- created_at,
- status,
- total_amount
- FROM (
- SELECT
- o.*,
- ROW_NUMBER() OVER (
- PARTITION BY customer_id
- ORDER BY created_at DESC, id DESC
- ) AS rn
- FROM orders o
- ) x
- WHERE rn = 1;
このSQLは「顧客ごとに並べ替えて先頭だけ残す」と読めるので、行数が増えてもロジックが崩れません。
「顧客が0件になる」などの取りこぼしが起きたら、どの条件を内側に置いたかを確認すると原因が見つかりやすいです。
取りこぼしを避けたい場合は、必要に応じてOUTER JOIN側で顧客マスタを基準にする設計も検討します。
「特定期間の注文だけから最新を取りたい」なら、期間条件は順位付けの前に入れます。
- SELECT *
- FROM (
- SELECT
- o.*,
- ROW_NUMBER() OVER (
- PARTITION BY customer_id
- ORDER BY created_at DESC, id DESC
- ) AS rn
- FROM orders o
- WHERE created_at >= ‘2026-01-01’
- ) x
- WHERE rn = 1;
期間を後から絞ると、そもそも最新行が対象外でもrn=1が残ってしまうため、順序が重要です。
逆に「期間内の最新が欲しい」なら、期間条件を内側に置くのが正解です。
期間の考え方は、レポート要件で頻繁にズレるので、言葉の定義を合わせておくと安全です。
さらに安全に書くコツ(同時刻・重複のtie-break)
結果が不安定になる最大の原因は、ORDER BYが一意になっていないことです。
created_atが同じ行が複数あるなら、idや連番、更新回数など、必ず追加キーを入れます。
追加キーは「その行を一意に決められる列」を使うのが鉄則です。
複合キーで一意にできるなら、ORDER BYに複数列を並べて確定させます。
もしidが存在しないなら、(業務的に許される範囲で)別の一意キーや複合キーで決めます。
同点の行を「全部欲しい」場合はROW_NUMBERではなくRANKやDENSE_RANKを使う選択肢もあります。
ただし、最新が同点で複数返ると後続処理が壊れることもあるため、要件として「1行に決める」か「同点は複数返す」かを先に決めます。
「どちらでも良い」状態で進めると、実装後に集計や画面表示でズレが出やすいです。
「1行に決める」要件なら、tie-breakを入れて必ず1位を1行にします。
DBMSごとの違いと書き方の比較
ウィンドウ関数(ROW_NUMBER)は多くのDBMSで共通の書き方ができます。
一方で、バージョンによって使えなかったり、より簡単な代替構文があったりするため、環境差を押さえると迷いが減ります。
「今のDBで使えるか」と「将来別DBへ移す可能性があるか」を意識して選ぶと失敗しにくいです。
移行予定があるなら、まずは標準寄りの書き方に寄せておくとコストが下がります。
ここでは「同じ発想で書ける部分」と「環境により選ぶ部分」を分けて整理します。
主要DBの共通パターン(MySQL8+/PostgreSQL/SQL Server/Oracle)
MySQL 8.0以降、PostgreSQL、SQL Server、Oracleでは、ROW_NUMBERを同様に使えます。
共通の型は「サブクエリ(またはCTE)でrnを作り、外側でrn=1に絞る」です。
CTEを使うと見やすくなるため、好みやチームルールに合わせて選びます。
- WITH ranked AS (
- SELECT
- o.*,
- ROW_NUMBER() OVER (
- PARTITION BY customer_id
- ORDER BY created_at DESC, id DESC
- ) AS rn
- FROM orders o
- )
- SELECT *
- FROM ranked
- WHERE rn = 1;
CTEは読みやすい一方で、DBやバージョンによって最適化の癖があるため、遅いときは実行計画で確認します。
SQL Serverでは同様に書けますし、実務ではインデックス設計と併せて性能を見ます。
SQL Serverは実行計画が読みやすいので、まずはソートとスキャンの有無を確認すると手がかりになります。
Oracleでも同様に動きますが、実行計画の見方やヒント句は環境ルールに従います。
代替案の位置づけ(DISTINCT ON / APPLY / サブクエリJOIN)
PostgreSQLにはDISTINCT ONという便利な構文があり、用途によっては短く書けます。
ただし、DB依存になるため、移植性が必要ならROW_NUMBERの方が無難です。
SQL ServerにはAPPLY(CROSS APPLY / OUTER APPLY)があり、TOP 1と組み合わせてグループ別最新を組めます。
ただし、これもDB依存なので、チームで統一できるならROW_NUMBERに寄せるのが分かりやすいです。
MySQL 5.7以前ではウィンドウ関数が使えないため、サブクエリとJOINで代替することが多いです。
代替案は複雑になりやすく、同時刻のtie-break処理が漏れやすい点に注意します。
例として「顧客ごとに最大created_atを求めてJOINする」だけでは、同時刻があると複数行が返ることがあります。
そのため、どうしても5.7以前で1行に決めるなら、created_atに加えてidなどを組み合わせたルールを別途設計します。
まずは8.0以降に上げられるかを検討し、難しい場合だけ代替案を採用する方針にすると安全です。
以下は「最新日時の行を取る」レベルの参考例で、同時刻の1行固定までは保証しません。
- SELECT o.*
- FROM orders o
- JOIN (
- SELECT customer_id, MAX(created_at) AS max_created_at
- FROM orders
- GROUP BY customer_id
- ) m
- ON o.customer_id = m.customer_id
- AND o.created_at = m.max_created_at;
この例をそのまま本番に入れず、同点処理が必要なら別の手段を検討するのがポイントです。
本番で使うなら、返る行数が想定どおりかを必ずテストデータで確認します。
ROW_NUMBERを使う時の注意点
ROW_NUMBERは便利ですが、書き方を誤ると「結果が揺れる」「遅い」「意図と違う」になりがちです。
特に、ORDER BYの曖昧さと、順位付けの前後での絞り込み順が大きな差を生みます。
結果が揺れると検証ができなくなり、遅いと運用で詰まるため、ここは確実に押さえます。
このセクションでは、ありがちなNG例を先に示して、OK例へ直す形で理解を固めます。
また、SQLが正しくてもテストデータが偏っていると問題を見落とすので、同点データも用意します。
並び順が曖昧だと結果が安定しない(NG例→OK例)
created_atだけで並べると、同時刻の行が複数ある場合にどれが1位かが不定になります。
同時刻が起きるのは、バッチ投入や分散処理、外部連携などがあるときに特に多いです。
NG例は、ORDER BY created_at DESCだけでrnを作っているケースです。
- SELECT *
- FROM (
- SELECT
- o.*,
- ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) AS rn
- FROM orders o
- ) x
- WHERE rn = 1;
このSQLは動くことが多い一方で、同時刻の行があると実行ごとに違う行が返る可能性があります。
「たまに違う」問題は原因が追いづらいので、最初から一意の順序を作るのが近道です。
OK例は、必ず一意になる列を追加して並び順を固定することです。
- SELECT *
- FROM (
- SELECT
- o.*,
- ROW_NUMBER() OVER (
- PARTITION BY customer_id
- ORDER BY created_at DESC, id DESC
- ) AS rn
- FROM orders o
- ) x
- WHERE rn = 1;
「どちらを優先するか」を仕様として書き切ることで、レビューや保守も楽になります。
必要な範囲まで絞ってから順位付けする(NG例→OK例)
先にrnを付けてから外側で期間やステータス条件を足すと、意図しない行が落ちたり残ったりします。
このズレは「期間内の最新」なのか「最新のうち期間内」なのかの違いとして現れます。
NG例は、全件にrnを付けてから、外側で期間条件を付けるケースです。
- SELECT *
- FROM (
- SELECT
- o.*,
- ROW_NUMBER() OVER (
- PARTITION BY customer_id
- ORDER BY created_at DESC, id DESC
- ) AS rn
- FROM orders o
- ) x
- WHERE rn = 1
- AND created_at >= ‘2026-01-01’;
この場合、顧客の最新注文が2025年にあればrn=1が落ち、結果としてその顧客が消えます。
OK例は、期間や状態など「対象集合」を決める条件を内側で先に絞ることです。
- SELECT *
- FROM (
- SELECT
- o.*,
- ROW_NUMBER() OVER (
- PARTITION BY customer_id
- ORDER BY created_at DESC, id DESC
- ) AS rn
- FROM orders o
- WHERE created_at >= ‘2026-01-01’
- ) x
- WHERE rn = 1;
内側で絞ると、計算対象の行が減って性能面でも有利になりやすいです。
絞り込み条件があるなら、まずは「内側に寄せる」方針で考えると事故が減ります。
インデックスも確認する(目安と当たり所)
ROW_NUMBERは並べ替えを伴うため、データ量が大きいとコストが出ます。
グループ別最新なら、PARTITION BY列とORDER BY列に沿ったインデックスを検討します。
例としてorders(customer_id, created_at, id)のような複合インデックスが効く場合があります。
ただし、最適なインデックスは更新頻度や他クエリとの兼ね合いで変わるため、実行計画で確認します。
まずは「絞り込み→順位付け」の順を守り、不要な全件ソートを避けるだけでも効果が出ます。
SELECTする列を必要最小限にすることも、I/O削減として地味に効きます。
遅いと感じたら、まずはWHEREで対象行数を減らせているかを確認します。
よくある質問(Q & A)
最後に、最新取得でよく出る疑問を短く整理します。
FAQを読めば、「なぜROW_NUMBERを使うのか」と「いつ別の関数を使うのか」の判断がしやすくなります。
質問が多い箇所は、要件が曖昧になりやすい箇所でもあるので、定義を意識して読みます。
MAXで最新日時を取るのではだめですか?
MAX(created_at)で最新日時を取るだけだと、「その日時の行を1行に決める」問題が残ります。
同時刻の行が複数あると、JOINすると複数行が返り、どれが正しいかが曖昧になります。
また、最新日時は取れても、他の列(statusやtotal_amountなど)を一意に決められない点も弱点です。
実務では「最新日時は分かったが、どの注文IDかが決まらない」状態がよく起きます。
ROW_NUMBERなら「並び順のルール」を明示できるため、最新行を1行に固定して取得できます。
ROW_NUMBERとRANKの違いは何ですか?
ROW_NUMBERは必ず1,2,3…と連番になり、同点でも順位が分かれます。
RANKは同点を同順位として扱うため、同点があると1位が複数行になることがあります。
DENSE_RANKは同順位があっても次の順位が飛ばない点が違います。
「最新が同時刻なら全部欲しい」要件ならRANKやDENSE_RANKが向く場合があります。
「最新を必ず1行に決めたい」要件なら、ROW_NUMBER+tie-breakが分かりやすい選択です。
MySQLでも同じSQLを使えますか?
MySQLは8.0以降ならROW_NUMBERを含むウィンドウ関数が使えるため、ほぼ同じSQLを書けます。
MySQL 5.7以前では使えないため、サブクエリJOINなどの代替案になりますが、同時刻処理が難しくなります。
代替案は「動くが読みにくい」ことが多いので、保守期間が長いなら特に注意します。
可能ならMySQL 8.0以降へ上げるか、最新決定ルールをDB設計側で担保する方が安全です。
まとめ
最新レコード取得は、ROW_NUMBERで順位を付けてrn=1に絞る型を覚えると一気に楽になります。
グループ別はPARTITION BYを足すだけなので、基本形がそのまま応用に繋がります。
結果を安定させるには、ORDER BYを一意にするtie-breakを必ず入れます。
性能を落とさないためには、先に対象を絞ってから順位付けし、必要ならインデックスも確認します。
迷ったら「最新の定義」「同点時の決め方」「絞り込み位置」の3点に戻ると整理できます。
この3点を決めるだけで、SQLの迷いが減り、レビューでも指摘が出にくくなります。
まずは自分のテーブルで「最新の定義」と「同点時の決め方」を決め、この記事の型に当てはめて動かしてみてください。