-- 01_schema_setup.sql -- Creates the demo schema, legacy table, OLTP workload table, -- and all monitoring views for the CI migration danger demo. -- -- Run as superuser (postgres) against knoe-db. -- Safe to re-run: all objects use IF NOT EXISTS / CREATE OR REPLACE. CREATE SCHEMA IF NOT EXISTS demo; -- ── Legacy table (source of the backfill) ──────────────────────────────────── -- Represents the old non-partitioned ci_job_artifacts table on a 10 TB GitLab -- DB. In our scaled demo we'll fill this with ~5 million rows (~1 GB). CREATE TABLE IF NOT EXISTS demo.ci_job_artifacts_legacy ( id BIGSERIAL PRIMARY KEY, 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, expire_at TIMESTAMP, locked BOOLEAN DEFAULT FALSE ); CREATE INDEX IF NOT EXISTS idx_legacy_created_at ON demo.ci_job_artifacts_legacy (created_at); -- ── OLTP workload table (concurrent traffic during migration) ───────────────── -- Simulates background CI pipeline activity during the migration window. -- UPDATEs to this table generate dead tuples that autovacuum must clean. -- When a long-running migration transaction holds an old snapshot, autovacuum -- is blocked and dead tuples pile up — this is the MVCC bloat we'll observe. CREATE TABLE IF NOT EXISTS demo.ci_build_status ( id BIGSERIAL PRIMARY KEY, job_id BIGINT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); -- ── Monitoring views ────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW demo.mvcc_bloat_monitor AS 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, last_autovacuum, last_autoanalyze, pg_size_pretty(pg_total_relation_size('demo.' || relname)) AS total_size, pg_size_pretty(pg_relation_size('demo.' || relname)) AS table_size FROM pg_stat_user_tables WHERE schemaname = 'demo' ORDER BY n_dead_tup DESC; -- Shows any transaction open longer than 5 seconds — the migration blocker. CREATE OR REPLACE VIEW demo.long_running_tx AS SELECT pid, now() - xact_start AS tx_duration, now() - query_start AS query_duration, state, wait_event_type, wait_event, left(query, 200) AS query_snippet, backend_type, application_name FROM pg_stat_activity WHERE xact_start IS NOT NULL AND now() - xact_start > INTERVAL '5 seconds' AND pid <> pg_backend_pid() ORDER BY tx_duration DESC; -- WAL generation tracker — shows how fast WAL is growing. -- Run before and after to calculate the delta. CREATE OR REPLACE VIEW demo.wal_progress AS SELECT pg_current_wal_lsn() AS current_lsn, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0'::pg_lsn)) AS total_wal, (SELECT count(*) FROM pg_ls_waldir()) AS wal_segment_count, pg_size_pretty((SELECT sum(size) FROM pg_ls_waldir())) AS wal_dir_size; -- Lock wait graph — shows who is blocking whom. CREATE OR REPLACE VIEW demo.lock_waits AS SELECT blocked_a.pid AS blocked_pid, blocking_a.pid AS blocking_pid, now() - blocked_a.query_start AS wait_duration, left(blocked_a.query, 120) AS blocked_query, left(blocking_a.query, 120) AS blocking_query, blocked_l.locktype, blocked_l.relation::regclass AS locked_relation FROM pg_catalog.pg_locks blocked_l JOIN pg_catalog.pg_locks blocking_l ON blocking_l.locktype = blocked_l.locktype AND blocking_l.relation = blocked_l.relation AND blocking_l.granted AND NOT blocked_l.granted JOIN pg_stat_activity blocked_a ON blocked_a.pid = blocked_l.pid JOIN pg_stat_activity blocking_a ON blocking_a.pid = blocking_l.pid ORDER BY wait_duration DESC; -- Autovacuum activity — shows if autovacuum is currently running or stuck. CREATE OR REPLACE VIEW demo.autovacuum_status AS SELECT pid, now() - xact_start AS running_for, query AS vacuum_query, wait_event_type, wait_event FROM pg_stat_activity WHERE query LIKE 'autovacuum:%' ORDER BY xact_start; -- Oldest transaction horizon — the horizon that blocks dead tuple cleanup. -- When this is far in the past and dead tuples are climbing, we have MVCC bloat. CREATE OR REPLACE VIEW demo.xmin_horizon AS SELECT pid, backend_xmin AS xmin, age(backend_xmin) AS xmin_age, now() - xact_start AS tx_age, state, left(query, 120) AS query_snippet FROM pg_stat_activity WHERE backend_xmin IS NOT NULL ORDER BY age(backend_xmin) DESC; \echo 'Schema and views created. Run 02_generate_data.sql next.'