| From: | Michael Paquier <michael(at)paquier(dot)xyz> |
|---|---|
| To: | Zhijie Hou <houzhijie22(at)gmail(dot)com> |
| Cc: | shihao zhong <zhong950419(at)gmail(dot)com>, Takeshi Ideriha <iderihatakeshi(at)gmail(dot)com>, Ayush Tiwari <ayushtiwari(dot)slg01(at)gmail(dot)com>, alpkeles99(at)gmail(dot)com, pgsql-bugs(at)lists(dot)postgresql(dot)org |
| Subject: | Re: BUG #19760: CLUSTER loses a row the transaction inserted and then updated in a rolled-back subtransaction |
| Date: | 2026-10-22 02:22:08 |
| Message-ID: | atlzUChbZatun_HQ@paquier.xyz |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-bugs |
On Wed, Oct 21, 2026 at 06:01:28PM +0800, Zhijie Hou wrote:
> + if (!(tuple->t_infomask & HEAP_XMAX_INVALID) &&
> + !HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask) &&
> + !HeapTupleHeaderIsOnlyLocked(tuple))
> + {
> + TransactionId xmax = HeapTupleHeaderGetUpdateXid(tuple);
> +
> + if (!TransactionIdIsInProgress(xmax) &&
> + !TransactionIdDidCommit(xmax))
> + SetHintBits(tuple, buffer, HEAP_XMAX_INVALID,
> + InvalidTransactionId);
>
> If a tuple's Xmax is a MultiXact and the updating subtransaction is still in
> progress when HeapTupleHeaderIsOnlyLocked() is called, it returns false.
> However, if that updating subtransaction aborts before the second
> TransactionIdIsInProgress() check, the second check returns false and sets
> HEAP_XMAX_INVALID on the tuple even though the MultiXact still has active
> members, which I think violates the MultiXact hint-bit standard.
Perhaps also check for HEAP_XMAX_IS_MULTI to skip the multixacts and
make sure that active members are not lost, then? I am not completely
sure if this would be always right, though, these code paths are the
hard-level definition of tricky.
What I am really wondering is why do we actually need to fix *two*
problems at once? The first SetHintBits() looks like a no-brainer to
me, and links to the second permutation in the test "s1_plain
s1_count" where we do a CLUSTER after an UPDATE rollbacked to a
previous savepoint. I don't think that there is a need for an
isolation test on this one, either, wouldn't a plain SQL sequence be
sufficient?
That's not to mention that the first one can be hit with DDLs that do
some update on catalogs, as well. Say, an ANALYZE with a CLUSTER
(this SQL sequence is a courtesy of Claude, I've just pointed to it
the possibility and the agent has confirmed the sequence):
BEGIN;
-- force the index-scan path
SET LOCAL random_page_cost = 0.0001;
SET LOCAL seq_page_cost = 1000;
CREATE TABLE s (a int);
INSERT INTO s VALUES (1);
ANALYZE s;
SAVEPOINT sp;
INSERT INTO s SELECT g FROM generate_series(2, 1000) g;
-- updates pg_statistic row
ANALYZE s;
ROLLBACK TO sp;
RELEASE sp;
CLUSTER VERBOSE pg_statistic USING pg_statistic_relid_att_inh_index;
COMMIT;
-- one row expected, none on HEAD.
SELECT count(*) FROM pg_statistic WHERE starelid = 's'::regclass;
The VACUUM FULL on pg_class is less exciting, points to the second
SetHintBits(). And as far as I can see, this one is actually the
source of a measurable regression. To prove my point, see the SQL
scripts attached. Use them like that:
1) Run setup.sql, for some relations.
2) Run holders.sql through a bunch of sessions (64 here, could be
more). This is important to stress TransactionIdIsInProgress():
for i in $(seq 64); do
psql -X -f holders.sql > /dev/null 2>&1 &
done
3) In a new session, run either subxact_updates.sql, delete.sql or
insert_only.sql.
3) Run vacuum.sql, measure its runtime.
Then with 500k rows, here are some VACUUM average runtimes in ms from
one of my laptops:
HEAD Shihao's v2
subxact_updates 1408 2123
delete 686 1369
insert_only 689 693
The subxact_updates is an edge case, fine, still it regresses badly.
The DELETE case is a much more common case. It regresses even more
badly, and that does not look acceptable to me. That should be
optimizable, of course, I don't really know yet how to do that. I
know however that regressions like these ones are never good.
So the first step to me would be a "Divide and Conquer" strategy,
where we should look at each problem independently instead of trying
to do too much at once.
--
Michael
| Attachment | Content-Type | Size |
|---|---|---|
| setup.sql | application/sql | 152 bytes |
| delete.sql | application/sql | 193 bytes |
| insert_only.sql | application/sql | 145 bytes |
| subxact_updates.sql | application/sql | 412 bytes |
| vacuum.sql | application/sql | 173 bytes |
| holders.sql | application/sql | 346 bytes |
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Takeshi Ideriha | 2026-10-22 02:53:29 | Re: BUG #19760: CLUSTER loses a row the transaction inserted and then updated in a rolled-back subtransaction |
| Previous Message | Richard Guo | 2026-10-22 01:50:39 | Re: BUG #19765: Wrong result from a chain of LEFT JOINs since v16 |