diff --git a/priv/repo/migrations/20260421181818_add_hrrr_profiles_valid_time_latlon_index.exs b/priv/repo/migrations/20260421181818_add_hrrr_profiles_valid_time_latlon_index.exs new file mode 100644 index 00000000..86ad570d --- /dev/null +++ b/priv/repo/migrations/20260421181818_add_hrrr_profiles_valid_time_latlon_index.exs @@ -0,0 +1,78 @@ +defmodule Microwaveprop.Repo.Migrations.AddHrrrProfilesValidTimeLatlonIndex do + use Ecto.Migration + + # Matches the hot range-scan access pattern in + # `Weather.find_nearest_hrrr/3` and `Weather.find_nearest_native_profile/3`: + # WHERE valid_time BETWEEN ? AND ? + # AND lat BETWEEN ? AND ? + # AND lon BETWEEN ? AND ? + # `valid_time` leads because the ±1h window collapses the search to a + # single partition and a narrow btree slice; lat/lon then filter the + # ~9×9 tile grid within that bucket. The existing unique index + # `(lat, lon, valid_time)` services upsert dedup but its first column + # is lat — it can't serve a three-range scan that is selective only + # on valid_time. + # + # `hrrr_profiles` is partitioned by `valid_time`, so CREATE INDEX + # CONCURRENTLY cannot run directly on the partitioned parent. We use + # the PG 11+ pattern: create an invalid index on ONLY the parent, + # concurrently build a matching index on each partition, then ATTACH + # each partition index to the parent — Postgres validates the parent + # once every child is attached. + # + # `hrrr_native_profiles` is a regular table, so the standard + # `create index(..., concurrently: true)` form applies. + + @disable_ddl_transaction true + @disable_migration_lock true + + def up do + # Parent index (invalid until partitions attach). + execute """ + CREATE INDEX IF NOT EXISTS hrrr_profiles_valid_time_lat_lon_index + ON ONLY hrrr_profiles (valid_time, lat, lon) + """ + + for partition <- hrrr_profile_partitions() do + child_idx = "#{partition}_valid_time_lat_lon_index" + + execute """ + CREATE INDEX CONCURRENTLY IF NOT EXISTS #{child_idx} + ON #{partition} (valid_time, lat, lon) + """ + + execute """ + ALTER INDEX hrrr_profiles_valid_time_lat_lon_index + ATTACH PARTITION #{child_idx} + """ + end + + # Native profiles: not partitioned, so concurrently works directly. + execute """ + CREATE INDEX CONCURRENTLY IF NOT EXISTS hrrr_native_profiles_valid_time_lat_lon_index + ON hrrr_native_profiles (valid_time, lat, lon) + """ + end + + def down do + execute "DROP INDEX IF EXISTS hrrr_native_profiles_valid_time_lat_lon_index" + execute "DROP INDEX IF EXISTS hrrr_profiles_valid_time_lat_lon_index" + end + + defp hrrr_profile_partitions do + {:ok, %{rows: rows}} = + repo().query( + """ + SELECT child.relname + FROM pg_inherits + JOIN pg_class parent ON pg_inherits.inhparent = parent.oid + JOIN pg_class child ON pg_inherits.inhrelid = child.oid + WHERE parent.relname = 'hrrr_profiles' + ORDER BY child.relname + """, + [] + ) + + Enum.map(rows, fn [name] -> name end) + end +end