prole/demo/ci-migration/06_fixed_migration.sql

145 lines
5.8 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 06_fixed_migration.sql
-- THE FIXED MIGRATION — safe to run on a live database.
-- Demonstrates the pg_chunker approach: small autonomous transactions,
-- each committing independently so autovacuum stays unblocked throughout.
--
-- Key differences from the broken version:
-- ✓ No wrapping BEGIN/COMMIT — each chunk is its own transaction
-- ✓ Partitions cover BOTH historical and future months
-- ✓ Indexes created with CONCURRENTLY after data load (no table lock)
-- ✓ pg_sleep(0.05) breathing room between chunks for autovacuum
-- ✓ Progress reporting at each chunk boundary
-- ✓ Idempotent: safe to re-run if interrupted (ON CONFLICT DO NOTHING)
--
-- Reference: https://github.com/eyupmiduck/pg_chunker
-- (The DO $$ chunker loop below implements the same chunked-keyset pattern.)
-- ── Step 1: Create the partitioned table (outside any transaction) ────────────
DROP TABLE IF EXISTS demo.ci_job_artifacts CASCADE;
CREATE TABLE demo.ci_job_artifacts (
id BIGINT NOT NULL,
job_id BIGINT NOT NULL,
project_id BIGINT NOT NULL,
file_type SMALLINT NOT NULL DEFAULT 0,
size_bytes BIGINT,
file_store SMALLINT NOT NULL DEFAULT 1,
checksum TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
expire_at TIMESTAMP,
locked BOOLEAN DEFAULT FALSE,
partition_key DATE NOT NULL,
PRIMARY KEY (id, partition_key)
) PARTITION BY RANGE (partition_key);
-- ── Step 2: Create partitions covering past 6 months AND next 6 months ────────
DO $$
DECLARE
start_date DATE := DATE_TRUNC('month', CURRENT_DATE - INTERVAL '6 months');
partition_name TEXT;
i INT;
BEGIN
FOR i IN 0..11 LOOP
partition_name := 'ci_job_artifacts_'
|| TO_CHAR(start_date + (i || ' months')::INTERVAL, 'YYYY_MM');
EXECUTE FORMAT(
'CREATE TABLE IF NOT EXISTS demo.%I
PARTITION OF demo.ci_job_artifacts
FOR VALUES FROM (%L) TO (%L)',
partition_name,
(start_date + (i || ' months')::INTERVAL)::DATE,
(start_date + ((i+1) || ' months')::INTERVAL)::DATE
);
END LOOP;
RAISE NOTICE 'Created 12 partitions from % to %',
start_date,
start_date + INTERVAL '12 months';
END $$;
-- ── Step 3: Chunked backfill (pg_chunker keyset pattern) ─────────────────────
-- Each iteration is a small autonomous transaction (~50K rows).
-- Autovacuum runs freely between chunks because no long snapshot is held.
-- On a 10 TB table at real scale: 200M chunks × 50K rows each = fine.
DO $$
DECLARE
v_min_id BIGINT;
v_max_id BIGINT;
v_cursor BIGINT := 0;
v_chunk INT := 50000;
v_inserted INT := 0;
v_total INT := 0;
v_chunk_num INT := 0;
BEGIN
SELECT COALESCE(MIN(id), 0), COALESCE(MAX(id), 0)
INTO v_min_id, v_max_id
FROM demo.ci_job_artifacts_legacy
WHERE created_at >= NOW() - INTERVAL '6 months';
RAISE NOTICE 'Backfill range: id % to % (eligible rows for past 6 months)',
v_min_id, v_max_id;
v_cursor := v_min_id - 1;
WHILE v_cursor < v_max_id LOOP
-- Each INSERT is its own implicit transaction (no explicit BEGIN here).
-- In a real migration script this would be called from a shell loop or
-- pg_chunker, which gives each chunk its own connection/transaction.
INSERT INTO demo.ci_job_artifacts
(id, job_id, project_id, file_type, size_bytes, file_store,
checksum, created_at, expire_at, locked, partition_key)
SELECT
id, job_id, project_id, file_type, size_bytes, file_store,
checksum, created_at, expire_at, locked,
DATE_TRUNC('month', created_at)::DATE AS partition_key
FROM demo.ci_job_artifacts_legacy
WHERE id > v_cursor
AND id <= v_cursor + v_chunk
AND created_at >= NOW() - INTERVAL '6 months'
ON CONFLICT (id, partition_key) DO NOTHING; -- idempotent re-run
GET DIAGNOSTICS v_inserted = ROW_COUNT;
v_total := v_total + v_inserted;
v_chunk_num := v_chunk_num + 1;
v_cursor := v_cursor + v_chunk;
IF v_chunk_num % 20 = 0 THEN
RAISE NOTICE 'Chunk %: inserted % rows this chunk, % total, cursor at %',
v_chunk_num, v_inserted, v_total, v_cursor;
END IF;
-- Yield briefly so autovacuum can run between chunks.
-- This is what keeps dead_pct from exploding during the backfill.
PERFORM pg_sleep(0.05);
END LOOP;
RAISE NOTICE 'Backfill complete: % total rows inserted in % chunks',
v_total, v_chunk_num;
END $$;
-- ── Step 4: Indexes CONCURRENTLY — no table lock, runs alongside traffic ──────
-- Must be run OUTSIDE any transaction block (cannot use CONCURRENTLY in a tx).
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_cia_job_id
ON demo.ci_job_artifacts (job_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_cia_project_id
ON demo.ci_job_artifacts (project_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_cia_expire_at
ON demo.ci_job_artifacts (expire_at);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_cia_checksum
ON demo.ci_job_artifacts (checksum);
\echo ''
\echo 'Fixed migration complete. Check demo.mvcc_bloat_monitor:'
\echo ' dead_rows on ci_build_status should be near zero throughout.'
SELECT
relname AS table_name,
n_live_tup AS live_rows,
n_dead_tup AS dead_rows,
ROUND(n_dead_tup::NUMERIC / NULLIF(n_live_tup,0) * 100, 2) AS dead_pct
FROM pg_stat_user_tables
WHERE schemaname = 'demo'
ORDER BY n_dead_tup DESC;