skip to content

A reporting query has been running for six hours, and another session has sat idle inside an open transaction all morning. What does that do to version cleanup, undo or rollback-segment growth, and table size, and how would you diagnose and prevent it?

level: seniorimportance: must knowfreq 48%

answer

  1. Both pin the horizon; cleanup frees nothing
  2. Undo growth + long version chains vs heap/index bloat
  3. Sort sessions by transaction age, check state
  4. Hidden holders: prepared txns, replication slots
  5. idle_in_transaction timeout + chunked batches

basics

~20 s

Both pin the cleanup horizon, so no version expired since they started can be reclaimed anywhere. Undo segments grow, tables and indexes bloat, version chains lengthen and reads slow down. Diagnose by finding the oldest transaction and its state; prevent with idle-in-transaction and statement timeouts, chunked batches, and monitoring oldest-transaction age.

solid answer

~50 s

Both sessions hold the visibility horizon at the moment they started. Cleanup keeps running but reclaims nothing, so: - **Undo-based engines**: undo/rollback segments grow without bound; reading a hot row means walking a long version chain, so read latency rises even for untouched tables, and in the worst case undo space exhaustion stops writes. - **Append-style engines**: dead tuples pile up in heaps and indexes; tables grow, scans read mostly garbage pages, the buffer pool caches dead data, and plans degrade. **Diagnose** by listing transactions by age with their state — a session `idle in transaction` is the classic culprit — and by checking for abandoned prepared (two-phase) transactions and stale replication slots, which survive restarts. Compare the oldest transaction's start time to when growth began. **Prevent**: idle-in-transaction timeout, statement timeout on OLTP paths, never hold a transaction across an external call, chunk long batch jobs into many short transactions, run heavy reports on a replica, and alert on oldest-transaction age rather than waiting for disk alerts.

code

text · 9 lines
text
cleanup ran 12 times in the last hour   -> it is not starved
dead version estimate: 41M -> 41M        -> nothing was reclaimable
oldest transaction age: 6h03m (active)   -> horizon is pinned

vs.

cleanup ran 1 time in the last hour      -> throttled/starved
dead version estimate: 12M -> 30M        -> churn outruns capacity
oldest transaction age: 40s              -> horizon is fine

go deeper

for a junior

Say that a long-open transaction stops old versions from being cleaned up, so the database grows, and that transactions should be kept short.

for a middle

Name both symptom families — undo growth versus heap and index bloat — and the standard prevention: short transactions and timeouts.

for a senior

Lead with the diagnostic sequence, distinguish pinned horizon from insufficient cleanup capacity by evidence, and prescribe timeouts, chunked batches and oldest-transaction-age alerting.

for a principal

Make bounded transaction lifetime a platform invariant enforced by role-level timeouts and CI/code review, and reason explicitly about the replica-feedback and reporting-workload placement trade-offs.

