Re: Performance Degradation (Table becomes bloat) During Repeated Bulk UPDATE Operations

From: Bharath Rupireddy <bharath(dot)rupireddyforpostgres(at)gmail(dot)com>
To: Jeyaprakash Rajamani <jeyaprakash(dot)rajamani(at)chainsys(dot)com>
Cc: pgsql-hackers(at)lists(dot)postgresql(dot)org
Subject: Re: Performance Degradation (Table becomes bloat) During Repeated Bulk UPDATE Operations
Date: 2026-06-19 02:21:15
Message-ID: CALj2ACWuuNSHhDtQcGAk4TOBo7hv0LFU_ja_V-cBK-bRE_0TMw@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

On Thu, Jun 18, 2026 at 10:41 AM Jeyaprakash Rajamani <
jeyaprakash(dot)rajamani(at)chainsys(dot)com> wrote:
>
> Vacuum verbose doesn't show anything in detail. It only shows 'vacuuming
table_name'.

It means the vacuum is still running.

FWIW, [1] is how the output looks when the vacuum is able to remove dead
rows. [2] is how the output looks when the vacuum is NOT able to remove
dead rows. The interesting pieces to look for are below. In this case, I
started a long-running transaction with txn-id = 716 in one session,
updated the rows in another transaction with txn-id = 717 in a second
session, then ran vacuum - it says that any dead rows with xmax < 716 are
removable, but the update created dead rows with xmax = 717 and the
long-running transaction (which started before the update) may still
reference those rows, so vacuum will NOT remove them.
tuples: 0 removed, 221478602 remain, 12950000 are dead but not yet removable
removable cutoff: 716, which was 3 XIDs old when operation ended
new relfrozenxid: 716, which is 1 XIDs ahead of previous value

It will be helpful to read through the vacuum documentation
https://www.postgresql.org/docs/current/routine-vacuuming.html and try a
few experiments on the test environments. It makes these concepts much
easier to reason about and understand the issue you are facing.

Also, it is worth looking at the vacuum/autovacuum configuration
parameters:
https://www.postgresql.org/docs/current/runtime-config-vacuum.html
[1]
postgres=# vacuum (verbose, analyze) foo;
INFO: vacuuming "postgres.public.foo"
INFO: finished vacuuming "postgres.public.foo": index scans: 0
pages: 0 removed, 541593 remain, 320165 scanned (59.12% of total), 0
eagerly scanned
tuples: 10391455 removed, 63587201 remain, 0 are dead but not yetremovable
removable cutoff: 710, which was 0 XIDs old when operation ended
new relfrozenxid: 710, which is 16 XIDs ahead of previous value
frozen: 320165 pages from table (59.12% of total) had 61965817 tuples frozen
visibility map: 320165 pages set all-visible, 320165 pages set all-frozen
(0 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item
identifiers removed
avg read rate: 124.141 MB/s, avg write rate: 130.467 MB/s
buffer usage: 335810 hits, 304707 reads, 320235 dirtied
WAL usage: 640390 records, 320240 full page images, 2798348920 bytes,
2613151324 full page image bytes, 15653 buffers full
memory usage: dead item storage 0.02 MB accumulated across 0 resets (limit
64.00 MB each)
system usage: CPU: user: 4.25 s, system: 2.31 s, elapsed: 19.17 s
INFO: analyzing "public.foo"
INFO: "foo": scanned 30000 of 541593 pages, containing 5654455 live rows
and 0 dead rows; 30000 rows in sample, 102080442 estimated total rows
INFO: finished analyzing table "postgres.public.foo"
avg read rate: 995.019 MB/s, avg write rate: 0.171 MB/s
buffer usage: 935 hits, 29166 reads, 5 dirtied
WAL usage: 5 records, 4 full page images, 25816 bytes, 25324 full page
image bytes, 0 buffers full
system usage: CPU: user: 0.10 s, system: 0.11 s, elapsed: 0.22 s
VACUUM
postgres=#

[2]
postgres=# vacuum (verbose, analyze) foo;
INFO: vacuuming "postgres.public.foo"
INFO: finished vacuuming "postgres.public.foo": index scans: 0
pages: 0 removed, 1004868 remain, 780310 scanned (77.65% of total), 0
eagerly scanned
tuples: 0 removed, 221478602 remain, 12950000 are dead but not yet removable
removable cutoff: 716, which was 3 XIDs old when operation ended
new relfrozenxid: 716, which is 1 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were
all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item
identifiers removed
avg read rate: 86.102 MB/s, avg write rate: 82.757 MB/s
buffer usage: 796234 hits, 764731 reads, 735024 dirtied
WAL usage: 735024 records, 735024 full page images, 6033799850 bytes,
5997783604 full page image bytes, 13243 buffers full
memory usage: dead item storage 0.02 MB accumulated across 0 resets (limit
64.00 MB each)
system usage: CPU: user: 6.71 s, system: 8.01 s, elapsed: 69.38 s
INFO: analyzing "public.foo"
INFO: "foo": scanned 30000 of 1004868 pages, containing 6076054 live rows
and 397314 dead rows; 30000 rows in sample, 203521074 estimated total rows
INFO: finished analyzing table "postgres.public.foo"
avg read rate: 862.319 MB/s, avg write rate: 0.029 MB/s
buffer usage: 465 hits, 29581 reads, 1 dirtied
WAL usage: 2 records, 1 full page images, 1491 bytes, 1168 full page image
bytes, 0 buffers full
system usage: CPU: user: 0.12 s, system: 0.11 s, elapsed: 0.26 s
VACUUM
postgres=#

--
Bharath Rupireddy
Amazon Web Services: https://aws.amazon.com

In response to

Responses

Browse pgsql-hackers by date

  From Date Subject
Next Message Richard Guo 2026-06-19 03:03:13 Re: assertion failure with unique index + partitioning + join
Previous Message Jeff Davis 2026-06-19 02:13:38 Re: Avoid orphaned objects dependencies, take 3