メインコンテンツへスキップ
ブログ一覧に戻る
Web制作・運用

ページ送りで同じ行が二度出るのは「並び順を一意に決めていない」から|公式が書いているのは別のこと

2026年10月26日
13分で読めます

この記事でわかること

一覧のページングで2ページ目に同じデータが出る、CSVの行数は合うのに中身が毎回違う——そんな症状を調べている方へ。PostgreSQL公式が書いているのは「ORDER BYを付けろ」ではなく「結果行を一意な順序に制約するORDER BYを付けろ」です。この一語の差と、同値行について公式が何を書いていないかを整理します。

一覧画面のページングで、こういう報告が来ることがあります。「2ページ目に、1ページ目と同じ行が出ている」。あるいは「全件エクスポートしたCSVの行数は合っているのに、中身が毎回少し違う」。

調べる側はまず SQL を見ます。そして ORDER BY があることを確認して、別の原因を探しに行きます。キャッシュか、データの更新タイミングか、集計の二重計上か。

ここが分かれ目です。ORDER BY を付けることと、並び順が一意に決まることは、別の状態です。

PostgreSQL の公式ドキュメントを読み直すと、書かれているのは「ORDER BY を付けろ」ではありません。「結果行を一意な順序に制約する ORDER BY 句を使うことが重要だ」です。原文では "into a unique order" という語が入っています。この一語があるかどうかで、同じ対処をしたつもりでも結果が変わります。

扱わないものを先に置きます。PostgreSQL 以外のデータベースの順序保証は扱いません(今回確認していないためです)。カーソル方式のページングの実装手順も扱いません。ここで扱うのは、公式が何を書き、何を書いていないかと、そこから導ける範囲の対処だけです。

30秒で要点

  • 公式は「ORDER BY を付けろ」ではなく「結果行を一意な順序に制約する ORDER BY を使え」と書いている。付けるだけでは条件を満たさない
  • ORDER BY に複数の式を書いた場合、後ろの式は「前の値で等しい行」の並べ替えに使われる
  • すべてのソート式で同値になった行同士の順序について、参照したページには記述が無い。安定とも不安定とも書けない
  • ソートを選ばなければ行は未規定の順序で返る。実際の順序は実行計画とディスク上の並びに依存するが、依拠してはならないと明記されている
  • LIMIT / OFFSET の値が違えば実行計画が変わり行順も変わりうる。公式はこれをバグではないと書いている
  • OFFSET で飛ばされる行もサーバ内部では計算されるため、大きな OFFSET は非効率になりうる
  • 判断軸は「ORDER BY があるか」ではなく「同値になりうる行が残っていないか」

表が収まらない場合は、横にスクロールできます

用語意味
ページング(ページ送り)一覧を一定件数ずつに区切って返す仕組み
LIMIT / OFFSET返す件数の上限と、先頭から飛ばす件数を指定する句
一意な順序どの2行をとっても前後が1通りに決まる並び。同順位が残らない状態
実行計画データベースがそのクエリをどう処理するか決めた手順。同じ結果でも手順は複数ありうる
クエリオプティマイザ実行計画を選ぶ仕組み
同値行指定したソート式で値が等しくなる複数の行
API(システム同士をつなぐ窓口)他のプログラムからデータを取得するための入口

公式が書いているのは「一意な順序」である

まず原文の位置を確定させます。参照するのは PostgreSQL 公式ドキュメントの2ページです。

どちらも current の URL で、2026年10月26日に確認した時点では PostgreSQL 18 を指しています(ページのドキュメント表記は 18.6)。current のリンク先は将来の版へ動くため、確認日とあわせて読んでください。

LIMIT のページには、こう書かれています。LIMIT を使うときは、結果行を一意な順序に制約する ORDER BY 句を使うことが重要であり、そうしなければクエリの行の予測不能な部分集合を得ることになる、と。続けて、10番目から20番目の行を求めているとしても「何の順序における10番目から20番目なのか」と問い返し、ORDER BY を指定しない限り順序は不明だと述べています。

ここで注目したいのは、この文が2段構えになっていることです。「ORDER BY を指定しない限り順序は不明」は、よく引用される部分です。しかしその前に、「一意な順序に制約する」という条件が置かれています。

ORDER BY を付けた時点で、1段目は満たされます。2段目——一意かどうか——は、付けただけでは満たされません。

なぜ「並べた」で足りると感じるのか

この差を見落とすのには、構造的な理由があります。

第一に、手元では再現しないことが多いからです。 開発環境のデータ量では、同値行が複数あっても、実行計画が変わらない限り順序は安定して見えます。同じ順序が10回続けば、決まっていると判断するのが自然です。「決まっている」と「たまたま毎回同じだった」は、観察からは区別できません。

