Set up pg_stat_statements extension to collect query statistics, analyze slow queries, and optimize PostgreSQL database performance with automated monitoring alerts.
Prerequisites
- PostgreSQL 12 or newer installed
- sudo access
- Basic SQL knowledge
What this solves
The pg_stat_statements extension tracks execution statistics for all SQL statements in PostgreSQL, giving you detailed insights into query performance, resource usage, and bottlenecks. This tutorial shows you how to enable the extension, configure optimal settings, analyze query data, and set up automated monitoring with Grafana dashboards for production database optimization.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you have the latest PostgreSQL packages available.
sudo apt update && sudo apt upgrade -y
Install PostgreSQL contrib package
The pg_stat_statements extension is included in the postgresql-contrib package. Install it for your PostgreSQL version.
sudo apt install -y postgresql-contrib
Configure shared_preload_libraries
Add pg_stat_statements to PostgreSQL's shared_preload_libraries setting. This requires a database restart to take effect.
sudo -u postgres psql -c "ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';"
sudo systemctl restart postgresql
Create pg_stat_statements extension
Connect to your database and create the extension. Run this on each database where you want to collect statistics.
sudo -u postgres psql -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"
sudo -u postgres psql -c "SELECT * FROM pg_stat_statements LIMIT 1;"
Configure pg_stat_statements parameters
Optimize the extension settings for better query tracking and performance analysis. These settings control how many statements to track and what data to collect.
sudo -u postgres psql -c "ALTER SYSTEM SET pg_stat_statements.max = 10000;"
sudo -u postgres psql -c "ALTER SYSTEM SET pg_stat_statements.track = 'all';"
sudo -u postgres psql -c "ALTER SYSTEM SET pg_stat_statements.track_utility = 'on';"
sudo -u postgres psql -c "ALTER SYSTEM SET pg_stat_statements.save = 'on';"
sudo systemctl reload postgresql
Query analysis and optimization
Analyze top slow queries
Use pg_stat_statements to identify the slowest queries by total execution time. This query shows the top 10 queries consuming the most database time.
sudo -u postgres psql -c "
SELECT
round((100 * total_exec_time / sum(total_exec_time) OVER())::numeric, 2) AS percent_total,
round(total_exec_time::numeric, 2) AS total_time,
calls,
round(mean_exec_time::numeric, 2) AS mean_time,
substring(query, 1, 80) AS query_snippet
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
"
Find queries with high I/O usage
Identify queries that perform the most disk reads and writes. High I/O queries often indicate missing indexes or inefficient query patterns.
sudo -u postgres psql -c "
SELECT
calls,
shared_blks_read + shared_blks_hit AS total_blocks,
shared_blks_read,
shared_blks_hit,
round((shared_blks_read * 100.0 / (shared_blks_read + shared_blks_hit))::numeric, 2) AS miss_ratio,
substring(query, 1, 80) AS query_snippet
FROM pg_stat_statements
WHERE shared_blks_read + shared_blks_hit > 1000
ORDER BY shared_blks_read DESC
LIMIT 10;
"
Identify queries with high variance
Find queries with inconsistent execution times. High standard deviation indicates queries that sometimes run fast and sometimes slow.
sudo -u postgres psql -c "
SELECT
calls,
round(mean_exec_time::numeric, 2) AS mean_time,
round(stddev_exec_time::numeric, 2) AS stddev_time,
round((stddev_exec_time / mean_exec_time)::numeric, 2) AS variance_ratio,
substring(query, 1, 80) AS query_snippet
FROM pg_stat_statements
WHERE calls > 100 AND stddev_exec_time > 0
ORDER BY stddev_exec_time DESC
LIMIT 10;
"
Reset statistics for fresh analysis
Clear pg_stat_statements data to start collecting fresh statistics. This is useful when you want to analyze query patterns over a specific time period.
sudo -u postgres psql -c "SELECT pg_stat_statements_reset();"
Performance monitoring dashboards
Install PostgreSQL Prometheus exporter
Install postgres_exporter to collect PostgreSQL metrics including pg_stat_statements data for Grafana visualization.
wget https://github.com/prometheus-community/postgres_exporter/releases/download/v0.15.0/postgres_exporter-0.15.0.linux-amd64.tar.gz
tar xzf postgres_exporter-0.15.0.linux-amd64.tar.gz
sudo mv postgres_exporter-0.15.0.linux-amd64/postgres_exporter /usr/local/bin/
sudo chown root:root /usr/local/bin/postgres_exporter
sudo chmod 755 /usr/local/bin/postgres_exporter
Create monitoring user and configure access
Create a dedicated PostgreSQL user for metrics collection with minimal required privileges.
sudo -u postgres psql -c "
CREATE USER postgres_exporter WITH PASSWORD 'SecureMonitoringPass2024!';
GRANT CONNECT ON DATABASE postgres TO postgres_exporter;
GRANT SELECT ON pg_stat_database TO postgres_exporter;
GRANT SELECT ON pg_stat_statements TO postgres_exporter;
GRANT SELECT ON pg_stat_user_tables TO postgres_exporter;
GRANT SELECT ON pg_stat_user_indexes TO postgres_exporter;
"
Configure postgres_exporter service
Create a systemd service file for the PostgreSQL exporter with proper configuration and security settings.
[Unit]
Description=PostgreSQL Prometheus Exporter
After=network.target
[Service]
Type=simple
User=nobody
Group=nogroup
Environment=DATA_SOURCE_NAME=postgresql://postgres_exporter:SecureMonitoringPass2024!@localhost:5432/postgres?sslmode=disable
ExecStart=/usr/local/bin/postgres_exporter --extend.query-path=/etc/postgres_exporter/queries.yaml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Create custom query configuration
Configure custom queries to collect pg_stat_statements metrics for detailed query performance monitoring.
sudo mkdir -p /etc/postgres_exporter
pg_stat_statements:
query: |
SELECT
queryid,
calls,
total_exec_time,
mean_exec_time,
max_exec_time,
min_exec_time,
stddev_exec_time,
rows,
shared_blks_hit,
shared_blks_read,
shared_blks_dirtied,
shared_blks_written,
local_blks_hit,
local_blks_read,
local_blks_dirtied,
local_blks_written,
temp_blks_read,
temp_blks_written,
substring(query, 1, 100) as query_snippet
FROM pg_stat_statements
WHERE calls > 10
ORDER BY total_exec_time DESC
LIMIT 100;
metrics:
- queryid:
usage: LABEL
description: "Query ID"
- query_snippet:
usage: LABEL
description: "Query snippet"
- calls:
usage: COUNTER
description: "Number of times executed"
- total_exec_time:
usage: COUNTER
description: "Total time spent executing"
- mean_exec_time:
usage: GAUGE
description: "Mean execution time"
- max_exec_time:
usage: GAUGE
description: "Maximum execution time"
- shared_blks_read:
usage: COUNTER
description: "Shared blocks read from disk"
Start PostgreSQL exporter service
Enable and start the postgres_exporter service to begin collecting metrics for Prometheus.
sudo systemctl daemon-reload
sudo systemctl enable --now postgres_exporter
sudo systemctl status postgres_exporter
Automated query optimization alerts
Configure Prometheus alert rules
Set up alert rules to notify you when query performance degrades or resource usage spikes occur.
groups:
- name: postgresql_performance
rules:
- alert: PostgreSQLSlowQueries
expr: increase(pg_stat_statements_mean_exec_time[5m]) > 1000
for: 2m
labels:
severity: warning
annotations:
summary: "PostgreSQL slow query detected"
description: "Query {{ $labels.queryid }} has mean execution time above 1000ms"
- alert: PostgreSQLHighIOWait
expr: rate(pg_stat_statements_shared_blks_read[5m]) > 1000
for: 3m
labels:
severity: warning
annotations:
summary: "PostgreSQL high I/O detected"
description: "Query {{ $labels.queryid }} is reading {{ $value }} blocks per second from disk"
- alert: PostgreSQLConnectionSpike
expr: pg_stat_database_numbackends > 80
for: 1m
labels:
severity: critical
annotations:
summary: "PostgreSQL connection count high"
description: "Database {{ $labels.datname }} has {{ $value }} active connections"
Create automated query analysis script
Set up a script to regularly analyze pg_stat_statements data and generate performance reports.
#!/bin/bash
PostgreSQL Query Analysis Script
REPORT_DIR="/var/log/postgresql-reports"
DATE=$(date +%Y-%m-%d_%H-%M)
mkdir -p $REPORT_DIR
echo "PostgreSQL Query Performance Report - $DATE" > "$REPORT_DIR/query_report_$DATE.txt"
echo "================================================" >> "$REPORT_DIR/query_report_$DATE.txt"
echo "" >> "$REPORT_DIR/query_report_$DATE.txt"
echo "TOP 10 SLOWEST QUERIES BY TOTAL TIME:" >> "$REPORT_DIR/query_report_$DATE.txt"
sudo -u postgres psql -c "
SELECT
round((100 * total_exec_time / sum(total_exec_time) OVER())::numeric, 2) AS percent_total,
round(total_exec_time::numeric, 2) AS total_time,
calls,
round(mean_exec_time::numeric, 2) AS mean_time,
substring(query, 1, 120) AS query_snippet
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
" >> "$REPORT_DIR/query_report_$DATE.txt"
echo "" >> "$REPORT_DIR/query_report_$DATE.txt"
echo "TOP 10 QUERIES BY I/O USAGE:" >> "$REPORT_DIR/query_report_$DATE.txt"
sudo -u postgres psql -c "
SELECT
calls,
shared_blks_read + shared_blks_hit AS total_blocks,
shared_blks_read,
round((shared_blks_read * 100.0 / (shared_blks_read + shared_blks_hit))::numeric, 2) AS miss_ratio,
substring(query, 1, 120) AS query_snippet
FROM pg_stat_statements
WHERE shared_blks_read + shared_blks_hit > 100
ORDER BY shared_blks_read DESC
LIMIT 10;
" >> "$REPORT_DIR/query_report_$DATE.txt"
echo "Query analysis report saved to: $REPORT_DIR/query_report_$DATE.txt"
sudo chmod 755 /usr/local/bin/analyze_queries.sh
sudo mkdir -p /var/log/postgresql-reports
Schedule automated analysis
Set up a cron job to run query analysis every hour and generate performance reports automatically.
sudo crontab -e
Add this line to run analysis every hour at minute 30:
30 /usr/local/bin/analyze_queries.sh
Verify your setup
Check that pg_stat_statements is collecting data and your monitoring is working correctly.
# Verify extension is loaded
sudo -u postgres psql -c "SELECT * FROM pg_available_extensions WHERE name = 'pg_stat_statements';"
Check statistics collection
sudo -u postgres psql -c "SELECT count(*) FROM pg_stat_statements;"
Test postgres_exporter
curl -s http://localhost:9187/metrics | grep pg_stat_statements | head -5
Verify service status
sudo systemctl status postgres_exporter
Run manual analysis
sudo /usr/local/bin/analyze_queries.sh
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Extension not found after creation | shared_preload_libraries not configured | Add pg_stat_statements to shared_preload_libraries and restart PostgreSQL |
| No query statistics collected | Extension created in wrong database | Create extension in each database where you want statistics: CREATE EXTENSION pg_stat_statements; |
| postgres_exporter connection failed | Wrong connection string or permissions | Check DATA_SOURCE_NAME in service file and verify user grants |
| High memory usage with pg_stat_statements | pg_stat_statements.max set too high | Reduce pg_stat_statements.max value: ALTER SYSTEM SET pg_stat_statements.max = 5000; |
| Missing query text in statistics | Query longer than track_activity_query_size | Increase track_activity_query_size: ALTER SYSTEM SET track_activity_query_size = '2048'; |
| Statistics reset unexpectedly | Database restart without pg_stat_statements.save | Enable persistent statistics: ALTER SYSTEM SET pg_stat_statements.save = 'on'; |
Next steps
- Monitor PostgreSQL performance with Prometheus and Grafana dashboards for advanced visualization
- Install and configure PgBouncer for PostgreSQL connection pooling to optimize connection management
- Configure PostgreSQL 17 streaming replication for high availability for production resilience
- Set up PostgreSQL automated backups with monitoring for data protection
- Optimize PostgreSQL performance for high-traffic applications with advanced tuning
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
# Script configuration
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_FILE="/var/log/pg_stat_statements_install.log"
# Function definitions
log() {
echo -e "$1" | tee -a "$LOG_FILE"
}
error() {
log "${RED}ERROR: $1${NC}"
exit 1
}
success() {
log "${GREEN}$1${NC}"
}
warning() {
log "${YELLOW}WARNING: $1${NC}"
}
cleanup() {
if [[ $? -ne 0 ]]; then
error "Installation failed. Check $LOG_FILE for details."
fi
}
trap cleanup ERR
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]
Install and configure PostgreSQL pg_stat_statements extension for query monitoring.
OPTIONS:
-d, --database DATABASE Target database name (default: postgres)
-h, --help Show this help message
Example:
$SCRIPT_NAME -d myapp
EOF
exit 1
}
check_prerequisites() {
log "[1/8] Checking prerequisites..."
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root or with sudo"
fi
if ! command -v systemctl &> /dev/null; then
error "systemctl not found. This script requires systemd."
fi
success "Prerequisites check passed"
}
detect_distro() {
log "[2/8] Detecting distribution..."
if [[ ! -f /etc/os-release ]]; then
error "Cannot detect distribution. /etc/os-release not found."
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
POSTGRESQL_SERVICE="postgresql"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
POSTGRESQL_SERVICE="postgresql"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
POSTGRESQL_SERVICE="postgresql"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
POSTGRESQL_SERVICE="postgresql"
;;
*)
error "Unsupported distribution: $ID"
;;
esac
success "Detected $PRETTY_NAME using $PKG_MGR"
}
update_system() {
log "[3/8] Updating system packages..."
eval "$PKG_UPDATE" || error "Failed to update system packages"
success "System packages updated"
}
install_postgresql_contrib() {
log "[4/8] Installing PostgreSQL contrib package..."
# Check if PostgreSQL is installed
if ! command -v psql &> /dev/null; then
error "PostgreSQL not found. Please install PostgreSQL first."
fi
# Install contrib package
case "$PKG_MGR" in
apt)
# Find PostgreSQL version and install matching contrib
PG_VERSION=$(psql --version | sed 's/.* \([0-9]\+\)\..*/\1/')
$PKG_INSTALL postgresql-contrib-${PG_VERSION} || $PKG_INSTALL postgresql-contrib
;;
dnf|yum)
$PKG_INSTALL postgresql-contrib
;;
esac
success "PostgreSQL contrib package installed"
}
configure_shared_preload() {
log "[5/8] Configuring shared_preload_libraries..."
# Check if PostgreSQL service is running
if ! systemctl is-active --quiet "$POSTGRESQL_SERVICE"; then
systemctl start "$POSTGRESQL_SERVICE" || error "Failed to start PostgreSQL service"
fi
# Configure shared_preload_libraries
sudo -u postgres psql -c "ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';" || \
error "Failed to configure shared_preload_libraries"
# Restart PostgreSQL to apply changes
systemctl restart "$POSTGRESQL_SERVICE" || error "Failed to restart PostgreSQL"
# Wait for PostgreSQL to be ready
sleep 5
success "Shared preload libraries configured"
}
create_extension() {
log "[6/8] Creating pg_stat_statements extension..."
# Create extension in target database
sudo -u postgres psql -d "$TARGET_DATABASE" -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;" || \
error "Failed to create pg_stat_statements extension"
# Verify extension creation
sudo -u postgres psql -d "$TARGET_DATABASE" -c "SELECT extname FROM pg_extension WHERE extname = 'pg_stat_statements';" | \
grep -q pg_stat_statements || error "Extension not found after creation"
success "pg_stat_statements extension created in database: $TARGET_DATABASE"
}
configure_parameters() {
log "[7/8] Configuring pg_stat_statements parameters..."
# Set optimal parameters for pg_stat_statements
sudo -u postgres psql -c "ALTER SYSTEM SET pg_stat_statements.max = 10000;" || \
error "Failed to set pg_stat_statements.max"
sudo -u postgres psql -c "ALTER SYSTEM SET pg_stat_statements.track = 'all';" || \
error "Failed to set pg_stat_statements.track"
sudo -u postgres psql -c "ALTER SYSTEM SET pg_stat_statements.track_utility = 'on';" || \
error "Failed to set pg_stat_statements.track_utility"
sudo -u postgres psql -c "ALTER SYSTEM SET pg_stat_statements.save = 'on';" || \
error "Failed to set pg_stat_statements.save"
# Reload PostgreSQL configuration
systemctl reload "$POSTGRESQL_SERVICE" || error "Failed to reload PostgreSQL configuration"
success "pg_stat_statements parameters configured"
}
verify_installation() {
log "[8/8] Verifying installation..."
# Test basic query on pg_stat_statements
if sudo -u postgres psql -d "$TARGET_DATABASE" -c "SELECT count(*) FROM pg_stat_statements;" &> /dev/null; then
success "pg_stat_statements is working correctly"
else
error "pg_stat_statements verification failed"
fi
# Show current configuration
log "Current pg_stat_statements configuration:"
sudo -u postgres psql -c "SELECT name, setting FROM pg_settings WHERE name LIKE 'pg_stat_statements%';"
success "Installation completed successfully!"
log ""
log "Next steps:"
log "1. Run queries on your database to generate statistics"
log "2. Use provided analysis queries to monitor performance"
log "3. Consider setting up automated monitoring with Grafana"
log ""
log "Example analysis query:"
log "sudo -u postgres psql -d $TARGET_DATABASE -c \"SELECT calls, mean_exec_time, query FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 5;\""
}
main() {
TARGET_DATABASE="postgres"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-d|--database)
TARGET_DATABASE="$2"
shift 2
;;
-h|--help)
usage
;;
*)
error "Unknown option: $1"
;;
esac
done
# Create log file
touch "$LOG_FILE"
chmod 644 "$LOG_FILE"
log "Starting pg_stat_statements installation at $(date)"
log "Target database: $TARGET_DATABASE"
check_prerequisites
detect_distro
update_system
install_postgresql_contrib
configure_shared_preload
create_extension
configure_parameters
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh