| From: | Wei Sun <936739278(at)qq(dot)com> |
|---|---|
| To: | Osama Abdul Qader <osamaabdulqader(dot)cs(at)gmail(dot)com> |
| Cc: | pgsql-hackers <pgsql-hackers(at)postgresql(dot)org> |
| Subject: | Re: Severe performance degradation with concurrent updates due to excessive EvalPlanQual (EPQ) re‑evaluation |
| Date: | 2026-09-15 12:59:33 |
| Message-ID: | tencent_9C21F023220622D80C3FDB5E95063A2A3908@qq.com |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
Hi
Thanks for your reply.
At first, I suspected that for every row with an update conflict,
the subquery would be executed tofetches the new tuple version.
Because from the stack, some nodes obviously should not be called recursively.
But from the degree of slowing down, it doesn't seem to be like that.
The number of conflicting rows and different join operator will have
an impact on the degree of slowing down. especially when the subquery
needs to write temporary files, the slowdown will be even more severe.
I am currently trying to create different scenarios locally,
and if I make any new discoveries, I will also synchronize with you.
Regards,
Wei Sun
原始邮件
发件人:Osama Abdul Qader <osamaabdulqader(dot)cs(at)gmail(dot)com>
发件时间:2026年9月15日 18:49
收件人:Wei Sun <936739278(at)qq(dot)com>
抄送:pgsql-hackers <pgsql-hackers(at)postgresql(dot)org>
主题:Re: Severe performance degradation with concurrent updates due to excessive EvalPlanQual (EPQ) re‑evaluation
Hi again,
I was able to reproduce the reported slowdown locally on PostgreSQL 20devel.
In my reproduction, the second concurrent UPDATE initially waits on a transactionid lock. After the first transaction commits, the second UPDATE completes but can take tens of seconds. For example, with 10,000 conflicting rows I measured 69.57 seconds. The original query shape with the subquery took approximately 102 seconds in another run.
I also tested a simplified form using WHERE id <= N, which still exhibits a significant slowdown. This suggests that the subquery may amplify the issue but is not necessarily the sole cause.
I am currently instrumenting the ModifyTable UPDATE path around table_tuple_lock() and EvalPlanQual() to determine where the post-lock-release time is actually being spent.
I have not yet determined whether this is an EPQ issue or another executor/locking-related performance problem. I wanted to share the reproduction and preliminary observations before proceeding further.
On Tue, Sep 15, 2026 at 2:45 PM Osama Abdul Qader <osamaabdulqader(dot)cs(at)gmail(dot)com> wrote:
Hi,
I'm interested in working on the bug, I'll let you know once I finish reproducing it.
With Regards,
Osama Abdul Qader
On Tue, Sep 15, 2026 at 1:36 PM Wei Sun <936739278(at)qq(dot)com> wrote:
Hi hackers,
I encountered a serious performance regression when running concurrent
UPDATE statements targeting the same set of rows on PostgreSQL 18.1.
The second update session runs extremely slow due to excessive
EvalPlanQual (EPQ) re‑evaluation logic.
## Test setup
Create test table and populate 1000000 rows of mock bond trading data,
no user‑defined indexes (only identity primary key on `id`).
Then create a copy table `bond_deal_detail_sw` for concurrent update tests.
This issue occurs when read committed isolation level.
```sql
DROP TABLE IF EXISTS bond_deal_detail;
CREATE TABLE bond_deal_detail (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
deal_no TEXT,
bond_code TEXT,
bond_name TEXT,
trade_date DATE,
trade_time TIME,
buy_inst TEXT,
sell_inst TEXT,
deal_amt NUMERIC(20,4),
deal_price NUMERIC(12,6),
yield_rate NUMERIC(10,6),
trade_type TEXT,
settle_date DATE,
create_at TIMESTAMP
);
INSERT INTO bond_deal_detail(
deal_no, bond_code, bond_name, trade_date, trade_time,
buy_inst, sell_inst, deal_amt, deal_price, yield_rate,
trade_type, settle_date, create_at
)
SELECT
'DEAL' || LPAD(i::TEXT,10,'0'),
'10' || LPAD((i % 99999)::TEXT,8,'0'),
'SimBond_' || (i % 2000),
'2025-01-01'::DATE + (i % 365),
('09:00:00'::TIME + (i % 32400) * INTERVAL '1 second'),
'Inst_' || (i % 1500),
'Inst_' || ((i + 777) % 1500),
(random() * 500000000)::NUMERIC(20,4),
(90 + random() * 20)::NUMERIC(12,6),
(1.5 + random() * 3.5)::NUMERIC(10,6),
CASE WHEN i % 5 = 0 THEN 'Repo' ELSE 'SpotBond' END,
'2025-01-01'::DATE + (i % 365) + (CASE WHEN i%5=0 THEN 1 ELSE 0 END),
NOW()
FROM generate_series(1,1000000) AS t(i);
-- create working table for concurrent update
CREATE TABLE bond_deal_detail_sw(LIKE bond_deal_detail);
INSERT INTO bond_deal_detail_sw SELECT * FROM bond_deal_detail;
## Concurrent reproduction steps
Open two independent sessions and run below UPDATE SQL
simultaneously against table `bond_deal_detail_sw`.
Both queries try to update the same top‑100 000 rows derived
from the source table `bond_deal_detail`.
Session 1:
EXPLAIN ANALYZE UPDATE bond_deal_detail_sw
SET deal_price = 26915
WHERE deal_no IN (SELECT deal_no FROM bond_deal_detail ORDER BY deal_no LIMIT 100000);
Session 2 (run concurrently with session 1):
EXPLAIN ANALYZE UPDATE bond_deal_detail_sw
SET deal_price = 26915
WHERE deal_no IN (SELECT deal_no FROM bond_deal_detail ORDER BY deal_no LIMIT 100000);
1. Session 1 executes quickly, it locks and updates those 100 000 target rows.
2. Session 2 blocks waiting for row‑level locks. After session 1 commits,
session 2 resumes execution but becomes extremely slow.
3. From execution plan and trace, the slowdown comes from massive EvalPlanQual (EPQ) re‑evaluation:
for each row already modified and committed by transaction 1,
PostgreSQL fetches the new tuple version and re‑evaluates the whole sub‑query / qual tree for EPQ.
Even though the new value of `deal_price` is a constant literal (`SET deal_price = 26915`)
and does not depend on original row values, heavy EPQ overhead still occurs for every conflicting row.
since the target update value is a constant and does not reference any column of the updated table,
logically there is no need to recompute the target new value via EPQ for these rows.
But PostgreSQL still triggers full EPQ re‑evaluation for every updated tuple,
leading to huge overhead and long elapsed time for the second concurrent transaction.
Expectation / question
When the updated assignment is pure constant and does not reference
any columns from the target relation, could PostgreSQL skip the expensive EPQ re‑computation
for the new target value, even though it still needs to check row visibility and tuple versions?
Best regards,
Wei Sun
| From | Date | Subject | |
|---|---|---|---|
| Next Message | 신성준 | 2026-09-15 12:59:35 | Re: Add wait events for server logging destination writes |
| Previous Message | Nazir Bilal Yavuz | 2026-09-15 12:51:41 | Re: Add ASCII fast path to Unicode normalization functions |