Database Performance

PostgreSQL Index Bloat: Measurement, REINDEX CONCURRENTLY, and Prevention

Measure and reclaim PostgreSQL index bloat using pgstatindex and REINDEX CONCURRENTLY. Learn safe online index rebuilding and autovacuum tuning to prevent future bloat.

JusDB Team
March 24, 2025
5 min read
156 views

PostgreSQL indexes accumulate dead tuples just like heap tables. Index bloat slows down queries, wastes disk space, and increases I/O. Here is how to measure and reclaim index bloat.

What Causes Index Bloat?

When rows are updated or deleted, PostgreSQL marks the old tuple as dead but does not immediately remove it from indexes. VACUUM reclaims heap space but index bloat accumulates separately.

Measure Index Bloat

sql
-- Using pgstattuple extension
CREATE EXTENSION IF NOT EXISTS pgstattuple;

SELECT * FROM pgstatindex('idx_orders_created_at');
-- leaf_fragmentation > 30% indicates significant bloat

-- For all indexes on a table
SELECT indexname, pg_size_pretty(pg_relation_size(indexname::regclass)) AS size
FROM pg_indexes
WHERE tablename = 'orders'
ORDER BY pg_relation_size(indexname::regclass) DESC;

Bloat Estimation Query

sql
-- Estimate index bloat across all indexes
SELECT schemaname, tablename, indexname,
       pg_size_pretty(real_size) AS real_size,
       pg_size_pretty(bloat_size) AS bloat_size,
       round(bloat_ratio::numeric, 2) AS bloat_ratio
FROM (
  SELECT *, (real_size - estimated_useful_size) AS bloat_size,
            100 * (real_size - estimated_useful_size)::float / real_size AS bloat_ratio
  FROM pgstatindex_approx()
) t
WHERE bloat_ratio > 20
ORDER BY bloat_size DESC
LIMIT 20;

Fix 1: REINDEX CONCURRENTLY

sql
-- Rebuilds index without locking table (PostgreSQL 12+)
REINDEX INDEX CONCURRENTLY idx_orders_created_at;

-- Reindex all indexes on a table
REINDEX TABLE CONCURRENTLY orders;
Note: REINDEX CONCURRENTLY requires extra disk space for the new index while the old one still exists. Ensure you have sufficient free disk space before running.

Fix 2: CREATE INDEX CONCURRENTLY + DROP

sql
-- Build new index first
CREATE INDEX CONCURRENTLY idx_orders_created_at_new
  ON orders (created_at);

-- Swap names atomically
BEGIN;
ALTER INDEX idx_orders_created_at RENAME TO idx_orders_created_at_old;
ALTER INDEX idx_orders_created_at_new RENAME TO idx_orders_created_at;
COMMIT;

-- Drop old index
DROP INDEX CONCURRENTLY idx_orders_created_at_old;

Prevent Bloat: autovacuum Tuning

sql
-- Per-table autovacuum settings for high-churn tables
ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor = 0.01,
  autovacuum_analyze_scale_factor = 0.005,
  autovacuum_vacuum_cost_delay = 2
);

Key Takeaways

  • Use pgstatindex() to measure leaf_fragmentation — over 30% warrants action
  • REINDEX CONCURRENTLY is the safest way to rebuild bloated indexes online
  • Ensure enough free disk space (index size × 1.5) before reindexing
  • Tune autovacuum per-table for high-update/delete workloads to prevent bloat accumulation

JusDB Can Help

Index bloat silently degrades performance over time. JusDB can audit your PostgreSQL indexes and implement a maintenance schedule.

Share this article

JusDB Team

Official JusDB content team