Severe performance degradation with concurrent updates due to excessive EvalPlanQual (EPQ) re‑evaluation - #437
pg-hub-mirror[bot] wants to merge 1 commit into
Conversation
|
Earlier design discussion: Discussion #339 |
|
Wei Sun <936739278(at)qq(dot)com> via pgsql-hackers · original email Hi The subquery reads from
Test setupCreate test table and populate 1000000 rows of mock bond trading data, 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
<!-- pg_hub:message=51d0a4ee204bea729918fc16cb935d48135c8dd7d59577dccbcd7579aef039f6 --> |
pgsql-hackersCAC+8b5jknFT31LKSjZdac47Daw5f5=9MwJzcvZq4fNjZzdgwfQ@mail.gmail.comPatch files:
Hi again,
I have been investigating the reported regression involving UPDATE
execution and EvalPlanQual (EPQ), and I wanted to share my findings.
I reproduced the issue locally using the bond_deal_detail /
bond_deal_detail_sw reproducer and traced the relevant execution path
through the executor.
The affected path in nodeModifyTable.c is the TM_Updated case in
ExecUpdate(). When the tuple has been concurrently updated, PostgreSQL:
ExecUpdate(). When the tuple has been concurrently updated, PostgreSQL:
qualifications;
I also traced the EPQ implementation in execMain.c,
including EvalPlanQual(), EvalPlanQualSlot(), EvalPlanQualNext(),
EvalPlanQualBegin(), EvalPlanQualStart(), EvalPlanQualEnd().
In particular, EvalPlanQualNext() switches to the EPQ query context and
invokes ExecProcNode() on the EPQ plan tree. EvalPlanQualStart() creates a
child EState, initializes the required subplans, and initializes the EPQ
plan tree with ExecInitNode().
To get more concrete timing information, I temporarily instrumented the
TM_Updated path in nodeModifyTable.c to measure table_tuple_lock() and
EvalPlanQual() separately. The diagnostic patch is attached
as: epq-instrumentation.patch
The instrumentation produces separate log entries of the form:
EPQ DEBUG: table_tuple_lock took ... ms
EPQ DEBUG: EvalPlanQual took ... ms
This allowed me to distinguish the time spent acquiring/fetching the
latest tuple version from the time spent executing the EPQ recheck
itself.
The relevant source path is approximately:
ExecUpdate()
-> table_tuple_lock()
-> EvalPlanQual()
-> EvalPlanQualBegin()
-> EvalPlanQualNext()
-> ExecProcNode()
I have also verified the patch with git diff --check.
At this point, I believe we have enough evidence to narrow the
investigation to the EPQ/concurrent-update path rather than treating
the overall UPDATE runtime as a single operation. I would appreciate
your thoughts on whether this is the expected execution behavior, and
whether there are particular executor or EPQ areas you would recommend
investigating next.
I have attached the diagnostic patch for reference.
Best Regards:
Osama Abdul Qader
On Wed, Sep 16, 2026 at 12:57 PM Osama Abdul Qader <
osamaabdulqader(dot)cs(at)gmail(dot)com> wrote: