Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vgVHy-00EPvP-2i for pgsql-hackers@arkaria.postgresql.org; Thu, 15 Jan 2026 21:53:59 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1vgVHx-001POL-2v for pgsql-hackers@arkaria.postgresql.org; Thu, 15 Jan 2026 21:53:58 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vgVEj-001LYO-1e for pgsql-hackers@lists.postgresql.org; Thu, 15 Jan 2026 21:50:38 +0000 Received: from mail-ej1-x62c.google.com ([2a00:1450:4864:20::62c]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.96) (envelope-from ) id 1vgUJD-000d4G-1v for pgsql-hackers@lists.postgresql.org; Thu, 15 Jan 2026 20:51:13 +0000 Received: by mail-ej1-x62c.google.com with SMTP id a640c23a62f3a-b872de50c91so207386466b.2 for ; Thu, 15 Jan 2026 12:51:12 -0800 (PST) ARC-Seal: i=1; a=rsa-sha256; t=1768510269; cv=none; d=google.com; s=arc-20240605; b=McYCBD9iqyJY/JaOTqXyNsjhKtl4q9qetufalG6hpnX3+OTsifBDJlKPJ7yf6NsvhK /01pLvK84/eQlwj3isIGNdDCY5lnUZVOaY2pHzmF+w8NARDawpbfLf+S3QyFth4+Znnt nH4a77AW9dotkM2LOLB9gNQ2Nysq8utvynTuStwyGWhJ740bMOWJTObaEsYK5A/XTjI4 wBdgdR+QfAoNCbV+FSynqbCHR1qtBFX/737JqvfPxBOGSD8XkEzNrjs65Ko5Sqr6WZmb Qa8I5O9QO8C8OAdMze4knI/M0cUayGA/VwbU5g/9I9VwXZ4nDYR4uYUcLTeSffUCa3Cr vt1A== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:dkim-signature; bh=hHYBn5O/WajQjC2CajpMZ3iQdM685XSEDeyHJQn2DKw=; fh=ZShswJfyVFrIfGkzR20XgXXzolYT26qewrCEy3f0Lw0=; b=ShJBHcXKDNATAO0wiPv/bIrMtOyhCChts/t2tygCu4SFOXPhRZwjHrKFCLPAe+0OEG +FsXfEIyT7GOLDKPlA2FUtFPCKrVeMf0oVin9FLtVGW2DviYBrvDRdMwvoHP7zyJ2WyM nCJtcn96kC5iT6ZUxlWI5wdwq4AixZpOCD2zefH83U7lOL/n3EOqxP1lb7Yj6Bm1+T+K 6RrXe/M4HJzgyQJJeLgUSKxXtyEPcO3IeLCB5qgJH1L7eEacD0KmTjGrqzBtDJ+s9K0K okW1ipvPl4/Td5eclEFuKx4ryY109sVAj4Nh3bf0fHNR94JKXEky+a6f3IllX1Q7fpxg EL1Q==; darn=lists.postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1768510269; x=1769115069; darn=lists.postgresql.org; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=hHYBn5O/WajQjC2CajpMZ3iQdM685XSEDeyHJQn2DKw=; b=aRgLGqn/32iPYUkhsKRVzSTKVS1mJJCaB7mAS/CcLJKrBhDvBdPa4pysRRu+P3Cr1v z9zGiefAgsA7XxaSnAqYmddSFBEG5Z0JxszMzeclZtXpXVtygiheKS4KWTCw2wF2ylYd 0BiS0JtxPsDWYJBRHGLcsY7osixqZqUXj81rXlj94qko3fyrgklStEMu8XbhnR26pLJ3 mLr80gs4Oh9ppQAvZU52ibhl3gVE0tNfY1XS6j26ZILcgMJRC/0xhIUHZgeDtqhSA8BQ GSYkNxPK9HlNY/1bETjGRyvv4aky5Ra6ssya4JtvkcxOk2qsDoCa3+8t6uk5pOPKBZ8n HwEg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1768510269; x=1769115069; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=hHYBn5O/WajQjC2CajpMZ3iQdM685XSEDeyHJQn2DKw=; b=uB4yjReSzUBSwR0SQxNgJNX3EQ/m37t+i39BKHMTHrdgQTbpVV5uDKtwcbhj4/BsiF Dh1oP4C7yAofNwrFAQC3gZtdbXHfjrlLZxHoTsyWrW8Gp8rjCqDCbW0RR34rXk24EUb1 5yCEcatJKvOZl1mLVq9XatJSLgVJiVlOTKbuVkOD06m6nakKrEhGdV65jVYcCkGiGhw3 NE7kat761Trc9F/H9HiDlim/W5M9St8fPmPZxQZKEJgVnYh6dn4wIHgL1wwF/E6UsP54 9YKikxffgBhubAAADgdMFiD2HqP4Y2x4T0wURnXz44tKchzFBUEv/TlfxBGOVLm9bcJ3 XZrA== X-Gm-Message-State: AOJu0Yzr2adgYSO4RxOyvGCfLuKcQ5+IR89SBRAiPuNjWTnbiaD59ql8 fBYoPiLxePuWjLdnhpTH7wa3kXOaxQMEwCYUFfK/KhZ2ntuOvEIoKSVtf9JnMk2wza7of+lZtzA aZm0XcGj22RF7DAX+YVSM4c3TqcO1PpY= X-Gm-Gg: AY/fxX5+iSPw3JCYdljGW6Uhu1L5fDCD1QiGziiHszeIVSPYvip659qK1X1wnCY03eU cgSdwZUTnpYMr67Pv3mcKNi1M/08EK/bFm5reQgK37jPJXThiFlkwym8oTzO43MaimW4frr9qEI BsOjVZsVJXPlTvrNn7jS6OyJxGNL6T8CPgTS+GxQJhS5e/2CcWm72F9ablVG/OTGQb0emE5OTSt eWjTIyt97bBmLkgxnYASX5qMmq2CTvd/EQjumQJejtMundu0835C+RnK0tmXJeT2sB0G3EUQsdL wF1+NldV42TQCl/IUss+VpyceFxGBQ== X-Received: by 2002:a17:907:26c8:b0:b83:6e2b:890d with SMTP id a640c23a62f3a-b8792e156bbmr86902466b.25.1768510268910; Thu, 15 Jan 2026 12:51:08 -0800 (PST) MIME-Version: 1.0 References: In-Reply-To: From: Dharin Shah Date: Thu, 15 Jan 2026 21:50:54 +0100 X-Gm-Features: AZwV_QgVyN03KlF6d1mIs_h-Bj9p-O0WvNJJ-miTeZfCIdH8u1RcHY-Yonizh_g Message-ID: Subject: Re: [Patch] Add WHERE clause support to REFRESH MATERIALIZED VIEW To: Adam Brusselback Cc: PostgreSQL Hackers Content-Type: multipart/alternative; boundary="0000000000005c301e06487362cb" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --0000000000005c301e06487362cb Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable > > (repro_include_issue.sql) Typo fix : test_include_bug.sql (attached file) Thanks, Dharin On Thu, Jan 15, 2026 at 7:46=E2=80=AFPM Dharin Shah wrote: > Hey Adam, > > Apologies for the delay, and as promised on discord, I did a review of th= e > current patch (cf 6305) and wanted to share findings that line up with th= e > thread=E2=80=99s design discussion, plus one additional correctness bug t= hat I > could reproduce. > > 1. In the non-concurrent REFRESH ... WHERE .... path, the UPSERT SQL is > built using the unique index metadata. The code currently uses indnatts > when building the ON Conflict (...) target list. That includes INCLUDE > columns, so for an index like: > > CREATE UNIQUE INDEX ON mv(id) INCLUDE (extra); > the generated statement becomes effectively ON CONFLICT (id, extra) ..., > which fails with: > ERROR: there is no unique or exclusion constraint matching the ON CONFLIC= T > specification > > The fix appears straightforward: use indnkeyatts (key attributes only) > when generating the conflict target, and also when deciding which columns > are =E2=80=9Ckey=E2=80=9D for the UPDATE SET clause. I=E2=80=99ve attache= d a minimal repro SQL > script (repro_include_issue.sql) > > 2. Another small test quality issue: the regression script has a comment > =E2=80=9CSubqueries -> Error=E2=80=9D but the expected output shows no er= ror for the > schema-qualified subquery. There is no explicit check forbidding subqueri= es > in transformRefreshWhereClause(), so schema-qualified subqueries appear > allowed. > > Moving on to broader questions > > > >> I believe the issue is that the DELETE -> INSERT strategy leaves a >> consistency gap. Since we relied on ROW EXCLUSIVE locks to allow concurr= ent >> reads, the moment we delete the rows, we lose the physical lock on them.= If >> a concurrent transaction inserts a colliding row during that gap, the >> materialized view ends up inconsistent with the base query (or hits a >> constraint violation). > > > Consistency gap in the non-concurrent mode matches what I=E2=80=99d expec= t: with > ROW EXCLUSIVE you allow concurrent readers/writers, and a pure DELETE =E2= =86=92 > INSERT approach can create a window where the old tuple is gone and a > concurrent session can insert a conflicting logical row. > > That said, I think it would help the patch to explicitly define the > intended safety model: > 1. Is the goal to be safe against concurrent DML on base tables only > (i.e., refresh sees a snapshot and updates MV accordingly), or also to be > safe against concurrent partial refreshes and direct writes to the MV (wh= en > maintenance is enabled)? > 2. Should the non-concurrent partial refresh be =E2=80=9Cbest effort=E2= =80=9D like normal > DML (user coordinates), or should it be =E2=80=9Cmaintenance-like=E2=80= =9D (serialized / > logically safe by default)? > > If the intent is =E2=80=9Csafe by default=E2=80=9D, I=E2=80=99d encourage= documenting very clearly > what=E2=80=99s guaranteed, and adding regression/README-style notes for f= ootguns > > From a reviewer standpoint, I think the feature concept is sound and > valuable, but it needs a crisp statement of semantics and safety > boundaries. The tricky part is exactly what you called out: incremental > refresh implies concurrency questions that aren=E2=80=99t present with fu= ll rebuild > + strong locks. > > I=E2=80=99m happy to keep reviewing iterations (especially around the adv= isory > lock approach), and I=E2=80=99ll attach the reproduction scripts and note= s I used. > > As a possible staging approach: it might be simplest to start with a > conservative serialization model for non-concurrent WHERE (while still > allowing readers), and then iterate toward finer-grained logical locking > if/when needed for throughput. > > > Thanks, > Dharin > > > On Sun, Jan 4, 2026 at 3:56=E2=80=AFAM Adam Brusselback > wrote: > >> Hi all, >> >> I've been running some more concurrency tests against this patch >> (specifically looking for race conditions), and I found a flaw in the >> implementation for the REFRESH ... WHERE ... mode (without CONCURRENTLY= ). >> >> I believe the issue is that the DELETE -> INSERT strategy leaves a >> consistency gap. Since we relied on ROW EXCLUSIVE locks to allow concurr= ent >> reads, the moment we delete the rows, we lose the physical lock on them.= If >> a concurrent transaction inserts a colliding row during that gap, the >> materialized view ends up inconsistent with the base query (or hits a >> constraint violation). >> >> I initially was using SELECT ... FOR UPDATE to lock the rows before >> modification, but that lock is (now that I know) obviously lost when the >> row is deleted. >> >> My plan is to replace that row-locking strategy with transaction-level >> advisory locks inside the refresh logic: >> >> Before the DELETE, run a SELECT pg_advisory_xact_lock(mv_oid, >> hashtext(ROW(unique_keys)::text)) for the rows matching the WHERE clause= . >> >> This effectively locks the "logical" ID of the row, preventing concurren= t >> refreshes on the same ID even while the physical tuple is temporarily go= ne. >> Hash collisions should not have any correctness issues that I can think = of. >> >> However, before I sink time into implementing that fix: >> >> Is there general interest in having REFRESH MATERIALIZED VIEW ... WHERE >> ... in core? >> If the community feels this feature is a footgun or conceptually wrong >> for Postgres, I'd rather know now before spending more time on this. >> >> If the feature concept is sound, does the advisory lock approach seem >> like the right way to handle the concurrency safety here? >> >> Thanks, >> Adam Brusselback >> > --0000000000005c301e06487362cb Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
(repro_i= nclude_issue.sql)

