-- ============================================================================= -- 03_convert_partition_avec_donnees.sql — migre UNE partition contenant des -- données, de HASH×16 vers RANGE simple, sans perte. -- -- À lancer une partition à la fois, en passant son nom : -- docker exec -i wordpress-postgres psql -U pgz_admin -d wpz_postgres \ -- -v ON_ERROR_STOP=1 -v part=positions_2026_w31 \ -- < 03_convert_partition_avec_donnees.sql -- -- PRÉALABLE OBLIGATOIRE — sauvegarde de la partition concernée : -- docker exec wordpress-postgres pg_dump -U pgz_admin -d wpz_postgres \ -- -t "adsb.*" -Fc -f /tmp/.dump -- docker cp wordpress-postgres:/tmp/.dump /data/adsb/backups/ -- -- Principe : -- 1. DETACH de l'ancienne partition (les données restent, hors du parent) -- 2. CREATE de la nouvelle partition RANGE simple, attachée -- 3. INSERT ... SELECT depuis l'ancienne vers le parent -- 4. Contrôle des comptes AVANT/APRÈS — DROP uniquement si identiques -- -- Le tout dans UNE transaction : en cas de problème, ROLLBACK ramène l'état -- initial. Pendant l'opération, adsb2pg peut continuer d'insérer : ses lignes -- arrivent dans la nouvelle partition, déjà attachée. -- -- NOTE : sur la partition COURANTE, faire l'opération à faible trafic. Les -- lignes insérées entre le DETACH et le CREATE (quelques millisecondes) -- provoqueraient une erreur « no partition of relation found » côté adsb2pg, -- qui les réessaiera au cycle suivant. -- ============================================================================= \timing on \set ON_ERROR_STOP on \if :{?part} \else \echo 'ERREUR : passer -v part=' \quit 1 \endif BEGIN; -- Verrou explicite : empêche toute modification concurrente de la structure LOCK TABLE adsb.positions IN SHARE UPDATE EXCLUSIVE MODE; -- Transmission du nom de partition au bloc PL/pgSQL (les variables psql ne -- sont pas visibles depuis un DO $$ : il faut passer par un paramètre de -- session). SET LOCAL my.part = :'part'; DO $$ DECLARE p_old text := current_setting('my.part'); p_tmp text := current_setting('my.part') || '_old'; bornes text; d0 date; d1 date; n_avant bigint; n_apres bigint; cols text; BEGIN SELECT pg_get_expr(c.relpartbound, c.oid) INTO bornes FROM pg_class c JOIN pg_namespace ns ON ns.oid = c.relnamespace WHERE ns.nspname = 'adsb' AND c.relname = p_old; IF bornes IS NULL THEN RAISE EXCEPTION 'Partition adsb.% introuvable ou non attachée', p_old; END IF; d0 := (regexp_match(bornes, 'FROM \(''([0-9-]+)'))[1]::date; d1 := (regexp_match(bornes, 'TO \(''([0-9-]+)'))[1]::date; EXECUTE format('SELECT count(*) FROM adsb.%I', p_old) INTO n_avant; RAISE NOTICE 'Partition % : % ligne(s) à migrer (bornes % → %)', p_old, n_avant, d0, d1; -- 1. Détacher et renommer l'ancienne EXECUTE format('ALTER TABLE adsb.positions DETACH PARTITION adsb.%I', p_old); EXECUTE format('ALTER TABLE adsb.%I RENAME TO %I', p_old, p_tmp); -- 2. Créer la nouvelle, en RANGE simple EXECUTE format( 'CREATE TABLE adsb.%I PARTITION OF adsb.positions FOR VALUES FROM (%L) TO (%L)', p_old, d0, d1); -- 3. Recopier les données. -- geom est GENERATED ALWAYS : PostgreSQL refuse toute valeur explicite -- (« cannot insert a non-DEFAULT value into column geom »). Un SELECT * -- la remonterait, il faut donc énumérer les colonnes réelles et l'exclure. -- Elle sera recalculée automatiquement depuis lat/lon à l'insertion. SELECT string_agg(quote_ident(a.attname), ', ' ORDER BY a.attnum) INTO cols FROM pg_attribute a WHERE a.attrelid = format('adsb.%I', p_tmp)::regclass AND a.attnum > 0 AND NOT a.attisdropped AND a.attgenerated = ''; -- exclut les colonnes générées IF cols IS NULL OR cols = '' THEN RAISE EXCEPTION 'Impossible de déterminer les colonnes de adsb.%', p_tmp; END IF; RAISE NOTICE 'Colonnes copiées (geom exclue, recalculée) : %', cols; EXECUTE format('INSERT INTO adsb.%I (%s) SELECT %s FROM adsb.%I', p_old, cols, cols, p_tmp); EXECUTE format('SELECT count(*) FROM adsb.%I', p_old) INTO n_apres; -- 4. Contrôle : on ne supprime QUE si le compte correspond exactement. IF n_apres <> n_avant THEN RAISE EXCEPTION 'ÉCART DE COMPTE : % avant, % après — transaction annulée, aucune donnée perdue', n_avant, n_apres; END IF; RAISE NOTICE 'Copie vérifiée : % ligne(s) — suppression de l''ancienne structure', n_apres; EXECUTE format('DROP TABLE adsb.%I', p_tmp); -- 5. Index sur la nouvelle partition EXECUTE format('CREATE INDEX %I ON adsb.%I USING GIST (geom)', p_old||'_geom_gix', p_old); EXECUTE format('CREATE INDEX %I ON adsb.%I (ts DESC)', p_old||'_ts_idx', p_old); EXECUTE format('CREATE INDEX %I ON adsb.%I (icao, ts DESC)', p_old||'_icao_ts_idx', p_old); EXECUTE format('CREATE INDEX %I ON adsb.%I (inserted_at DESC)', p_old||'_inserted_at_idx', p_old); EXECUTE format('CREATE INDEX %I ON adsb.%I (callsign) WHERE callsign IS NOT NULL', p_old||'_callsign_idx', p_old); EXECUTE format('CREATE INDEX %I ON adsb.%I (snap_id)', p_old||'_snap_id_idx', p_old); EXECUTE format('CREATE INDEX %I ON adsb.%I (receiver_id)', p_old||'_receiver_id_idx', p_old); EXECUTE format('CREATE INDEX %I ON adsb.%I (aircraft_wtc)', p_old||'_aircraft_wtc_idx', p_old); EXECUTE format( 'ALTER TABLE adsb.%I ADD CONSTRAINT %I FOREIGN KEY (receiver_id) ' 'REFERENCES adsb.receivers(id) ON DELETE SET NULL', p_old, p_old||'_receiver_fk'); RAISE NOTICE 'Partition % convertie en RANGE simple avec ses index', p_old; END $$; COMMIT; -- ANALYZE hors transaction : indispensable, la nouvelle partition n'a aucune -- statistique et le planner ferait de mauvais choix. ANALYZE adsb.positions; \timing off