PostgreSQL’s planner takes an AccessShareLock on every relation it considers, which means every query acquires locks on every index of a table even when only one index is used. Through PostgreSQL 17, each backend has a 16-slot fast-path array for these weak relation locks; any additional locks overflow into the shared lock table, which is protected by 16 LWLocks. On busy, many-core systems a table with a primary key plus 20 secondary indexes causes every simple lookup to fill the fast-path and spill locks into the shared table, producing LWLock:LockManager contention, CPU saturation, and throughput collapse. Partitioned tables worsen the problem because the planner may lock each partition and its indexes, and the contention scales with overall query rate rather than being limited to the offending statement.
Two practical remedies exist. Prepared statements reach a cached generic plan after several executions, which skips planning and only locks relations present in the cached plan, turning dozens of relation locks into just the table and primary key; caveats are parameter-skewed plans and interaction problems with some poolers (PgBouncer versions and RDS Proxy behavior). PostgreSQL 18 eliminates the hard 16-slot limit by sizing the fast-path from max_locks_per_transaction (default 64), so the same query stays on the fast path. The long-term fix is schema hygiene: drop unused indexes (check idx_scan) or raise max_locks_per_transaction for heavily indexed or partitioned schemas.
Summary generated by AI from the linked article. hn.today is not affiliated with Hacker News or Y Combinator.