Typo fix : test_include_b= ug.sql (attached file)

Thanks,
Dharin

<= div class=3D"gmail_quote gmail_quote_container">
On Thu, Jan 15, 2026 at 7:46=E2=80=AFPM Dharin Shah <dharinshah95@gmail.com> wrote:
H= ey Adam,

Apologies for the delay, and as promised on dis= cord, I did a review of the current patch (cf 6305) and wanted to share fin= dings that line up with the thread=E2=80=99s design discussion, plus one ad= ditional correctness bug that I could reproduce.

1. In the no= n-concurrent REFRESH ... WHERE .... path, the UPSERT SQL is built using the= unique index metadata. The code currently uses indnatts when building the = ON Conflict (...) target list. That includes INCLUDE columns, so for an ind= ex like:

CREATE UNIQUE INDEX ON mv(id) INCLUDE (ex= tra);
the generated statement becomes effectively ON CONFLICT (id= , extra) ..., which fails with:
ERROR: there is no unique or excl= usion constraint matching the ON CONFLICT specification

=
The fix appears straightforward: use indnkeyatts (key attributes= only) when generating the conflict target, and also when deciding which co= lumns are =E2=80=9Ckey=E2=80=9D for the UPDATE SET clause. I=E2=80=99ve att= ached a minimal repro SQL script (repro_include_issue.sql)

