diff --git a/Darling/Darling.Tests/PayloadDimensionLiveTests.cs b/Darling/Darling.Tests/PayloadDimensionLiveTests.cs
index 0d84a55424..b300e2cea0 100644
--- a/Darling/Darling.Tests/PayloadDimensionLiveTests.cs
+++ b/Darling/Darling.Tests/PayloadDimensionLiveTests.cs
@@ -716,6 +716,146 @@ await LiveStoreCleanup.RunAsync(connectionString!, bodySucceeded, async (cleanup
}
}
+ // ── (e2) #4249: the WHERE NOT EXISTS pre-filter takes no lock on an already-fresh row ──
+
+ ///
+ /// #4249: before this fix, Postgres locked every conflicting row to evaluate the DO UPDATE
+ /// ... WHERE guard above, even on the digests that guard was about to keep un-updated — a
+ /// Heap/LOCK WAL record and a dirtied page for content that was already fresh. None of
+ /// that is visible from the SQL text alone (a source pin can grep for the WHERE NOT
+ /// EXISTS shape but not for whether Postgres actually took a lock), so this proves it
+ /// against a real server, three ways:
+ ///
+ /// (1) A second upsert of the SAME still-fresh batch, 30 minutes later, leaves every
+ /// row's xmax at 0 (never locked) and advances pg_current_wal_lsn() by under 1
+ /// KB for a 50-row batch — effectively a read-only transaction. This is the pin: reverting the
+ /// WHERE NOT EXISTS pre-filter (restoring the pre-#4249 shape) turns it red. Confirmed
+ /// once by hand with a raw-SQL rehearsal of both shapes against this rig: the OLD shape left a
+ /// real transaction id (781) in xmax on a row whose content and last_seen were
+ /// both unchanged; the NEW shape left xmax at 0. See PR #4288.
+ ///
+ /// (2) A row stamped two hours ago is still refreshed — the pre-filter's own staleness
+ /// read uses the same one-hour boundary the ON CONFLICT ... WHERE guard always used, so
+ /// a genuinely stale row still reaches the UPDATE.
+ ///
+ /// (3) A digest with no existing row at all is still inserted — the pre-filter is an
+ /// anti-join over existing rows, not a blanket skip of the statement.
+ ///
+ [Fact]
+ public async Task PreFilter_SkipsLockingFreshRows_ButStillRefreshesStaleOnes_AndInsertsNewDigests()
+ {
+ var connectionString = Environment.GetEnvironmentVariable("DARLING_TEST_PG");
+ Assert.SkipWhen(string.IsNullOrEmpty(connectionString), SkipReason);
+
+ var ct = TestContext.Current.CancellationToken;
+ var runId = Guid.NewGuid().ToString("N");
+
+ var freshPairs = Enumerable.Range(0, 50)
+ .Select(i =>
+ {
+ var payload = $"";
+ return (Digest: PayloadDimensions.Digest(payload), Payload: payload);
+ })
+ .ToArray();
+ var stalePayload = $"";
+ var staleDigest = PayloadDimensions.Digest(stalePayload);
+ var newPayload = $"";
+ var newDigest = PayloadDimensions.Digest(newPayload);
+ var freshDigests = freshPairs.Select(p => p.Digest).ToArray();
+ var everyDigestWritten = freshDigests.Append(staleDigest).Append(newDigest).ToArray();
+
+ var t0 = new DateTime(2026, 3, 1, 12, 0, 0, DateTimeKind.Unspecified);
+
+ await using var connection = await OpenMigratedStoreAsync(connectionString!, ct);
+ var bodySucceeded = false;
+ try
+ {
+ async Task FlushAsync(IEnumerable<(byte[] Digest, string Payload)> pairs, DateTime collectionTime)
+ {
+ var batch = new PayloadDimensionBatch();
+ foreach (var (digest, payload) in pairs)
+ {
+ batch.Add(PayloadDimensions.QueryPlanDimTable, digest, payload);
+ }
+
+ await using var transaction = await connection.BeginTransactionAsync(ct);
+ await PayloadDimensionWriter.FlushAsync(connection, transaction, batch, collectionTime, ct);
+ await transaction.CommitAsync(ct);
+ }
+
+ /* A dedicated ANY($1) command rather than the file's params-object ScalarAsync helper:
+ a byte[][] argument there is covariantly assignable to object[], so C# spreads it
+ across the params array (one digest per parameter) instead of binding it as ONE
+ bytea[] array parameter. Explicit typing sidesteps that entirely. */
+ async Task LockedRowCountAsync(byte[][] digests)
+ {
+ await using var command = new NpgsqlCommand(
+ "SELECT count(*) FROM query_plan_dim WHERE digest = ANY($1) AND xmax <> 0", connection);
+ command.Parameters.Add(new NpgsqlParameter
+ { NpgsqlDbType = NpgsqlDbType.Array | NpgsqlDbType.Bytea, Value = digests });
+ return (long)(await command.ExecuteScalarAsync(ct))!;
+ }
+
+ async Task ChangedLastSeenCountAsync(byte[][] digests, DateTime expected)
+ {
+ await using var command = new NpgsqlCommand(
+ "SELECT count(*) FROM query_plan_dim WHERE digest = ANY($1) AND last_seen <> $2", connection);
+ command.Parameters.Add(new NpgsqlParameter
+ { NpgsqlDbType = NpgsqlDbType.Array | NpgsqlDbType.Bytea, Value = digests });
+ command.Parameters.Add(new NpgsqlParameter
+ { NpgsqlDbType = NpgsqlDbType.Timestamp, Value = expected });
+ return (long)(await command.ExecuteScalarAsync(ct))!;
+ }
+
+ async Task CurrentWalLsnAsync()
+ => (string)(await ScalarAsync(connection, "SELECT pg_current_wal_lsn()::text", ct))!;
+
+ /* pg_wal_lsn_diff returns numeric, which Npgsql maps to decimal, not a bigint type. */
+ async Task WalBytesSinceAsync(string beforeLsn)
+ => (long)(decimal)(await ScalarAsync(
+ connection, "SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), $1::text::pg_lsn)", ct, beforeLsn))!;
+
+ // The initial collection cycle: 50 "hot" plans plus one that will go stale.
+ await FlushAsync(freshPairs.Append((staleDigest, stalePayload)), t0);
+ Assert.Equal(0, await LockedRowCountAsync(freshDigests));
+
+ // (1) Same batch, same digests, 30 minutes later -- still inside the one-hour
+ // freshness window. The pre-filter excludes every one of them before the statement
+ // ever reaches INSERT/ON CONFLICT: no lock taken, no last_seen change, near-zero WAL.
+ var lsnBefore = await CurrentWalLsnAsync();
+ await FlushAsync(freshPairs, t0.AddMinutes(30));
+ var walBytes = await WalBytesSinceAsync(lsnBefore);
+
+ Assert.Equal(0, await LockedRowCountAsync(freshDigests));
+ Assert.Equal(0, await ChangedLastSeenCountAsync(freshDigests, t0));
+ Assert.True(walBytes < 1024, $"expected a near-zero WAL delta for an all-fresh batch, saw {walBytes} bytes");
+
+ // (2) and (3): two hours after t0, the stale row is refreshed and a brand-new digest
+ // is inserted, in the same flush.
+ await FlushAsync([(staleDigest, stalePayload), (newDigest, newPayload)], t0.AddHours(2));
+
+ var refreshedStale = (DateTime)(await ScalarAsync(
+ connection, "SELECT last_seen FROM query_plan_dim WHERE digest = $1", ct, staleDigest))!;
+ Assert.Equal(t0.AddHours(2), refreshedStale);
+
+ var insertedNew = (DateTime)(await ScalarAsync(
+ connection, "SELECT last_seen FROM query_plan_dim WHERE digest = $1", ct, newDigest))!;
+ Assert.Equal(t0.AddHours(2), insertedNew);
+
+ bodySucceeded = true;
+ }
+ finally
+ {
+ await LiveStoreCleanup.RunAsync(connectionString!, bodySucceeded, async (cleanup, cleanupCt) =>
+ {
+ foreach (var digest in everyDigestWritten)
+ {
+ await DeleteDimRowAsync(cleanup, PayloadDimensions.QueryPlanDimTable, digest, cleanupCt);
+ }
+ });
+ }
+ }
+
// ── (f) the GC ──
[Fact]
diff --git a/Darling/Darling.Tests/PayloadDimensionTests.cs b/Darling/Darling.Tests/PayloadDimensionTests.cs
index 41390e4697..4e35822f93 100644
--- a/Darling/Darling.Tests/PayloadDimensionTests.cs
+++ b/Darling/Darling.Tests/PayloadDimensionTests.cs
@@ -445,10 +445,25 @@ public void UpsertSql_IsOneUnnestStatement_WithTheOnConflictStalenessGuard()
"INSERT INTO query_text_dim (digest, query_text, last_seen)\n" +
"SELECT u.digest, u.payload, $3\n" +
"FROM unnest($1::bytea[], $2::text[]) AS u(digest, payload)\n" +
+ "WHERE NOT EXISTS (\n" +
+ " SELECT 1 FROM query_text_dim d\n" +
+ " WHERE d.digest = u.digest\n" +
+ " AND d.last_seen >= $3 - INTERVAL '1 hour')\n" +
+ "ORDER BY u.digest\n" +
"ON CONFLICT (digest) DO UPDATE SET last_seen = EXCLUDED.last_seen\n" +
"WHERE query_text_dim.last_seen < EXCLUDED.last_seen - INTERVAL '1 hour'",
sql);
+ /* #4249: the pre-filter is a plain MVCC read (no lock) that keeps an already-fresh digest from
+ ever reaching INSERT or ON CONFLICT — that is what stops the write, not the conflict guard
+ above, which only ever stopped the UPDATE. */
+ Assert.Contains("WHERE NOT EXISTS (", sql, StringComparison.Ordinal);
+
+ /* #4249: the anti-join gives the planner a row source it can reorder (a hash anti-join returns
+ survivors in hash order), so client-side digest ordering alone no longer fixes the order rows
+ reach ON CONFLICT. Both branches must carry this. */
+ Assert.Contains("ORDER BY u.digest", sql, StringComparison.Ordinal);
+
/* The plan dimension upserts gzip BYTES into query_plan_gz (#2069) — same statement shape,
same conflict semantics, bytea payload array instead of text. The text column is never
written on new rows; pre-V54 rows keep theirs until the GC retires them. */
@@ -456,6 +471,11 @@ public void UpsertSql_IsOneUnnestStatement_WithTheOnConflictStalenessGuard()
"INSERT INTO query_plan_dim (digest, query_plan_gz, last_seen)\n" +
"SELECT u.digest, u.payload, $3\n" +
"FROM unnest($1::bytea[], $2::bytea[]) AS u(digest, payload)\n" +
+ "WHERE NOT EXISTS (\n" +
+ " SELECT 1 FROM query_plan_dim d\n" +
+ " WHERE d.digest = u.digest\n" +
+ " AND d.last_seen >= $3 - INTERVAL '1 hour')\n" +
+ "ORDER BY u.digest\n" +
"ON CONFLICT (digest) DO UPDATE SET last_seen = EXCLUDED.last_seen\n" +
"WHERE query_plan_dim.last_seen < EXCLUDED.last_seen - INTERVAL '1 hour'",
PayloadDimensions.UpsertSql(PayloadDimensions.QueryPlanDimTable));
@@ -467,6 +487,11 @@ public void UpsertSql_IsOneUnnestStatement_WithTheOnConflictStalenessGuard()
"INSERT INTO query_plan_dim (digest, query_plan_xml, last_seen)\n" +
"SELECT u.digest, u.payload, $3\n" +
"FROM unnest($1::bytea[], $2::text[]) AS u(digest, payload)\n" +
+ "WHERE NOT EXISTS (\n" +
+ " SELECT 1 FROM query_plan_dim d\n" +
+ " WHERE d.digest = u.digest\n" +
+ " AND d.last_seen >= $3 - INTERVAL '1 hour')\n" +
+ "ORDER BY u.digest\n" +
"ON CONFLICT (digest) DO UPDATE SET last_seen = EXCLUDED.last_seen\n" +
"WHERE query_plan_dim.last_seen < EXCLUDED.last_seen - INTERVAL '1 hour'",
PayloadDimensions.UpsertSql(PayloadDimensions.QueryPlanDimTable, compressContent: false));
@@ -479,6 +504,30 @@ public void UpsertSql_IsOneUnnestStatement_WithTheOnConflictStalenessGuard()
Assert.Throws(() => PayloadDimensions.UpsertSql("not_a_dim"));
}
+ ///
+ /// #4249's source pin: reads PayloadDimensions.cs itself and requires BOTH branches of
+ /// UpsertSql — the compressed-content early return and the shared text/plan-xml tail — to carry
+ /// ORDER BY u.digest. The exact-string assertions above already cover the shipped shape, but they
+ /// pin the STRING; this pins the SOURCE, so a future edit that special-cases one branch and drops the
+ /// other's ORDER BY fails here even if nobody updates the string pins to match.
+ ///
+ [Fact]
+ public void UpsertSql_SourceCarriesOrderByOnBothBranches()
+ {
+ var root = FindRepoRoot();
+ Assert.True(root is not null, RepoRootNotFound);
+
+ var source = File.ReadAllText(Path.Combine(
+ root!, "Darling", "PerformanceMonitor.Darling.Storage", "PayloadDimensions.cs"));
+
+ var body = MethodBody(source, "public static string UpsertSql(string dimTable, bool compressContent = true)");
+ Assert.False(string.IsNullOrEmpty(body),
+ "could not locate UpsertSql -- this guard must fail rather than silently pass on a parse miss");
+
+ var occurrences = System.Text.RegularExpressions.Regex.Matches(body, "ORDER BY u.digest").Count;
+ Assert.Equal(2, occurrences);
+ }
+
// ── the gzip codec (#2069) ──
[Fact]
diff --git a/Darling/Darling.Tests/QueryStoreFetchProbeLivePostgresTests.cs b/Darling/Darling.Tests/QueryStoreFetchProbeLivePostgresTests.cs
index 8e20ce3496..468496ceec 100644
--- a/Darling/Darling.Tests/QueryStoreFetchProbeLivePostgresTests.cs
+++ b/Darling/Darling.Tests/QueryStoreFetchProbeLivePostgresTests.cs
@@ -147,6 +147,102 @@ await LiveStoreCleanup.RunAsync(cs!, bodySucceeded, async (cleanup, cleanupCt) =
}
}
+ ///
+ /// #4250: the touch guard's freshness gate is the last_seen stored in the map, dimension and
+ /// text rows themselves — nothing about it lives in the writer's memory, because this design has none;
+ /// is static and every call is connection-scoped. So a process
+ /// restart — the nearest thing a live rig can rehearse is a brand-new connection, since there is no
+ /// other per-process state to reset — must not re-touch rows the guard already stamped fresh: probing
+ /// the SAME references again from a DIFFERENT connection, still inside the guard window, updates zero
+ /// rows anywhere.
+ ///
+ [Fact]
+ public async Task Restart_ANewConnectionRetouchesNothingFresh_TheGuardReadsStoredLastSeen()
+ {
+ var cs = ConnectionString;
+ Assert.SkipWhen(string.IsNullOrEmpty(cs), "Set DARLING_TEST_PG to a Postgres connection string to run the live fetch-probe test.");
+
+ var ct = TestContext.Current.CancellationToken;
+ using var connection = new NpgsqlConnection(cs);
+ await connection.OpenAsync(ct);
+ await PgMigrations.MigrateAsync(connection, ct);
+ await DeleteRowsAsync(connection, ct);
+
+ var bodySucceeded = false;
+ try
+ {
+ /* Older than the guard, so the FIRST touch below actually fires (same reasoning as the sibling
+ test above: a hard-coded age would silently stop exercising the touch once the width moves). */
+ var landedAt = DateTime.UtcNow.AddHours(-(QueryStoreLivenessTouchGuard.GuardHours + 1));
+
+ await QueryStorePlanWriter.WriteAsync(
+ connection, ServerId, Db,
+ new[] { new FetchedPlan(201, "", "0xR1") },
+ landedAt, TestTimeoutSeconds, ct);
+ await QueryStoreTextWriter.WriteAsync(
+ connection, ServerId, Db,
+ new[] { new FetchedQueryText(202, "SELECT 'restart'", "0xR2") },
+ landedAt, TestTimeoutSeconds, ct);
+
+ async Task<(long Map, long Dim, long Text)> FreshnessCountsAsync(DateTime stamp)
+ {
+ using var freshness = new NpgsqlCommand(@"
+SELECT
+ (SELECT COUNT(*) FROM collect.query_store_plan_map
+ WHERE server_id = $1 AND database_name = $2 AND last_seen = $3),
+ (SELECT COUNT(*) FROM collect.query_plan_dim d
+ JOIN collect.query_store_plan_map m ON m.digest = d.digest
+ WHERE m.server_id = $1 AND m.database_name = $2 AND d.last_seen = $3),
+ (SELECT COUNT(*) FROM collect.query_store_text
+ WHERE server_id = $1 AND database_name = $2 AND last_seen = $3)", connection);
+ freshness.Parameters.AddWithValue(ServerId);
+ freshness.Parameters.AddWithValue(Db);
+ freshness.Parameters.AddWithValue(stamp);
+ await using var reader = await freshness.ExecuteReaderAsync(ct);
+ await reader.ReadAsync(ct);
+ return (reader.GetInt64(0), reader.GetInt64(1), reader.GetInt64(2));
+ }
+
+ // The first touch, from the original connection: the rows landed older than the guard, so
+ // this fires and stamps all three relations with firstTouch.
+ var firstTouch = DateTime.UtcNow;
+ await QueryStoreFetchProbe.TouchAndProbePlansAsync(
+ connection, ServerId, Db, new[] { (201L, (string?)"0xR1") }, firstTouch, TestTimeoutSeconds, ct);
+ await QueryStoreFetchProbe.TouchAndProbeTextsAsync(
+ connection, ServerId, Db, new[] { (202L, (string?)"0xR2") }, firstTouch, TestTimeoutSeconds, ct);
+
+ var firstTouchStamp = QueryStorePlanMap.Naive(firstTouch);
+ Assert.Equal((1L, 1L, 1L), await FreshnessCountsAsync(firstTouchStamp));
+
+ // "Restart": a brand-new connection. Same references, five minutes later — still well inside
+ // the guard window, so nothing here should qualify for a re-touch.
+ using var freshConnection = new NpgsqlConnection(cs);
+ await freshConnection.OpenAsync(ct);
+ var secondTouch = firstTouch.AddMinutes(5);
+
+ var planVerdicts = await QueryStoreFetchProbe.TouchAndProbePlansAsync(
+ freshConnection, ServerId, Db, new[] { (201L, (string?)"0xR1") }, secondTouch, TestTimeoutSeconds, ct);
+ var textVerdicts = await QueryStoreFetchProbe.TouchAndProbeTextsAsync(
+ freshConnection, ServerId, Db, new[] { (202L, (string?)"0xR2") }, secondTouch, TestTimeoutSeconds, ct);
+
+ // Zero rows updated: every row still carries the FIRST touch's stamp, none carry the second.
+ Assert.Equal((1L, 1L, 1L), await FreshnessCountsAsync(firstTouchStamp));
+ Assert.Equal((0L, 0L, 0L), await FreshnessCountsAsync(QueryStorePlanMap.Naive(secondTouch)));
+
+ // The fresh connection's verdicts still resolve correctly — the restart cost nothing but the
+ // write it correctly skipped.
+ Assert.Equal(new FetchProbeVerdict(201, Resolved: true, HashStale: false), planVerdicts.Single());
+ Assert.Equal(new FetchProbeVerdict(202, Resolved: true, HashStale: false), textVerdicts.Single());
+
+ bodySucceeded = true;
+ }
+ finally
+ {
+ await LiveStoreCleanup.RunAsync(cs!, bodySucceeded, async (cleanup, cleanupCt) =>
+ await DeleteRowsAsync(cleanup, cleanupCt));
+ }
+ }
+
private static async Task DeleteRowsAsync(NpgsqlConnection connection, CancellationToken ct)
{
var sql =
diff --git a/Darling/PerformanceMonitor.Darling.Storage/PayloadDimensions.cs b/Darling/PerformanceMonitor.Darling.Storage/PayloadDimensions.cs
index b40b378e63..9f244c83f0 100644
--- a/Darling/PerformanceMonitor.Darling.Storage/PayloadDimensions.cs
+++ b/Darling/PerformanceMonitor.Darling.Storage/PayloadDimensions.cs
@@ -312,6 +312,17 @@ gzip bytes only. Other dims keep the original NOT NULL text shape. */
/// hour, 60x less churn, and stays correct as long as the GC margin exceeds an hour (it is a
/// full day).
///
+ /// #4249: the conflict guard only stops the UPDATE, not the WRITE. Postgres locks every
+ /// conflicting row BEFORE evaluating the WHERE on DO UPDATE — "all rows will be locked
+ /// when the ON CONFLICT DO UPDATE action is taken" per the INSERT reference — so a digest re-sighted
+ /// inside the hour still took a Heap/LOCK WAL record and a dirtied page every cycle, measured
+ /// at 12-23% of one store's WAL. The WHERE NOT EXISTS pre-filter on the SELECT fixes
+ /// that: it is a plain MVCC read, which takes no lock, so a digest whose stored is already within the hour never reaches INSERT or ON
+ /// CONFLICT at all and writes nothing. The ON CONFLICT ... WHERE arm stays for the race
+ /// the pre-filter cannot close: two sessions whose pre-filter reads both land before either commits
+ /// can both decide the same digest is stale and both attempt the insert.
+ ///
/// Callers MUST pass digest-deduplicated arrays: Postgres raises 21000 ("ON CONFLICT DO
/// UPDATE command cannot affect row a second time") when one statement presents the same key
/// twice, and a 200-row batch sharing a handful of plans presents duplicates constantly. The
@@ -321,9 +332,20 @@ gzip bytes only. Other dims keep the original NOT NULL text shape. */
/// supply: dedup makes a batch conflict-free with ITSELF, ordering makes it conflict-free with its
/// SIBLINGS. This statement takes one row lock per conflict key, so two concurrent sessions whose
/// batches share two digests in opposite relative order each hold the lock the other needs next and
- /// deadlock (40P01, #1801). PayloadDimensionBatch.ToArrays is the single point that imposes
- /// it -- deliberately client-side rather than an ORDER BY here, so the lock order is a property of
- /// the values bound and owes nothing to how the planner chooses to execute this statement.
+ /// deadlock (40P01, #1801). PayloadDimensionBatch.ToArrays imposes that order client-side, and
+ /// until #4249 that was the WHOLE mechanism — deliberately client-side rather than an ORDER BY
+ /// here, so the lock order owed nothing to how the planner executed the statement.
+ ///
+ /// #4249 added the WHERE NOT EXISTS anti-join above, and that changes the claim.
+ /// A join gives the planner a row source it can execute in whatever order it finds cheapest — a hash
+ /// anti-join against a large dim table returns survivors in HASH order, not unnest's input
+ /// order — so client order alone no longer fixes the order rows reach ON CONFLICT. Both
+ /// branches carry ORDER BY u.digest on the SELECT now, restoring a deterministic
+ /// acquisition order for the rows that actually proceed to lock. The client-side sort still matters:
+ /// it is what makes a batch conflict-free with itself (dedup) actually land in digest order for the
+ /// unnest arrays this ORDER BY re-sorts, and it is the order every other digest-keyed
+ /// caller in this design relies on. Belt and suspenders, not redundant — the sort fixes the INPUT
+ /// order, the ORDER BY fixes the order the FILTERED, JOINED plan actually locks in.
///
/// #2069: the compressed-content dim upserts gzip BYTES into
/// (text column left NULL on new rows); every other dim
@@ -341,6 +363,11 @@ public static string UpsertSql(string dimTable, bool compressContent = true)
$"INSERT INTO {dimTable} ({DigestColumn}, {CompressedContentColumn}, {LastSeenColumn})\n" +
$"SELECT u.digest, u.payload, $3\n" +
$"FROM unnest($1::bytea[], $2::bytea[]) AS u(digest, payload)\n" +
+ $"WHERE NOT EXISTS (\n" +
+ $" SELECT 1 FROM {dimTable} d\n" +
+ $" WHERE d.{DigestColumn} = u.digest\n" +
+ $" AND d.{LastSeenColumn} >= $3 - INTERVAL '1 hour')\n" +
+ $"ORDER BY u.digest\n" +
$"ON CONFLICT ({DigestColumn}) DO UPDATE SET {LastSeenColumn} = EXCLUDED.{LastSeenColumn}\n" +
$"WHERE {dimTable}.{LastSeenColumn} < EXCLUDED.{LastSeenColumn} - INTERVAL '1 hour'";
}
@@ -350,6 +377,11 @@ public static string UpsertSql(string dimTable, bool compressContent = true)
$"INSERT INTO {dimTable} ({DigestColumn}, {payloadColumn}, {LastSeenColumn})\n" +
$"SELECT u.digest, u.payload, $3\n" +
$"FROM unnest($1::bytea[], $2::text[]) AS u(digest, payload)\n" +
+ $"WHERE NOT EXISTS (\n" +
+ $" SELECT 1 FROM {dimTable} d\n" +
+ $" WHERE d.{DigestColumn} = u.digest\n" +
+ $" AND d.{LastSeenColumn} >= $3 - INTERVAL '1 hour')\n" +
+ $"ORDER BY u.digest\n" +
$"ON CONFLICT ({DigestColumn}) DO UPDATE SET {LastSeenColumn} = EXCLUDED.{LastSeenColumn}\n" +
$"WHERE {dimTable}.{LastSeenColumn} < EXCLUDED.{LastSeenColumn} - INTERVAL '1 hour'";
}
diff --git a/Darling/PerformanceMonitor.Darling.Storage/QueryStoreLivenessTouchGuard.cs b/Darling/PerformanceMonitor.Darling.Storage/QueryStoreLivenessTouchGuard.cs
index 966264a9ae..12e255dcaa 100644
--- a/Darling/PerformanceMonitor.Darling.Storage/QueryStoreLivenessTouchGuard.cs
+++ b/Darling/PerformanceMonitor.Darling.Storage/QueryStoreLivenessTouchGuard.cs
@@ -125,12 +125,21 @@ public static class QueryStoreLivenessTouchGuard
/// proportional to the width, so this divisor is also, to within the arrival pattern, the divisor on that
/// rate.
///
- /// A quarter leaves 4x headroom inside the margin for the collection cadence, for clock skew
- /// between the service and the store, and for a sweep that runs late. There is no measurement that
- /// prefers 4 to 3 or to 6; it is a safety factor, and it is stated as one so nobody reads the width
- /// below as a measured quantity.
+ /// #4250: measured at the PREVIOUS divisor (4, a 6-hour guard) on one production store — the
+ /// Query Store-ingesting store, a steady window with no restart inside — query_store_plan_map and
+ /// query_store_text together took about 163 K touches an hour, 3.9 M a day, all non-HOT, which was
+ /// roughly 13.5% of that store's WAL in the window. That is the cost a wider guard buys back, and it is
+ /// why the divisor moved to 2.
+ ///
+ /// Half leaves 2x headroom inside the NAMED one-day margin for the collection cadence, clock skew
+ /// between the service and the store, and a sweep that runs late — and a full 4x against the WORST-CASE
+ /// room: swept over the shipped retention arithmetic (dim-feeding retention floored at 1 day, the
+ /// plan-content knob off), the least room any reachable configuration leaves is TWO days, and 12 hours
+ /// still fits inside that floor 4 times. There is no measurement that prefers 2 to 1 or to 3; it is a
+ /// safety factor informed by, but not derived from, the write-volume measurement above, and it is stated
+ /// as one so nobody reads the width below as a measured quantity.
///
- public const int MarginShareDivisor = 4;
+ public const int MarginShareDivisor = 2;
///
/// The guard width in hours: divided by