Skip to content

Speed up queries via materialized view#1671

Open
Muhammad-Moiz626 wants to merge 35 commits into
mainfrom
feat/materialized-veiws
Open

Speed up queries via materialized view#1671
Muhammad-Moiz626 wants to merge 35 commits into
mainfrom
feat/materialized-veiws

Conversation

@Muhammad-Moiz626

@Muhammad-Moiz626 Muhammad-Moiz626 commented Aug 19, 2025

Copy link
Copy Markdown
Contributor

Description

Our data model with multiple beliefs per event time (multiple horizons as well as sources) makes queries computationally expensive. Most-used are queries where we look only for the most recent beliefs about each event, when given any moment in time as max belief time.

Pre-computation with Postgres' materialized views can make queries much faster. This PR will enable hosts to use this performance enhancement.

  • Created migrations file to create materialized view.
  • Use the MV in queries.
  • Use SQL Alchemy for MV.
  • Automate the refreshing of the MV.
  • Make MV optional / configurable.
  • Document the process

Performance (benchmarked)

Benchmarked with timely-beliefs>=4.0.1 (required by this PR — 4.0.1 pushes event-window filters down into the mview subquery and closes two correctness holes; without it, windowed queries scan the entire view and can even get slower). Setup: one bulk sensor with 10 belief revisions per event, on a deliberately de-clustered heap (per-sensor rows scattered across pages, as they are in practice when many sensors' data interleaves on arrival). Timings are min-of-4 after warm-up, via TimedBelief.search(most_recent_beliefs_only=True).

Query time vs. table size

365-day event window — first line: without mview (fallback), second line: with mview (ms):

xychart-beta
    title "365d window query time (ms) vs timed_belief rows"
    x-axis ["4M", "8M", "16M", "32M"]
    y-axis "ms" 0 --> 1100
    line [1048, 786, 677, 967]
    line [658, 582, 559, 613]
Loading

30-day event window — same series order (ms):

xychart-beta
    title "30d window query time (ms) vs timed_belief rows"
    x-axis ["4M", "8M", "16M", "32M"]
    y-axis "ms" 0 --> 200
    line [107, 119, 135, 170]
    line [122, 112, 151, 160]
Loading

Takeaways:

  • Wide windows: the mview wins ~1.2–1.6x, and the advantage holds as the table grows. Much of the remaining end-to-end time is pandas BeliefsDataFrame construction, so the SQL-side gain is larger than these end-to-end factors suggest.
  • Narrow windows: parity — the (event_start, sensor_id, source_id) INCLUDE (belief_horizon) covering index already bounds the fallback's work tightly at small windows.
  • These factors are conservative: on tables where many sensors interleave (worse per-sensor page locality than the benchmark heap), the fallback degrades further while the mview path doesn't.

One-time migration cost and refresh cost

First line: view + index creation (the migration); second line: non-concurrent REFRESH (including the built-in ANALYZE). Values at 64M and 128M are linear extrapolations (marked *):

xychart-beta
    title "Creation and refresh time (s) vs timed_belief rows"
    x-axis ["4.6M", "8.6M", "16.6M", "32.6M", "64M*", "128M*"]
    y-axis "seconds" 0 --> 550
    line [10, 25, 40, 132, 260, 520]
    line [8, 12, 25, 25, 50, 100]
Loading
  • Migration: roughly 1 minute per 15 million rows (so ~4 min at 64M, ~9 min at 128M). The changelog warning states this rate, and the migration logs an estimate for the host's own table size (from pg_class.reltuples) when it starts.
  • Refresh: ~25 s at 32M rows, extrapolating to ~1 min at 64M and ~2 min at 128M. Non-concurrent refresh builds a full second copy of the view before swapping — provision disk headroom accordingly (~5 GB per 32M rows), or use --concurrent.
  • Both the migration and the refresh command run ANALYZE on the view afterwards; without fresh statistics the planner picks bad plans and the view can lose to the fallback.

...

Further Improvements

  • the full queries (used in playbacks, getting fulls set of beliefs for each event) might also enjoy a materialized view, but maybe it is just too much data, then we should just limit playback to shorter time ranges
  • give hosts a range of options (e.g. refresh time)
  • actually move to TimeScaleDB, which auto-refreshes their materialized views on inserts

Related Items

Closes 108

...


Sign-off

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on code under GPL or other license that is incompatible with FlexMeasures

@Muhammad-Moiz626 Muhammad-Moiz626 marked this pull request as draft August 19, 2025 15:23
@nhoening nhoening added the Data label Aug 19, 2025
@nhoening nhoening changed the title WIP: added migration file. WIP: Speed up queries via materialized view Aug 19, 2025

@Flix6x Flix6x left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some early review comments. Nice to see the refresh logic taking shape.

Comment thread flexmeasures/api/dev/sensors.py Outdated
Comment thread flexmeasures/cli/db_ops.py Outdated
Comment thread flexmeasures/cli/db_ops.py Outdated
Comment thread flexmeasures/cli/db_ops.py Outdated
Comment thread flexmeasures/data/schemas/tests/test_input_schema.py Outdated
Comment thread flexmeasures/cli/db_ops.py Outdated
Comment thread flexmeasures/utils/config_defaults.py Outdated
@Muhammad-Moiz626 Muhammad-Moiz626 marked this pull request as ready for review August 27, 2025 12:00

@Flix6x Flix6x left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exciting work! Thanks for addressing my earlier comments. Just a short code review for now, so you don't have to wait for me testing. Looking forward to testing tomorrow.

Comment thread documentation/configuration.rst Outdated
Comment thread documentation/configuration.rst Outdated
Comment thread flexmeasures/utils/config_defaults.py Outdated
Comment thread flexmeasures/ui/views/assets/views.py Outdated
Comment thread flexmeasures/utils/validation_utils.py Outdated
Comment thread flexmeasures/ui/templates/base.html Outdated
Comment thread flexmeasures/ui/templates/base.html Outdated
Comment thread flexmeasures/utils/validation_utils.py Outdated
Comment thread flexmeasures/utils/validation_utils.py Outdated

@nhoening nhoening left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also a quick review from me, before having tested myself, mostly about concepts.

I also suggest moving get_timed_belief_min_v() to timely-beliefs and add basic documentation there (not much more than the SQL command so anybody reading only that could in principle use it)

Comment thread documentation/configuration.rst Outdated
Comment thread documentation/configuration.rst Outdated
Comment thread documentation/configuration.rst Outdated
Comment thread documentation/configuration.rst Outdated
Comment thread flexmeasures/data/migrations/versions/timed_beliefs_materialized_views.py Outdated
Comment thread flexmeasures/utils/validation_utils.py Outdated
@Muhammad-Moiz626 Muhammad-Moiz626 changed the title WIP: Speed up queries via materialized view Speed up queries via materialized view Sep 8, 2025
Comment thread documentation/configuration.rst Outdated
…erialized-veiws

# Conflicts:
#	documentation/changelog.rst
#	documentation/configuration.rst
#	flexmeasures/data/models/generic_assets.py
#	flexmeasures/ui/templates/base.html
Signed-off-by: F.N. Claessen <felix@seita.nl>
Ahmad-Wahid and others added 6 commits June 4, 2026 21:46
Resolved conflicts in changelog.rst, configuration.rst, time_series.py,
assets.py, base.html, and views.py, keeping materialized view features
alongside upstream changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
base.html gained ~950 lines of stale template blocks (copies of JS that lives in
includes/graphs.html) and openapi-specs.json had its version reverted. Restore both
to main; the materialized-view UI toggle will be re-added on top of the real
chart-loading code in includes/graphs.html.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qprz6PKnXkVE1TJ8d7PYe9
…recorded refresh time

- migration: frozen SQL from timely-beliefs' DDL generators (simplified view query, no
  CASCADE on downgrade), repointed onto main's migration head (drops the stale merge migration)
