業務システム開発・保守の実務メモを発信中

OracleでROWNUM = 1なのに最新データが取れない原因|正しい最新1件の取得方法

Oracleで更新日時が最も新しいレコードを1件取りたいとき、ROWNUM = 1ORDER BY updated_at DESCを同じSELECTに書いても、最新データを保証できません。SQLはエラーなく1件を返すため、気付きにくい書き方です。

Oracle 11g以前などでROWNUMを使うなら、先に副問い合わせで並べ替え、その外側でROWNUM = 1を指定します。12c以降ならFETCH FIRST 1 ROW ONLYの方が分かりやすく書けます。

この記事では、既存SQLのROWNUM = 1が最新1件を正しく取得できているかを判断できるように、誤った書き方と修正方法、索引によって結果が変わる理由を整理します。

目次

ROWNUM = 1とORDER BYを書いても最新1件になるとは限らない

次のSQLは、最新1件を取りたいときに書きやすい形です。

SQL
select
    history_id,
    status,
    updated_at
from test_order_history
where customer_id = 1001
  and rownum = 1
order by updated_at desc;

このSQLはエラーになりません。ただし、updated_atが最大の行を返すSQLにはなっていません

ROWNUMは、Oracleが行を選択した順番を表す疑似列です。同じ問い合わせブロックにORDER BYを書いた場合、行を1件に絞った後に並べ替えられるため、最新順に並べた全件から1位を選ぶ処理とは異なります

OracleのROWNUM擬似列の説明でも、ROWNUM条件とORDER BYを同じ問い合わせに置くと、アクセス方法によって返る行が変わり得ると案内されています。

問題は「必ず古い行になる」ことではなく、最新を保証できないことです。

ROWNUM = 1は「条件に合う最初の1件」です。ORDER BY updated_at DESCを書いても、それだけで「更新日時が最大の1件」にはなりません。

検証用データを作って実際に確認する

今回の検証はOracle Database 21c Express Editionで行いました。

customer_id = 1001には、更新日時と挿入順が一致しない3行を用意します。最新行を先に挿入すると、誤SQLでもたまたま正しい行が返るためです。

検証用テーブルを作成する

SQL
create table test_order_history (
    history_id  number primary key,
    customer_id number not null,
    status      varchar2(20) not null,
    updated_at  date not null
);

次に、古い行、中間の行、最新行の順に挿入します。history_id = 1が最新ですが、物理的には最後に追加しています。

SQL
insert into test_order_history values
    (2, 1001, '受付',   to_date('2026-08-01 09:00:00', 'YYYY-MM-DD HH24:MI:SS'));
insert into test_order_history values
    (3, 1001, '処理中', to_date('2026-08-03 10:00:00', 'YYYY-MM-DD HH24:MI:SS'));
insert into test_order_history values
    (1, 1001, '完了',   to_date('2026-08-05 15:00:00', 'YYYY-MM-DD HH24:MI:SS'));
insert into test_order_history values
    (20, 1002, '受付',  to_date('2026-08-01 09:00:00', 'YYYY-MM-DD HH24:MI:SS'));
insert into test_order_history values
    (10, 1002, '処理中', to_date('2026-08-05 10:00:00', 'YYYY-MM-DD HH24:MI:SS'));
insert into test_order_history values
    (11, 1002, '完了',  to_date('2026-08-05 10:00:00', 'YYYY-MM-DD HH24:MI:SS'));
commit;

customer_id = 1001で本当に欲しい最新レコードは、history_id = 1status = 完了updated_at = 2026-08-05 15:00:00です。customer_id = 1002の3行は、更新日時が同じときの並び順を後半で確認するために使います。

ORDER BYだけなら最新順になることを確認する

最初に、件数を絞らず更新日時の降順だけを指定します。

SQL
select
    history_id,
    status,
    updated_at
from test_order_history
where customer_id = 1001
order by updated_at desc;

