Shrink timed_belief value columns from float8 to float4 - #2332
Shrink timed_belief value columns from float8 to float4#2332Ahmad-Wahid wants to merge 6 commits into
Conversation
Store timed_belief's cumulative_probability and event_value as single-precision floats (float4/REAL, 4 bytes) instead of the double-precision (float8, 8 bytes) that timely_beliefs' mixin declares. These are the two per-row numeric value columns on what is typically the largest table, so halving their width shrinks it on disk by ~15-20%. Single precision keeps ~7 significant digits, which is plenty for sensor readings and for a cumulative probability in [0, 1]. - Override both columns on flexmeasures' TimedBelief subclass so fresh installs and the create_all()-based test schema match migrated DBs. - Add an Alembic migration altering the existing columns via op.alter_column (upgrade to real, downgrade to double precision). See #2331 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documentation build overview
63 files changed ·
|
Now that timed_belief stores cumulative_probability and event_value as single-precision floats (float4), values read back from the database are rounded, while an incoming candidate belief still carries full double precision. The exact-equality check in _drop_unchanged_beliefs_compared_to_db therefore saw a re-submission of already-stored data as a value change, which surfaced as a rejected replacement (403) instead of an idempotent "data has already been received" (200). Round the candidate (and, symmetrically, the DB) values to float4 before the comparison so re-ingesting identical data is correctly recognised as unchanged. Fixes test_post_sensor_data_twice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Felix Claessen <30658763+Flix6x@users.noreply.github.com>
…cy, tests Follow-up on the float4 storage change. Alembic head: the merge of main brought in 3c2f9e5a1d47, which already revises 4b0f2e9c1a6d, but this migration still pointed at 4b0f2e9c1a6d too. The branch therefore carried two heads. Repoint down_revision at 3c2f9e5a1d47. sum() over a float4 column: PostgreSQL's sum(real) both returns and accumulates in real, so the sensor-stats sum silently drops whole units once the running total passes 2^24. Sum over an explicit float8 cast instead. avg(real) already accumulates in float8 and min()/max() return an existing value, so only sum() needed this. Dedupe consistency: the comparison against the database rounded to float4, but the in-batch drop_duplicates just above it did not, so "unchanged" meant two different things in one function. Pull the rounding into a shared round_to_stored_precision() helper and use it in both places. The helper works on a copy, so the frame we return keeps the caller's original values rather than the rounded ones. Tests (the change had none): - both value columns are declared float4 -- this is what catches a future timely_beliefs release quietly reclaiming these columns; - round_to_stored_precision does not mutate its input; - re-submitting a value needing more than 7 significant digits is dropped as unchanged. Without the rounding this raises a duplicate key violation on timed_belief_pkey, so the dedupe fix is load-bearing, not cosmetic; - the sensor-stats sum does not accumulate in float4. Both values used are exactly representable in float4, isolating the accumulator from storage. Also: cite the PR rather than the issue in the changelog, add a release warning covering the table rewrite and the irreversibility, note in the migration docstring that downgrading restores the type but not the lost digits, and leave a TODO to swap the column override for timely_beliefs' value_column_type hook (SeitaBV/timely-beliefs#242) once a release carries it -- redeclaring the columns here makes cumulative_probability the leading primary-key column in create_all()-built schemas. Verified against PostgreSQL 15: data suite 248 passed, API suite 441 passed (the two test_closest_sensor failures pre-date this branch). The migration's upgrade and downgrade were also run against a real table: columns become real, the primary key survives with its order intact, NaN is preserved, and the downgrade confirms the rounding is not reversible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <felix@seita.nl>
|
Reviewed this and pushed the fixes directly to the branch (a219646) rather than leaving a list of asks — details below so you can check my work. The direction is right, and the dedupe fix in particular turned out to be load-bearing rather than defensive: without it, re-submitting a high-precision value raises a duplicate key violation on Fixed1. The branch had two Alembic heads. The merge of 2. 3. "Unchanged" meant two different things in one function. 4. Tests (there were none):
5. Docs. Changelog now cites the PR rather than the issue (matching the surrounding entries), plus a release VerificationBeyond "the SQL compiles", I ran the upgrade and downgrade against a real PostgreSQL 15 table shaped like
Test suites against PG 15: One thing I did not fix, deliberatelyRedeclaring these columns on the subclass works, but SQLAlchemy orders mixin columns by creation order, so it moves both to the front of the table and of the primary key — making the nearly-always-
You can see it in the failure output of the new dedupe test: Some drift already existed (from the The clean fix is upstream, so I opened SeitaBV/timely-beliefs#242: a value_column_type = db.Float(precision=24)There is a quieter reason to prefer it too: a subclass-body override silently discards whatever tb declares on those columns, so a future Worth deciding before mergeThe changelog warning now tells hosts to check their data first, but we can't check it for them, and "is float4 lossless for what we already store?" currently has no better answer than ad-hoc SQL. I filed SeitaBV/timely-beliefs#243 for an audit helper that reports the max relative error a float4 cast would introduce, per sensor. For our own deployments it'd be good to run that before shipping this — sensors holding cumulative meter readings or currency totals in the millions are the ones with no headroom at ~7 significant digits. 🤖 Generated with Claude Code |
round_to_stored_precision() hardcoded float4, which is only true once the 9f2c1d7b3a44 migration has run. In the window between deploying this code and running that migration -- the very maintenance window the changelog tells hosts to plan for -- the column is still float8, and rounding to float4 anyway would classify a genuine update as "unchanged" and silently drop it. That is real data loss, not a skipped redundant write. Read the precision off the mapped column instead, via a cached lookup. A Float with precision <= 24 is single precision on PostgreSQL; anything wider needs no rounding at all, so pre-migration the helper becomes a no-op. This also drops the "keep in sync with the migration" coupling, and makes the helper correct for free once the timely_beliefs value_column_type hook lands. The lookup is an attribute read on already-loaded table metadata, not a query, and it is lru_cached anyway. The import of TimedBelief is function-local because flexmeasures.data.models.time_series imports this module. Adds two tests: that no rounding happens when the column is not narrowed, and that the dtype is derived from the mapped column rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <felix@seita.nl>
The PR claimed roughly 15-20%. Measured against 12.9M rows of the ems production dump of 2026-07-28 (sensors 5 and 14), the real figure is substantially smaller: heap only 1037 MB -> 940 MB (-9.4%) heap + primary key 1760 MB -> 1663 MB (-5.5%) heap + all three indexes 2475 MB -> 2378 MB (-3.9%) Narrowing both columns saves 8 bytes of an 84.5-byte row. The estimate was high because it treated the two value columns as a larger share of the row than they are -- the 23-byte tuple header and the 16-byte belief_horizon interval dominate -- and because none of the indexes shrink: neither of the timely_beliefs search indexes contains a value column, and in the primary key the 4 bytes saved on cumulative_probability are already absorbed by btree deduplication, since every production row has cumulative_probability = 0.5. Update the changelog, the migration docstring and the ORM comment to the measured numbers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
Closes #2331.
What
Store
timed_belief'scumulative_probabilityandevent_valueas single-precision floats (float4/real, 4 bytes) instead of the double-precision (float8, 8 bytes) thattimely_beliefs'TimedBeliefDBMixindeclares. These are the two per-row numeric value columns on what is typically the largest table, so halving their width shrinks it on disk by roughly 15-20%.Single precision keeps ~7 significant decimal digits, which is plenty for sensor readings and for a cumulative probability in
[0, 1].Changes
flexmeasures/data/models/time_series.py): override both columns on FlexMeasures'TimedBeliefsubclass withdb.Float(precision=24)(→real), preservingcumulative_probability's primary-key membership,nullable=False, anddefault=0.5. This keeps fresh installs and thecreate_all()-based test schema consistent with migrated databases (tests build the schema from the ORM, not from migrations).9f2c1d7b3a44_shrink_timed_belief_values_to_float4.py): alter the existing columns viaop.alter_column— upgrade toreal, downgrade back todouble precision.float8↔float4casts are implicit in PostgreSQL, so noUSINGclause is needed.Notes / caveats
ALTER COLUMN ... TYPErewrites the whole table and rebuilds the primary key (sincecumulative_probabilityis part of it), so on a largetimed_beliefthis is a heavy, table-locking operation — run during a maintenance window. Documented in the migration docstring.float4on upgrade. Any use case genuinely needing >7 significant digits would lose precision.Verification
FLOAT(24)(→ Postgresreal) with the primary key intact.op.alter_columnemitsALTER COLUMN ... TYPE FLOAT(24)on upgrade and... TYPE FLOATon downgrade.🤖 Generated with Claude Code