Indexes accumulate bloat over time as MVCC creates dead tuples, page splits fragment the structure, and updates produce sparse pages. Bloated indexes waste space and slow scans, so PostgreSQL ships contrib tools for diagnostics: pgstattuple and pgstatindex report density and free space ratios, and queries against pg_stats and pg_class can estimate expected versus actual sizes. When bloat becomes excessive, REINDEX INDEX CONCURRENTLY idx_name rebuilds the index without taking an ACCESS EXCLUSIVE lock, which is critical on production systems. Plain REINDEX is faster but blocks all access to the table during the rebuild.
Several storage parameters shape how an index ages. fillfactor leaves a percentage of each page free for future in-place updates, reducing page splits at the cost of a slightly larger initial index; B-trees default to 90, and you can specify WITH (fillfactor = 70) at creation time. Since PostgreSQL 13, B-trees default to deduplicate_items = on, which stores only one copy of adjacent equal key values to save space on indexes with many duplicates such as those on status columns. Large builds can also use parallelism: setting max_parallel_maintenance_workers before CREATE INDEX launches parallel workers (PostgreSQL 11+) to speed up B-tree construction on big tables.
Creating and dropping indexes on busy tables deserves care. CREATE INDEX CONCURRENTLY builds the index without locking writes, at roughly 2-3x the time of a regular build; if it fails, it leaves an INVALID index that must be cleaned up manually. You can check validity through pg_index: indisready means the index is ready for updates while indisvalid means it is visible to query planning, and a failed CONCURRENTLY build typically has indisready = true but indisvalid = false. DROP INDEX CONCURRENTLY performs a similar lock-free removal. Tablespaces add another axis of operational flexibility: CREATE INDEX ... TABLESPACE fast_ssd places an index on fast storage, and ALTER INDEX idx SET TABLESPACE fast_ssd moves an existing one.