# Pagy::Keyset

In 

Implement wicked-fast keyset pagination for big data.

Try it now!

# Concept

The "Keyset" pagination, also known as "SQL Seek Method" (and often, improperly called "Cursor" pagination) is a technique that avoids the inevitable slowness of querying pages deep into a collection (i.e. when offset is a big number, you're going to get slower queries).

This technique comes with that huge advantage and a set of limitations that makes it particularly useful for APIs and less convenient for UIs.

# Glossary

Term Description
offset pagination Technique to fetch each page by changing the offset from the collection start.
It requires two queries per page (or one if countless): it's slow toward the end of big tables.
It can be used for a rich frontend: it's the regular pagy pagination.
keyset pagination Technique to fetch the next page starting from the latest fetched record in an uniquely ordered collection.
It requires only one query per page: it's very fast regardless the table size and position (if properly indexed). It has a very limited usage in frontend.
uniquely ordered When the concatenation of the values of the ordered columns is unique for each record. It is similar to a composite primary key for the ordered table, but dynamically based on the keyset columns.
set The uniquely ordered ActiveRecord::Relation or Sequel::Dataset collection to paginate.
keyset The hash of column/direction pairs. Pagy extracts it from the order of the set.
latest The hash of keyset attributes of the latest fetched record (from the latest page). Pagy decodes it from the :page variable, and uses it to filter out the already fetched records.
next The next page, i.e. the encoded reference to the last record of the current page.
page The current page, i.e. the encoded reference to the latest record of the latest page.

# Keyset or Offset pagination?

# Usage

# Constraints

# Setup

Pagy Keyset pagination does not waste resources and code complexity checking your set and your table config at every request.

That means that you have to be sure that your set is uniquely ordered and that your tables have the right indexes.

You do it once during development, and pagy will be fast at each request.

Depending on your order requirements, here is how you set it up:

# How Pagy::Keyset works

  • You pass an uniquely ordered set and Pagy::Keyset queries the page of records.
  • It keeps track of the latest fetched record by encoding its keyset attributes into the page query string param of the next URL.
  • At each request, the :page is decoded and used to prepare a when clause that filters out the records already fetched, and the :limit of requested records is pulled.
  • You know that you reached the end of the collection when pagy.next.nil?.

# ORMs

Pagy::Keyset implements the subclasses for ActiveRecord::Relation and Sequel::Dataset sets and instantiate them internally:

Pagy::Keyset.new(active_record_set)
#=> #<Pagy::Keyset::ActiveRecord:0x00000001066215e0>
 
Pagy::Keyset.new(sequel_set) 
#=> #<Pagy::Keyset::Sequel:0x00000001066545e0>

# Methods

The constructor takes the set, and an optional hash of variables. It returns a Pagy::Keyset::ActiveRecord or Pagy::Keyset::Sequel object (depending by the set class).

The next page, i.e. the encoded reference to the last record of the current page. It is nil for the last page.

The Array of fetched records for the current page.

# Variables

The current page, i.e. the encoded reference to the latest record of the latest page. Default nil for the first page.

The :limit per page. Default Pagy::DEFAULT[:limit]. You can use the limit extra to have it automatically assigned from the limit request param.

Boolean variable that enables the tuple comparison e.g. (brand, id) > (:brand, :id). It works only for same direction order, hence it's ignored for mixed order. Check how your DB supports it (your keyset should include only NOT NULL columns). Default nil.

Use this for DB-specific extra optimizations, if you know what you are doing.

If the :after_latest variable is set to a lambda, pagy will call it with the set and latest arguments instead of using its auto-generated query to filter out the records after the latest. It must return the filtered set. For example:

after_latest = lambda do |set, latest|
  set.where(my_optimized_query, **latest)
end

Pagy::Keyset(set, after_latest:)

A lambda to override the automatic typecasting of your ORM. For example: SQLite stores date and times as strings, and the query interpolation may fail composing and comparing string dates. The typecast_latest is an effective last-resort option when fixing the typecasting in your models and/or the data in your storage is not possible.

typecast_latest = lambda do |latest| 
  latest[:timestamp] = Time.parse(latest[:timestamp]).strftime('%F %T')
  latest
end

Pagy::Keyset(set, typecast_latest:)

# Attribute Readers

limit, latest, page, vars

# Troubleshooting