Analysis: bound the freshness query, let window sums skip chunks, share the Overview's catalog
ci / build-test (push) Successful in 2m34s
ci / build-test (push) Successful in 2m34s
Three things the 1,000-meter x 10-year measurement found, each proved by EXPLAIN or a statement count before and after. No tally moves. Freshness had two jobs in one unbounded query. The mark -- when a meter last delivered -- is now stored on meter_rollup_state and maintained by every recompute, with a one-pass backfill in the migration, so an import-only meter keeps its years-old last activity without reading a single raw row. The rhythm that decides stale versus live is sampled inside a 90-day window and only for meters that actually have a live source; a source silent for longer than that is re-read unbounded, so it is still called stale by its own rhythm rather than by a default. The portfolio query went from 13.8 ms planning plus 36.1 ms execution across all 123 reading chunks to 0.58 plus 0.44 ms across four. Window sums took their time bounds only from the unnest join, so the planner could not exclude chunks: a 1,960-window case scanned 1.39 M rows in parallel and spilled a 45 MB sort. Repeating the overall min and max as constants makes it five chunks and nested-loop index scans, 121.5 ms to 8.7 ms. The Overview read the catalog three times, once for the quantities and once for each of its two bills. One context and one catalog snapshot now feed all three: 31 statements per load to 23. The final timings on an idle machine are in docs/ANALYSIS_REPORT.md: the brief's target request (100 meters, ten years, monthly) is 286 ms against two seconds, and a startup rebuild of 1,000 meters is 279 s.
This commit is contained in:
@@ -304,6 +304,20 @@ public sealed class ReaderTimingTests(ITestOutputHelper output)
|
||||
""";
|
||||
const string raw = "SELECT meter_id, time, amount, quality FROM consumption WHERE meter_id = ANY(@i0) AND time >= @f0 AND time < @t0";
|
||||
const string windows = """
|
||||
SELECT w.idx, count(*)::int, sum(c.amount),
|
||||
coalesce(sum(c.amount) FILTER (WHERE c.quality = 0), 0),
|
||||
coalesce(sum(c.amount) FILTER (WHERE c.quality = 2), 0),
|
||||
coalesce(sum(c.amount) FILTER (WHERE c.quality = 3), 0),
|
||||
coalesce(sum(c.amount) FILTER (WHERE c.quality NOT IN (0, 2, 3)), 0)
|
||||
FROM unnest(@ids, @froms, @tos) WITH ORDINALITY AS w(meter_id, from_time, to_time, idx)
|
||||
JOIN consumption c ON c.meter_id = w.meter_id AND c.time >= w.from_time AND c.time < w.to_time
|
||||
WHERE c.time >= @min_from AND c.time < @max_to
|
||||
GROUP BY w.idx
|
||||
""";
|
||||
|
||||
// Not the reader's SQL any more (A-40): the same statement without the overall bounds, to show what plan-time
|
||||
// chunk exclusion saves.
|
||||
const string windowsUnbounded = """
|
||||
SELECT w.idx, count(*)::int, sum(c.amount),
|
||||
coalesce(sum(c.amount) FILTER (WHERE c.quality = 0), 0),
|
||||
coalesce(sum(c.amount) FILTER (WHERE c.quality = 2), 0),
|
||||
@@ -329,6 +343,15 @@ public sealed class ReaderTimingTests(ITestOutputHelper output)
|
||||
WHERE (f.flags & @flag) <> 0
|
||||
""";
|
||||
const string recent = """
|
||||
SELECT m.id, r.time
|
||||
FROM unnest(@ids) AS m(id)
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT time FROM reading WHERE meter_id = m.id AND time >= @since ORDER BY time DESC LIMIT @count) r
|
||||
""";
|
||||
|
||||
// Not the reader's SQL any more (A-40): the same statement without the window bound, which is what made every
|
||||
// non-quantities-only request plan across every raw chunk.
|
||||
const string recentUnbounded = """
|
||||
SELECT m.id, r.time
|
||||
FROM unnest(@ids) AS m(id)
|
||||
CROSS JOIN LATERAL (
|
||||
@@ -374,23 +397,23 @@ public sealed class ReaderTimingTests(ITestOutputHelper output)
|
||||
("i0", leaves), ("f0", Midnight(today)), ("t0", Midnight(today.AddDays(1))));
|
||||
await PlanAsync("(a) coverage runs", coverage, ("ids", leaves));
|
||||
await PlanAsync("(a) opening balances", opening, ("ids", leaves), ("flag", (int)RollupFlags.OpeningBalance));
|
||||
await PlanAsync("(a) freshness — the latest readings per meter (reading is compressed after 30 days)", recent, ("ids", leaves), ("count", FreshnessRules.RecentReadingCount));
|
||||
var since = now.AddDays(-FreshnessRules.RecentWindow.TotalDays);
|
||||
var liveMeters = manifest.Groups["live"];
|
||||
await PlanAsync(
|
||||
$"(a) freshness — the rhythm of the live meters among the leaves, {FreshnessRules.RecentWindow.TotalDays:F0} days back (reading is compressed after 30 days)",
|
||||
recent, ("ids", leaves.Where(liveMeters.Contains).ToArray()), ("since", since), ("count", FreshnessRules.RecentReadingCount));
|
||||
await PlanAsync("(a) freshness — the latest event per meter", events, ("ids", leaves));
|
||||
await PlanAsync($"(b) freshness — the latest readings of every physical meter ({portfolio.Length}), as a portfolio request loads them", recent,
|
||||
("ids", portfolio), ("count", FreshnessRules.RecentReadingCount));
|
||||
await PlanAsync($"(b) freshness — the rhythm of every live meter ({liveMeters.Length}), as a portfolio request loads it", recent,
|
||||
("ids", liveMeters), ("since", since), ("count", FreshnessRules.RecentReadingCount));
|
||||
await PlanAsync(
|
||||
$"(comparison, not the reader's SQL) the same statement unbounded for every physical meter ({portfolio.Length}), as it was sent before A-40",
|
||||
recentUnbounded, ("ids", portfolio), ("count", FreshnessRules.RecentReadingCount));
|
||||
|
||||
// Not the reader's SQL: the same statement with a constant lower time bound, to show what chunk exclusion saves.
|
||||
var daily = manifest.Samples["dailyMeter"];
|
||||
const string recentBounded = """
|
||||
SELECT m.id, r.time
|
||||
FROM unnest(@ids) AS m(id)
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT time FROM reading WHERE meter_id = m.id AND time >= @since ORDER BY time DESC LIMIT @count) r
|
||||
""";
|
||||
await PlanAsync("(d) freshness — one daily meter, as every single-meter request sends it", recent,
|
||||
await PlanAsync("(d) freshness — one live meter, as a single-meter request sends it", recent,
|
||||
("ids", new[] { manifest.Samples["liveMeter"] }), ("since", since), ("count", FreshnessRules.RecentReadingCount));
|
||||
await PlanAsync("(comparison, not the reader's SQL) the same for one daily meter, unbounded, as it was sent before A-40", recentUnbounded,
|
||||
("ids", new[] { daily }), ("count", FreshnessRules.RecentReadingCount));
|
||||
await PlanAsync("(comparison, not the reader's SQL) the same for one daily meter with a constant 90-day lower bound", recentBounded,
|
||||
("ids", new[] { daily }), ("since", now.AddDays(-90)), ("count", FreshnessRules.RecentReadingCount));
|
||||
await PlanAsync($"(b) month rollups — portfolio, {portfolio.Length} physical meters × the 11 complete months of the last 12", month,
|
||||
("ids", portfolio), ("froms", portfolio.Select(_ => currentMonth.AddMonths(-11)).ToArray()), ("tos", portfolio.Select(_ => currentMonth).ToArray()));
|
||||
|
||||
@@ -401,13 +424,20 @@ public sealed class ReaderTimingTests(ITestOutputHelper output)
|
||||
await PlanAsync("(b'') window sums — one meter's two partial days (today, and its image a year ago): the small case", windows,
|
||||
("ids", new[] { single, single }),
|
||||
("froms", new[] { Midnight(today), Midnight(yearAgo) }),
|
||||
("tos", new[] { now, yearAgoCut }));
|
||||
("tos", new[] { now, yearAgoCut }),
|
||||
("min_from", Midnight(yearAgo)), ("max_to", now));
|
||||
|
||||
var stressIds = portfolio.Concat(portfolio).ToArray();
|
||||
var stressFroms = portfolio.Select(_ => Midnight(today)).Concat(portfolio.Select(_ => Midnight(yearAgo))).ToArray();
|
||||
var stressTos = portfolio.Select(_ => now).Concat(portfolio.Select(_ => yearAgoCut)).ToArray();
|
||||
await PlanAsync(
|
||||
$"(b'') window sums — stress case, not what (b'') sends (see its SQL list): today's partial day and its image a year ago for all {portfolio.Length} meters",
|
||||
windows,
|
||||
("ids", portfolio.Concat(portfolio).ToArray()),
|
||||
("froms", portfolio.Select(_ => Midnight(today)).Concat(portfolio.Select(_ => Midnight(yearAgo))).ToArray()),
|
||||
("tos", portfolio.Select(_ => now).Concat(portfolio.Select(_ => yearAgoCut)).ToArray()));
|
||||
("ids", stressIds), ("froms", stressFroms), ("tos", stressTos),
|
||||
("min_from", stressFroms.Min()), ("max_to", stressTos.Max()));
|
||||
await PlanAsync(
|
||||
"(comparison, not the reader's SQL) the same stress case without the overall bounds, as it was sent before A-40",
|
||||
windowsUnbounded, ("ids", stressIds), ("froms", stressFroms), ("tos", stressTos));
|
||||
|
||||
await PlanAsync("(d'') day rollups — one daily meter, the last 365 days", day,
|
||||
("ids", new[] { daily }), ("froms", new[] { today.AddDays(-364) }), ("tos", new[] { today }),
|
||||
|
||||
Reference in New Issue
Block a user