Re: Row pattern recognition

From: Henson Choi <assam258(at)gmail(dot)com>
To: Tatsuo Ishii <ishii(at)postgresql(dot)org>, jian(dot)universality(at)gmail(dot)com
Cc: zsolt(dot)parragi(at)percona(dot)com, sjjang112233(at)gmail(dot)com, vik(at)postgresfriends(dot)org, er(at)xs4all(dot)nl, jacob(dot)champion(at)enterprisedb(dot)com, david(dot)g(dot)johnston(at)gmail(dot)com, peter(at)eisentraut(dot)org, li(dot)evan(dot)chao(at)gmail(dot)com, pgsql-hackers(at)postgresql(dot)org
Subject: Re: Row pattern recognition
Date: 2026-09-17 01:21:27
Message-ID: CAAAe_zCOk51HQRdcvZiactC+ZdMhk--XCdrxv9uKRG+kK9SyWA@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi Tatsuo, Jian,

Following up on the second item in the "Known issues" list from the
last increment: partition tuplestore spills plus FIRST()/PREV(FIRST())
navigation in DEFINE turn quadratic once the partition spills. Here's
the reproduction and root cause.

Reproduction:

SET work_mem='64kB';
SELECT count(*) FROM (
SELECT id, count(*) OVER w cnt
FROM (SELECT g AS id, repeat('x', 50) AS pad, (g % 11) AS v
FROM generate_series(0, 3999) g) t
WINDOW w AS (ORDER BY id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
AFTER MATCH SKIP PAST LAST ROW
PATTERN (S A+) DEFINE S AS TRUE, A AS v >= FIRST(v))
) s WHERE cnt > 0;
RESET work_mem;

work_mem=64kB (Storage: Disk), rows -> elapsed:

2,000 0.84 s
4,000 5.4 s
8,000 24.3 s

4.5-6.4x per doubling. The same 8,000 rows in memory: 2.6 ms -- about
9,000x apart. Two controls rule out the obvious explanations: PREV()
in place of FIRST() on the same data stays at 3.3 ms spilled, and a
non-RPR last_value() reaching an equally distant frame under the same
work_mem costs 12.4 ms. So it isn't spilling itself, and it isn't
distance into the partition; it's specifically FIRST()-style
navigation to a fixed match-start row.

Root cause: once a partition's tuplestore spills, only one disk block
is ever resident in memory for it at a time. Re-reading a fixed row
while another position keeps advancing means walking that single
resident block back to the fixed row and out again every time, and
the cost grows with how far apart the two positions have drifted.

This isn't specific to RPR either -- I confirmed it against unpatched
core. Under work_mem=64kB, a plain `count(*), first_value(v),
last_value(v) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND
UNBOUNDED FOLLOWING)` -- first_value pinned at row 0, last_value at
the partition end -- takes temp read from 6 blocks (count(*) alone)
to 12,017 for the same 2,000 rows, and 8,000 rows goes from 1.9 ms to
24.7 ms just from adding those two fixed-but-distant functions. So
this disk I/O behavior isn't something RPR introduced -- it's a
tuplestore limitation that any query touching more than one position
at once runs into.

Short of a fix, the practical workaround today is to raise work_mem
enough that the whole partition stays in memory -- if it never
spills, none of this applies. Not a real answer for a large
partition, but worth saying plainly since it already makes the
problem disappear for anyone who hits this now.

I'm leaving this as a known limitation. Let me know if you see it
differently.

Best regards,
Henson

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Bharath Rupireddy 2026-09-17 01:37:13 Re: [PATCH] Release replication slot on error in SQL-callable slot functions
Previous Message Richard Guo 2026-09-17 01:14:01 ERROR: too late to create a new PlaceHolderInfo