Skip to content

Wind energy workshop - #13

Open
sergioferragut wants to merge 2 commits into
mainfrom
wind-energy-workshop
Open

Wind energy workshop#13
sergioferragut wants to merge 2 commits into
mainfrom
wind-energy-workshop

Conversation

@sergioferragut

Copy link
Copy Markdown
Contributor

See commits for detailed descriptions.

This PR adds a static location PostGIS + timescaleDB example to the workshop library.
The use case is Wind Energy Production monitoring across multiple plants across the world.

sergioferragut and others added 2 commits August 11, 2026 09:47
Teaches PostGIS and TimescaleDB together on fixed-location assets: a portfolio
of wind plants across six real regions, laid out on a geodesic grid with
ST_Project and modelling how the turbines steal each other's wind.

Contents:
  * 14 numbered SQL steps, run top to bottom in psql
  * reset_demo.sh, orchestration only — it runs the SQL and prints verification
  * four provisioned Grafana dashboards forming a global > region > plant >
    turbine drill-through with cascading filters

Notable design decisions, each measured rather than assumed:

  * Turbine layout is a STAGGERED checkerboard, not a plain grid. Aligned rows
    put the worst wake exactly on the prevailing wind: 25.8% mean power loss at
    the design bearing, against 0.0% once staggered, with the loss reappearing
    as narrow 29% spikes at the +/-17 degree diagonal alignment.

  * The turbine view selects its own source — raw hypertable, hourly aggregate
    or daily aggregate — from the current zoom level, with the untaken UNION ALL
    branches constant-folded out of the plan.

  * The backfill pre-creates chunks before fanning out. Chunk creation takes
    ShareUpdateExclusiveLock on the parent and holds it to commit, so workers
    that create their own chunks serialise: 1.09x on four workers against 3.67x
    with the chunks already in place.

  * The aggregate fill parallelises by WINDOW, aligned to each aggregate's
    materialization chunk interval, with the aggregates themselves kept in
    dependency order — refreshing a child before its parent leaves the child
    silently empty.

  * All data is generated by SQL functions an attendee can read and re-run.
    Volume is parameterised (--plants, --turbines-per-plant, --days), so the
    same workshop runs at 5,000 rows or 13 million.

Also adds a repo-root .env.example: one set of connection settings shared by
every workshop, which each reset_demo.sh reads as ../.env.

Three repo-root working notes come with it. These are for us, not for workshop
attendees, and they are deliberately WORKSHOP-AGNOSTIC: they describe how the
platforms behave, in neutral vocabulary, so they stay useful whatever the next
workshop is about. They exist because this material is easy to get wrong in ways
that produce no error — a stale doc claim or a mis-set panel field just renders
the wrong answer.

  * CLAUDE.md — how to work in this repo. Conventions (one workshop per
    top-level directory, SQL house style, the README skeleton), the standing
    rule to verify platform behaviour against the docs and source rather than
    from recall, and the non-negotiables: everything must work self-service on a
    fresh Tiger Cloud service, iterate at the smallest data volume that
    reproduces the behaviour, and data generation lives in SQL.

    It also carries the rules for maintaining the two platform files, since they
    are the part of this repo that accumulates: every workshop is an occasion to
    extend them, an entry must generalise away from the workshop that prompted
    it, confidence labels have to be earned, and a disproven entry gets corrected
    in place rather than left to be trusted.

  * TIGER_PLATFORM.md — durable facts about TimescaleDB, Tiger Cloud and PostGIS
    as they actually behave, each entry labelled VERIFIED, DOCUMENTED or
    ASSUMED, with the measurement or source that earned the label. Covers
    continuous aggregates, Hypercore and the columnstore, retention and tiering,
    the real extension inventory and which features are gated, idempotent
    generation, and PostGIS pitfalls.

  * GRAFANA_PLATFORM.md — the same, for Grafana: panel JSON and the grid,
    provisioning and datasource wiring, geomap view and layer behaviour, and the
    SQL that has to live inside a panel. Separate from the database notes
    because a wrong field name in panel JSON raises nothing at all — the panel
    simply renders wrong, so these findings need somewhere to live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 08 filled its eight aggregates through a generated \gexec block guarded by
a config flag, with a companion cagg_fill_plan() window planner that only
reset_demo.sh ever called. That obscured the lesson it was supposed to teach, so
the fill is now eight plain CALL refresh_continuous_aggregate() statements,
parents before children.

Removing the parallel path was a deliberate trade, not an oversight. Since
TimescaleDB 2.28 the call batches incrementally on its own, which retires the
MEMORY argument for hand-rolled windowing and leaves only speed — and speed has
to be judged against the whole build. The measured 2.83x applied to ~16s of a
~100s build, so it saved ~10s in exchange for a window planner, a config key, a
conditional in the SQL and 70 lines of shell that the default path never ran.
The raw-data backfill keeps its --jobs parallelism, where the same arithmetic
comes out the other way (3.67x on the dominant phase).

The prose now states what the defaults actually are, because both are easy to
misread: buckets_per_batch is 10 BUCKETS (not chunks, not rows), and the batches
are SEQUENTIAL, each in its own transaction — that is what bounds memory, and it
is not concurrency. Includes an xact_commit recipe to observe it: 1202 commits by
default against 6 with buckets_per_batch: 0.

Fix a real bug found while verifying the above. A refresh range's upper bound
snaps DOWN to a bucket boundary, but a refresh ending inside an OPEN bucket
materialises that whole bucket and pushes the watermark to its end — leaving the
watermark ahead of the wall clock. Real-time aggregation only unions rows above
the watermark, so rows written into that bucket afterwards are invisible, and
force => true cannot reach them because its upper bound snaps down identically.
Step 10's NULL,NULL refresh set the condition; step 12 then wrote at now() and
refreshed to now(), losing exactly one row per turbine it had just added (721 raw
against 720 aggregated). Step 12 now bounds at the end of the current bucket, and
reset_demo.sh asserts SUM(readings) = COUNT(*), which nothing else here caught.
Verified sensitive: injecting one row into a materialised bucket reports -1.

Explain the backfill worker count instead of just reporting "serial", which reads
as a missing feature. It falls to 1 on small services on purpose: the load is
CPU-bound in one backend, so extra connections need spare cores, and below 2 CPU
they cost time. Measured at 563,588 rows/hypertable — 0.5 CPU: 50s serial vs 68s
at 2 jobs; 1 CPU: 21s vs 24s vs 30s at 4; 4 CPU: 20s vs 15s vs 13s.

Also: answer the enable_merge_on_cagg_refresh question in the file, since anyone
reading the docs will reach for it. It is silently ignored on any aggregate with
columnstore enabled (the gate is compression_enabled on the AGGREGATE, not
whether the chunk is compressed yet), and does nothing for a cold fill either.
Correct two stale comments — the backfill batches on chunk boundaries, not
one-month windows — and give the migration's aggregates IF NOT EXISTS, which
continuous aggregates do honour, so it is re-runnable.

TIGER_PLATFORM.md gains the generalised versions: the open-bucket hole and how
to assert against it, the batching defaults, the merge GUC's two conditions, and
the rule that connection-level parallelism for a generated load scales with cores
and is negative below two.

Verified at 6.73M rows per hypertable (12 plants x 8 turbines x 730 days) in
126s with no OOM, all eight aggregates non-empty, step 08 and the migration both
re-runnable under ON_ERROR_STOP=1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Aug 11, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants