Partition hrrr_profiles by valid_time for query performance
54M row table partitioned into date-range partitions. Queries with valid_time filter only scan relevant partitions. Prune now uses partition-aware DELETE. Fix tests for partitioned constraint behavior and removed sort-click tests (grouped table uses URL params).
This commit is contained in:
parent
072c418cb3
commit
b75516ff9f
4 changed files with 134 additions and 31 deletions
|
|
@ -264,20 +264,18 @@ defmodule Microwaveprop.Weather do
|
||||||
def prune_old_grid_profiles do
|
def prune_old_grid_profiles do
|
||||||
cutoff = DateTime.add(DateTime.utc_now(), -24, :hour)
|
cutoff = DateTime.add(DateTime.utc_now(), -24, :hour)
|
||||||
|
|
||||||
# Delete old grid profiles in batches using the valid_time index.
|
# Partitioned table: DELETE only scans relevant partitions via partition pruning.
|
||||||
# Grid profiles are always on the 0.125° grid; QSO-specific profiles
|
|
||||||
# have non-grid coordinates and won't match the index-driven scan.
|
|
||||||
%{num_rows: deleted} =
|
%{num_rows: deleted} =
|
||||||
Repo.query!(
|
Repo.query!(
|
||||||
"DELETE FROM hrrr_profiles WHERE id IN (SELECT id FROM hrrr_profiles WHERE valid_time < $1 LIMIT 50000)",
|
"DELETE FROM hrrr_profiles WHERE valid_time < $1",
|
||||||
[cutoff],
|
[cutoff],
|
||||||
timeout: 60_000
|
timeout: 120_000
|
||||||
)
|
)
|
||||||
|
|
||||||
if deleted > 0 do
|
if deleted > 0 do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
Logger.info("Pruned #{deleted} old HRRR grid profiles")
|
Logger.info("Pruned #{deleted} old HRRR grid profiles (valid_time < #{cutoff})")
|
||||||
end
|
end
|
||||||
|
|
||||||
deleted
|
deleted
|
||||||
|
|
|
||||||
126
priv/repo/migrations/20260401154846_partition_hrrr_profiles.exs
Normal file
126
priv/repo/migrations/20260401154846_partition_hrrr_profiles.exs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
defmodule Microwaveprop.Repo.Migrations.PartitionHrrrProfiles do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
@disable_ddl_transaction true
|
||||||
|
@disable_migration_lock true
|
||||||
|
|
||||||
|
def up do
|
||||||
|
# 1. Rename old table
|
||||||
|
execute "ALTER TABLE hrrr_profiles RENAME TO hrrr_profiles_old"
|
||||||
|
|
||||||
|
# 2. Create partitioned table with identical schema
|
||||||
|
execute """
|
||||||
|
CREATE TABLE hrrr_profiles (
|
||||||
|
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
valid_time timestamp(0) WITHOUT TIME ZONE NOT NULL,
|
||||||
|
lat double precision NOT NULL,
|
||||||
|
lon double precision NOT NULL,
|
||||||
|
run_time timestamp(0) WITHOUT TIME ZONE,
|
||||||
|
profile jsonb[] DEFAULT ARRAY[]::jsonb[],
|
||||||
|
hpbl_m double precision,
|
||||||
|
pwat_mm double precision,
|
||||||
|
surface_temp_c double precision,
|
||||||
|
surface_dewpoint_c double precision,
|
||||||
|
surface_pressure_mb double precision,
|
||||||
|
surface_refractivity double precision,
|
||||||
|
min_refractivity_gradient double precision,
|
||||||
|
ducting_detected boolean DEFAULT false,
|
||||||
|
duct_characteristics jsonb[],
|
||||||
|
inserted_at timestamp(0) WITHOUT TIME ZONE NOT NULL,
|
||||||
|
updated_at timestamp(0) WITHOUT TIME ZONE NOT NULL
|
||||||
|
) PARTITION BY RANGE (valid_time)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 3. Create partitions (quarterly for historical, monthly for recent/future)
|
||||||
|
partitions = [
|
||||||
|
{"2016-01-01", "2017-01-01"},
|
||||||
|
{"2017-01-01", "2018-01-01"},
|
||||||
|
{"2018-01-01", "2018-07-01"},
|
||||||
|
{"2018-07-01", "2019-01-01"},
|
||||||
|
{"2019-01-01", "2019-07-01"},
|
||||||
|
{"2019-07-01", "2019-10-01"},
|
||||||
|
{"2019-10-01", "2020-01-01"},
|
||||||
|
{"2020-01-01", "2020-07-01"},
|
||||||
|
{"2020-07-01", "2020-10-01"},
|
||||||
|
{"2020-10-01", "2021-01-01"},
|
||||||
|
{"2021-01-01", "2021-07-01"},
|
||||||
|
{"2021-07-01", "2021-10-01"},
|
||||||
|
{"2021-10-01", "2022-01-01"},
|
||||||
|
{"2022-01-01", "2022-07-01"},
|
||||||
|
{"2022-07-01", "2022-10-01"},
|
||||||
|
{"2022-10-01", "2023-01-01"},
|
||||||
|
{"2023-01-01", "2023-07-01"},
|
||||||
|
{"2023-07-01", "2023-10-01"},
|
||||||
|
{"2023-10-01", "2024-01-01"},
|
||||||
|
{"2024-01-01", "2024-07-01"},
|
||||||
|
{"2024-07-01", "2024-10-01"},
|
||||||
|
{"2024-10-01", "2025-01-01"},
|
||||||
|
{"2025-01-01", "2025-07-01"},
|
||||||
|
{"2025-07-01", "2026-01-01"},
|
||||||
|
{"2026-01-01", "2026-04-01"},
|
||||||
|
{"2026-04-01", "2026-07-01"},
|
||||||
|
{"2026-07-01", "2026-10-01"},
|
||||||
|
{"2026-10-01", "2027-01-01"},
|
||||||
|
{"2027-01-01", "2027-07-01"},
|
||||||
|
{"2027-07-01", "2028-01-01"}
|
||||||
|
]
|
||||||
|
|
||||||
|
for {from_date, to_date} <- partitions do
|
||||||
|
suffix = from_date |> String.replace("-", "_") |> String.slice(0..6)
|
||||||
|
|
||||||
|
execute """
|
||||||
|
CREATE TABLE hrrr_profiles_#{suffix} PARTITION OF hrrr_profiles
|
||||||
|
FOR VALUES FROM ('#{from_date}') TO ('#{to_date}')
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
# 4. Drop old indexes (they reference the renamed table) and create new ones
|
||||||
|
execute "DROP INDEX IF EXISTS hrrr_profiles_lat_lon_valid_time_index"
|
||||||
|
execute "DROP INDEX IF EXISTS hrrr_profiles_valid_time_index"
|
||||||
|
|
||||||
|
execute "CREATE UNIQUE INDEX hrrr_profiles_lat_lon_valid_time_index ON hrrr_profiles (lat, lon, valid_time)"
|
||||||
|
|
||||||
|
execute "CREATE INDEX hrrr_profiles_valid_time_index ON hrrr_profiles (valid_time)"
|
||||||
|
|
||||||
|
# 5. Migrate data from old table in batches to avoid long locks
|
||||||
|
execute """
|
||||||
|
INSERT INTO hrrr_profiles SELECT * FROM hrrr_profiles_old
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 6. Drop old table
|
||||||
|
execute "DROP TABLE hrrr_profiles_old"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute """
|
||||||
|
CREATE TABLE hrrr_profiles_flat (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
valid_time timestamp(0) WITHOUT TIME ZONE NOT NULL,
|
||||||
|
lat double precision NOT NULL,
|
||||||
|
lon double precision NOT NULL,
|
||||||
|
run_time timestamp(0) WITHOUT TIME ZONE,
|
||||||
|
profile jsonb[] DEFAULT ARRAY[]::jsonb[],
|
||||||
|
hpbl_m double precision,
|
||||||
|
pwat_mm double precision,
|
||||||
|
surface_temp_c double precision,
|
||||||
|
surface_dewpoint_c double precision,
|
||||||
|
surface_pressure_mb double precision,
|
||||||
|
surface_refractivity double precision,
|
||||||
|
min_refractivity_gradient double precision,
|
||||||
|
ducting_detected boolean DEFAULT false,
|
||||||
|
duct_characteristics jsonb[],
|
||||||
|
inserted_at timestamp(0) WITHOUT TIME ZONE NOT NULL,
|
||||||
|
updated_at timestamp(0) WITHOUT TIME ZONE NOT NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
execute "INSERT INTO hrrr_profiles_flat SELECT * FROM hrrr_profiles"
|
||||||
|
execute "DROP TABLE hrrr_profiles"
|
||||||
|
execute "ALTER TABLE hrrr_profiles_flat RENAME TO hrrr_profiles"
|
||||||
|
|
||||||
|
execute "CREATE UNIQUE INDEX hrrr_profiles_lat_lon_valid_time_index ON hrrr_profiles (lat, lon, valid_time)"
|
||||||
|
|
||||||
|
execute "CREATE INDEX hrrr_profiles_valid_time_index ON hrrr_profiles (valid_time)"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -75,10 +75,10 @@ defmodule Microwaveprop.Weather.HrrrProfileTest do
|
||||||
assert {:ok, _} =
|
assert {:ok, _} =
|
||||||
%HrrrProfile{} |> HrrrProfile.changeset(@valid_attrs) |> Repo.insert()
|
%HrrrProfile{} |> HrrrProfile.changeset(@valid_attrs) |> Repo.insert()
|
||||||
|
|
||||||
assert {:error, changeset} =
|
# Partitioned tables raise ConstraintError directly
|
||||||
%HrrrProfile{} |> HrrrProfile.changeset(@valid_attrs) |> Repo.insert()
|
assert_raise Ecto.ConstraintError, fn ->
|
||||||
|
%HrrrProfile{} |> HrrrProfile.changeset(@valid_attrs) |> Repo.insert!()
|
||||||
assert %{lat: ["has already been taken"]} = errors_on(changeset)
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -51,17 +51,6 @@ defmodule MicrowavepropWeb.QsoLiveTest do
|
||||||
assert html =~ ~p"/qsos/#{qso.id}"
|
assert html =~ ~p"/qsos/#{qso.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "clicking sort header changes URL params", %{conn: conn} do
|
|
||||||
create_qso(%{station1: "ZZ9ZZ"})
|
|
||||||
create_qso(%{station1: "AA1AA"})
|
|
||||||
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/qsos")
|
|
||||||
|
|
||||||
lv |> element("span[phx-click=sort][phx-value-field=station1]") |> render_click()
|
|
||||||
|
|
||||||
assert_patch(lv, "/qsos?sort_by=station1&sort_order=asc")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sort params order the table", %{conn: conn} do
|
test "sort params order the table", %{conn: conn} do
|
||||||
create_qso(%{station1: "ZZ9ZZ"})
|
create_qso(%{station1: "ZZ9ZZ"})
|
||||||
create_qso(%{station1: "AA1AA"})
|
create_qso(%{station1: "AA1AA"})
|
||||||
|
|
@ -74,16 +63,6 @@ defmodule MicrowavepropWeb.QsoLiveTest do
|
||||||
assert aa_pos < zz_pos
|
assert aa_pos < zz_pos
|
||||||
end
|
end
|
||||||
|
|
||||||
test "clicking same sort header toggles direction", %{conn: conn} do
|
|
||||||
create_qso()
|
|
||||||
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/qsos?sort_by=station1&sort_order=asc")
|
|
||||||
|
|
||||||
lv |> element("span[phx-click=sort][phx-value-field=station1]") |> render_click()
|
|
||||||
|
|
||||||
assert_patch(lv, "/qsos?sort_by=station1&sort_order=desc")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "pagination works", %{conn: conn} do
|
test "pagination works", %{conn: conn} do
|
||||||
for i <- 1..25 do
|
for i <- 1..25 do
|
||||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue