はじめに
業務システムの一覧画面でページングを実装するとき、一番最初に思いつくのは「OFFSET/LIMITで取得」だと思います。
ところが実際に動かしてみると「次ページに進んだら同じ行がまた出てきた」「さっきあったデータが消えてる」「1000ページ目を開いたらレスポンスが返ってこない」。OFFSET/LIMIT だけでは解決できない問題が次々と出てきます。
いま自作の Javaフレームワークにページング機能を追加しようとしていて、あらためて方式を整理しました。これまでの業務開発で実際に使ったことのある4つの方式を、問題点・利点とあわせて紹介します。
ページングに求められること
まず、ページングが満たすべき要件を整理します。
- 正しいデータが表示される(抜け・重複がない)
- 他ユーザーの更新が影響しにくい(一貫性)
- DBへの負荷が許容範囲内
- ソート順はユーザーが指定できる
この記事では特に重要な2つの現象を取り上げます。
- 行ズレ … ページ遷移の間に他ユーザーがデータを追加・削除すると、行の抜けや重複が起きる現象。
- ステールリード … 初回取得時点の古いデータを表示し続け、他ユーザーの更新が反映されない現象。
行ズレの例を示します。1ページ10件で1ページ目を表示中に、他ユーザーが ID:03 を削除した場合:
通常
--- 1ページ目取得時 --- → --- 2ページ目取得時 ---
ID: 01(ソート順 1件目) ID: 11(ソート順 11件目)
ID: 02 ID: 12
ID: 03 ID: 13
... ...
ID: 10(ソート順 10件目) ID: 20
↓2ページ目表示前に他ユーザーが ID:03 を削除した場合
--- 2ページ目取得時 ---
ID: 12 ※1件削除されたため ID:11 が1ページ目に移動し、2ページ目には表示されない
ID: 13
ID: 14
...
ID: 21
これらをすべて完璧に満たす方式は存在しないと考えており、プロジェクトの要件に合わせてトレードオフを選ぶことになります。
また、フレームワーク側で請け負うとなると下記の要件も加わります。
- シンプルに実装できる(透過性)
- 各種 DBMS に対応
方式1:毎回DBアクセス — 行番号指定(OFFSET)
SELECT * FROM orders
ORDER BY id
LIMIT 10 OFFSET 20 -- 3ページ目(1ページ10件)
最もシンプルな方式です。ページ移動のたびに「N行目から M件取得する」とDBに指定します。
問題点
-
DBMS ごとに構文が異なる。 Oracle は
FETCH FIRST N ROWS ONLY、MySQL はLIMIT ... OFFSET、SQL Server はOFFSET ... FETCH NEXTなど。 - DB負荷が高い。 OFFSET が大きくなるほど、DBは先頭から行を数えてスキップする必要があります。
- 他ユーザーの更新によって行ズレが起きる。 (前述の図を参照)
方式2:毎回DBアクセス — 空回し
OFFSET をDBに任せず、アプリケーション側で目的の行までスキップ(空回し)する方式です。結果セット(ResultSet)を先頭から読み進めて、N件目以降の M件を取り出します。
Java での実装イメージ(JDBC):
try (PreparedStatement ps = con.prepareStatement("SELECT * FROM orders ORDER BY id");
ResultSet rs = ps.executeQuery()) {
// 空回し:先頭から N 件を読み飛ばす
for (int i = 0; i < n; i++) {
if (!rs.next()) {
break; // データがそれ以上ない場合
}
}
// M件 リストに保管して返す
...
}
方式1と同様に毎回DBへアクセスします。OFFSET をDBに渡さないため、DBMS を問わず同じコードで実装できます。
問題点
- 読み飛ばす行数が増えるほどアプリ側の処理コストが上がる。 100ページ目を見るなら、先頭から990件を空回しすることになります。
- 行ズレは方式1と同様に発生します。
応用:キー保管との組み合わせ
前回取得した最終行のキー値を保持しておき、次ページの取得時にWHERE id > :lastIdで絞り込めば、空回し行数を減らしつつ行ズレも緩和できます。ただしソート順の変更やページの直接ジャンプには対応しにくくなりますし、フレームワークへの組み込みには向きません。
ここまでの方式1・2は「毎回DBにアクセスする」グループです。ここからの方式3・4は「初回だけDBにアクセスし、結果をキャッシュする」グループになります。
方式3:初回DBアクセス — ワークファイル出力
初回の検索時にDBから全件取得してファイルに保存し、2回目以降はファイルを参照する方式です。
[初回] DB → 全件取得 → ファイル保存 → 1ページ目を表示
[2回目以降] ファイル参照 → N ページ目を表示
メリット
- 2回目以降はDBアクセスが発生しないため、DB負荷が低い
- ページ遷移中の一貫性が確保できる(行ズレが起きない)
問題点
- ステールリードが発生する。 初回取得時点のスナップショットを表示し続けるため、他ユーザーの更新がページ遷移中に反映されません。
- 複数 Webサーバー構成でファイルの配置が問題になる。 共有ストレージやオブジェクトストレージが必要です。
- ファイルをいつ消すのか? セッション終了時・ログアウト時・一定時間後など、削除タイミングの設計が必要です。
- ファイルが大量になる可能性がある。 複数ユーザーが同時に使えばその分のファイルが増えます。
- 初回の検索が大量件数の場合、メモリとI/Oに負荷がかかります(上限を設けることで回避可能)。
方式4:初回DBアクセス — ワークDB出力
初回検索時にDBから全件取得してワーク用のテーブルに INSERT し、2回目以降はワークテーブルを SELECT する方式です。
[初回] DB → 全件取得 → ワークテーブルに INSERT → ワークテーブル SELECT → 1ページ目を表示
[2回目以降] ワークテーブル SELECT → N ページ目を表示
方式3のファイルをDBに置き換えたものです。仕組みはほぼ同じです。
メリット
- ファイル方式と同様に一貫性が確保でき、2回目以降はDB本体への負荷が低い
- 複数 Webサーバー構成でも問題ない。 ファイル方式のように配置先を気にする必要がありません。
- ページ送りの実装が INSERT/SELECT だけで済む
- 行番号を付与できるのでページ指定しやすい
問題点
- ステールリードが発生する。 方式3と同様です。
- ワークテーブルをいつ消すのか? 定期バッチで掃除するか、セッション管理と連動させるか、削除タイミングの設計が必要です。
- 大量ユーザーが同時に検索するとワークテーブルが膨れます。
- 検索処理ごとにワークテーブルのレイアウトが必要。
比較まとめ
4つの方式を2グループに分けて整理します。
毎回DBアクセス(方式1・2)
他ユーザーの更新がリアルタイムに反映される(ステールリードなし)一方、行ズレが発生します。
| 方式1:行番号指定(OFFSET) | 方式2:空回し | |
|---|---|---|
| 負荷の所在 | DB側(OFFSETスキャン) | アプリ側(読み飛ばし処理) |
| DBMS対応 | 構文が DBMS ごとに異なる | DBMS を問わず同じコードで実装可能 |
| 行ズレ | あり | あり |
| ステールリード | なし | なし |
初回のみDBアクセス(方式3・4)
初回取得時のスナップショットを保持するため行ズレは起きませんが、ステールリードが発生します。
| 方式3:ワークファイル | 方式4:ワークDB | |
|---|---|---|
| ページ送りの仕組み | ファイルI/O | INSERT/SELECT |
| 複数Webサーバー | 共有ストレージが必要 | 問題なし |
| ワーク削除設計 | 必要 | 必要 |
| 行ズレ | なし | なし |
| ステールリード | あり | あり |
どの方式が向いているか
| ケース | 向いている方式 |
|---|---|
| 更新頻度が低い参照系データ(マスタ一覧など) | 方式1(OFFSET)— 最もシンプル |
| 多少の行ズレが許容できる業務 | 方式1 または 方式2 |
| OFFSET 構文がない DBMS、またはDBMS 非依存にしたい | 方式2(空回し) |
| ページ遷移中の一貫性が重要(承認・帳票など) | 方式3 または 方式4 |
| 複数 Webサーバー構成 | 方式4(ワークDB) |
ワークファイル・ワークDB方式は一貫性の観点では優れていますが、「いつ消すか」の設計コストがネックになります。
おわりに
今回はDBのページング方式4つを整理しました。どれが「正解」というわけではなく、プロジェクトの特性に合わせて選ぶことになります。
自分のフレームワークでは多様な DBMS に対応したいので方式2(空回し)をベースに実装しようかと考えていますが、正直まだ悩んでいます。「うちのプロジェクトではこうやった」「こんな方式もあるよ」といった情報があれば、ぜひコメントで教えてください!
SIcoreフレームワーク リンク
自作フレームワークはこちらで公開しています。
- HP: https://onepg.com/ja/
- GitHub: https://github.com/sugaiketadao/sicore-ja
- サンプル画面の確認方法: https://github.com/sugaiketadao/sicore-ja#%EF%B8%8F-%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E7%94%BB%E9%9D%A2%E3%81%AE%E7%A2%BA%E8%AA%8D%E6%96%B9%E6%B3%95---vs-code
- AI開発の始め方: https://github.com/sugaiketadao/sicore-ja#-ai%E9%96%8B%E7%99%BA%E3%81%AE%E5%A7%8B%E3%82%81%E6%96%B9
関連記事リンク
他の記事もぜひご覧ください!
読んでいただきありがとうございました!
❤いいね!をしていただけると励みになります。