mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
145 lines
5.8 KiB
SQL
145 lines
5.8 KiB
SQL
-- 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;
|