-- Function to create monthly partitions automaticallyCREATEORREPLACEFUNCTIONcreate_monthly_partition(table_nametext,start_datedate)RETURNSvoidAS$$DECLAREpartition_nametext;end_datedate;start_monthtext;BEGINstart_month:=to_char(start_date,'YYYY_MM');partition_name:=table_name||'_'||start_month;end_date:=start_date+interval'1 month';EXECUTEformat('CREATE TABLE %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',partition_name,table_name,start_date,end_date);-- Create indexes on the new partitionEXECUTEformat('CREATE INDEX %I ON %I (sensor_id)','idx_'||partition_name||'_sensor_id',partition_name);EXECUTEformat('CREATE INDEX %I ON %I (timestamp)','idx_'||partition_name||'_timestamp',partition_name);RAISENOTICE'Created partition % for date range % to %',partition_name,start_date,end_date;END;$$LANGUAGEplpgsql;-- Create partitions for the next 12 monthsDO$$DECLAREcurrent_datedate:=date_trunc('month',CURRENT_DATE);iinteger;BEGINFORiIN0..11LOOPPERFORMcreate_monthly_partition('sensor_data',current_date+(i||' months')::interval);ENDLOOP;END$$;
-- Create hash-partitioned table for user dataCREATETABLEuser_events(idBIGSERIAL,user_idINTEGERNOTNULL,event_typeVARCHAR(50)NOTNULL,event_dataJSONB,created_atTIMESTAMPTZDEFAULTCURRENT_TIMESTAMP)PARTITIONBYHASH(user_id);-- Create hash partitionsCREATETABLEuser_events_0PARTITIONOFuser_eventsFORVALUESWITH(modulus4,remainder0);CREATETABLEuser_events_1PARTITIONOFuser_eventsFORVALUESWITH(modulus4,remainder1);CREATETABLEuser_events_2PARTITIONOFuser_eventsFORVALUESWITH(modulus4,remainder2);CREATETABLEuser_events_3PARTITIONOFuser_eventsFORVALUESWITH(modulus4,remainder3);-- Create indexes on each partitionCREATEINDEXidx_user_events_0_user_idONuser_events_0(user_id);CREATEINDEXidx_user_events_0_created_atONuser_events_0(created_at);CREATEINDEXidx_user_events_0_event_typeONuser_events_0(event_type);CREATEINDEXidx_user_events_1_user_idONuser_events_1(user_id);CREATEINDEXidx_user_events_1_created_atONuser_events_1(created_at);CREATEINDEXidx_user_events_1_event_typeONuser_events_1(event_type);-- Similar indexes for other partitions...
# partition_management/partition_manager.pyimportpsycopg2fromdatetimeimportdatetime,timedeltaimportloggingclassPostgreSQLPartitionManager: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__)defcreate_monthly_partition(self,table_name,start_date):"""Create monthly partition for a table."""conn=psycopg2.connect(**self.conn_params)try:withconn.cursor()ascur:# Generate partition namepartition_name=f"{table_name}_{start_date.strftime('%Y_%m')}"# Create partitioncreate_sql=f""" CREATE TABLE {partition_name} PARTITION OF {table_name} FOR VALUES FROM ('{start_date}') TO ('{start_date+timedelta(days=32)}') """cur.execute(create_sql)# Create indexesself.create_partition_indexes(cur,partition_name,table_name)conn.commit()self.logger.info(f"Created partition {partition_name}")exceptExceptionase:self.logger.error(f"Error creating partition: {e}")conn.rollback()finally:conn.close()defcreate_partition_indexes(self,cursor,partition_name,table_name):"""Create indexes on a partition."""# Get table structure to create appropriate indexescursor.execute(""" SELECT column_name, data_type FROM information_schema.columns WHERE table_name = %s AND table_schema = 'public' ORDER BY ordinal_position """,(table_name,))columns=cursor.fetchall()# Create common indexes based on column typesforcolumn_name,data_typeincolumns:ifdata_typein['timestamp with time zone','timestamp without time zone']:index_name=f"idx_{partition_name}_{column_name}"cursor.execute(f"CREATE INDEX {index_name} ON {partition_name} ({column_name})")elifdata_type=='integer':index_name=f"idx_{partition_name}_{column_name}"cursor.execute(f"CREATE INDEX {index_name} ON {partition_name} ({column_name})")defdrop_old_partitions(self,table_name,retention_months=12):"""Drop partitions older than retention period."""conn=psycopg2.connect(**self.conn_params)try:withconn.cursor()ascur:# Find old partitionscutoff_date=datetime.now()-timedelta(days=retention_months*30)cur.execute(""" SELECT schemaname, tablename FROM pg_tables WHERE tablename LIKE %s AND tablename ~ %s ORDER BY tablename """,(f"{table_name}_%",r'\d{4}_\d{2}'))partitions=cur.fetchall()forschema,partition_nameinpartitions:# Extract date from partition namedate_part=partition_name.split('_')[-2]+'_'+partition_name.split('_')[-1]partition_date=datetime.strptime(date_part,'%Y_%m')ifpartition_date<cutoff_date:drop_sql=f"DROP TABLE {schema}.{partition_name}"cur.execute(drop_sql)self.logger.info(f"Dropped old partition {partition_name}")conn.commit()exceptExceptionase:self.logger.error(f"Error dropping partitions: {e}")conn.rollback()finally:conn.close()defget_partition_info(self,table_name):"""Get information about table partitions."""conn=psycopg2.connect(**self.conn_params)try:withconn.cursor()ascur:cur.execute(""" SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size, n_tup_ins as inserts, n_tup_upd as updates, n_tup_del as deletes, n_live_tup as live_tuples, n_dead_tup as dead_tuples FROM pg_tables pt LEFT JOIN pg_stat_user_tables pst ON pt.tablename = pst.relname WHERE pt.tablename LIKE %s ORDER BY pt.tablename """,(f"{table_name}_%",))partitions=cur.fetchall()returnpartitionsexceptExceptionase:self.logger.error(f"Error getting partition info: {e}")return[]finally:conn.close()defmaintain_partitions(self,table_name,months_ahead=3,retention_months=12):"""Maintain partitions for a table."""self.logger.info(f"Maintaining partitions for {table_name}")# Create future partitionscurrent_date=datetime.now().replace(day=1)foriinrange(months_ahead):partition_date=current_date+timedelta(days=32*i)self.create_monthly_partition(table_name,partition_date)# Drop old partitionsself.drop_old_partitions(table_name,retention_months)# Get partition infopartition_info=self.get_partition_info(table_name)self.logger.info(f"Partition maintenance completed. {len(partition_info)} partitions exist.")# Usageif__name__=="__main__":manager=PostgreSQLPartitionManager({'host':'localhost','database':'production','user':'partition_manager','password':'partition_password'})# Maintain partitions for sensor_data tablemanager.maintain_partitions('sensor_data',months_ahead=3,retention_months=12)
-- Optimize queries for partition pruning-- Good: Query with partition key in WHERE clauseEXPLAIN(ANALYZE,BUFFERS)SELECT*FROMsensor_dataWHEREtimestamp>='2024-01-01'ANDtimestamp<'2024-02-01'ANDsensor_id=123;-- Good: Query with multiple partition keysEXPLAIN(ANALYZE,BUFFERS)SELECTsensor_id,AVG(value)asavg_valueFROMsensor_dataWHEREtimestamp>='2024-01-01'ANDtimestamp<'2024-04-01'GROUPBYsensor_id;-- Bad: Query without partition key (scans all partitions)EXPLAIN(ANALYZE,BUFFERS)SELECT*FROMsensor_dataWHEREsensor_id=123;-- Better: Include partition key even if not needed for logicEXPLAIN(ANALYZE,BUFFERS)SELECT*FROMsensor_dataWHEREsensor_id=123ANDtimestamp>='2024-01-01'ANDtimestamp<'2024-12-31';
-- Enable parallel query execution for partitioned tablesSETmax_parallel_workers_per_gather=4;SETparallel_tuple_cost=0.1;SETparallel_setup_cost=10.0;-- Query that can benefit from parallel executionEXPLAIN(ANALYZE,BUFFERS,VERBOSE)SELECTsensor_id,DATE_TRUNC('hour',timestamp)ashour,AVG(value)asavg_value,COUNT(*)asreading_countFROMsensor_dataWHEREtimestamp>='2024-01-01'ANDtimestamp<'2024-02-01'GROUPBYsensor_id,DATE_TRUNC('hour',timestamp)ORDERBYsensor_id,hour;
#!/bin/bash# scripts/partition_maintenance.sh# ConfigurationDB_HOST="localhost"DB_NAME="production"DB_USER="partition_manager"DB_PASSWORD="partition_password"TABLES=("sensor_data""log_entries""user_events")# Function to create future partitionscreate_future_partitions(){localtable_name=$1localmonths_ahead=${2:-3}echo"Creating future partitions for $table_name"psql-h$DB_HOST-d$DB_NAME-U$DB_USER-c" DO \$\$ DECLARE current_date date := date_trunc('month', CURRENT_DATE); i integer; BEGIN FOR i IN 0..$months_ahead LOOP PERFORM create_monthly_partition('$table_name', current_date + (i || ' months')::interval); END LOOP; END \$\$; "}# Function to drop old partitionsdrop_old_partitions(){localtable_name=$1localretention_months=${2:-12}echo"Dropping old partitions for $table_name"psql-h$DB_HOST-d$DB_NAME-U$DB_USER-c" DO \$\$ DECLARE cutoff_date date := CURRENT_DATE - INTERVAL '$retention_months months'; partition_record RECORD; BEGIN FOR partition_record IN SELECT schemaname, tablename FROM pg_tables WHERE tablename LIKE '$table_name_%' AND tablename ~ '\d{4}_\d{2}' LOOP -- Extract date from partition name and drop if old IF to_date(split_part(partition_record.tablename, '_', -2) || '_' || split_part(partition_record.tablename, '_', -1), 'YYYY_MM') < cutoff_date THEN EXECUTE 'DROP TABLE ' || partition_record.schemaname || '.' || partition_record.tablename; RAISE NOTICE 'Dropped old partition %', partition_record.tablename; END IF; END LOOP; END \$\$; "}# Main executionfortablein"${TABLES[@]}";doecho"Maintaining partitions for $table"create_future_partitions"$table"3drop_old_partitions"$table"12doneecho"Partition maintenance completed"
# monitoring/partition_monitor.pyimportpsycopg2importjsonfromdatetimeimportdatetime,timedeltaimportloggingclassPartitionMonitor: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_partition_statistics(self,table_name):"""Get statistics for all partitions of a table."""conn=psycopg2.connect(**self.conn_params)try:withconn.cursor()ascur:cur.execute(""" SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size, pg_total_relation_size(schemaname||'.'||tablename) as size_bytes, n_tup_ins as inserts, n_tup_upd as updates, n_tup_del as deletes, n_live_tup as live_tuples, n_dead_tup as dead_tuples, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze FROM pg_tables pt LEFT JOIN pg_stat_user_tables pst ON pt.tablename = pst.relname WHERE pt.tablename LIKE %s ORDER BY pt.tablename """,(f"{table_name}_%",))partitions=cur.fetchall()# Convert to list of dictionariespartition_stats=[]forpartitioninpartitions:partition_stats.append({'schema':partition[0],'table':partition[1],'size':partition[2],'size_bytes':partition[3],'inserts':partition[4]or0,'updates':partition[5]or0,'deletes':partition[6]or0,'live_tuples':partition[7]or0,'dead_tuples':partition[8]or0,'last_vacuum':partition[9],'last_autovacuum':partition[10],'last_analyze':partition[11],'last_autoanalyze':partition[12]})returnpartition_statsexceptExceptionase:self.logger.error(f"Error getting partition statistics: {e}")return[]finally:conn.close()defcheck_partition_health(self,table_name):"""Check health of partitions."""partition_stats=self.get_partition_statistics(table_name)health_issues=[]forpartitioninpartition_stats:# Check for high dead tuple ratioifpartition['live_tuples']>0:dead_ratio=partition['dead_tuples']/partition['live_tuples']ifdead_ratio>0.1:# 10% thresholdhealth_issues.append({'partition':partition['table'],'issue':'high_dead_tuple_ratio','ratio':dead_ratio,'message':f"Dead tuple ratio {dead_ratio:.2%} exceeds threshold"})# Check for missing statisticsifnotpartition['last_analyze']andnotpartition['last_autoanalyze']:health_issues.append({'partition':partition['table'],'issue':'missing_statistics','message':'No analyze has been run on this partition'})# Check for large partition sizeifpartition['size_bytes']>100*1024*1024*1024:# 100GB thresholdhealth_issues.append({'partition':partition['table'],'issue':'large_partition_size','size':partition['size'],'message':f"Partition size {partition['size']} exceeds threshold"})returnhealth_issuesdefgenerate_partition_report(self,table_name):"""Generate comprehensive partition report."""partition_stats=self.get_partition_statistics(table_name)health_issues=self.check_partition_health(table_name)report={'table_name':table_name,'report_timestamp':datetime.now().isoformat(),'total_partitions':len(partition_stats),'total_size_bytes':sum(p['size_bytes']forpinpartition_stats),'total_live_tuples':sum(p['live_tuples']forpinpartition_stats),'total_dead_tuples':sum(p['dead_tuples']forpinpartition_stats),'health_issues':health_issues,'partitions':partition_stats}returnreport# Usageif__name__=="__main__":monitor=PartitionMonitor({'host':'localhost','database':'production','user':'monitor_user','password':'monitor_password'})# Generate report for sensor_data tablereport=monitor.generate_partition_report('sensor_data')print(json.dumps(report,indent=2))