- CLI: refresh-materialized-views records its run in latest_task_run via
  @task_with_status_report, supports --concurrent (autocommit connection), aborts with an
  error exit code on failure; monitorable with 'flexmeasures monitor latest-run'
- config: replace FLEXMEASURES_MVIEW_REFRESH_INTERVAL with
  FLEXMEASURES_MVIEW_ALWAYS_INCLUDE_LIVE_TAIL (the refresh cadence now lives solely in how
  hosts schedule the CLI command; the recorded refresh time is the single source of truth)
- queries: TimedBelief.search decides centrally whether to trust the view (it must exist at
  startup and have a recent enough recorded refresh) and passes a cutoff so events recorded
  since the last refresh are looked up in the beliefs table (live tail); new include_live_tail
  param threaded through Sensor/GenericAsset.search_beliefs and the chart data API endpoints
- UI: 'Include latest data' toggle on asset graphs (shown only when the view is in use and
  the live tail is not already always included), wired to the real chart-loading code in
  includes/graphs.html; tooltip shows the actual last refresh time
- docs: configuration.rst setting description, hosting docs section with cron example,
  changelog entry
- tests: CLI refresh success/failure recording, mview/live search equivalence

Note: requires the timely-beliefs release containing SeitaBV/timely-beliefs#204
(materialized_views module); the dependency pin should be bumped once that is released.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qprz6PKnXkVE1TJ8d7PYe9
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedtimely-beliefs@​3.5.5 ⏵ 4.0.1100 +1100100100100

View full report

Flix6x and others added 9 commits July 6, 2026 10:40
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…at/materialized-veiws

# Conflicts:
#	uv.lock
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Signed-off-by: F.N. Claessen <claessen@seita.nl>
After CREATE MATERIALIZED VIEW or REFRESH MATERIALIZED VIEW, autovacuum
may not get around to analyzing the view right away, leaving the
Postgres planner without statistics for it. This can make queries that
go through most_recent_beliefs_mview slower than the fallback path,
until a manual ANALYZE fixes it. Run ANALYZE explicitly after the
migration creates the view and after the refresh-materialized-views CLI
command refreshes it (on the same connection/session used for the
refresh, so it also works for the CONCURRENTLY branch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4jYQMv87WJt1Y1SB7gi1w
…wn and correctness fixes

4.0.1 pushes the caller's event-window bounds down into the materialized
view subquery (Postgres does not propagate range predicates across the
equijoin, so windowed queries used to scan the entire view), and disables
the view when belief-time filters or non-group-constant custom filter
criteria could silently drop events.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4jYQMv87WJt1Y1SB7gi1w
…imate

Benchmarks (see PR #1671) measured view + index creation at roughly
1 minute per 15 million timed_belief rows, scaling linearly. The
changelog warning now states that rate, and the migration logs an
estimate for the host's own table size (via pg_class.reltuples, to
avoid paying for an exact count) before it starts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4jYQMv87WJt1Y1SB7gi1w
@Flix6x Flix6x added this to the 1.0.0 milestone Jul 10, 2026
…visible

FlexMeasures' logging configuration does not surface the alembic logger,
so the estimate added in f725653 never reached the terminal (verified
against a full production-scale restore, where the whole migration took
~1 minute for 118.6M rows - the stated 1-minute-per-15M-rows rate is a
conservative upper bound on idle NVMe hardware). Print the message as
well, and phrase the estimate as an upper bound.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4jYQMv87WJt1Y1SB7gi1w
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants