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 1w4gCM-002RYG-14 for pgsql-hackers@arkaria.postgresql.org; Mon, 23 Mar 2026 14:24:06 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1w4gCK-000e1H-2X for pgsql-hackers@arkaria.postgresql.org; Mon, 23 Mar 2026 14:24:05 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1w4gCK-000e19-1V for pgsql-hackers@lists.postgresql.org; Mon, 23 Mar 2026 14:24:04 +0000 Received: from mail-pj1-x1033.google.com ([2607:f8b0:4864:20::1033]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1w4gCI-00000000haf-13s3 for pgsql-hackers@lists.postgresql.org; Mon, 23 Mar 2026 14:24:04 +0000 Received: by mail-pj1-x1033.google.com with SMTP id 98e67ed59e1d1-35b99b7cbaeso227506a91.2 for ; Mon, 23 Mar 2026 07:24:02 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1774275840; cv=none; d=google.com; s=arc-20240605; b=Eizf94sJP86H3guzizyrSnZIj+NPSxP/VRLqkZooxx3JukjGzdbk6vKDHNYa8grGwY irdFs5zFgncelsBBMuNSYych9R4GvlF6BNEz9HwABq2leRwdKYKV7eVydvXdB6PpdmFP JEQ8Rt71a+JuBl2Of8kMD005Dsh8yYN7+Hu4iOel8kzebgt0e2Xv2xQLp3uEXwqNwBau Nfru48QVPT7rJnh1Y4dSFlU4ThGmct3SY8O56UHeF4uvbeVcX3bsCmsCjJcALs69Obpl VPqB1lTbSNmVgw31O7NTPV0BmXvqP8+f4AkD/CICOPHRmRCEGoljkKM8ZZEH3hL0LmrX 6PQQ== 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=Xk38qcvXx2AFfrlGm3uYPduOhLuLM9h9UikHllXYF6g=; fh=Z+ut1I7vD7jEynComERW53U6C/xvzX28aYvKh6S1AIs=; b=hzg5Mqb1bhnlSU9GDW7EasbZVDtTBj7DJCAmtfZM8GSlZurYhvGT6S3fXgSPlnDPZu OcAD8Fv39PsgKgTJhN+JGQ7qnwwvFO3RMcoyoJ04AO+ds1Rsuz0eU1gp+wpXwrc5uYps 2uQ3PXAWMXRf/paoWvT2zv2ba+xuzXzclPYdvKPzEjH6WLJpksL57Ush79Z7TitN6b80 Z/pQgRMBvUip6QXcb1YWupgwiEF/fbmdw5oVlB/geW9hgebDXrCrtRLDqdXBxvOCJHEd ZapNQWAQFl7SJDJnDIHdY4rI0e5U5OO5ggGwmN0HYC0+1QFYY4WBWvvHVIG8/IoReOSa cEqQ==; 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=1774275840; x=1774880640; 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=Xk38qcvXx2AFfrlGm3uYPduOhLuLM9h9UikHllXYF6g=; b=FGIsUA42Dpp2JNUptyb/uqCR1DjuytU6/IrsOqh55IiABPorQsH+0sNjHWr//3TuNC WVeSTiCU3RRvU81S/WIEe+NS0sMuOEJnpjXRXpHFNduMbVIYtRyqiwtsibDVuHfxZ4FR zdaezGXzLHNQe99xq4HIl6oRZ3Z/URbHLxkdjUxTCZBMU5whOrerW+g91QEinoTA0nCg IXlab/ZT/O7IpOXdkgOyteqotVr71ybSD6ktgk1hed8SrXWdw7tuwVm2R62Uf7nP8ny/ ZYZbVt94W1IoaXL9mUsobcRu3ZpPGzyFRU/D5j0rvawF0TsYVlPL7bi7ZLQsUXTWSkl6 sb1w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1774275840; x=1774880640; 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=Xk38qcvXx2AFfrlGm3uYPduOhLuLM9h9UikHllXYF6g=; b=ChKBQcI9CG5FWbBiZzh+7AbegyHdVgOAG3Qkz28gN+LFhbmm89ZYPnoQgYH2ozRGGj aQEnRqWqJCyfoaAekMBgHh16Vrngss5k75vuc4RXy2apNl+wrv7AKD1pWOWDCkNf9A82 YamyuyIy+Tj8BwL8Xafp1xClr9hoCfg4y4EZvjPgxyPjNx21rXaKRdZKNJds/iNVEy5x LVWsRuXiez5PMSr49PyswmCJcxSUsAuf5esNo3MJB0VU/Eglgw7HU7fIIKgB1TyuWzBJ lfwZJ4Eq38+kYuGZZ7+WXhZWULVKOJLmEkHuP2nYEHzdizc2+BHkKBm6n6VMgxKft8vd lQ6A== X-Gm-Message-State: AOJu0YyG3EL5h6te5X+msVCFHuCKvNvCkZfkqV2PkC39BQzvQ4Ci8eU4 hSdZH7hMlAd4UfCUH0eojNE+eSRq0GPPH3EWUFg0G6xwQmjRq36K8B2RNcx8Pdr6j1z3vYbQaiN XY4DkNIxsBE3qFHASp/QGQYrjy0NfKbQ= X-Gm-Gg: ATEYQzxCSHlkbDlbkMNYS4ju17i7m5n1zLvu2JppvpHBGdrhlZkRfvYfqXYDn0nuScm giFgjuf5tYDIAZYbqJpqZS5xz5lfY4CAVcZZqFZy/toULAYLpZ3fPdktiDjE2yJXfoWHUw5va+M 7gQmbWNwMp5YwWYFmih7sX5YbqDveIsUlPtc7oocWNbQOCY04VzW39vNrJzFaiTYB7e8tiRz0w8 dJGeKxuEng4X7MVMib/ZlAOOs41ojDMcQoRPd+jvszmyKXhJO94vERSNTpePznkyIAocs2PmToa KcfudMg= X-Received: by 2002:a17:90b:28c5:b0:352:d19c:684f with SMTP id 98e67ed59e1d1-35bd2d39bb6mr6622143a91.8.1774275840167; Mon, 23 Mar 2026 07:24:00 -0700 (PDT) MIME-Version: 1.0 References: In-Reply-To: From: Adam Brusselback Date: Mon, 23 Mar 2026 10:23:43 -0400 X-Gm-Features: AaiRm52aE-AW9NyFzOOae4SUImYUblIYMOHuCa8MljN96_PJ8TJiZqPOv-RVA7o Message-ID: Subject: Re: [Patch] Add WHERE clause support to REFRESH MATERIALIZED VIEW To: Dharin Shah Cc: PostgreSQL Hackers Content-Type: multipart/alternative; boundary="0000000000002fd14e064db1c975" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --0000000000002fd14e064db1c975 Content-Type: text/plain; charset="UTF-8" Hi Dharin, thanks for the review. > 1. indnatts vs indnkeyatts Good catch. Will fix. > 2. Subqueries -> Error That comment is wrong, I never added a check for that because it turned out to be unnecessary. Will remove. > 3. Concurrency gap / safety model To answer your questions directly: 1. The goal is to be safe against concurrent partial refreshes on overlapping rows, not just concurrent DML on base tables. 2. The intent is maintenance-like and safe by default. Because we lose the physical lock on the row after the DELETE, I plan to enforce that safety default via transaction-level advisory locks acquired before the DELETE with somethin like: SELECT pg_advisory_xact_lock(matviewOid, hashtext(ROW(key_cols)::text)) FROM matview WHERE (condition); Concurrent refreshes on the same logical rows will serialize while non-overlapping rows still run in parallel. This also made me think about whether the CONCURRENTLY keyword is doing the right thing here. Here's how the guarantees break down across all the refresh modes: Refresh Command / State | Base Table Lock | Concurrent Reads? | Concurrent Writes? | Same-Row Concurrent Refreshes ----------------------------------------+------------------+-------------------+--------------------+------------------------------ Standard Full Refresh | ACCESS EXCLUSIVE | Blocked | Blocked | Blocked (Table Level) CONCURRENTLY (Full) | EXCLUSIVE | Allowed | Blocked | Blocked (Table Level) Partial (WHERE) - Current Patch | ROW EXCLUSIVE | Allowed | Allowed | Race condition (Fails) Partial (WHERE) - With Advisory Locks | ROW EXCLUSIVE | Allowed | Allowed | Serialized (Waits) Partial (CONCURRENTLY WHERE) | EXCLUSIVE | Allowed | Blocked | Serialized (Waits) Because of this, the `CONCURRENTLY` distinction gets inverted with a `WHERE` clause. With a full refresh, `CONCURRENTLY` is the more permissive option (allowing readers). But here, the bare `WHERE` path allows both reads and writes, while `CONCURRENTLY WHERE` blocks writers. Non-concurrent ends up being the more permissive option, which goes against what the keyword generally implies. One option is to swap the two implementations to restore that intuition. `CONCURRENTLY WHERE` becomes the advisory locks approach (maximum throughput), and bare `WHERE` becomes the diff approach (conservative, blocks writers). On the other hand, `CONCURRENTLY` has historically meant the diff-based algorithm specifically, not just a lower lock level. I don't have a strong opinion here and would rather let the community decide. The updated patch will leave the algorithms as-is for now. Happy to swap them if that's the preferred direction. Will post an updated patch soon. Thanks, Adam Brusselback --0000000000002fd14e064db1c975 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Hi Dharin, thanks for the review.

> 1. indnatts = vs indnkeyatts

Good catch. Will fix.

> 2. Subqueries ->= Error

That comment is wrong, I never added a check for that because= it turned out to be unnecessary. Will remove.

> 3. Concurrency g= ap / safety model

To answer your questions directly:

1. The g= oal is to be safe against concurrent partial refreshes on overlapping rows,= not just concurrent DML on base tables.
2. The intent is maintenance-li= ke and safe by default.

Because we lose the physical lock on the row= after the DELETE, I plan to enforce that safety default via transaction-le= vel advisory locks acquired before the DELETE with somethin like:

= =C2=A0 =C2=A0 SELECT pg_advisory_xact_lock(matviewOid, hashtext(ROW(key_col= s)::text))
=C2=A0 =C2=A0 FROM matview=C2=A0
=C2=A0 =C2=A0= WHERE (condition);

Concurrent refreshes on the same logical rows = will serialize while non-overlapping rows still run in parallel.

Thi= s also made me think about whether the CONCURRENTLY keyword is doing the ri= ght thing here. Here's how the guarantees break down across all the ref= resh modes:

Refresh Command / State =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 | Base Table Lock =C2=A0| Concurrent Reads? | Conc= urrent Writes? | Same-Row Concurrent Refreshes
-------------------------= ---------------+------------------+-------------------+--------------------= +------------------------------
Standard Full Refresh =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 | ACCESS EXCLUSIVE | Blocked = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 | Blocked =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0| Blocked (Table Level)
CONCURRENTLY (Full) =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 | EXCLUSIVE =C2=A0 = =C2=A0 =C2=A0 =C2=A0| Allowed =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 | Blocked = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0| Blocked (Table Level)
Partial= (WHERE) - Current Patch =C2=A0 =C2=A0 =C2=A0 =C2=A0 | ROW EXCLUSIVE =C2=A0= =C2=A0| Allowed =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 | Allowed =C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0| Race condition (Fails)
Partial (WHERE) - W= ith Advisory Locks =C2=A0 | ROW EXCLUSIVE =C2=A0 =C2=A0| Allowed =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 | Allowed =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0| Serialized (Waits)
Partial (CONCURRENTLY WHERE) =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0| EXCLUSIVE =C2=A0 =C2=A0 =C2=A0 =C2=A0| Allowed = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 | Blocked =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0| Serialized (Waits)

Because of this, the `CONCURRENTLY= ` distinction gets inverted with a `WHERE` clause. With a full refresh, `CO= NCURRENTLY` is the more permissive option (allowing readers). But here, the= bare `WHERE` path allows both reads and writes, while `CONCURRENTLY WHERE`= blocks writers. Non-concurrent ends up being the more permissive option, w= hich goes against what the keyword generally implies.

One option is = to swap the two implementations to restore that intuition. `CONCURRENTLY WH= ERE` becomes the advisory locks approach (maximum throughput), and bare `WH= ERE` becomes the diff approach (conservative, blocks writers). On the other= hand, `CONCURRENTLY` has historically meant the diff-based algorithm speci= fically, not just a lower lock level.

I don't have a strong opin= ion here and would rather let the community decide. The updated patch will = leave the algorithms as-is for now. Happy to swap them if that's the pr= eferred direction.

Will post an updated patch soon.

Thanks,Adam Brusselback
--0000000000002fd14e064db1c975--