2. Another small test quality issue: the regression script has a c= omment =E2=80=9CSubqueries -> Error=E2=80=9D but the expected output sho= ws no error for the schema-qualified subquery. There is no explicit check f= orbidding subqueries in transformRefreshWhereClause(), so schema-qualified = subqueries appear allowed.

Moving on to broader question= s=C2=A0

=C2=A0
I believe the issue is that the DELETE -> INSE= RT strategy leaves a consistency gap. Since we relied on ROW EXCLUSIVE lock= s to allow concurrent reads, the moment we delete the rows, we lose the phy= sical lock on them. If a concurrent transaction inserts a colliding row dur= ing that gap, the materialized view ends up inconsistent with the base quer= y (or hits a constraint violation).

Consist= ency gap in the non-concurrent mode matches what I=E2=80=99d expect: with R= OW EXCLUSIVE you allow concurrent readers/writers, and a pure DELETE =E2=86= =92 INSERT approach can create a window where the old tuple is gone and a c= oncurrent session can insert a conflicting logical row.=C2=A0
That said, I think it would help the patch to explicitly define= the intended safety model:
1. Is the goal to be safe against concurrent= DML on base tables only (i.e., refresh sees a snapshot and updates MV acco= rdingly), or also to be safe against concurrent partial refreshes and direc= t writes to the MV (when maintenance is enabled)?
2. Should the non-conc= urrent partial refresh be =E2=80=9Cbest effort=E2=80=9D like normal DML (us= er coordinates), or should it be =E2=80=9Cmaintenance-like=E2=80=9D (serial= ized / logically safe by default)?

