Offset-free Paging for SQLAlchemy
sqlakeyset implements keyset-based paging for SQLAlchemy (both ORM and core). It provides an efficient alternative to traditional offset-based pagination, which can become slow with deep pages. The library supports SQLAlchemy 2.0, includes type hints, and is tested with PostgreSQL, MariaDB/MySQL, and SQLite. The current version is 2.0.1775222100. While no explicit release cadence is stated, the GitHub repository shows regular updates and contributions.
Warnings
- breaking Python versions earlier than 3.8 are no longer supported. Users on older Python versions must upgrade or use an older `sqlakeyset` release.
- gotcha Keysets must be unique per row. Always include primary key column(s) at the end of your `order_by` clause to ensure uniqueness and prevent skipped or repeated rows.
- gotcha Rows containing `NULL` values in keyset columns will be omitted from results, as SQL comparisons against `NULL` are always false. Ordering columns should be `NOT NULL`.
- gotcha The built-in keyset serialization currently handles only basic data/column types (strings, ints, floats, datetimes, dates, booleans). More advanced custom types may require extending the serialization.
- deprecated The `get_page` function is for legacy SQLAlchemy 1.3 style ORM queries. For SQLAlchemy 2.0 style queries using `select()`, `select_page` should be used.
Install
-
pip install sqlakeyset
Imports
- select_page
from sqlakeyset import select_page
- get_page
from sqlakeyset import get_page
- select_page (asyncio)
from sqlakeyset.asyncio import select_page
Quickstart
from sqlalchemy import create_engine, select, String, Integer
from sqlalchemy.orm import declarative_base, Session, Mapped, mapped_column
from sqlakeyset import select_page
# 1. Define Base
Base = declarative_base()
# 2. Define a simple model
class Book(Base):
__tablename__ = "books"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
author: Mapped[str] = mapped_column(String(50))
title: Mapped[str] = mapped_column(String(100))
def __repr__(self):
return f"Book(id={self.id}, author='{self.author}', title='{self.title}')"
# 3. Create an in-memory SQLite engine
engine = create_engine("sqlite:///:memory:")
# 4. Create tables
Base.metadata.create_all(engine)
# 5. Insert some sample data
with Session(engine) as session:
session.add_all([
Book(author="Stephen King", title="It"),
Book(author="J.R.R. Tolkien", title="The Hobbit"),
Book(author="Stephen King", title="The Stand"),
Book(author="Frank Herbert", title="Dune"),
Book(author="J.R.R. Tolkien", title="The Lord of the Rings"),
])
session.commit()
# 6. Perform keyset pagination
with Session(engine) as session:
# Build a query with ordering, including a unique key (id) at the end
q = select(Book).order_by(Book.author, Book.title, Book.id)
# Get the first page
page1 = select_page(session, q, per_page=2)
print(f"Page 1 items: {page1.items}")
print(f"Page 1 has next: {page1.paging.has_next}")
print(f"Page 1 next bookmark: {page1.paging.bookmark_next}")
# Get the second page using the bookmark from the first page
if page1.paging.has_next:
page2 = select_page(session, q, per_page=2, bookmark=page1.paging.bookmark_next)
print(f"Page 2 items: {page2.items}")
print(f"Page 2 has next: {page2.paging.has_next}")
print(f"Page 2 next bookmark: {page2.paging.bookmark_next}")