Skip to content

Performance

The most impactful optimization is skipping the COUNT(*) query, which can be expensive on large tables:

use paginator_rs::Paginator;
let params = Paginator::new()
.per_page(20)
.disable_total_count()
.build();

When disabled:

  • meta.total and meta.total_pages will be None
  • meta.has_next and meta.has_prev still work correctly
  • The database only executes one query instead of two

For large datasets, cursor pagination outperforms offset-based pagination:

use paginator_rs::{Paginator, CursorValue};
// First page
let params = Paginator::new()
.per_page(20)
.sort().asc("id")
.disable_total_count()
.build();
// Subsequent pages use cursor
let params = Paginator::new()
.per_page(20)
.cursor()
.after("id", CursorValue::Int(last_id))
.apply()
.disable_total_count()
.build();
Offset-basedCursor-based
OFFSET 10000 LIMIT 20 scans 10,020 rowsWHERE id > 42 LIMIT 20 uses index
Gets slower on later pagesConsistent performance
May return inconsistent results with concurrent writesAlways consistent

Ensure database indexes exist on fields used for:

  • Sorting (sort_by)
  • Filtering (filter fields)
  • Cursor pagination (cursor field)
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_name_email ON users(name, email); -- For search

CTE queries (WITH clauses) work seamlessly and can improve performance for complex queries:

let result = paginate_query::<_, User>(
pool,
"WITH active AS (
SELECT * FROM users WHERE active = true
)
SELECT * FROM active",
&params,
).await?;