If the inte= nt is =E2=80=9Csafe by default=E2=80=9D, I=E2=80=99d encourage documenting = very clearly what=E2=80=99s guaranteed, and adding regression/README-style = notes for footguns=C2=A0

From a reviewer standpoin= t, I think the feature concept is sound and valuable, but it needs a crisp = statement of semantics and safety boundaries. The tricky part is exactly wh= at you called out: incremental refresh implies concurrency questions that a= ren=E2=80=99t present with full rebuild + strong locks.

I=E2=80=99m = happy to keep reviewing iterations (especially around the advisory lock app= roach), and I=E2=80=99ll attach the reproduction scripts and notes I used.<= /div>

As a possible staging approach: it might be simple= st to start with a conservative serialization model for non-concurrent WHER= E (while still allowing readers), and then iterate toward finer-grained log= ical locking if/when needed for throughput.


Thanks,
Dharin


On Sun, Jan 4, 2026 at 3:5= 6=E2=80=AFAM Adam Brusselback <adambrusselback@gmail.com> wrote:
Hi all,
=
I've been running some more concurrency tests against this patch (s= pecifically looking for race conditions), and I found a flaw in the impleme= ntation for the=C2=A0 REFRESH ... WHERE ... mode (without CONCURRENTLY).
I believe the issue is that the DELETE -> INSERT strategy leaves a = consistency gap. Since we relied on ROW EXCLUSIVE locks to allow concurrent= reads, the moment we delete the rows, we lose the physical lock on them. I= f a concurrent transaction inserts a colliding row during that gap, the mat= erialized view ends up inconsistent with the base query (or hits a constrai= nt violation).

I initially was using SELECT ... FOR UPDATE to lock t= he rows before modification, but that lock is (now that I know) obviously l= ost when the row is deleted.

My plan is to replace that row-locking = strategy with transaction-level advisory locks inside the refresh logic:
Before the DELETE, run a SELECT pg_advisory_xact_lock(mv_oid, hashtext= (ROW(unique_keys)::text)) for the rows matching the WHERE clause.

Th= is effectively locks the "logical" ID of the row, preventing conc= urrent refreshes on the same ID even while the physical tuple is temporaril= y gone. Hash collisions should not have any correctness issues that I can t= hink of.

However, before I sink time into implementing that fix:
=
Is there general interest in having REFRESH MATERIALIZED VIEW ... WHERE= ... in core?
If the community feels this feature is a footgun or concep= tually wrong for Postgres, I'd rather know now before spending more tim= e on this.

If the feature concept is sound, does the advisory lock a= pproach seem like the right way to handle the concurrency safety here?
<= br>Thanks,
Adam Brusselback
--0000000000005c301e06487362cb--