私と2つのページング物語

おとよ
Aug 28 · 8 min read

皆さんはページングについてどんな印象をお持ちでしょうか?

好き・嫌い以外にも「興味ない」「めんどくさいやつ」などいろんな感情があると思います。人間だもの。仕方ないことです。

いざページングを実装してみると、次のページ、前のページ、ページ番号のリンクといったページャ、次のページ・前のページが存在するかどうかのチェック、定められた件数ごとの情報取得など、意外と世話のかかるやつという一面が見えてきます。

ページングと出会ったのは私がまだ入社して間もない駆け出しエンジニアだった頃で、履歴の一覧画面を管理ツールに実装する案件がきっかけでした。

普段ページングに着目することはあまりないと思いますが、1ページあたりの件数は、一般的なユーザー向けのアプリよりも、管理ツールの方が多くなるでしょう。ユーザー向けアプリでは体験(UX)を重視するのに対し、管理ツールでは一画面あたりの情報量を重視するからです。

初めてページングを使って履歴画面を実装した私は、先輩エンジニアから次のような指摘を受けます。

「OFFSETではなくプライマリキーでページングしましょう」

それまでページングと言えばOFFSETを使うものと思い込んでいた私は、このとき初めてOFFSETを使わないページングがあることを知るのです。

おっと、そういえば自己紹介がまだでした。私はCREという、CSスタッフのために管理ツールを開発したり、お問い合わせを受けて技術調査したりするエンジニアです。

CREについて

CREは、このブログでも度々登場していますが、Customer Reliability Engineerの略で、お客様のサービスに対する信頼を最大化することがミッションのエンジニア集団です。

ミクシィのCREは、CSスタッフをサポートすることで、CSスタッフを介してお客様の信頼を高めることを目指しています。

CSスタッフをサポートするために、CSスタッフ向けの管理ツールを開発したり、問い合わせを受けて技術調査をしたり、不正対策をしたり、他にもCSスタッフの業務効率化のためにいろいろなシステムを開発しています。

CREチームが設立された経緯については、過去のエントリで紹介していますので、興味のある方はご覧になってください。

カーソルベースページングとの出会い

話を戻しましょう。OFFSETベースのページングに対して、プライマリキーやタイムスタンプなどを基点とするタイプのページングは、よくカーソルベースページング(Cursor Based Paging)と呼ばれています。

MySQLやPostgreSQLにおけるOFFSETは、ご存知の方も多いと思いますが、OFFSET値以前のレコードもスキャンします。これは、ページが進むにつれてスキャンするレコード数が増えることを意味しています。

とはいえ、レスポンスタイムが気になるほどのオーダーのOFFSETを使う場面はあまりないと思います。実際、管理ツールにカーソルベースページングを採用したのは、未知の将来に備えた予防措置的な意味合いが強いです。けれども個人的に、カーソルベースページングには興味があります。無駄がないのはいつだって魅力的です。

無駄がないならページングは全てカーソルベースにした方が良いかというと、そうではありません。それぞれのページングの特徴を簡単に説明します。

OFFSETベースのページングの特徴

OFFSETベースのページング
  • OFFSET値以前のレコードもスキャンする
  • ページ番号を指定してレコードを取得できる
  • 前のページ・次のページがあるかどうかのチェックが簡単

カーソルベースページングの特徴

カーソルベースページング
  • 必要なレコードだけスキャンする
  • ページ番号を指定してレコードが取得できない
  • 前のページ・次のページがあるかどうかのチェックは工夫が必要

それぞれの特徴を踏まえると、ユースケースに合わせて使い分けるべきというのがおわかりいただけると思います。

カーソルベースページングのRubyGemsを作った話

カーソルベースページングは見ての通り、OFFSETベースよりもロジックが少し複雑になります。MVCモデルのControllerに書くには、もう少しスッキリしたいところです。

何事も実際に手を動かしてみると理解が早いと言います。そういうわけで作ったのが cursor-paginate というRubyGemsです。

先に自白しておくと、車輪の再発明にならないように要件に合致するRubyGemsを軽く探したのですが、作ってからその存在に気付いたので、開き直ってここでは自作の cursor-paginate と、既存の activerecord-cursor の両方を紹介したいと思います(笑) paginate とか pagination 的な単語が入ってないから気付かなかったんだ…

cursor-paginate で工夫した点

OFFSETベースのページングと違い、カーソルベースページングではページ番号をページングに用いないため、次のページがあるかどうかのチェックは少し工夫が必要です。

すぐに思いつくのは、最後尾のレコードを取得し、現在のカーソル値と比較することです。最後尾のレコードを得るためにはページングの方向とは逆順にソートし、1件だけ取得すれば良いですが、もっと簡単な方法があります。

cursor-paginate で工夫したのはこの点で、cursor-paginate では1ページあたりの件数(per_page)よりも1件多く取得を試み、実際に取得できた件数がper_pageよりも多ければ、次のページが存在すると判断しています。

1件多く取得を試みるというアイデアは、実は次に紹介する activerecord-cursor でも採用されています。

activerecord-cursor の良いところ

カーソルベースページングでは、プライマリキー以外のカラムをカーソルにすることもできます。しかし、値がユニークでないカラムをカーソルする場合は注意が必要です。ページ境界での重複値の扱いです。

カーソルが重複値を取ると…

上図のようにカーソルが重複値を取った場合、単一カラムをカーソルにしていると正しくページングすることができません。次のページで、カーソルと同じ値のレコードが再び全て表示されたり、全て表示されなかったりということが起こります。activerecord-cursor ではこの問題を上手く解決しています。

activerecord-cursor では、カーソルとしてプライマリキー以外のカラムを用いることができますが、例えばカーソルに created_at カラムを使ったとしても、created_at だけを頼りにページングしているわけではありません。

activerecord-cursor は、選んだカラムと、プライマリキー(id)をエンコードしたものをカーソルパラメータとして使っています。

そしてページングの際は、カーソルパラメータをデコードして得られた created_at と id を使い、created_at が重複した場合でも id でページ境界を一意に決定できるようにしています。素晴らしいアイデアですね。

まとめ

ページングにはOFFSETベースのものとカーソルベースのものがあることを紹介し、それぞれの特徴を簡単に説明しました。また、カーソルベースページングを行うための自作のRubyGemsと、既存のもっと素晴らしいRubyGemsを紹介しました。ページングについて考えるきっかけになれば幸いです。

mixi developers

ミクシィグループのエンジニアやデザイナーによるブログです。

    おとよ

    Written by

    おとよ

    アルパカだよ。

    mixi developers

    ミクシィグループのエンジニアやデザイナーによるブログです。

    Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
    Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
    Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade