All posts
Data Platform

Postgres Pagination, Counting and Bloat, Measured: OFFSET vs Keyset, count(*) and VACUUM

By

November 9, 2026

6 min read

Page 45,000 of a table with OFFSET took 134 ms; the same page by keyset took 0.27 ms. An exact count of a million rows took 103 ms against 0.12 ms for the planner's estimate. Five full-table updates grew a table from 15 MB to 92 MB, and VACUUM didn't give any of it back.

Benchmark
Postgres Performance
Row 1,0000.67 ms0.29 msRow 10,0001.6 ms0.30 msRow 100,00014 ms0.30 msRow 900,000134 ms0.27 ms

OFFSET

Keyset (WHERE id > last)

Fetching 20 rows at increasing depth in a 1 million row table, median (log scale). Our measurements, November 2026.

The data
OFFSETKeyset (WHERE id > last)
Row 1,0000.67 ms0.29 ms
Row 10,0001.6 ms0.30 ms
Row 100,00014 ms0.30 ms
Row 900,000134 ms0.27 ms

The full report covers counting, large ANY() lists, and the update bloat test: sizes after each step, what VACUUM and VACUUM FULL each did, and how long they took.

Slow lists or growing tables?

We'll look at your heaviest queries and tables with you. 30 minutes, free.

hoolco Monthly Review

Once a month: what changed in tech that month and what it means for you, from cloud and AI to security and the tools you build with. Every item links to its source. No sales pitch, unsubscribe in one click.

As of November 9, 2026. Provided as is, for general information, and accurate to the best of our knowledge on the date shown. Not legal, compliance, tax, financial or other professional advice. Prices, limits and vendor services change; check current vendor documentation before relying on anything here. Terms of use.