第二に、ORDER BY が「順序を決める句」として説明されるからです。 入門的な説明では、ORDER BY created_at は「作成日時の順に並べる」と説明されます。この説明は正しい。ただし、作成日時が同じ行が2件あるときに何が起きるかは、この説明の射程外です。

第三に、症状が順序の問題として見えないからです。 「同じ行が二度出る」は重複の問題に見えます。重複を疑えば、DISTINCT や結合条件を見に行きます。並び順を疑うには、「ページ2の1行目が、前回のページ1の末尾と入れ替わった」という形で症状を読み替える必要があります。

この3つが揃うと、ORDER BY の存在確認で調査が止まります。止まった先に、2段目の条件が残っています。

複数の式を書いたとき、何が決まるのか

ORDER BY のページには、複数式の扱いが書かれています。複数の式を指定した場合、後ろの値は、前の値において等しい行を並べ替えるために使われます。

つまり、ORDER BY created_at, id と書けば、created_at が同じ行同士は id で並びます。id が一意なら、この2つを並べた時点で全体が一意な順序になります。これが1段目と2段目を同時に満たす形です。

重要なのは、この規定が「前の値で等しい行」についてのものだということです。では、すべてのソート式で値が等しくなった行同士はどうなるのか。

参照したページには、その記述がありません。

これは「不安定である」という記述が無いという意味ではなく、どちらの記述も無いという意味です。ソートの安定性(stability)への言及もありません。したがって、「PostgreSQL のソートは安定だ」と書くことも、「不安定だ」と書くこともできません。

言えるのは一つだけです。すべてのソート式で同値になった行の順序は、ここでは保証されていない。

「書かれていない」を、どう扱うか

ここは、この記事で最も持ち帰ってほしい部分です。

「保証されていない」と「保証されていないと書いてある」は違います。今回確認できたのは前者で、ページに明示的な禁止文があるわけではありません。記述が無いという観察です。

実務では、この種の空白が最も危険です。禁止が書かれていれば避けます。保証が書かれていれば依拠します。何も書かれていないとき、人は観察した挙動を保証と読み替えます。 10回同じ順序だったという観察が、「この順序は決まっている」という前提に変わる。その前提は、仕様書のどこにも書かれていません。

ORDER BY のページは、まさにこの点について別の角度から釘を刺しています。ソートを選ばなければ行は未規定の順序で返り、実際の順序はスキャンや結合の実行計画の種別とディスク上の並びに依存するが、依拠してはならないと。そして、特定の出力順序が保証されるのは、ソートのステップが明示的に選ばれた場合のみだと述べています。

この文の構造が参考になります。「実際にはこう決まる」という説明と、「それに依拠してはならない」という禁止が、同じ段落に並んでいます。挙動の説明は、保証の代わりにはならないという書き方です。

同値行についても、同じ態度を適用できます。挙動が安定して見えることは、保証ではありません。

実行計画が変わると、順序も変わる

LIMIT のページには、もう一つ押さえるべき記述があります。

クエリオプティマイザは LIMIT を考慮して実行計画を作ります。 そのため、LIMIT と OFFSET に与える値が違えば、違う実行計画——つまり違う行順——になる可能性が非常に高い。したがって、異なる LIMIT / OFFSET の値でクエリ結果の別の部分集合を取ると、ORDER BY で予測可能な結果順序を強制しない限り、一貫しない結果になると書かれています。

そして公式は続けます。これはバグではなく、ORDER BY が使われない限り SQL が特定の順序で結果を返すことを約束しないという事実の、当然の帰結であると。

この一文は、調査の方向を変えます。「ページ1とページ2で結果が食い違う」という現象を、実装の不具合として探さなくてよいということです。同時に、「次のリリースで直る」類のものでもありません。順序を制約していない限り、仕様どおりの振る舞いです。

OFFSET のコストについて書かれていること

性能面についても、確認できた記述が1つあります。OFFSET 句で飛ばされる行も、サーバ内部では計算される必要があるため、大きな OFFSET は非効率になりうる。

ここも表現に注意が必要です。「なりうる」であり、「必ず遅い」ではありません。何件を超えると問題になるのか、どれくらい遅くなるのかという閾値や計測値は、このページには示されていません。したがって、「◯万件を超えたら別の方式に」という線引きは、このページからは引けません。自分の環境での測定が必要です。

症状を1つの原因に決めつけない

ここまでの内容は、「順序を一意にしなければ LIMIT / OFFSET の結果は一貫しない」という仕様上の帰結です。

一方で、「同じ行が二度出る」「行が抜ける」という症状が、必ずこの原因から来ると言うことはできません。参照した2ページは、ページ送りの途中でデータが挿入・削除された場合について触れていません。 OFFSET が先頭から一定数の行を飛ばす句であることを踏まえれば、1ページ目を読んだあとに新しい行が入ったときに飛ばす対象がずれる、という筋道は立ちます。ただしこれは仕様から導いた推論であり、公式ページがこの場合について書いているわけではありません。

