PostgreSQL Troubleshooting Best Practices¶
Objective: Master senior-level PostgreSQL troubleshooting patterns for production systems. When you need to diagnose database issues, when you want to implement systematic debugging, when you need enterprise-grade troubleshooting strategies—these best practices become your weapon of choice.
Core Principles¶
- Systematic Approach: Follow structured troubleshooting methodology
- Log Analysis: Leverage PostgreSQL logs for diagnosis
- Performance Monitoring: Use metrics to identify bottlenecks
- Root Cause Analysis: Identify underlying issues, not just symptoms
- Prevention: Implement proactive monitoring and alerting
Diagnostic Tools and Queries¶
System Health Checks¶
-- Create diagnostic queries table
CREATE TABLE diagnostic_queries (
id SERIAL PRIMARY KEY,
query_name VARCHAR(100) UNIQUE NOT NULL,
query_sql TEXT NOT NULL,
description TEXT,
category VARCHAR(50) NOT NULL,
severity VARCHAR(20) DEFAULT 'info',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Insert common diagnostic queries
INSERT INTO diagnostic_queries (query_name, query_sql, description, category, severity) VALUES
('active_connections',
'SELECT COUNT(*) as active_connections FROM pg_stat_activity WHERE state = ''active''',
'Check number of active connections', 'connections', 'warning'),
('long_running_queries',
'SELECT pid, now() - pg_stat_activity.query_start AS duration, query FROM pg_stat_activity WHERE (now() - pg_stat_activity.query_start) > interval ''5 minutes''',
'Find long-running queries', 'performance', 'critical'),
('database_size',
'SELECT pg_size_pretty(pg_database_size(current_database())) as database_size',
'Check database size', 'storage', 'info'),
('table_bloat',
'SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||''.''||tablename)) as size FROM pg_tables ORDER BY pg_total_relation_size(schemaname||''.''||tablename) DESC LIMIT 10',
'Find largest tables', 'storage', 'info');
-- Create function to run diagnostic query
CREATE OR REPLACE FUNCTION run_diagnostic_query(p_query_name VARCHAR(100))
RETURNS TABLE (
result_data JSONB
) AS $$
DECLARE
query_sql TEXT;
BEGIN
SELECT dq.query_sql INTO query_sql
FROM diagnostic_queries dq
WHERE dq.query_name = p_query_name;
IF query_sql IS NULL THEN
RAISE EXCEPTION 'Diagnostic query % not found', p_query_name;
END IF;
RETURN QUERY EXECUTE query_sql;
END;
$$ LANGUAGE plpgsql;
Performance Diagnostics¶
-- Create function to analyze slow queries
CREATE OR REPLACE FUNCTION analyze_slow_queries(p_duration_threshold INTERVAL DEFAULT '1 minute')
RETURNS TABLE (
pid INTEGER,
duration INTERVAL,
query TEXT,
state VARCHAR(20),
wait_event_type VARCHAR(50),
wait_event VARCHAR(50)
) AS $$
BEGIN
RETURN QUERY
SELECT
a.pid,
now() - a.query_start as duration,
a.query,
a.state,
a.wait_event_type,
a.wait_event
FROM pg_stat_activity a
WHERE a.query_start IS NOT NULL
AND now() - a.query_start > p_duration_threshold
AND a.state != 'idle'
ORDER BY duration DESC;
END;
$$ LANGUAGE plpgsql;
-- Create function to check lock contention
CREATE OR REPLACE FUNCTION check_lock_contention()
RETURNS TABLE (
blocked_pid INTEGER,
blocked_query TEXT,
blocking_pid INTEGER,
blocking_query TEXT,
lock_type VARCHAR(50),
mode VARCHAR(20),
granted BOOLEAN
) AS $$
BEGIN
RETURN QUERY
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.query AS blocked_query,
blocking_locks.pid AS blocking_pid,
blocking_activity.query AS blocking_query,
blocked_locks.locktype AS lock_type,
blocked_locks.mode AS mode,
blocked_locks.granted AS granted
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
END;
$$ LANGUAGE plpgsql;
Common Issues and Solutions¶
Connection Issues¶
-- Create function to diagnose connection issues
CREATE OR REPLACE FUNCTION diagnose_connection_issues()
RETURNS TABLE (
issue_type VARCHAR(50),
description TEXT,
current_value TEXT,
recommended_value TEXT,
severity VARCHAR(20)
) AS $$
BEGIN
-- Check max_connections
RETURN QUERY
SELECT
'max_connections'::VARCHAR(50),
'Maximum number of concurrent connections'::TEXT,
current_setting('max_connections')::TEXT,
'200'::TEXT,
CASE
WHEN current_setting('max_connections')::INTEGER < 100 THEN 'critical'
WHEN current_setting('max_connections')::INTEGER < 200 THEN 'warning'
ELSE 'info'
END::VARCHAR(20);
-- Check active connections
RETURN QUERY
SELECT
'active_connections'::VARCHAR(50),
'Current number of active connections'::TEXT,
(SELECT COUNT(*)::TEXT FROM pg_stat_activity)::TEXT,
'Less than 80% of max_connections'::TEXT,
CASE
WHEN (SELECT COUNT(*) FROM pg_stat_activity)::FLOAT / current_setting('max_connections')::FLOAT > 0.8 THEN 'critical'
WHEN (SELECT COUNT(*) FROM pg_stat_activity)::FLOAT / current_setting('max_connections')::FLOAT > 0.6 THEN 'warning'
ELSE 'info'
END::VARCHAR(20);
-- Check connection timeout
RETURN QUERY
SELECT
'idle_in_transaction_session_timeout'::VARCHAR(50),
'Timeout for idle transactions'::TEXT,
current_setting('idle_in_transaction_session_timeout')::TEXT,
'300000'::TEXT,
CASE
WHEN current_setting('idle_in_transaction_session_timeout')::INTEGER = 0 THEN 'warning'
ELSE 'info'
END::VARCHAR(20);
END;
$$ LANGUAGE plpgsql;
Performance Issues¶
-- Create function to diagnose performance issues
CREATE OR REPLACE FUNCTION diagnose_performance_issues()
RETURNS TABLE (
issue_type VARCHAR(50),
description TEXT,
current_value TEXT,
recommended_value TEXT,
severity VARCHAR(20)
) AS $$
BEGIN
-- Check shared_buffers
RETURN QUERY
SELECT
'shared_buffers'::VARCHAR(50),
'Shared buffer pool size'::TEXT,
current_setting('shared_buffers')::TEXT,
'25% of total RAM'::TEXT,
CASE
WHEN current_setting('shared_buffers')::INTEGER < 128 THEN 'critical'
WHEN current_setting('shared_buffers')::INTEGER < 256 THEN 'warning'
ELSE 'info'
END::VARCHAR(20);
-- Check work_mem
RETURN QUERY
SELECT
'work_mem'::VARCHAR(50),
'Memory for sorting and hashing'::TEXT,
current_setting('work_mem')::TEXT,
'4MB'::TEXT,
CASE
WHEN current_setting('work_mem')::INTEGER < 1024 THEN 'critical'
WHEN current_setting('work_mem')::INTEGER < 4096 THEN 'warning'
ELSE 'info'
END::VARCHAR(20);
-- Check effective_cache_size
RETURN QUERY
SELECT
'effective_cache_size'::VARCHAR(50),
'Estimated cache size for query planning'::TEXT,
current_setting('effective_cache_size')::TEXT,
'75% of total RAM'::TEXT,
CASE
WHEN current_setting('effective_cache_size')::INTEGER < 1024 THEN 'critical'
WHEN current_setting('effective_cache_size')::INTEGER < 2048 THEN 'warning'
ELSE 'info'
END::VARCHAR(20);
END;
$$ LANGUAGE plpgsql;
Log Analysis¶
Log Parsing and Analysis¶
-- Create log analysis table
CREATE TABLE log_analysis (
id SERIAL PRIMARY KEY,
log_level VARCHAR(20) NOT NULL,
log_message TEXT NOT NULL,
timestamp TIMESTAMPTZ NOT NULL,
process_id INTEGER,
session_id VARCHAR(50),
user_name VARCHAR(100),
database_name VARCHAR(100),
application_name VARCHAR(100),
client_addr INET,
parsed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Create function to parse log entries
CREATE OR REPLACE FUNCTION parse_log_entry(p_log_line TEXT)
RETURNS TABLE (
log_level VARCHAR(20),
timestamp TIMESTAMPTZ,
process_id INTEGER,
session_id VARCHAR(50),
user_name VARCHAR(100),
database_name VARCHAR(100),
application_name VARCHAR(100),
client_addr INET,
message TEXT
) AS $$
DECLARE
log_parts TEXT[];
BEGIN
-- Parse PostgreSQL log format
-- Format: timestamp [pid] level: message
log_parts := string_to_array(p_log_line, ' ');
IF array_length(log_parts, 1) >= 4 THEN
RETURN QUERY
SELECT
log_parts[3]::VARCHAR(20) as log_level,
log_parts[1]::TIMESTAMPTZ as timestamp,
(regexp_match(log_parts[2], '\[(\d+)\]'))[1]::INTEGER as process_id,
NULL::VARCHAR(50) as session_id,
NULL::VARCHAR(100) as user_name,
NULL::VARCHAR(100) as database_name,
NULL::VARCHAR(100) as application_name,
NULL::INET as client_addr,
array_to_string(log_parts[4:], ' ')::TEXT as message;
END IF;
END;
$$ LANGUAGE plpgsql;
Error Pattern Detection¶
-- Create function to detect error patterns
CREATE OR REPLACE FUNCTION detect_error_patterns(p_hours INTEGER DEFAULT 24)
RETURNS TABLE (
error_pattern VARCHAR(100),
error_count BIGINT,
first_occurrence TIMESTAMPTZ,
last_occurrence TIMESTAMPTZ,
severity VARCHAR(20)
) AS $$
BEGIN
RETURN QUERY
SELECT
CASE
WHEN la.log_message ILIKE '%connection%' THEN 'connection_errors'
WHEN la.log_message ILIKE '%timeout%' THEN 'timeout_errors'
WHEN la.log_message ILIKE '%deadlock%' THEN 'deadlock_errors'
WHEN la.log_message ILIKE '%constraint%' THEN 'constraint_errors'
WHEN la.log_message ILIKE '%permission%' THEN 'permission_errors'
ELSE 'other_errors'
END as error_pattern,
COUNT(*) as error_count,
MIN(la.timestamp) as first_occurrence,
MAX(la.timestamp) as last_occurrence,
CASE
WHEN COUNT(*) > 100 THEN 'critical'
WHEN COUNT(*) > 50 THEN 'warning'
ELSE 'info'
END as severity
FROM log_analysis la
WHERE la.log_level IN ('ERROR', 'FATAL', 'PANIC')
AND la.timestamp > CURRENT_TIMESTAMP - (p_hours || ' hours')::interval
GROUP BY error_pattern
ORDER BY error_count DESC;
END;
$$ LANGUAGE plpgsql;
Automated Troubleshooting¶
Issue Detection¶
# troubleshooting/postgres_troubleshooter.py
import psycopg2
import json
import re
from datetime import datetime, timedelta
import logging
class PostgreSQLTroubleshooter:
def __init__(self, connection_params):
self.conn_params = connection_params
self.setup_logging()
def setup_logging(self):
"""Setup logging."""
logging.basicConfig(level=logging.INFO)
self.logger = logging.getLogger(__name__)
def run_health_checks(self):
"""Run comprehensive health checks."""
conn = psycopg2.connect(**self.conn_params)
try:
with conn.cursor() as cur:
# Check active connections
cur.execute("SELECT COUNT(*) FROM pg_stat_activity WHERE state = 'active'")
active_connections = cur.fetchone()[0]
# Check long-running queries
cur.execute("""
SELECT COUNT(*) FROM pg_stat_activity
WHERE state = 'active'
AND now() - query_start > interval '5 minutes'
""")
long_queries = cur.fetchone()[0]
# Check lock contention
cur.execute("SELECT COUNT(*) FROM pg_locks WHERE NOT granted")
lock_contention = cur.fetchone()[0]
# Check database size
cur.execute("SELECT pg_database_size(current_database())")
db_size = cur.fetchone()[0]
return {
'active_connections': active_connections,
'long_queries': long_queries,
'lock_contention': lock_contention,
'database_size': db_size
}
except Exception as e:
self.logger.error(f"Error running health checks: {e}")
return {}
finally:
conn.close()
def diagnose_performance_issues(self):
"""Diagnose performance issues."""
conn = psycopg2.connect(**self.conn_params)
try:
with conn.cursor() as cur:
# Get slow queries
cur.execute("""
SELECT pid, now() - query_start as duration, query
FROM pg_stat_activity
WHERE state = 'active'
AND now() - query_start > interval '1 minute'
ORDER BY duration DESC
""")
slow_queries = cur.fetchall()
# Get lock information
cur.execute("""
SELECT blocked_locks.pid as blocked_pid,
blocked_activity.query as blocked_query,
blocking_locks.pid as blocking_pid,
blocking_activity.query as blocking_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
WHERE NOT blocked_locks.granted
""")
lock_info = cur.fetchall()
return {
'slow_queries': slow_queries,
'lock_info': lock_info
}
except Exception as e:
self.logger.error(f"Error diagnosing performance issues: {e}")
return {}
finally:
conn.close()
def analyze_logs(self, log_file_path):
"""Analyze PostgreSQL logs."""
error_patterns = {
'connection_errors': r'connection.*refused|connection.*reset',
'timeout_errors': r'timeout|timed out',
'deadlock_errors': r'deadlock|deadlock detected',
'constraint_errors': r'constraint.*violation',
'permission_errors': r'permission.*denied|access.*denied'
}
error_counts = {}
try:
with open(log_file_path, 'r') as f:
for line in f:
for pattern_name, pattern in error_patterns.items():
if re.search(pattern, line, re.IGNORECASE):
error_counts[pattern_name] = error_counts.get(pattern_name, 0) + 1
return error_counts
except Exception as e:
self.logger.error(f"Error analyzing logs: {e}")
return {}
def generate_troubleshooting_report(self):
"""Generate comprehensive troubleshooting report."""
health_checks = self.run_health_checks()
performance_issues = self.diagnose_performance_issues()
report = {
'report_timestamp': datetime.now().isoformat(),
'health_checks': health_checks,
'performance_issues': performance_issues,
'recommendations': self.generate_recommendations(health_checks, performance_issues)
}
return report
def generate_recommendations(self, health_checks, performance_issues):
"""Generate troubleshooting recommendations."""
recommendations = []
# Connection recommendations
if health_checks.get('active_connections', 0) > 100:
recommendations.append({
'issue': 'High connection count',
'recommendation': 'Consider connection pooling or increasing max_connections',
'severity': 'warning'
})
# Performance recommendations
if performance_issues.get('slow_queries'):
recommendations.append({
'issue': 'Slow queries detected',
'recommendation': 'Review and optimize slow queries, consider adding indexes',
'severity': 'critical'
})
if performance_issues.get('lock_info'):
recommendations.append({
'issue': 'Lock contention detected',
'recommendation': 'Review transaction isolation levels and query patterns',
'severity': 'warning'
})
return recommendations
# Usage
if __name__ == "__main__":
troubleshooter = PostgreSQLTroubleshooter({
'host': 'localhost',
'database': 'production',
'user': 'troubleshooter_user',
'password': 'troubleshooter_password'
})
report = troubleshooter.generate_troubleshooting_report()
print(json.dumps(report, indent=2))
Emergency Procedures¶
Emergency Response¶
-- Create emergency procedures table
CREATE TABLE emergency_procedures (
id SERIAL PRIMARY KEY,
procedure_name VARCHAR(100) UNIQUE NOT NULL,
description TEXT NOT NULL,
sql_command TEXT NOT NULL,
risk_level VARCHAR(20) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Insert emergency procedures
INSERT INTO emergency_procedures (procedure_name, description, sql_command, risk_level) VALUES
('kill_long_queries',
'Kill long-running queries',
'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = ''active'' AND now() - query_start > interval ''10 minutes''',
'high'),
('kill_idle_connections',
'Kill idle connections',
'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = ''idle'' AND now() - state_change > interval ''1 hour''',
'medium'),
('force_vacuum',
'Force VACUUM on all tables',
'VACUUM ANALYZE',
'low'),
('check_disk_space',
'Check disk space usage',
'SELECT pg_size_pretty(pg_database_size(current_database()))',
'low');
-- Create function to execute emergency procedure
CREATE OR REPLACE FUNCTION execute_emergency_procedure(p_procedure_name VARCHAR(100))
RETURNS TEXT AS $$
DECLARE
procedure_sql TEXT;
result_text TEXT;
BEGIN
SELECT sql_command INTO procedure_sql
FROM emergency_procedures
WHERE procedure_name = p_procedure_name;
IF procedure_sql IS NULL THEN
RAISE EXCEPTION 'Emergency procedure % not found', p_procedure_name;
END IF;
EXECUTE procedure_sql;
RETURN 'Emergency procedure ' || p_procedure_name || ' executed successfully';
END;
$$ LANGUAGE plpgsql;
TL;DR Runbook¶
Quick Start¶
-- 1. Run health checks
SELECT * FROM diagnose_connection_issues();
SELECT * FROM diagnose_performance_issues();
-- 2. Check slow queries
SELECT * FROM analyze_slow_queries('1 minute');
-- 3. Check lock contention
SELECT * FROM check_lock_contention();
-- 4. Run emergency procedures if needed
SELECT execute_emergency_procedure('kill_long_queries');
Essential Patterns¶
# Complete PostgreSQL troubleshooting setup
def setup_postgresql_troubleshooting():
# 1. Diagnostic tools and queries
# 2. Common issues and solutions
# 3. Log analysis
# 4. Automated troubleshooting
# 5. Emergency procedures
# 6. Performance diagnostics
# 7. Issue detection
# 8. Root cause analysis
print("PostgreSQL troubleshooting setup complete!")
This guide provides the complete machinery for PostgreSQL troubleshooting excellence. Each pattern includes implementation examples, diagnostic strategies, and real-world usage patterns for enterprise PostgreSQL troubleshooting systems.