PostgREST pagination latency during high offset requests
0 reputation · 23 May 2025, 07:07 UTC
0 reputation · 23 May 2025, 07:07 UTC
Supabase leverages PostgREST to translate API requests into SQL queries. When retrieving large datasets using the limit and offset parameters, the underlying PostgreSQL engine must scan and discard rows prior to returning the requested page.
As the offset value increases, there is a potential for linear degradation in query performance, leading to increased CPU utilization and higher response latency. This behavior is typical of standard SQL OFFSET clauses but becomes a critical bottleneck in high-concurrency environments with millions of records.
While keyset pagination is documented as a best practice for performance, the default SDK patterns often rely on offset-based navigation.
pg_stat_statements be used to differentiate between slow index scans and the overhead caused specifically by high offset values?1. Threshold for non‑linear latency spikes
There is no single “magic” number that applies to every Supabase deployment. In practice the planner begins to flip from an Index Scan that can skip rows efficiently to a Bitmap Heap Scan or even a Seq Scan when the cost of skipping the requested rows exceeds the cost of a full scan. That inflection point depends on:
work_mem and shared_buffers (larger buffers allow more rows to be kept in memory).On modest instances (e.g., 2‑4 GB RAM, 10‑20 GB tables) you will typically notice a linear degradation once the offset reaches a few tens of thousands of rows. On well‑provisioned hardware with large work_mem the same behaviour may only appear when the offset is in the hundreds of thousands or millions.
2. Using pg_stat_statements to differentiate overhead
When you run identical queries that differ only in the OFFSET value, pg_stat_statements will aggregate the following metrics per query ID:
total_time – overall CPU + I/O time.shared_blks_read – blocks fetched from disk.shared_blks_hit – blocks served from shared_buffers.blk_read_time – time spent reading blocks.To isolate the offset cost you can:
track_io_timing and track_functions=pl in postgresql.conf and reload.EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM big_table ORDER BY id LIMIT 100 OFFSET X;pg_stat_statements before each run or filter by queryid to isolate the offset effect.shared_blks_read per 10k‑row increment. Linear growth indicates pure offset overhead; a sudden jump in total_time with little change in block counts indicates a planner switch to a more expensive scan type.EXPLAIN ANALYZE to confirm whether the plan changed to a Bitmap Heap Scan or Seq Scan.WHERE id > last_seen_id ORDER BY id LIMIT 100) for deep traversal. Supabase’s SDK allows this via .range() with a cursor or by using the Range-Unit: items header in PostgREST.ORDER BY and WHERE to keep the heap scan minimal.pgbouncer pool usage if you’re on transaction pooling; long offset queries can hold a connection and increase queue latency.What is the average row width and which indexes cover the ORDER BY column in your table? Knowing this helps fine‑tune work_mem and anticipate the offset threshold for your specific workload.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.