Maintenance
Maintenance
Section titled “Maintenance”Routine maintenance tasks to keep your Mantis database healthy and performant.
Routine Maintenance
Section titled “Routine Maintenance”Autovacuum
Section titled “Autovacuum”PostgreSQL autovacuum handles most maintenance automatically:
# postgresql.confautovacuum = onautovacuum_max_workers = 3autovacuum_naptime = 60autovacuum_vacuum_threshold = 50autovacuum_analyze_threshold = 50autovacuum_vacuum_scale_factor = 0.2autovacuum_analyze_scale_factor = 0.1Monitor Autovacuum
Section titled “Monitor Autovacuum”-- Check autovacuum activitySELECT relname, last_vacuum, last_autovacuum, last_analyze, last_autoanalyzeFROM pg_stat_user_tablesORDER BY last_autovacuum DESC NULLS LAST;
-- Tables needing vacuumSELECT schemaname, relname, n_dead_tup, n_live_tup, round(n_dead_tup * 100.0 / NULLIF(n_live_tup, 0), 2) as dead_pctFROM pg_stat_user_tablesWHERE n_dead_tup > 1000ORDER BY n_dead_tup DESC;Manual Vacuum
Section titled “Manual Vacuum”For immediate maintenance:
-- Vacuum specific tableVACUUM (VERBOSE) deployment_history;
-- Vacuum and analyzeVACUUM ANALYZE deployment_history;
-- Full vacuum (requires exclusive lock)VACUUM FULL deployment_history;Index Maintenance
Section titled “Index Maintenance”Check Index Health
Section titled “Check Index Health”-- Index usage statisticsSELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetchFROM pg_stat_user_indexesORDER BY idx_scan DESC;
-- Unused indexes (candidates for removal)SELECT schemaname, tablename, indexname, idx_scanFROM pg_stat_user_indexesWHERE idx_scan = 0AND indexrelname NOT LIKE 'pk_%'ORDER BY schemaname, tablename;
-- Index sizesSELECT indexrelname as index_name, pg_size_pretty(pg_relation_size(indexrelid)) as index_sizeFROM pg_stat_user_indexesORDER BY pg_relation_size(indexrelid) DESC;Reindex
Section titled “Reindex”Rebuild corrupted or bloated indexes:
-- Reindex single indexREINDEX INDEX idx_dh_status;
-- Reindex tableREINDEX TABLE deployment_history;
-- Reindex concurrently (PostgreSQL 12+)REINDEX INDEX CONCURRENTLY idx_dh_status;Index Bloat Detection
Section titled “Index Bloat Detection”-- Check for bloated indexesSELECT current_database() AS db, schemaname, tablename, indexrelname AS index_name, pg_size_pretty(index_size) AS index_size, pg_size_pretty(index_size - expected_size) AS bloat, round((index_size - expected_size) * 100.0 / index_size, 2) AS bloat_pctFROM ( SELECT schemaname, tablename, indexrelname, pg_relation_size(indexrelid) AS index_size, (avg_leaf_density / 90.0) * pg_relation_size(indexrelid) AS expected_size FROM pg_stat_user_indexes JOIN pg_index USING (indexrelid) JOIN pg_class ON indexrelid = pg_class.oid CROSS JOIN LATERAL ( SELECT (100.0 - COALESCE(avg_leaf_density, 90.0)) AS avg_leaf_density FROM pg_stats WHERE tablename = pg_class.relname LIMIT 1 ) s WHERE pg_relation_size(indexrelid) > 10485760 -- > 10MB) tWHERE index_size > expected_size * 1.3 -- > 30% bloatORDER BY bloat DESC;Table Maintenance
Section titled “Table Maintenance”Check Table Statistics
Section titled “Check Table Statistics”-- Table sizesSELECT relname, pg_size_pretty(pg_total_relation_size(relid)) as total_size, pg_size_pretty(pg_relation_size(relid)) as table_size, pg_size_pretty(pg_indexes_size(relid)) as index_sizeFROM pg_stat_user_tablesORDER BY pg_total_relation_size(relid) DESC;
-- Row countsSELECT relname, n_live_tup as row_countFROM pg_stat_user_tablesORDER BY n_live_tup DESC;Table Bloat
Section titled “Table Bloat”-- Estimate table bloatSELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) as total_size, pg_size_pretty( pg_total_relation_size(schemaname || '.' || tablename) - pg_relation_size(schemaname || '.' || tablename) ) as bloat_estimateFROM pg_stat_user_tablesORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC;Reduce Bloat
Section titled “Reduce Bloat”-- Option 1: VACUUM FULL (locks table)VACUUM FULL tablename;
-- Option 2: pg_repack (online, no locks)-- Install extension firstCREATE EXTENSION pg_repack;
-- Repack tableSELECT pg_repack.repack_table('public.deployment_history');Monitoring Queries
Section titled “Monitoring Queries”Slow Queries
Section titled “Slow Queries”-- Enable query loggingALTER SYSTEM SET log_min_duration_statement = 1000; -- 1 secondSELECT pg_reload_conf();
-- Find slow queries (PG13+ renamed these columns; Mantis needs PG16+)SELECT query, calls, mean_exec_time, total_exec_timeFROM pg_stat_statementsORDER BY mean_exec_time DESCLIMIT 20;Lock Monitoring
Section titled “Lock Monitoring”-- Current locksSELECT pid, relation::regclass, mode, grantedFROM pg_locksWHERE relation IS NOT NULLORDER BY relation;
-- Blocked queriesSELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query, blocking.pid AS blocking_pid, blocking.query AS blocking_queryFROM pg_stat_activity blockedJOIN pg_locks blocked_locks ON blocked.pid = blocked_locks.pidJOIN pg_locks blocking_locks ON blocked_locks.locktype = blocking_locks.locktype AND blocked_locks.relation = blocking_locks.relation AND blocked_locks.pid != blocking_locks.pidJOIN pg_stat_activity blocking ON blocking_locks.pid = blocking.pidWHERE NOT blocked_locks.granted;Connection Monitoring
Section titled “Connection Monitoring”-- Connection summarySELECT state, count(*)FROM pg_stat_activityWHERE datname = 'mantis'GROUP BY state;
-- Active queriesSELECT pid, usename, application_name, now() - query_start as query_duration, state, queryFROM pg_stat_activityWHERE datname = 'mantis'AND state = 'active'ORDER BY query_start;Scheduled Maintenance
Section titled “Scheduled Maintenance”Daily Tasks
Section titled “Daily Tasks”Create maintenance script:
#!/bin/bash# Analyze tablespsql -U mantis -d mantis -c "ANALYZE;"
# Check for bloatpsql -U mantis -d mantis -c "SELECT relname, n_dead_tupFROM pg_stat_user_tablesWHERE n_dead_tup > 10000ORDER BY n_dead_tup DESC;"Weekly Tasks
Section titled “Weekly Tasks”#!/bin/bash# Reindex concurrentlypsql -U mantis -d mantis -c "REINDEX INDEX CONCURRENTLY idx_dh_status;REINDEX INDEX CONCURRENTLY idx_execution_steps_deployment_id;"
# Vacuum verbosepsql -U mantis -d mantis -c "VACUUM VERBOSE;"Schedule with Cron
Section titled “Schedule with Cron”# Daily at 3 AM0 3 * * * mantis /opt/mantis/scripts/daily-maintenance.sh >> /var/log/mantis/maintenance.log 2>&1
# Weekly on Sunday at 4 AM0 4 * * 0 mantis /opt/mantis/scripts/weekly-maintenance.sh >> /var/log/mantis/maintenance.log 2>&1Data Retention
Section titled “Data Retention”Deployment History
Section titled “Deployment History”Clean up old deployment data:
-- Delete deployment data older than 90 days.-- Order matters: several plain (NO ACTION) FKs reference deployment_history and-- must be cleared FIRST, or the final DELETE raises a foreign-key violation.WITH old AS ( SELECT id FROM deployment_history WHERE created_at < NOW() - INTERVAL '90 days')-- 1. deployment_logs has NO FK on deployment_id, so it must be deleted explicitly., _logs AS ( DELETE FROM deployment_logs WHERE deployment_id IN (SELECT id FROM old))-- 2. Clear the referencing columns that use NO ACTION:-- promotion_requests.deployment_id, and deployment_history's own-- triggered_rollback_id / triggered_failure_handler_id self-references., _promo AS ( DELETE FROM promotion_requests WHERE deployment_id IN (SELECT id FROM old)), _selfrefs AS ( UPDATE deployment_history SET triggered_rollback_id = NULL, triggered_failure_handler_id = NULL WHERE (triggered_rollback_id IN (SELECT id FROM old) OR triggered_failure_handler_id IN (SELECT id FROM old)))-- 3. execution_steps CASCADES from deployment_history, so no explicit delete needed.DELETE FROM deployment_history WHERE id IN (SELECT id FROM old);
-- Vacuum after large deletesVACUUM ANALYZE deployment_history, execution_steps, deployment_logs;Audit Logs
Section titled “Audit Logs”audit_log_entries is append-only and partitioned, so ordinary
DELETE/TRUNCATE/VACUUM do not work here:
DELETEis blocked byprevent_audit_log_deleteunless you firstSET ROLE mantis_audit_retention(which auto-records the purge inaudit_log_deletions);TRUNCATEis blocked outright.- Reading the table needs the
mantis_audit_readerrole and themantis.audit_scopeGUC, orSELECTreturns only NULL-tenant rows. - It is a partitioned parent with quarterly children
(
audit_log_entries_YYYY_qN), so age data out by dropping whole partitions, and VACUUM per partition.
-- Preferred: drop an entire aged-out quarterly partition (fast, no row scan).DROP TABLE audit_log_entries_2024_q1;
-- Or, to purge by age, run under the retention role so the immutability trigger-- allows it and the deletion is logged:SET ROLE mantis_audit_retention;DELETE FROM audit_log_entries WHERE occurred_at < NOW() - INTERVAL '365 days';RESET ROLE;
-- VACUUM the specific child partitions, not the parent:VACUUM ANALYZE audit_log_entries_2024_q2;Automated Retention Script
Section titled “Automated Retention Script”#!/bin/bashRETENTION_DAYS=${RETENTION_DAYS:-90}
psql -U mantis -d mantis <<EOFBEGIN;
-- Delete old deployment dataDELETE FROM deployment_logsWHERE deployment_id IN ( SELECT id FROM deployment_history WHERE created_at < NOW() - INTERVAL '${RETENTION_DAYS} days');
DELETE FROM execution_stepsWHERE deployment_id IN ( SELECT id FROM deployment_history WHERE created_at < NOW() - INTERVAL '${RETENTION_DAYS} days');
DELETE FROM deployment_historyWHERE created_at < NOW() - INTERVAL '${RETENTION_DAYS} days';
COMMIT;
VACUUM ANALYZE;EOFPerformance Tuning
Section titled “Performance Tuning”Query Optimization
Section titled “Query Optimization”-- Analyze query planEXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)SELECT * FROM deployment_historyWHERE tenant_id = '...' AND status = 'success'ORDER BY created_at DESCLIMIT 50;
-- Check for sequential scansSELECT relname, seq_scan, seq_tup_read, idx_scan, idx_tup_fetchFROM pg_stat_user_tablesWHERE seq_scan > 0ORDER BY seq_tup_read DESC;Missing Indexes
Section titled “Missing Indexes”-- Find queries that might need indexesSELECT schemaname, tablename, seq_scan, seq_tup_read, idx_scan, idx_tup_fetch, seq_tup_read / NULLIF(seq_scan, 0) as avg_seq_tupFROM pg_stat_user_tablesWHERE seq_scan > 100AND seq_tup_read / NULLIF(seq_scan, 0) > 1000ORDER BY seq_tup_read DESC;Statistics Update
Section titled “Statistics Update”-- Update table statisticsANALYZE deployment_history;
-- Increase statistics target for frequently queried columnsALTER TABLE deployment_history ALTER COLUMN status SET STATISTICS 500;ANALYZE deployment_history;Health Checks
Section titled “Health Checks”Automated Health Check Script
Section titled “Automated Health Check Script”#!/bin/bash# Connection testif ! psql -U mantis -d mantis -c "SELECT 1" > /dev/null 2>&1; then echo "ERROR: Cannot connect to database" exit 1fi
# Check for long-running queriesLONG_QUERIES=$(psql -U mantis -d mantis -t -c "SELECT count(*) FROM pg_stat_activityWHERE state = 'active'AND now() - query_start > interval '5 minutes'")
if [ "$LONG_QUERIES" -gt 0 ]; then echo "WARNING: $LONG_QUERIES long-running queries detected"fi
# Check for high bloatBLOATED_TABLES=$(psql -U mantis -d mantis -t -c "SELECT count(*) FROM pg_stat_user_tablesWHERE n_dead_tup > 100000")
if [ "$BLOATED_TABLES" -gt 0 ]; then echo "WARNING: $BLOATED_TABLES tables with high dead tuple count"fi
# Check connection countCONNECTIONS=$(psql -U mantis -d mantis -t -c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'mantis'")MAX_CONN=$(psql -U mantis -d mantis -t -c "SHOW max_connections")
if [ "$CONNECTIONS" -gt $((MAX_CONN * 80 / 100)) ]; then echo "WARNING: Connection usage above 80% ($CONNECTIONS/$MAX_CONN)"fi
echo "Database health check completed"Next Steps
Section titled “Next Steps”- PostgreSQL Setup - Initial setup
- Connection Pooling - Pool management
- Backup & Recovery - Backup procedures
