feat(db): normalise series index into shared index_axis table#747
Conversation
Series metric values previously stored their full index array and index name inline on every row. In production a handful of axes (e.g. a common monthly time axis) are shared by tens of thousands of series, so the index dominated the database (~40% of a 922MB production database, almost entirely redundant). Move the index into a deduplicated `index_axis` table keyed by a content hash, and replace `metric_value.index` / `index_name` with an `index_id` foreign key. `SeriesMetricValue.index` and `index_name` remain available as read-only properties resolved from the shared axis, so existing readers (including the ref-app API) are unaffected. The migration is lossless; verified against a copy of the production database it reclaimed 388MB (42%), with no hash collisions and no reconstruction mismatches.
Codecov Report❌ Patch coverage is
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 3 files with indirect coverage changes 🚀 New features to boost your workflow:
|
`index` is not in SQLAlchemy's dialect reserved-word tables, so it was emitted unquoted and PostgreSQL rejected `DROP COLUMN index`. Force the quote via `quoted_name` so the DDL is valid on both PostgreSQL and SQLite. The unit suite only exercises SQLite, which recreates the table in batch mode and so never hit the bare identifier.
Set `notify.after_n_builds` (and `comment.after_n_builds`) to 6 so codecov waits for every coverage report before posting any status, instead of marking `codecov/project` failed against partial coverage while jobs are still running. Count = tests-core (1) + tests-integration (1) + tests-providers (4).
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughIntroduces a ChangesSeriesIndex Normalisation
Codecov Configuration
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/climate-ref/src/climate_ref/executor/result_handling.py (1)
180-201: 💤 Low valueMinor: Double hash computation per series.
The hash is computed twice for each
series_result— once in the first loop (line 185) and once in the second loop (line 192). This is not incorrect, but you could store the digest alongside the series data in the first pass to avoid recomputing it.♻️ Suggested optimisation
axis_id_by_hash: dict[str, int] = {} - for series_result in series_values: - digest = SeriesIndex.compute_hash(series_result.index_name, series_result.index) + series_with_digest: list[tuple[str, Any]] = [] + for series_result in series_values: + digest = SeriesIndex.compute_hash(series_result.index_name, series_result.index) + series_with_digest.append((digest, series_result)) if digest not in axis_id_by_hash: axis = SeriesIndex.get_or_create(database.session, series_result.index_name, series_result.index) axis_id_by_hash[digest] = axis.id new_values = [] - for series_result in series_values: - digest = SeriesIndex.compute_hash(series_result.index_name, series_result.index) + for digest, series_result in series_with_digest: new_values.append( { "execution_id": execution.id,
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a9823cbe-f78f-4252-966a-426a9d4a18e6
📒 Files selected for processing (7)
changelog/747.improvement.mdcodecov.ymlpackages/climate-ref/src/climate_ref/executor/result_handling.pypackages/climate-ref/src/climate_ref/migrations/versions/2026-06-19T0000_d4e5f6a7b8c9_normalise_series_index.pypackages/climate-ref/src/climate_ref/models/__init__.pypackages/climate-ref/src/climate_ref/models/metric_value.pypackages/climate-ref/tests/unit/models/test_metric_value.py
…migration Setting `stream_results=True` via `bind.execution_options(...)` mutates the shared migration connection, so the option persisted to the subsequent DDL. On PostgreSQL that wraps the `DROP COLUMN` in a server-side cursor (`DECLARE ... CURSOR WITHOUT HOLD FOR ALTER TABLE ...`), which is a syntax error. Apply the option to the SELECT statement only. Verified end-to-end against PostgreSQL 16; SQLite was unaffected (no server-side cursors), which is why the unit suite missed it.
Description
Series metric values previously stored their full index array (and index name) inline on every row. In production a handful of axes — for example a single common monthly time axis — are shared by tens of thousands of series, so the index column dominated the database: it accounted for ~40% of a 922MB production database and was almost entirely redundant.
This moves the index into a deduplicated
index_axistable keyed by a content hash (sha256over(name, values)), and replacesmetric_value.index/metric_value.index_namewith anindex_idforeign key. Ingestion (ingest_series_values) now resolves the distinct axes for a batch once viaSeriesIndex.get_or_createand references them by id.SeriesMetricValue.indexandSeriesMetricValue.index_nameremain available as read-only properties resolved from the shared axis (loaded eagerly vialazy="joined"), so existing readers — including the ref-app API and the series length validator — are unaffected. The on-diskseries.jsonshape (theclimate_ref_corePydantic type) is unchanged.The Alembic migration is lossless: it streams the series rows, deduplicates axes by content hash, backfills
index_idin batches, then drops the old columns. Verified against a copy of the production database it reclaimed 388MB (42%, 924MB -> 536MB) with zero hash collisions and zero reconstruction mismatches across the sampled rows. Note that aVACUUMis required after applying the migration for SQLite to release the freed pages on disk.Checklist
Please confirm that this pull request has done the following:
changelog/Summary by CodeRabbit
Database Optimisation
VACUUMafter the upgrade to reclaim freed disk space.Chores