-- Create vacuum monitoring tableCREATETABLEvacuum_history(idSERIALPRIMARYKEY,table_nameVARCHAR(100)NOTNULL,vacuum_typeVARCHAR(20)NOTNULL,started_atTIMESTAMPTZNOTNULL,completed_atTIMESTAMPTZ,duration_secondsINTEGER,pages_removedINTEGER,pages_remainINTEGER,pages_newINTEGER,pages_deletedINTEGER,pages_vacuumedINTEGER,pages_frozenINTEGER,tuples_deletedINTEGER,tuples_updatedINTEGER,tuples_movedINTEGER,statusVARCHAR(20)DEFAULT'running',created_atTIMESTAMPTZDEFAULTCURRENT_TIMESTAMP);-- Create function to start vacuum monitoringCREATEORREPLACEFUNCTIONstart_vacuum_monitoring(p_table_nameVARCHAR(100),p_vacuum_typeVARCHAR(20)DEFAULT'manual')RETURNSINTEGERAS$$DECLAREvacuum_idINTEGER;BEGININSERTINTOvacuum_history(table_name,vacuum_type,started_at)VALUES(p_table_name,p_vacuum_type,CURRENT_TIMESTAMP)RETURNINGidINTOvacuum_id;RETURNvacuum_id;END;$$LANGUAGEplpgsql;-- Create function to complete vacuum monitoringCREATEORREPLACEFUNCTIONcomplete_vacuum_monitoring(p_vacuum_idINTEGER,p_pages_removedINTEGERDEFAULT0,p_pages_remainINTEGERDEFAULT0,p_pages_newINTEGERDEFAULT0,p_pages_deletedINTEGERDEFAULT0,p_pages_vacuumedINTEGERDEFAULT0,p_pages_frozenINTEGERDEFAULT0,p_tuples_deletedINTEGERDEFAULT0,p_tuples_updatedINTEGERDEFAULT0,p_tuples_movedINTEGERDEFAULT0)RETURNSVOIDAS$$DECLAREstart_timeTIMESTAMPTZ;duration_secondsINTEGER;BEGIN-- Get start timeSELECTstarted_atINTOstart_timeFROMvacuum_historyWHEREid=p_vacuum_id;-- Calculate durationduration_seconds:=EXTRACT(EPOCHFROM(CURRENT_TIMESTAMP-start_time));-- Update vacuum recordUPDATEvacuum_historySETcompleted_at=CURRENT_TIMESTAMP,duration_seconds=duration_seconds,pages_removed=p_pages_removed,pages_remain=p_pages_remain,pages_new=p_pages_new,pages_deleted=p_pages_deleted,pages_vacuumed=p_pages_vacuumed,pages_frozen=p_pages_frozen,tuples_deleted=p_tuples_deleted,tuples_updated=p_tuples_updated,tuples_moved=p_tuples_moved,status='completed'WHEREid=p_vacuum_id;END;$$LANGUAGEplpgsql;
-- Create maintenance schedule tableCREATETABLEmaintenance_schedule(idSERIALPRIMARYKEY,task_nameVARCHAR(100)UNIQUENOTNULL,task_typeVARCHAR(50)NOTNULL,schedule_cronVARCHAR(100)NOTNULL,is_enabledBOOLEANDEFAULTTRUE,last_runTIMESTAMPTZ,next_runTIMESTAMPTZ,created_atTIMESTAMPTZDEFAULTCURRENT_TIMESTAMP);-- Create function to schedule maintenance taskCREATEORREPLACEFUNCTIONschedule_maintenance_task(p_task_nameVARCHAR(100),p_task_typeVARCHAR(50),p_schedule_cronVARCHAR(100),p_is_enabledBOOLEANDEFAULTTRUE)RETURNSVOIDAS$$BEGININSERTINTOmaintenance_schedule(task_name,task_type,schedule_cron,is_enabled)VALUES(p_task_name,p_task_type,p_schedule_cron,p_is_enabled)ONCONFLICT(task_name)DOUPDATESETtask_type=EXCLUDED.task_type,schedule_cron=EXCLUDED.schedule_cron,is_enabled=EXCLUDED.is_enabled;END;$$LANGUAGEplpgsql;-- Create function to run maintenance taskCREATEORREPLACEFUNCTIONrun_maintenance_task(p_task_nameVARCHAR(100))RETURNSBOOLEANAS$$DECLAREtask_typeVARCHAR(50);task_sqlTEXT;BEGIN-- Get task typeSELECTtask_typeINTOtask_typeFROMmaintenance_scheduleWHEREtask_name=p_task_nameANDis_enabled=TRUE;IFtask_typeISNULLTHENRETURNFALSE;ENDIF;-- Execute task based on typeCASEtask_typeWHEN'vacuum'THENEXECUTEformat('VACUUM ANALYZE %I',p_task_name);WHEN'reindex'THENEXECUTEformat('REINDEX TABLE %I',p_task_name);WHEN'analyze'THENEXECUTEformat('ANALYZE %I',p_task_name);WHEN'vacuum_full'THENEXECUTEformat('VACUUM FULL %I',p_task_name);ELSERAISEEXCEPTION'Unknown task type: %',task_type;ENDCASE;-- Update last run timeUPDATEmaintenance_scheduleSETlast_run=CURRENT_TIMESTAMPWHEREtask_name=p_task_name;RETURNTRUE;END;$$LANGUAGEplpgsql;
-- Create function to get table statisticsCREATEORREPLACEFUNCTIONget_table_statistics(p_table_nameVARCHAR(100))RETURNSTABLE(column_nameVARCHAR(100),n_distinctNUMERIC,correlationNUMERIC,most_common_valsTEXT,most_common_freqsTEXT,histogram_boundsTEXT,null_fracNUMERIC)AS$$BEGINRETURNQUERYSELECTs.attname::VARCHAR(100)ascolumn_name,s.n_distinct,s.correlation,s.most_common_vals::TEXT,s.most_common_freqs::TEXT,s.histogram_bounds::TEXT,s.null_fracFROMpg_statssWHEREs.tablename=p_table_nameORDERBYs.attname;END;$$LANGUAGEplpgsql;-- Create function to check statistics ageCREATEORREPLACEFUNCTIONcheck_statistics_age(p_table_nameVARCHAR(100))RETURNSTABLE(table_nameVARCHAR(100),last_analyzeTIMESTAMPTZ,last_autoanalyzeTIMESTAMPTZ,analyze_age_hoursNUMERIC,autoanalyze_age_hoursNUMERIC)AS$$BEGINRETURNQUERYSELECTt.tablename::VARCHAR(100)astable_name,t.last_analyze,t.last_autoanalyze,EXTRACT(EPOCHFROM(CURRENT_TIMESTAMP-t.last_analyze))/3600asanalyze_age_hours,EXTRACT(EPOCHFROM(CURRENT_TIMESTAMP-t.last_autoanalyze))/3600asautoanalyze_age_hoursFROMpg_stat_user_tablestWHEREt.tablename=p_table_name;END;$$LANGUAGEplpgsql;
-- Create function to update statistics for all tablesCREATEORREPLACEFUNCTIONupdate_all_statistics()RETURNSINTEGERAS$$DECLAREtable_recordRECORD;updated_countINTEGER:=0;BEGINFORtable_recordINSELECTschemaname,tablenameFROMpg_tablesWHEREschemaname='public'LOOPEXECUTEformat('ANALYZE %I.%I',table_record.schemaname,table_record.tablename);updated_count:=updated_count+1;ENDLOOP;RETURNupdated_count;END;$$LANGUAGEplpgsql;-- Create function to update statistics for stale tablesCREATEORREPLACEFUNCTIONupdate_stale_statistics(p_hours_thresholdINTEGERDEFAULT24)RETURNSINTEGERAS$$DECLAREtable_recordRECORD;updated_countINTEGER:=0;BEGINFORtable_recordINSELECTschemaname,tablenameFROMpg_stat_user_tablesWHERElast_autoanalyze<CURRENT_TIMESTAMP-(p_hours_threshold||' hours')::intervalORlast_autoanalyzeISNULLLOOPEXECUTEformat('ANALYZE %I.%I',table_record.schemaname,table_record.tablename);updated_count:=updated_count+1;ENDLOOP;RETURNupdated_count;END;$$LANGUAGEplpgsql;
-- Create function to check index healthCREATEORREPLACEFUNCTIONcheck_index_health()RETURNSTABLE(schemanameVARCHAR(100),tablenameVARCHAR(100),indexnameVARCHAR(100),index_sizeTEXT,idx_scanBIGINT,idx_tup_readBIGINT,idx_tup_fetchBIGINT,bloat_ratioNUMERIC,is_healthyBOOLEAN)AS$$BEGINRETURNQUERYSELECTs.schemaname::VARCHAR(100),s.tablename::VARCHAR(100),s.indexname::VARCHAR(100),pg_size_pretty(pg_relation_size(s.indexrelid))asindex_size,s.idx_scan,s.idx_tup_read,s.idx_tup_fetch,CASEWHENs.idx_tup_read>0THEN(s.idx_tup_read-s.idx_tup_fetch)::NUMERIC/s.idx_tup_readELSE0ENDasbloat_ratio,(s.idx_scan>0ANDs.idx_tup_fetch>0)asis_healthyFROMpg_stat_user_indexessORDERBYs.idx_scanDESC;END;$$LANGUAGEplpgsql;-- Create function to find unused indexesCREATEORREPLACEFUNCTIONfind_unused_indexes()RETURNSTABLE(schemanameVARCHAR(100),tablenameVARCHAR(100),indexnameVARCHAR(100),index_sizeTEXT,index_size_bytesBIGINT)AS$$BEGINRETURNQUERYSELECTs.schemaname::VARCHAR(100),s.tablename::VARCHAR(100),s.indexname::VARCHAR(100),pg_size_pretty(pg_relation_size(s.indexrelid))asindex_size,pg_relation_size(s.indexrelid)asindex_size_bytesFROMpg_stat_user_indexessWHEREs.idx_scan=0ANDs.indexnameNOTLIKE'%_pkey'ANDs.indexnameNOTLIKE'%_unique_%'ORDERBYpg_relation_size(s.indexrelid)DESC;END;$$LANGUAGEplpgsql;
-- Create function to reindex unused indexesCREATEORREPLACEFUNCTIONreindex_unused_indexes()RETURNSINTEGERAS$$DECLAREindex_recordRECORD;reindexed_countINTEGER:=0;BEGINFORindex_recordINSELECTschemaname,indexnameFROMfind_unused_indexes()LOOPEXECUTEformat('REINDEX INDEX %I.%I',index_record.schemaname,index_record.indexname);reindexed_count:=reindexed_count+1;ENDLOOP;RETURNreindexed_count;END;$$LANGUAGEplpgsql;-- Create function to drop unused indexesCREATEORREPLACEFUNCTIONdrop_unused_indexes()RETURNSINTEGERAS$$DECLAREindex_recordRECORD;dropped_countINTEGER:=0;BEGINFORindex_recordINSELECTschemaname,indexnameFROMfind_unused_indexes()LOOPEXECUTEformat('DROP INDEX %I.%I',index_record.schemaname,index_record.indexname);dropped_count:=dropped_count+1;ENDLOOP;RETURNdropped_count;END;$$LANGUAGEplpgsql;
# monitoring/maintenance_monitor.pyimportpsycopg2importjsonfromdatetimeimportdatetime,timedeltaimportloggingclassMaintenanceMonitor:def__init__(self,connection_params):self.conn_params=connection_paramsself.setup_logging()defsetup_logging(self):"""Setup logging."""logging.basicConfig(level=logging.INFO)self.logger=logging.getLogger(__name__)defget_vacuum_statistics(self):"""Get vacuum statistics."""conn=psycopg2.connect(**self.conn_params)try:withconn.cursor()ascur:cur.execute(""" SELECT table_name, COUNT(*) as vacuum_count, AVG(duration_seconds) as avg_duration, MAX(duration_seconds) as max_duration, SUM(pages_removed) as total_pages_removed, SUM(tuples_deleted) as total_tuples_deleted FROM vacuum_history WHERE started_at > CURRENT_DATE - INTERVAL '7 days' GROUP BY table_name ORDER BY vacuum_count DESC """)vacuum_stats=cur.fetchall()returnvacuum_statsexceptExceptionase:self.logger.error(f"Error getting vacuum statistics: {e}")return[]finally:conn.close()defget_index_health(self):"""Get index health metrics."""conn=psycopg2.connect(**self.conn_params)try:withconn.cursor()ascur:cur.execute("SELECT * FROM check_index_health()")index_health=cur.fetchall()returnindex_healthexceptExceptionase:self.logger.error(f"Error getting index health: {e}")return[]finally:conn.close()defget_maintenance_summary(self):"""Get maintenance summary."""conn=psycopg2.connect(**self.conn_params)try:withconn.cursor()ascur:cur.execute(""" SELECT 'vacuum' as task_type, COUNT(*) as task_count, AVG(duration_seconds) as avg_duration FROM vacuum_history WHERE started_at > CURRENT_DATE - INTERVAL '7 days' UNION ALL SELECT 'analyze' as task_type, COUNT(*) as task_count, AVG(duration_seconds) as avg_duration FROM vacuum_history WHERE vacuum_type = 'analyze' AND started_at > CURRENT_DATE - INTERVAL '7 days' """)summary=cur.fetchall()returnsummaryexceptExceptionase:self.logger.error(f"Error getting maintenance summary: {e}")return[]finally:conn.close()defgenerate_maintenance_report(self):"""Generate maintenance report."""vacuum_stats=self.get_vacuum_statistics()index_health=self.get_index_health()maintenance_summary=self.get_maintenance_summary()report={'report_timestamp':datetime.now().isoformat(),'vacuum_statistics':vacuum_stats,'index_health':index_health,'maintenance_summary':maintenance_summary}returnreport# Usageif__name__=="__main__":monitor=MaintenanceMonitor({'host':'localhost','database':'production','user':'monitor_user','password':'monitor_password'})report=monitor.generate_maintenance_report()print(json.dumps(report,indent=2))