したがって、調査としては2つを分けて確かめることになります。並び順が一意かどうかと、読み取りの最中にデータが動いているかどうかです。前者は SQL を見れば判定できます。後者は、発生時刻と更新ログを突き合わせる必要があります。

確かなことと、確かでないこと

確かなこと(2026年10月26日に PostgreSQL 公式ドキュメントの該当2ページを確認した範囲。current はこの時点で PostgreSQL 18、ページ表記 18.6):LIMIT を使うときは結果行を一意な順序に制約する ORDER BY が重要であり、そうしなければ予測不能な部分集合が返ると書かれていること。ORDER BY に複数の式を指定した場合、後ろの値が前の値において等しい行の並べ替えに使われること。ソートを選ばなければ行が未規定の順序で返り、実際の順序は実行計画の種別とディスク上の並びに依存するが依拠してはならず、特定の出力順序が保証されるのはソートのステップが明示的に選ばれた場合のみだと書かれていること。クエリオプティマイザが LIMIT を考慮するため値によって実行計画と行順が変わりえ、公式がこれをバグではなく SQL の性質の帰結だと説明していること。OFFSET で飛ばされる行もサーバ内部で計算されるため大きな OFFSET が非効率になりうること。

確かでないこと:すべてのソート式で同値になった行同士の順序については、参照したページに記述がありません。ソートの安定性への言及もないため、安定だとも不安定だとも書けません。また、ページ送り中のデータ更新による症状については、参照した2ページが扱っていないため、ここからは何も言えません。他のデータベース製品の順序保証も今回確認していません。再現条件や件数の目安についても、実測していないため示せません。

判断軸は「ORDER BY があるか」ではない

対処は、公式の記述から導ける範囲に限ります。

やることは1つです。ORDER BY の並びに、一意性のある列を最後に足す。 ORDER BY created_at を ORDER BY created_at, id にする、という形です。複数式の規定——後ろの値が前の値で等しい行を並べ替える——から、同値行が残らない状態を作れます。

そのうえで、確認する問いが変わります。チェックリストとしてはこうなります。

  1. その ORDER BY の全列が同じ値になる行が、2件以上存在しうるか。 存在しうるなら、順序は一意ではありません
  2. 最後のソート列は一意か。 主キーのように重複しない列であれば、その時点で全体が一意になります
  3. ソート列の値は、読み取りの最中に変わりうるか。 更新で変わる列を唯一のソート条件にしていると、順序の前提が途中で変わります
  4. OFFSET の大きさは、どこまで想定しているか。 飛ばす行も計算されるという記述があるため、深いページでのコストは設計時に見積もる対象です

ここで「一意になりうるか」を考えるときの注意点があります。created_at のような時刻列は、一意に見えて一意ではありません。 一括登録やバッチ処理では、同じ時刻の行が大量に生まれます。開発環境では手で1件ずつ入れるため重複せず、本番でだけ同値行が現れる——という順序で表面化します。

データベースの基礎的な前提については、データベース入門:データを効率的に管理する仕組みでも「行の順序は ORDER BY なしでは保証されない」という点を扱っています。この記事はその一段先、ORDER BY を付けても同値行は決まっていないという層です。

手元で試せる、最小の検証

いま動いている一覧のクエリを1つ開いてください。画面でもAPIでも構いません。

そのクエリの ORDER BY に並んでいる列を、すべて書き出します。 そして1つだけ確認します。この列の組み合わせが、すべて同じ値になる行は存在しうるか。

判定は、データを見るのではなく定義を見ます。その列に一意制約があるか。無ければ、同値行は存在しうると考えます。created_at だけ、status と name だけ、updated_at DESC だけ——このどれも、一意性の保証を持ちません。

存在しうるなら、ORDER BY の末尾に主キーを足して終わりです。数分の変更です。

存在しえないなら、その一覧の順序は一意です。そして、症状が出ているのであれば原因は別のところにあります。 読み取りの最中にデータが動いていないかを、次に確かめる対象として残せます。どちらに転んでも、調査の範囲が1つ狭まります。

ページングを設計する前に立ち返る問い

立ち返りたいのは、「この並び順は、どの2行をとっても前後が決まるか」という問いです。「並んでいるか」ではありません。並んでいても、同順位が残っていれば、残った部分は誰も決めていません。

もう1つは、「この挙動は、保証されているのか、観察されただけなのか」という問いです。10回同じ結果だったことは、11回目を保証しません。公式が「依拠してはならない」と書いているのは、まさにこの読み替えに対してです。

同値行の順序について公式が何も書いていないという事実は、不親切ではありません。書かれていないものに依拠しない、という読み方を求めているだけです。

参考資料・引用元

よくある質問(FAQ)