Analysis: bound the freshness query, let window sums skip chunks, share the Overview's catalog
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:
Florian Schmidt
2026-09-20 11:16:48 +02:00
parent ec51419f64
commit 5a6f34a467
21 changed files with 1857 additions and 87 deletions
@@ -10,6 +10,7 @@ using MeterVault.Infrastructure.Ingestion;
using MeterVault.Infrastructure.Normalization;
using MeterVault.Infrastructure.Options;
using MeterVault.Infrastructure.Persistence;
using MeterVault.Integration.Tests.Performance;
using Microsoft.EntityFrameworkCore;
using static MeterVault.Integration.Tests.Reconciliation.ReconciliationSupport;
@@ -694,6 +695,49 @@ public sealed class AnalysisReaderTests(TimescaleFixture fx) : IAsyncLifetime
Assert.DoesNotContain(result.Problems, p => p.Kind == AnalysisProblemKind.StaleSource && p.MeterId != stale);
}
[Fact]
public async Task An_import_only_meter_keeps_its_years_old_mark_without_reading_the_raw_series()
{
// D-18, A-40: the mark is the meter's own last reading — for an import-only meter as old as its data — and it
// is read from the stored rollup state, so the request never plans across the raw chunks to find it.
var type = await TypeAsync();
var historical = await MeterAsync(type, MeterMode.CumulativeCounter, "kWh", installedAt: new DateOnly(2018, 1, 1));
await ReadingsAsync(historical, (Local(Berlin, 2018, 3, 5, 9), 100), (Local(Berlin, 2018, 4, 5, 9), 140));
using var counter = CommandCounter.Start();
var result = await Reader().ReadAsync(new AnalysisRequest(AnalysisScope.ForMeter(historical), Resolve(PeriodPreset.MonthToDate)));
var freshness = result.SeriesFor(historical)!.Freshness;
Assert.Equal(FreshnessState.Historical, freshness.State);
Assert.Equal(Local(Berlin, 2018, 4, 5, 9), freshness.LastActivity);
Assert.DoesNotContain(counter.Statements, s => s.Contains("SELECT m.id, r.time", StringComparison.Ordinal));
}
[Fact]
public async Task A_live_source_silent_for_longer_than_the_recent_window_is_still_stale_by_its_own_rhythm()
{
// A-40: the bounded sample finds nothing for a meter that has been quiet for months, so its whole history is
// read for those few meters — the verdict and the mark are the same as before the bound existed.
var type = await TypeAsync();
var meter = await MeterAsync(type, MeterMode.CumulativeCounter, "kWh", installedAt: new DateOnly(2026, 1, 1));
await using (var db = fx.CreateContext())
{
db.MeterSources.Add(new MeterSource { MeterId = meter, SourceType = SourceType.Mqtt, Config = """{"topic":"tele/d/SENSOR"}""" });
await db.SaveChangesAsync();
}
// Hourly for a day in February, then nothing: seven months of silence, far outside FreshnessRules.RecentWindow.
var start = Local(Berlin, 2026, 2, 1);
await ReadingsAsync(meter, [.. Enumerable.Range(0, 25).Select(h => (start.AddHours(h), (double)h))]);
var result = await Reader().ReadAsync(new AnalysisRequest(AnalysisScope.ForMeter(meter), Resolve(PeriodPreset.YearToDate)));
var freshness = result.SeriesFor(meter)!.Freshness;
Assert.Equal(FreshnessState.Stale, freshness.State);
Assert.Equal(TimeSpan.FromHours(3), freshness.StaleAfter);
Assert.Equal(start.AddHours(24), freshness.LastActivity);
}
// ------------------------------------------------------------------------------------------------ limits, availability
[Fact]
@@ -0,0 +1,184 @@
using MeterVault.Core.Domain;
using MeterVault.Core.Normalization;
using MeterVault.Infrastructure.Normalization;
using MeterVault.Infrastructure.Options;
using MeterVault.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace MeterVault.Integration.Tests.Analysis;
/// <summary>
/// The window-sum statement of the reader (D-15, A-40). Its windows arrive through an <c>unnest</c> join, so their
/// bounds are columns: without the overall <c>[min from, max to)</c> repeated as constants, PostgreSQL has nothing to
/// exclude chunks by and plans — and, with enough windows, scans — the whole <c>consumption</c> hypertable. The bounds
/// are pure arithmetic over the window set, so they may never change a tally; this pins both halves of that.
/// </summary>
[Collection("Timescale")]
public sealed class WindowSumPlanTests(TimescaleFixture fx) : IAsyncLifetime
{
private const string BerlinId = "Europe/Berlin";
private static readonly TimeZoneInfo Berlin = TimeZoneInfo.FindSystemTimeZoneById(BerlinId);
/// <summary>The reader's statement (<c>AnalysisQueries.WindowSumsAsync</c>).</summary>
private const string Bounded = """
SELECT w.idx, count(*)::int, sum(c.amount)
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
""";
/// <summary>The same without the overall bounds, as it was sent before A-40.</summary>
private const string Unbounded = """
SELECT w.idx, count(*)::int, sum(c.amount)
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
GROUP BY w.idx
""";
private int _meter;
private short _type;
public async Task InitializeAsync()
{
await using var db = fx.CreateContext();
var type = new EnergyType
{
Key = $"windows-{Guid.NewGuid():N}",
DisplayName = "Window sums",
BaseUnit = "kWh",
DefaultMode = MeterMode.CumulativeCounter,
};
db.EnergyTypes.Add(type);
await db.SaveChangesAsync();
_type = type.Id;
var meter = new Meter
{
Name = $"windows-{Guid.NewGuid():N}",
EnergyTypeId = _type,
Mode = MeterMode.CumulativeCounter,
Unit = "kWh",
InstalledAt = new DateOnly(2020, 1, 1),
};
db.Meters.Add(meter);
await db.SaveChangesAsync();
_meter = meter.Id;
// Three years of daily readings: `consumption` is chunked by 90 days, so the meter's own rows alone spread
// over a dozen chunks — enough for plan-time exclusion to be visible.
var register = 0d;
for (var day = new DateOnly(2020, 1, 2); day <= new DateOnly(2022, 12, 31); day = day.AddDays(1))
{
register += 1.5;
db.Readings.Add(new Reading
{
MeterId = _meter,
Time = GapAttribution.LocalMidnight(day, Berlin).AddHours(6),
Value = register,
Quality = ReadingQuality.Measured,
});
}
await db.SaveChangesAsync();
await Normalization(db).RecomputeMeterAsync(_meter, null);
await db.SaveChangesAsync();
}
public async Task DisposeAsync()
{
await using var db = fx.CreateContext();
await db.Consumption.Where(c => c.MeterId == _meter).ExecuteDeleteAsync();
await db.Readings.Where(r => r.MeterId == _meter).ExecuteDeleteAsync();
await db.Meters.Where(m => m.Id == _meter).ExecuteDeleteAsync();
await db.EnergyTypes.Where(t => t.Id == _type).ExecuteDeleteAsync();
}
[Fact]
public async Task The_overall_bounds_exclude_chunks_and_change_no_tally()
{
await using var connection = new NpgsqlConnection(fx.ConnectionString);
await connection.OpenAsync();
// Two windows, both inside the last three months of the meter's history.
var from = GapAttribution.LocalMidnight(new DateOnly(2022, 12, 20), Berlin);
var ids = new[] { _meter, _meter };
var froms = new[] { from, from.AddDays(3) };
var tos = new[] { from.AddDays(1), from.AddDays(4) };
var bounded = await TalliesAsync(connection, Bounded, ids, froms, tos, bounds: true);
var unbounded = await TalliesAsync(connection, Unbounded, ids, froms, tos, bounds: false);
Assert.Equal(2, bounded.Count);
Assert.Equal(unbounded, bounded);
var boundedChunks = await ChunksAsync(connection, Bounded, ids, froms, tos, bounds: true);
var unboundedChunks = await ChunksAsync(connection, Unbounded, ids, froms, tos, bounds: false);
// The old statement has to keep every chunk of the table in its plan; the bounded one keeps the few the
// windows can fall into. A table with a single chunk would make this vacuous, so the shape is asserted too.
Assert.True(unboundedChunks > 4, $"expected a chunked consumption table, saw {unboundedChunks} chunks in the plan");
Assert.True(
boundedChunks < unboundedChunks,
$"the bounded statement planned {boundedChunks} chunks, the unbounded one {unboundedChunks}");
Assert.True(boundedChunks <= 2, $"two windows three days apart should reach at most two chunks, not {boundedChunks}");
}
private static async Task<List<(long Index, int Rows, double Sum)>> TalliesAsync(
NpgsqlConnection connection, string sql, int[] ids, DateTimeOffset[] froms, DateTimeOffset[] tos, bool bounds)
{
await using var command = Prepare(connection, sql, ids, froms, tos, bounds);
var rows = new List<(long, int, double)>();
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
rows.Add((reader.GetInt64(0), reader.GetInt32(1), reader.GetDouble(2)));
}
rows.Sort();
return rows;
}
/// <summary>How many hypertable chunks the plan of <paramref name="sql"/> mentions.</summary>
private static async Task<int> ChunksAsync(
NpgsqlConnection connection, string sql, int[] ids, DateTimeOffset[] froms, DateTimeOffset[] tos, bool bounds)
{
await using var command = Prepare(connection, "EXPLAIN " + sql, ids, froms, tos, bounds);
var chunks = new HashSet<string>(StringComparer.Ordinal);
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
foreach (var word in reader.GetString(0).Split([' ', '(', ')', ','], StringSplitOptions.RemoveEmptyEntries))
{
if (word.StartsWith("_hyper_", StringComparison.Ordinal))
{
chunks.Add(word);
}
}
}
return chunks.Count;
}
private static NpgsqlCommand Prepare(
NpgsqlConnection connection, string sql, int[] ids, DateTimeOffset[] froms, DateTimeOffset[] tos, bool bounds)
{
var command = new NpgsqlCommand(sql, connection);
command.Parameters.AddWithValue("ids", ids);
command.Parameters.AddWithValue("froms", froms);
command.Parameters.AddWithValue("tos", tos);
if (bounds)
{
command.Parameters.AddWithValue("min_from", froms.Min());
command.Parameters.AddWithValue("max_to", tos.Max());
}
return command;
}
private static NormalizationService Normalization(MeterVaultDbContext db) =>
new(db, NormalizationEngine.CreateDefault(),
Microsoft.Extensions.Options.Options.Create(new MeterVaultOptions { TimeZone = BerlinId }),
TimeProvider.System);
}
@@ -0,0 +1,134 @@
using MeterVault.App;
using MeterVault.Core.Analysis;
using MeterVault.Infrastructure.Costing;
using MeterVault.Infrastructure.Dashboard;
using MeterVault.Infrastructure.Import;
using MeterVault.Infrastructure.Options;
using MeterVault.Infrastructure.Persistence;
using MeterVault.Integration.Tests.Performance;
using Microsoft.EntityFrameworkCore;
using static MeterVault.Integration.Tests.Costing.CostSandbox;
namespace MeterVault.Integration.Tests.Overview;
/// <summary>
/// What one Overview load costs in SQL (D-15, D-56, A-40). The Overview is the widest read in the app — every energy
/// type, every meter's series, the bill with its composition and the comparison's bill — and the page builds all of it
/// from one <see cref="DashboardService.GetOverviewAsync"/>. That call must stay a constant handful of statements
/// whatever the instance holds: one quantity read, one bill for the period and one for the comparison, each reusing the
/// one result for the cards, the change table and the composition. A figure computed twice shows up here as a repeated
/// catalog, tariff or manual-cost load long before it shows up as a slow page.
/// </summary>
/// <remarks>The Overview reads the whole instance, so every test starts from — and leaves — an instance without meters.</remarks>
[Collection("Timescale")]
public sealed class OverviewReadBudgetTests(TimescaleFixture fx) : IAsyncLifetime
{
/// <summary>
/// What one load with a comparison sends: four for the shared catalog, seven for the quantities, two for the
/// manual-cost dates and the energy types, and five for each of the two bills. It is asserted exactly, so any new
/// per-type, per-category or per-meter read fails here instead of being measured later.
/// </summary>
private const int Budget = 23;
private static readonly ComparisonRequest PreviousYear = new(ComparisonKind.PreviousYear);
public async Task InitializeAsync()
{
await using var db = fx.CreateContext();
await ClearAsync(db);
}
public async Task DisposeAsync()
{
await using var db = fx.CreateContext();
await ClearAsync(db);
}
[Fact]
public async Task One_overview_load_prices_the_period_once_and_its_comparison_once()
{
await LoadReferenceDataAsync();
var dashboard = Dashboard();
// Warm up: the first call of the process compiles EF queries and opens the pool, which sends its own statements.
await dashboard.GetOverviewAsync(Preset(PeriodPreset.PreviousYear), BucketSize.Auto, PreviousYear);
using var counter = CommandCounter.Start();
var overview = await dashboard.GetOverviewAsync(Preset(PeriodPreset.PreviousYear), BucketSize.Auto, PreviousYear);
var statements = counter.Statements;
// The figures are the ones OverviewDataTests pins; this test only counts what it took to get them.
Assert.Equal(12, overview.Plan.Buckets.Count);
Assert.NotNull(overview.PreviousCost);
Assert.NotEmpty(overview.CategoryChanges);
Assert.NotEmpty(overview.LineChanges);
Assert.True(
statements.Count == Budget,
$"One Overview load sent {statements.Count} statements, not {Budget}:\n{string.Join('\n', statements)}");
// The catalog — meters, tanks, links, rollup states — is loaded once and shared by all three reads (A-40).
Assert.Equal(1, Count(statements, "m.initial_baseline"));
Assert.Equal(1, Count(statements, "m.meter_id, m.built_at"));
// One bill for the period and one for the comparison, each loading tariffs, manual costs and categories once.
Assert.Equal(2, Count(statements, "t.valid_from"));
Assert.Equal(2, Count(statements, "m.amount, m.category_id"));
Assert.Equal(2, Count(statements, "c.color_hex"));
// One quantity read for the page, plus the one each bill makes for the meters it prices.
Assert.Equal(3, Count(statements, "span_from"));
Assert.Equal(3, Count(statements, "r.meter_id, r.month"));
// The freshness mark comes from the stored rollup state, so nothing reads the raw hypertable: the seeded
// instance has no live source, and an import-only meter's mark is not sampled from recent readings (A-40).
Assert.Equal(0, Count(statements, "SELECT m.id, r.time"));
}
[Fact]
public async Task An_overview_without_a_comparison_prices_the_period_once()
{
await LoadReferenceDataAsync();
var dashboard = Dashboard();
await dashboard.GetOverviewAsync(Preset(PeriodPreset.PreviousYear), BucketSize.Auto, new ComparisonRequest(ComparisonKind.None));
using var counter = CommandCounter.Start();
await dashboard.GetOverviewAsync(Preset(PeriodPreset.PreviousYear), BucketSize.Auto, new ComparisonRequest(ComparisonKind.None));
Assert.Equal(1, Count(counter.Statements, "t.valid_from"));
Assert.Equal(1, Count(counter.Statements, "m.amount, m.category_id"));
Assert.Equal(1, Count(counter.Statements, "m.initial_baseline"));
}
private static int Count(IReadOnlyList<string> statements, string fragment) =>
statements.Count(s => s.Contains(fragment, StringComparison.OrdinalIgnoreCase));
private DashboardService Dashboard()
{
var clock = new FixedTimeProvider(Now);
var options = Microsoft.Extensions.Options.Options.Create(new MeterVaultOptions { TimeZone = BerlinId });
return new DashboardService(fx, new CostService(fx, options, clock), clock);
}
private async Task LoadReferenceDataAsync()
{
await using var db = fx.CreateContext();
var importer = new ReferenceDataImporter(db, new ImportService(db, Normalization(db)), new CsvImporter());
await importer.LoadAsync(Path.Combine(AppContext.BaseDirectory, "fixtures"));
}
private static async Task ClearAsync(MeterVaultDbContext db)
{
await db.MeterLinks.ExecuteDeleteAsync();
await db.Consumption.ExecuteDeleteAsync();
await db.Readings.ExecuteDeleteAsync();
await db.MeterEvents.ExecuteDeleteAsync();
await db.ManualCosts.ExecuteDeleteAsync();
await db.CostCategoryMembers.ExecuteDeleteAsync();
await db.Tariffs.ExecuteDeleteAsync();
await db.Tanks.ExecuteDeleteAsync();
await db.MeterSources.ExecuteDeleteAsync();
await db.Meters.ExecuteDeleteAsync();
await db.ImportBatches.ExecuteDeleteAsync();
}
}
@@ -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 }),