実行結果は次の順でした。

history_idstatusupdated_at
1完了2026-08-05 15:00:00
3処理中2026-08-03 10:00:00
2受付2026-08-01 09:00:00

ここで、今回の最新行をhistory_id = 1として確定できます。以降のSQLでどの行が返ったかを、この結果と比べます。

ROWNUM = 1を同じSELECTに追加して結果を確認する

ここで、最初の誤SQLを同じデータに実行します。

SQL
select
    history_id,
    status,
    updated_at
from test_order_history
where customer_id = 1001
  and rownum = 1
order by updated_at desc;

今回の実行では、返ったのは最新行ではなく、次の1行でした。

history_idstatusupdated_at
2受付2026-08-01 09:00:00

実行計画は、全表走査で行を選び、ROWNUM = 1で1行に絞った後にSORT ORDER BYを行う形でした。1行しか残っていないため、その後に並べ替えても最新行にはなりません。

同じSQLでも、customer_id, updated_at DESCの索引を追加した後は、history_id = 1の完了行が返りました。索引の降順走査でたまたま最新行からアクセスされたためです。

ここで重要なのは、索引を追加すれば直るという話ではないことです。

同じ誤SQLが、全表走査では古い行を返し、索引走査では最新行を返しました。結果が正しく見える環境があっても、SQLとして最新を保証しているわけではありません。

tomo

「自分の環境では最新行が返った」は安全性の根拠になりません。実行計画や索引が変われば、同じSQLの結果も変わり得ます。

なぜROWNUM = 1では最新データを保証できないのか

ROWNUMは、ORDER BY後の順位ではありません。

概念的には、Oracleが行を選んだ順にROWNUMが付きます。そのため、同じSELECTにROWNUM = 1ORDER BY updated_at DESCを書いても、全件を更新日時順に並べてから先頭を選んでいるわけではありません。

全表走査だった今回の実行では、古い行を1件選んでから、その1件を並べ替えていました。一方、索引を使った実行では、更新日時の降順に近いアクセス経路になり、同じSQLでも最新行が返りました。

Oracleの内部処理を単純な固定順序として理解する必要はありません。実務で押さえるべきことは、同じ問い合わせブロックのROWNUMとORDER BYの組み合わせに「最新1件」という意味はない、という点です。

ROWNUMで最新1件を取得する正しいSQL

Oracle 11g以前などでROWNUMを使う場合は、内側の問い合わせで先に並べ替えます。その結果に対して、外側でROWNUMを適用します。

SQL
select
    history_id,
    status,
    updated_at
from (
    select
        history_id,
        status,
        updated_at
    from test_order_history
    where customer_id = 1001
    order by updated_at desc
)
where rownum = 1;

内側の問い合わせが更新日時の降順に並べた結果を返し、外側のROWNUM = 1がその先頭行を取ります。今回の実行結果はhistory_id = 1完了2026-08-05 15:00:00でした。

Oracle 12c以降ならFETCH FIRST 1 ROW ONLYで書ける

Oracle 12c以降では、ORDER BYの後にFETCH FIRSTを書けます。行数制限が並べ替えの後にあるため、最新1件を取る意図がSQLから読み取りやすくなります。

SQL
select
    history_id,
    status,
    updated_at
from test_order_history
where customer_id = 1001
order by updated_at desc
fetch first 1 row only;

こちらもhistory_id = 1の完了行を返します。OracleのSELECT文リファレンスでは、row_limiting_clauseをTop-N取得に使えることと、一貫した結果には決定的なORDER BYを指定することを案内しています。

ROWNUMとFETCH FIRSTのどちらを使う?

Oracleのバージョンにもよりますが、基本は以下で判断することをおすすめします。

Oracleのバージョン・状況最新1件の取得方法
11g以前ORDER BYした副問い合わせ + ROWNUM = 1
12c以降の新規SQLORDER BY … FETCH FIRST 1 ROW ONLY
既存SQLを保守するROWNUMの位置を直し、同じ結果を確認する

