| From: | Floris Van Nee <florisvannee(at)Optiver(dot)com> |
|---|---|
| To: | "pgsql-hackers(at)lists(dot)postgresql(dot)org" <pgsql-hackers(at)lists(dot)postgresql(dot)org> |
| Subject: | OOM kill during planning - pg_terminate_backend is ignored |
| Date: | 2026-09-01 19:53:05 |
| Message-ID: | MRWPR09MB8357932120A9FE5B897AF23CCCA82@MRWPR09MB8357.eurprd09.prod.outlook.com |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
Hi,
We had a backend OOM-killed in production, bringing down the database.
It turned out to be an analytics query generated by an LLM, and nothing
was ever executed - it was an EXPLAIN (FORMAT JSON).
All of the work was in the planner.
It reached 2 TB RSS over about an hour. pg_terminate_backend() was issued more
than 20 minutes before the OOM killer ended it, without effect.
We analyzed the query and since it was generated by an LLM it was obviously
complex and not written very well. Still, it was only ~10kB so it's not outside
of the realm of possibilities that such a query is ever executed by anyone. We
traced the issue to a reproducible scenario. A query with the following pattern
is the problem:
EXPLAIN (SUMMARY ON)
WITH c0 AS (SELECT relpages::float8 AS x FROM pg_class),
c1 AS (SELECT (x + x + x + x) AS x FROM c0),
c2 AS (SELECT (x + x + x + x) AS x FROM c1),
... c11 ...
SELECT x FROM c11;
Planning time grows ~4.2x per level, matching the fanout:
6 levels 7.7 ms
7 levels 31.1 ms
8 levels 134.2 ms
9 levels 613.9 ms
10 levels 2570.7 ms
At 12 levels with statement_timeout = '2s':
t=4s backend active, 100% CPU, 3346 MB (timeout expired 2s ago)
pg_cancel_backend() -> true, ignored
t=8s 100% CPU, 6376 MB
pg_terminate_backend() -> true, then ignored for a further 33.7s
while RSS grew to 25.7 GB
41s wall in total, for a statement with a 2s timeout
It seems like there's two issues here:
* The query not checking for interrupts from pg_terminate_backend for a long
time - in the real-world case for long enough for the database to get OOM killed.
* A relatively simple query being allowed to consume this much memory in the
first place. Should there be a bound on how much memory a query can consume during
planning? It seems like I can get any arbitrary database to get an OOM kill (if OOM
kill is configured) with a query like this.
Below I've put some analysis from Claude after it ran the reproducer on a local db.
I haven't verified those code paths, but it might help someone on the list here,
so I have included it.
-Floris
From Claude:
There is exactly one CHECK_FOR_INTERRUPTS() on this path, at the top of
pull_up_subqueries_recurse() (prepjointree.c:1278). It fires once per subquery
level, and all the expensive work happens between two consecutive checks. In
total src/backend/optimizer/ contains five: that one, two in predtest.c, and
two in pathnode.c's add_path()/add_partial_path(). src/backend/nodes/ contains
none at all -- no walker, mutator or copy function has one.
Making that one check finer-grained would not be sufficient. Sampling the
stack through one 12-level run shows pull-up finishing with roughly a third of
planning still to go:
t=6s .. 41s pull_up_subqueries_recurse
t=43s .. 50s eval_const_expressions
t=51s .. 53s convert_saop_to_hashed_saop
t=54s .. 56s query_planner
t=58s .. 59s grouping_planner
t=61s .. 62s set_plan_references / fix_scan_expr
(then executor startup)
Everything after pull-up lives in clauses.c, planner.c, planmain.c and
setrefs.c, which have no CHECK_FOR_INTERRUPTS between them, so ~20s of that run
is unresponsive no matter how often the pull-up recursion checks. (gdb attach
inflates the total; the proportions are the point.)
I am not sure what the right fix is. CHECK_FOR_INTERRUPTS() in
expression_tree_walker_impl()/expression_tree_mutator_impl() and
copyObjectImpl() would cover it, but those are relatively hot; an
amortised check every N nodes would be cheaper. Separately,
nothing bounds planner memory in aggregate, so there is no ceiling short of the
OOM killer -- but that seems like its own thread.
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Matheus Alcantara | 2026-09-01 20:10:22 | Re: Show expression of virtual columns in error messages |
| Previous Message | Matheus Alcantara | 2026-09-01 19:37:57 | Re: Enable partitionwise join for partition keys wrapped by RelabelType |