41. Table bloat that VACUUM will not fix
A PostgreSQL table keeps growing and queries get slower. Autovacuum runs often, but the number of dead rows does not go down. What is the most likely cause?
- A.Autovacuum is turned off by default and must be enabled
- B.The table has too many indexes, so autovacuum gives up before finishing each run and leaves dead rows behind
- C.The table has no primary key
- D.An old open transaction (e.g. "idle in transaction") or replication slot holds back cleanup
Show answer
Answer: D. An old open transaction (e.g. "idle in transaction") or replication slot holds back cleanup
With MVCC, VACUUM can only remove dead row versions that no open transaction could still need. One forgotten transaction from days ago pins that horizon for the whole database. Look in pg_stat_activity for old xact_start values, set idle_in_transaction_session_timeout, and check replication slots.
42. Queries keep running after the client quits
An app sets a 3-second client timeout on reports. After timeouts, the slow queries keep running in PostgreSQL for minutes, holding locks and CPU. What should be added?
- A.A longer client timeout of 10 minutes
- B.A second read replica for the same queries
- C.A retry so the client gets a result eventually
- D.A server-side statement_timeout
Show answer
Answer: D. A server-side statement_timeout
A client timeout only stops the client waiting; the database keeps executing unless the query is cancelled. Setting statement_timeout (per role, session or transaction) or sending a cancel when the client gives up stops the work. Retrying just starts another copy of the slow query.
43. Too many database connections
After moving the API to serverless functions, PostgreSQL starts failing with "too many connections" during traffic spikes. What is the best fix?
- A.Open a new database connection inside every request and never close it
- B.Use a connection pooler such as PgBouncer, RDS Proxy or a provider pooled endpoint, and create the client once outside the handler so warm instances reuse it
- C.Increase max_connections to 100000
- D.Switch every query to run inside one long transaction
Show answer
Answer: B. Use a connection pooler such as PgBouncer, RDS Proxy or a provider pooled endpoint, and create the client once outside the handler so warm instances reuse it
Each function instance can open its own connections, and spikes create hundreds of instances. A pooler multiplexes many clients onto a small number of real database connections, and reusing the client across warm invocations avoids reconnecting. Huge max_connections wastes memory and hurts Postgres performance.
44. Unique email with soft deletes
users has UNIQUE(email) and soft deletes via a deleted_at column. A user deletes their account and later signs up again with the same email, which fails with a unique violation. What is the best fix in PostgreSQL?
- A.Replace it with UNIQUE(email, deleted_at)
- B.Drop the constraint and check for duplicates in application code before inserting
- C.CREATE UNIQUE INDEX ON users (email) WHERE deleted_at IS NULL
- D.Switch to hard deletes; soft deletes cannot work with unique columns
Show answer
Answer: C. CREATE UNIQUE INDEX ON users (email) WHERE deleted_at IS NULL
A partial unique index enforces uniqueness only among active rows, so deleted rows no longer block re-signups. UNIQUE(email, deleted_at) looks right but is broken: NULLs count as distinct, so two active rows with the same email would both be allowed. Checking in application code has a race between the check and the insert.
45. pg_wal growing without limit
There are no replication slots, but pg_wal keeps growing and the disk will be full in hours. pg_stat_archiver shows failed_count rising. What is happening?
- A.archive_command is failing, so PostgreSQL keeps every unarchived WAL segment; fix the archiving destination or credentials
- B.max_wal_size is too small
- C.Autovacuum writes extra WAL that is never removed
- D.A checkpoint is stuck because of a long SELECT
Show answer
Answer: A. archive_command is failing, so PostgreSQL keeps every unarchived WAL segment; fix the archiving destination or credentials
With archive_mode on, a WAL segment cannot be recycled until archive_command succeeds for it, so a broken bucket, expired credentials or full archive disk makes WAL pile up. Alert on pg_stat_archiver failures and archive lag. Never delete files from pg_wal by hand; fix the archiver and let PostgreSQL clean up.
46. Native incremental backups in PostgreSQL 17
PostgreSQL 17 added pg_basebackup --incremental. What else is needed to get a restorable data directory?
- A.pg_combinebackup to merge the chain
- B.pg_resetwal to join the backups
- C.pg_dumpall to replay changes
- D.Nothing; each incremental is complete
Show answer
Answer: A. pg_combinebackup to merge the chain
An incremental backup contains only blocks changed since a prior backup, using WAL summaries (summarize_wal = on) to find them. pg_combinebackup merges the chain into a full data directory you can start or recover from. Losing any backup in the chain breaks the restore.
47. Running PostgreSQL on Kubernetes
A team runs PostgreSQL in a plain Deployment with one replica and a network volume, and calls it HA because Kubernetes restarts the pod. What is missing?
- A.A larger CPU limit on the pod
- B.A second Service object
- C.An Ingress for the database port
- D.A standby and managed failover
Show answer
Answer: D. A standby and managed failover
Restarting a single pod brings back the same instance after detaching and reattaching its volume, which can take minutes and does nothing if the data is damaged. Real HA needs streaming replicas and managed failover. Operators like CloudNativePG or Patroni-based ones run that on StatefulSet-style storage.
48. JIT makes a query slower
A PostgreSQL query that should take 30 ms now takes 400 ms. EXPLAIN ANALYZE shows "JIT: Functions: 120, Timing: Generation 20 ms, Optimization 180 ms, Emission 150 ms". What is the fix?
- A.Increase shared_buffers to the maximum
- B.Add more indexes so JIT has less to compile
- C.Run ANALYZE on the tables more often
- D.Raise jit_above_cost or turn jit off
Show answer
Answer: D. Raise jit_above_cost or turn jit off
JIT compiles expressions to machine code when the estimated cost is high. If estimates are inflated, it can spend hundreds of milliseconds compiling a query that runs in milliseconds. Raising jit_above_cost or disabling JIT for OLTP workloads avoids this.
49. Wrong results after an OS upgrade
After moving a PostgreSQL data directory to a new OS image with a newer glibc, some unique indexes on text columns return wrong results and allow duplicates. What happened?
- A.The new OS turned off data checksums
- B.Text sort order changed, so indexes need REINDEX
- C.pg_hba.conf rules were reset by the OS
- D.The WAL format changed with the new kernel
Show answer
Answer: B. Text sort order changed, so indexes need REINDEX
With libc collations, PostgreSQL relies on the OS for text sort order. glibc 2.28 changed many collation rules, so B-tree indexes built under the old order are now in the wrong order for lookups. Rebuild affected indexes; ICU or the builtin provider makes versioning clearer.
50. Checking an index for corruption
You suspect a B-tree index is corrupt after a storage incident but want proof before a long REINDEX. Which tool checks it?
- A.The amcheck extension's bt_index_check()
- B.pg_stat_statements for the index's queries
- C.pg_dumpall --globals-only
- D.ANALYZE VERBOSE on the table
Show answer
Answer: A. The amcheck extension's bt_index_check()
amcheck verifies B-tree structure and ordering rules, and bt_index_parent_check() goes deeper with stronger locks. pg_amcheck runs these checks across a whole database. ANALYZE only samples rows for statistics and would not find structural damage.
51. ADD COLUMN that rewrote the table
On PostgreSQL 16, ALTER TABLE t ADD COLUMN a int DEFAULT 0 finished instantly, but ADD COLUMN b uuid DEFAULT gen_random_uuid() locked the table for an hour. Why?
- A.uuid columns always need a full table rewrite
- B.The second command had no index on the column
- C.gen_random_uuid() takes a lock on every row
- D.A volatile default must be stored in every row
Show answer
Answer: D. A volatile default must be stored in every row
Since PostgreSQL 11, a constant default is stored once in the catalog, so existing rows do not change. A volatile default like gen_random_uuid() gives each row a different value, so the table is rewritten under an ACCESS EXCLUSIVE lock. Add the column without a default and backfill in batches instead.
52. Disk full with a large pg_wal
The disk is 100% full and PostgreSQL has stopped. A teammate wants to delete old files in pg_wal by hand to free space. What should you do instead?
- A.Delete every pg_wal file older than one hour
- B.Add disk space, then fix why WAL is kept, e.g. a stuck slot
- C.Run pg_resetwal so recovery is skipped
- D.Delete the newest pg_wal files, since older ones are archived
Show answer
Answer: B. Add disk space, then fix why WAL is kept, e.g. a stuck slot
WAL files may still be needed for crash recovery, so deleting them by hand can make the cluster unrecoverable, and pg_resetwal can silently corrupt data. Free space safely, start the server, then fix why WAL was kept, such as an inactive replication slot or a broken archive_command.
53. Unlogged table empty after a crash
A team made a staging table UNLOGGED to load faster. After a server crash and restart the table is empty, and a replica never had its data. Why?
- A.A crash always truncates every table in the database
- B.Replicas copy unlogged tables only once a day
- C.Unlogged tables skip WAL, so they are reset after a crash
- D.The table was not vacuumed before the crash
Show answer
Answer: C. Unlogged tables skip WAL, so they are reset after a crash
UNLOGGED tables do not write WAL, which makes writes faster but means they cannot be recovered or replicated. After a crash PostgreSQL truncates them to avoid partial data. Use them only for data you can rebuild.
54. Index-only scans slow on an insert-only table
On PostgreSQL 12, an insert-only audit table never had autovacuum run, and index-only scans keep visiting the heap. What changed in PostgreSQL 13 to address this?
- A.Index-only scans no longer check visibility
- B.Insert-only tables are frozen at insert time
- C.Autovacuum now also runs after many inserts
- D.The visibility map is kept by ANALYZE only
Show answer
Answer: C. Autovacuum now also runs after many inserts
Before 13, autovacuum vacuum runs were triggered only by dead tuples, so insert-only tables were never vacuumed except for wraparound. Without vacuum, pages are not marked all-visible and index-only scans must check the heap. autovacuum_vacuum_insert_threshold and scale factor fix this.
55. Standbys after pg_upgrade --link
You ran pg_upgrade --link on the primary of a cluster with two physical standbys. What is the documented way to upgrade the standbys quickly?
- A.Start them on the new binaries; they catch up from WAL
- B.Run pg_upgrade on each standby on its own
- C.Convert them to logical replicas automatically
- D.Use the rsync method from the docs before starting the primary
Show answer
Answer: D. Use the rsync method from the docs before starting the primary
Physical standbys cannot replay WAL across a major version change. The pg_upgrade docs describe using rsync with --hard-links from the old and new directories on the primary, run before the new primary starts, so standbys are updated quickly. Otherwise you rebuild them with a fresh base backup.
56. Phantoms at PostgreSQL Repeatable Read
The SQL standard allows phantom reads at Repeatable Read. What does PostgreSQL's Repeatable Read do when another transaction inserts and commits a matching row?
- A.The new row appears in the next SELECT
- B.The new row stays invisible for the whole transaction
- C.The second SELECT fails with an error
- D.The insert is blocked until the reader commits
Show answer
Answer: B. The new row stays invisible for the whole transaction
PostgreSQL's Repeatable Read is snapshot isolation: every query in the transaction sees the same snapshot, so newly committed rows never appear. This exceeds the standard's minimum. It still allows write skew, which only Serializable prevents.
57. How readers know a row committed
A row's xmin is transaction 9001. How does a reader in PostgreSQL find out whether 9001 committed or aborted?
- A.It reads the WAL from the start
- B.From pg_xact, or the row's hint bits
- C.It asks the transaction's backend process
- D.It assumes commit if the row is on disk
Show answer
Answer: B. From pg_xact, or the row's hint bits
PostgreSQL records each transaction's final status in the commit log, stored in pg_xact. Readers check it, then set hint bits on the row so later readers can skip the lookup. Rows from aborted transactions stay on disk until vacuum removes them.
58. Median per department in PostgreSQL
An analyst writes PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) OVER (PARTITION BY dept) in PostgreSQL and gets an error. Why?
- A.In PostgreSQL it is an ordered-set aggregate, not a window function
- B.PERCENTILE_CONT only accepts 0.25 and 0.75
- C.It needs ROWS BETWEEN UNBOUNDED PRECEDING in the OVER
- D.Median cannot be computed on salary data types
Show answer
Answer: A. In PostgreSQL it is an ordered-set aggregate, not a window function
PostgreSQL supports PERCENTILE_CONT only with GROUP BY, not with OVER. Compute the median per dept in a grouped CTE and join it back to the rows. SQL Server and some warehouses do allow it with OVER (PARTITION BY ...), so syntax that works there may fail here.
59. VACUUM and time travel
A Delta table runs VACUUM daily with the default 7-day retention. An auditor asks to query the table as it was 30 days ago. What happens?
- A.The query fails or is incomplete, because data files needed by that old version were already deleted
- B.It works, because the transaction log keeps all data forever
- C.Delta automatically restores the files from the cloud provider
- D.It works, but only for tables smaller than 1 GB
Show answer
Answer: A. The query fails or is incomplete, because data files needed by that old version were already deleted
Time travel needs both the log entries and the data files of the old version. VACUUM physically deletes files no longer referenced by versions newer than the retention period. Longer history means longer retention settings for files and logs, and higher storage cost.
60. A buggy upcoming-renewals query
This PostgreSQL query should list active contracts ending in the next 90 days, but it returns contracts that ended last year. What is the bug?
SELECT account_name, end_date, arr
FROM contracts
WHERE status = 'active'
AND end_date <= CURRENT_DATE + INTERVAL '90 days'
ORDER BY end_date;
- A.INTERVAL cannot be added to CURRENT_DATE
- B.No lower bound, so past end dates also match
- C.ORDER BY must come before WHERE
- D.The status filter removes every renewal
Show answer
Answer: B. No lower bound, so past end dates also match
The condition only says "end date is before 90 days from now", which every past date also meets. Adding `AND end_date >= CURRENT_DATE` limits it to the coming window; the past rows also show stale data where expired contracts were never marked inactive.