Re: Potential "AIO / io workers" inter-worker locking issue in PG18?
Marco Boeringa <marco@boeringa.demon.nl>
From: Marco Boeringa <marco@boeringa.demon.nl>
To: Andres Freund <andres@anarazel.de>
Cc: pgsql-bugs@lists.postgresql.org, Thom Brown <thom@linux.com>
Date: 2025-10-08T20:22:48Z
Lists: pgsql-bugs
I noticed the formatting of the last email was totally screwed when displayed on postgresql.org's mail archive making it hard to read there, so a re-post of the last email, hopefully it will be better this time. Answers are intermingled with all the quotes, read carefully. Op 8-10-2025 om 22:09 schreef Marco Boeringa: > > Hi Andres, > Even just knowing whether the "normal query plan" is the > same one as we see in > profiles of "stuck" backends is valuable. Even > if the query plan is perfectly > normal, it *still* is very important > to know in which order the joins are > evaluated etc. But there also > might be changes in the query plan between 17 > and 18 that trigger > the issue... > > Without more details about what is expected to be run > and what is actually > happening, it's just about impossible for us to > debug this without a > reproducer that we can run and debug ourselves. > I now encountered the auto_explain option in the PostgreSQL help. May > sound stupid, but I hadn't been aware of this option. This might help > in getting an explain during the actual execution of my tool, if I > understand the option properly. This would be far more valuable - as > being the "real" thing - than some contrived reproduction case. I will > need to investigate this a bit more: > https://www.postgresql.org/docs/current/auto-explain.html >>> Making > vacuum more aggressive won't really help much if you have >> > longrunning queries/sessions, since vacuum can't clean up row versions > that >> are still visibile to some of the transactions. >> >> My code > batches the updates in sets of 2000 records at a time and then >> > COMMITs, so the transactions themselves are limited in time and size, > which >> should allow vacuum to do its job. > > Are the "stuck" > backends stuck within one 2000 record batch, or are they > "just" > slower processing each batch? I can't tell. But to explain: each > thread has its own set of jobs assigned, and each job will be batched > in sets of 2000 records until COMMIT. So if one job has 100k records > to process, 50 commits should occur for that job by one Python thread. > I take care to avoid to process records totally randomly, which could > cause conflicts and locking issues between threads attempting to > access the same locked database page, significantly slowing down the > processing. Records are assigned by database page (and depending on > some other parameters), which has worked really well so far. Note that > this is just a simplified version of the different processing modes I > developed for different challenges and geoprocessing steps. >> 26.48% > postgres postgres [.] LWLockAttemptLock >> | >> ---LWLockAttemptLock > >> | >> |--23.15%--heapam_index_fetch_tuple.lto_priv.0 >> | > index_fetch_heap >> | IndexNext >> | ExecScan >> | ExecNestLoop >> | > ExecNestLoop >> | ExecModifyTable >> | standard_ExecutorRun >> | > ProcessQuery > > So the query plan we have is a nested loop between at > least three tables > (there are two joins, c.f. the two ExecNestLoop > calls), where there presumably > are a lot of row [versions] on the > inner side of the innermost join. > > In [1] you showed a query. > Reformated that looks like this: > > UPDATE > osm.landcover_scrubs_small_scale_2_ply AS t1 > SET area_geo = > t2.area_geo, > perim_geo = t2.perim_geo, > compact_geo = CASE WHEN > t2.area_geo > 0 THEN ((power(t2.perim_geo,2) / t2.area_geo) / (4 * > pi())) ELSE 0 END, > npoints_geo = t2.npoints_geo, > comp_npoints_geo > = CASE WHEN t2.npoints_geo > 0 THEN (CASE WHEN t2.area_geo > 0 THEN > ((power(t2.perim_geo,2) / t2.area_geo) / (4 * pi())) ELSE 0 END / > t2.npoints_geo) ELSE 0 END, > convex_ratio_geo = CASE WHEN > ST_Area(ST_ConvexHull(way)::geography,true) > 0 THEN (t2.area_geo / > ST_Area(ST_ConvexHull(way)::geography,true)) ELSE 1 END > FROM ( > > SELECT > objectid, > ST_Area(way::geography,true) AS area_geo, > > ST_Perimeter(way::geography,true) AS perim_geo, > ST_NPoints(way) AS > npoints_geo > FROM osm.landcover_scrubs_small_scale_2_ply) AS t2 > > WHERE (t2.objectid = t1.objectid) > AND t1.objectid IN (SELECT > t3.objectid FROM mini_test.osm.osm_tmp_28128_ch5 AS t3) > > > Which > certainly fits with two nested loops, although I don't think I can > infer > which order it the joins are in. > > > Is > osm.landcover_scrubs_small_scale_2_ply.object_id unique? Yes. > Can > there be multiple rows for one object_id in > > mini_test.osm.osm_tmp_28128_ch5? No. This table contains the records > to process, which are unique. It is the job. > > It is a one-to-one join. > > > Are there indexes on mini_test.osm.osm_tmp_28128_ch5.unique_id and > osm.landcover_scrubs_small_scale_2_ply? Yes, the unique ids / > objectid fields are indexed to allow an efficient join. >