WHERE (created_at, id) < ($1, $2)WHERE (created_at, id) < ($1, $2) is correct. It is a row value comparison, and PostgreSQL compares it in order, like words in a dictionary: first created_at, then id only when created_at is equal. So it means created_at < $1 OR (created_at = $1 AND id < $2). An index on (created_at, id) can serve this condition.
Why the others are wrong:
• WHERE created_at < $1 skips the rows that share the last row's timestamp.
• WHERE created_at <= $1 AND id < $2 wrongly drops older rows that have a higher id.
• WHERE created_at < $1 OR id < $2 wrongly includes newer rows that have a lower id.
Read more
- We need tool support for keyset pagination (https://use-the-index-luke.com/no-offset)use-the-index-luke.com