MySQL
SQL
architecture
paging

一覧画面のページングについていろいろ考えた

More than 2 years have passed since last update.

一覧画面のページング処理はフレームワークに頼ってしまうのが手取り早いですが、内部の実装がDBと相性が悪かったりするとパフォーマンスが悪くなることがあります。
ページング処理について考察したことをまとめてみました。

ページング

ユーザーのためのページング

全部はいらない

人間が処理できるデータ数には限界があります。
googleなどで検索結果が100万件ヒットしても、すべてを見る人はまずいません。人間が閲覧操作する画面に、全データが必要になることは殆どないのです。なので、一度に見せるデータを減らすことでユーザーにもパフォーマンスにも優い画面にしてくれるのがページングです。

ページング

人間が一度に扱いやすい量をページごとに区切って表示するのがページングです。個人的に、1ページあたり10~100行程度が扱いやすいと思っています。
1ページ目に1~10が表示されていると、2ページ目は11~20が表示される…と直感的にわかります。

paging.PNG

パフォーマンスのためのページング

ページ数計算にカウントが必要

ここで問題なのが、ページ数を把握するには全件をカウントしなければならないことです。件数は検索条件によって動的に変化するので毎回カウントが必要です。実際には見ない数万件をカウントするためにパフォーマンスが悪くなるのは避けたいところです。

ページの制限

パフォーマンスが悪い全件カウントを減らすため、1回のカウント取得レコードを制限してしまいます。上記例では100件10ページ=1000件までにしてしまいます。この上限は要件やDBのパフォーマンス(カウント、全件フェッチできる)によって調整します。この制限で取得できるページ数を1ページセットとしてそれを越えるページを要求した場合には改めて次のページセットをカウントします。

SQLDBの場合

  1. 最大検索数で検索し件数カウント
  2. 最大検索数=件数カウントだったら続きがあるかも
  3. オフセット計算
  4. 最初のページを検索(offset=0 and limit = ページあたりの件数)
  5. ページが変わったら(offset=オフセット and limit = ページあたりの件数)
  6. 最後のページを表示したら次の最大検索数分(offset=最大検索数 and limit = 最大検索数)を取得

参考:
MySQL (LIMIT句を使用)
http://dev.mysql.com/doc/refman/5.7/en/select.html

PostgreSQL (LIMIT OFFSET を使用)
https://www.postgresql.jp/document/9.2/html/queries-limit.html

Oracle (ROWNUM BETWEEN) を使用
http://otn.oracle.co.jp/otn_pl/otn_tool/code_detail?n_code_id=106

計算

ページ数

ページ数の計算は普通に割り算で、端数は切り上げ(天井関数)になります。

ページ数[pages] = ceil( データ数[records] / 1ページあたりの表示数[records/page] )

例えば、512件に対して20件ごとにページングすると26ページになります。

データオフセット

各ページのオフセット(ページの先頭のレコード位置)は、ページ番号が1から始まるとして

オフセットページ数[pages] = ページ番号 - 1
オフセットレコード数[records] = オフセットページ数[pages] × 1ページあたりの表示数[records/page]

例えば128件に対して20件ごとにページングした3ページ目は40件オフセットして41~60件目、7ページ目は121~128件目になります。

表示ページオフセット

表示するページ数も最大を設定して最大ページ数に達したら、さらに次のページ番号を表示するようにします。

現在ページのオフセット = 現在のページ番号- ceil(最大表示ページ数 / 2)
現在ページのオフセット < 0 の場合は現在ページのオフセット = 0

最大表示ページ = 現在ページのオフセット + 最大表示ページ数
最大表示ページ > 全ページ数 の場合は、最大表示ページ = 全ページ数分まで

見つかった件数

最大ページセット= ceil(最大表示ページ / 最大表示ページ数)

最大ページセットのレコード数 < 1ページあたりの表示数 の場合
(最大ページセット -1) × 1ページあたりの表示数 + 最大ページセットのレコード数

最大ページセットのレコード数 ≧ 1ページあたりの表示数 の場合
最大レコード数 = 最大ページセット × 1ページあたりの表示数 ※続きがあるかもしれないので、この件数以上 

1ページ3件、最大5ページ、全32件の場合の例
例

諦める

CSVダウンロード

ページングによるパフォーマンス戦略が採用できない場合もあります。例えば、「CSVでダウンロード」の場合です。CSVダウンロードするユーザーは、大抵「別のシステムに取り込みたい」、「Excelで開きたい」といった場合なので、ファイルを分割するなんて忌々しいことはされたくありません。
この場合のページングは諦めて、重いDB処理を許容するか、ダウンロードさせないようにするなどが必要です。

ダーティーリード

ページ切替えでページを再現するには、すべてのページのデータを保持しておく必要があります。ただし、ページングが必要なくらいデータ量が多い場合は、メモリに保持しておくことは現実的ではありません。
かといってDBから再現しようとすると、2ページ目読み込んだときにデータが更新されて変わってしまっていたなんてことが発生します。閲覧中ずっとロックしておくわけにもいかないので、この場合は諦めてダーティーリードを許容します。

そもそもトランザクションが多い

トランザクションが激しいと、あるデータが存在するページが別のページに変わってしまうなど、意図したとおり機能しない場合があります。
生成・更新時間帯を指定して検索、もしくは時間帯ごとのページで表示したほうが現実的です。
時間帯で切り取るなら、ロールウインドウも手です。(ロールウインドウもそのうち投稿します。)