FETCH FIRSTが使える環境でも、既存SQLを無条件に書き換える必要はありません。対象のOracleバージョン、SQLを呼ぶアプリケーション、既存テストを確認して選びます。

更新日時が同じレコードが複数ある場合にも注意する

更新日時が同じ行が複数あると、ORDER BY updated_at DESCだけではどちらを1件目にするか決まりません。検証用データでは、customer_id = 1002に同じ更新日時のhistory_id = 10history_id = 11を用意しました。

SQL
select
    history_id,
    status,
    updated_at
from test_order_history
where customer_id = 1002
order by updated_at desc
fetch first 1 row only;

今回の実行ではhistory_id = 10が返りました。しかし、updated_atが同じ2行のどちらを選ぶかは、このORDER BYだけでは決めていません。

業務上、同時刻なら大きい履歴IDを優先するルールなら、第2ソートキーまで書きます。

SQL
select
    history_id,
    status,
    updated_at
from test_order_history
where customer_id = 1002
order by updated_at desc, history_id desc
fetch first 1 row only;

このSQLではhistory_id = 11が返りました。更新日時が最新で、同時刻なら履歴IDが大きい方を選ぶというルールがSQLに残ります。

顧客ごとに最新1件が必要ならROWNUMではなくROW_NUMBERを使う

ここまで扱ったのは、全体または1顧客の中から最新1件を取るSQLです。顧客ごとに最新履歴を1件ずつ取りたい場合は、ROW_NUMBER()で顧客単位の順位を付けます。

SQL
select
    history_id,
    customer_id,
    status,
    updated_at
from (
    select
        h.*,
        row_number() over (
            partition by customer_id
            order by updated_at desc, history_id desc
        ) as rn
    from test_order_history h
)
where rn = 1
order by customer_id;

customer_id = 1001ではhistory_id = 1customer_id = 1002ではhistory_id = 11でした。PARTITION BY customer_idが顧客ごとに順位を付け、history_idが同時刻の優先順位を決めています。

Oracleの分析関数の説明でも、ROW_NUMBERはORDER BYで同値が残ると非決定的になり得るため、必要に応じて複数のソートキーで順序を決めるよう説明されています。

全体から最新1件を取る問題と、グループごとに最新1件を取る問題は別です。後者をROWNUMだけで処理しようとすると、最初の1行で全体が終わってしまいます。

ROWNUM = 1が正しく使えるケースもある

ROWNUM = 1自体が誤りなのではありません。順序を問わず、条件に一致する行が存在するかだけを確認したいなら、任意の1件を取る用途に使えます。

SQL
select
    history_id,
    status,
    updated_at
from test_order_history
where customer_id = 1001
  and rownum = 1;

このSQLで返る行が最新である必要はありません。問題になるのは、「最新」「最大」「最小」のように順序に意味がある1件を、先にROWNUMで絞ろうとする場合です。

まとめ

ROWNUM = 1は最新1件を意味しません。更新日時順に1件を選ぶなら、並べ替えの後に行数を絞るSQLにします。

やりたいこと方法
条件に一致するどれか1件ROWNUM = 1
最新1件・Oracle 11g以前ORDER BYした副問い合わせ + ROWNUM = 1
最新1件・Oracle 12c以降ORDER BY … FETCH FIRST 1 ROW ONLY
顧客ごとの最新1件ROW_NUMBER() OVER (PARTITION BY …)
同じ更新日時が存在する第2ソートキーまで指定

今回の検証では、同じ誤SQLが全表走査では古い行を返し、索引追加後には最新行を返しました。

SQLがたまたま期待どおりに見えることと、最新を保証していることは別です。

既存SQLを直すときは、ROWNUMの位置、ORDER BY、同時刻の扱いまで確認することをおすすめします。

よかったらシェアしてね!
  • URLをコピーしました!
目次