## The mechanism in one line Both sessions have a snapshot, so the engine must keep every row version that any snapshot that old could need. Cleanup cannot advance past them, and every table in the database pays. ## What you observe, by engine family **Undo-based (InnoDB, Oracle).** The current row lives in place; older versions are reconstructed from undo. While an old read view exists, undo cannot be purged, so: - the undo tablespace or rollback segments grow steadily; - **version chains lengthen**: a row updated 200,000 times since the old snapshot started requires walking many undo records to build any older view, and even ordinary reads pay more; - delete-marked index records are not purged, so indexes are scanned with dead entries in them; - in the extreme, undo space is exhausted and DML starts failing, which is a full write outage. A notorious pattern is a single hot counter row updated thousands of times a second while a long report runs: the chain for that one row grows so long that reads on it collapse. **Append-style (PostgreSQL-style heaps).** Dead tuples accumulate in the table's pages and in every index: - table and index files grow continuously even at constant row count; - sequential scans read mostly dead tuples, so I/O per useful row rises; - the buffer pool fills with pages of garbage, evicting useful data; - autovacuum wakes, scans, finds everything above the horizon, and exits with nothing freed — repeatedly, burning I/O for no benefit; - in the extreme, transaction-id wraparound protection kicks in and the engine takes aggressive measures to prevent data loss, up to refusing new write transactions. ## Diagnosis, in order 1. **Find the oldest transaction and its state.** Sort sessions by transaction start time. Record: age, state (`active` vs `idle in transaction`), the application name, the client host and the current or last statement. This one query usually names the culprit. 2. **Check the non-obvious horizon holders**, because they do not appear as ordinary sessions: abandoned **prepared/two-phase transactions** (they survive restarts and are invisible in most dashboards) and **stale replication slots** or a replica with feedback enabled running a long report. 3. **Correlate timelines.** Growth that began exactly when the oldest transaction started is conclusive; growth from a genuinely higher write rate is not the same problem. 4. **Confirm the cleanup side.** Look at dead-version estimates and last-cleanup timestamps: cleanup *running* but dead counts *not falling* is the signature of a pinned horizon, as opposed to cleanup being starved of I/O or throttled too aggressively. 5. **Distinguish the two failure modes.** Pinned horizon means cleanup cannot free anything. Insufficient cleanup capacity means it could, but is not keeping up with churn. The fixes are opposite — kill a session versus tune concurrency and throttling — so getting this wrong wastes an outage. ## Remediation Short term: end the offending transaction. Cancel the query or terminate the session — after checking what it is, because killing a legitimate six-hour month-end report has its own cost. Once the horizon advances, cleanup can reclaim; note that reclaimed space is returned for reuse inside the object, so files will not shrink and the table will simply stop growing. Medium term, the controls that actually prevent recurrence: - **`idle_in_transaction_session_timeout`** (or its equivalent) — the single highest-value setting, since idle transactions have no legitimate reason to persist. - **Statement timeout** on OLTP roles, with a separate, more permissive role for reporting so one policy does not have to serve both. - **Transaction hygiene in application code**: begin late, commit early, and never hold a transaction across an HTTP call, a queue publish, or user think time. Autocommit-by-default plus explicit short transactions beats a framework that silently opens one per request. - **Chunked batch jobs**: delete or backfill in bounded batches, committing between them. One 50-million-row transaction pins the horizon for hours *and* produces a mountain of garbage at once; 5,000 batches of 10,000 rows let cleanup interleave and keep the horizon young. - **Move long reads off the primary**, accepting that if you enable replica feedback to stop query cancellation you have re-coupled the primary's cleanup to the replica's longest query — a deliberate trade, not a free win. - **Monitor oldest-transaction age** with an alert well before the pain (say 15 minutes for OLTP), plus undo size or dead-tuple ratio. Age is the leading indicator; disk usage is lagging. ## The framing that impresses Say explicitly that a long transaction is not a local cost to its own session — it is a **global tax on reclamation for the whole database**, and that the durable fix is bounding transaction lifetime by policy rather than hunting offenders one at a time.

  • Cleanup is running frequently but the dead-version count keeps rising. What does that tell you, and what would you check next?
    It rules out cleanup being starved or throttled: the worker is getting scheduled but finds nothing reclaimable, which points at a pinned visibility horizon. Next, find the oldest transaction by start time and its state, then check the non-session holders — abandoned prepared transactions, stale replication slots, and replicas with feedback enabled running long queries.
  • A nightly job deletes 50 million rows in a single transaction. Why is that doubly harmful, and what is the better shape?
    It pins the horizon for the whole run, so nothing anywhere in the database can be reclaimed meanwhile, and it also produces 50 million dead versions in one burst that cleanup must then chase. The better shape is bounded batches — delete a few thousand rows per transaction with a commit between them — so the horizon stays young and cleanup interleaves. Better still, partition by time and drop whole partitions, which frees space immediately as a metadata operation.
  • Enabling replica feedback stops long reports on the standby from being cancelled. What is the cost?
    You are deliberately extending the primary's cleanup horizon to cover the replica's oldest snapshot, so a six-hour report on the standby now blocks reclamation on the primary exactly as if it ran there. It is a legitimate trade when report completion matters more than bloat, but it must be a conscious choice with monitoring on the replica's longest query.

saying these in an interview costs you the question

  • Blaming cleanup tuning when the real cause is a pinned horizon
  • Ignoring idle-in-transaction sessions because they use no CPU
  • Assuming a restart clears the problem — prepared transactions and replication slots survive it
  • Expecting files to shrink once the blocking transaction ends
  • Running huge single-transaction batch deletes and treating the resulting bloat as unavoidable

context