Add PostgresSubprocessException and refactor database initialization logic

- Introduced PostgresSubprocessException to handle errors during PostgreSQL subprocess execution.
- Refactored PostgresDatabaseProvider to improve schema initialization script discovery.
- Enhanced error handling for psql command execution, including logging of output and errors.
- Implemented methods to find the schema initialization script and the psql executable path.
This commit is contained in:
2026-07-08 11:53:36 -04:00
parent 4f651fc239
commit 1abf61a05f
52 changed files with 723 additions and 9854268 deletions
+365
View File
@@ -0,0 +1,365 @@
-- PostgreSQL Performance Diagnostics for Jellyfin
-- Run this to diagnose current performance issues
\timing on
\x auto
\echo '========================================='
\echo 'Jellyfin PostgreSQL Performance Report'
\echo '========================================='
\echo ''
-- ============================================================================
-- 1. Connection Pool Status
-- ============================================================================
\echo '1. CONNECTION POOL STATUS'
\echo '-----------------------------------------'
SELECT
count(*) as total_connections,
count(*) FILTER (WHERE state = 'active') as active,
count(*) FILTER (WHERE state = 'idle') as idle,
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
count(*) FILTER (WHERE state = 'idle in transaction (aborted)') as aborted,
count(*) FILTER (WHERE wait_event_type IS NOT NULL) as waiting
FROM pg_stat_activity
WHERE datname = 'jellyfin';
\echo ''
-- ============================================================================
-- 2. Long-Running Queries
-- ============================================================================
\echo '2. LONG-RUNNING QUERIES (>1 second)'
\echo '-----------------------------------------'
SELECT
pid,
usename,
application_name,
now() - query_start as duration,
state,
wait_event_type,
wait_event,
left(query, 100) as query_preview
FROM pg_stat_activity
WHERE datname = 'jellyfin'
AND state != 'idle'
AND query_start < now() - interval '1 second'
ORDER BY duration DESC
LIMIT 10;
\echo ''
-- ============================================================================
-- 3. Table Sizes
-- ============================================================================
\echo '3. TABLE SIZES (Top 10)'
\echo '-----------------------------------------'
SELECT
s.schemaname,
s.relname as tablename,
pg_size_pretty(pg_total_relation_size(s.schemaname||'.'||s.relname)) AS total_size,
pg_size_pretty(pg_relation_size(s.schemaname||'.'||s.relname)) AS table_size,
pg_size_pretty(pg_total_relation_size(s.schemaname||'.'||s.relname) - pg_relation_size(s.schemaname||'.'||s.relname)) AS index_size,
s.n_live_tup as row_count
FROM pg_stat_user_tables s
WHERE s.schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
ORDER BY pg_total_relation_size(s.schemaname||'.'||s.relname) DESC
LIMIT 10;
\echo ''
-- ============================================================================
-- 4. Index Usage Statistics
-- ============================================================================
\echo '4. INDEX USAGE (Tables with low index usage)'
\echo '-----------------------------------------'
SELECT
schemaname,
relname as tablename,
seq_scan,
idx_scan,
n_live_tup as rows,
CASE
WHEN seq_scan + idx_scan > 0
THEN round(idx_scan::numeric / (seq_scan + idx_scan) * 100, 2)
ELSE 0
END as index_usage_percent
FROM pg_stat_user_tables
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
AND n_live_tup > 100
ORDER BY
CASE
WHEN seq_scan + idx_scan > 0
THEN idx_scan::numeric / (seq_scan + idx_scan)
ELSE 0
END ASC
LIMIT 10;
\echo ''
-- ============================================================================
-- 5. Missing Indexes (High Sequential Scans)
-- ============================================================================
\echo '5. TABLES WITH HIGH SEQUENTIAL SCANS'
\echo '-----------------------------------------'
SELECT
schemaname,
relname as tablename,
seq_scan,
seq_tup_read,
idx_scan,
n_live_tup,
CASE
WHEN seq_scan > 0
THEN seq_tup_read / seq_scan
ELSE 0
END as avg_rows_per_seq_scan
FROM pg_stat_user_tables
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
AND seq_scan > 100
AND seq_tup_read > 10000
ORDER BY seq_tup_read DESC
LIMIT 10;
\echo ''
-- ============================================================================
-- 6. Table Bloat (Dead Tuples)
-- ============================================================================
\echo '6. TABLE BLOAT (Dead Tuples)'
\echo '-----------------------------------------'
SELECT
schemaname,
relname as tablename,
n_dead_tup as dead_tuples,
n_live_tup as live_tuples,
CASE
WHEN n_live_tup > 0
THEN round(n_dead_tup::numeric / n_live_tup * 100, 2)
ELSE 0
END as dead_tuple_percent,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
AND n_dead_tup > 100
ORDER BY n_dead_tup DESC
LIMIT 10;
\echo ''
-- ============================================================================
-- 7. Cache Hit Ratio
-- ============================================================================
\echo '7. CACHE HIT RATIO (Should be >95%)'
\echo '-----------------------------------------'
SELECT
'Buffer Cache Hit Ratio' as metric,
round(
sum(blks_hit) / nullif(sum(blks_hit) + sum(blks_read), 0) * 100,
2
) as hit_ratio_percent
FROM pg_stat_database
WHERE datname = 'jellyfin';
\echo ''
-- ============================================================================
-- 8. Index Efficiency
-- ============================================================================
\echo '8. UNUSED OR RARELY USED INDEXES'
\echo '-----------------------------------------'
SELECT
schemaname,
relname as tablename,
indexrelname as indexname,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
idx_scan as times_used,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
AND idx_scan < 50
AND pg_relation_size(indexrelid) > 1000000
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 10;
\echo ''
-- ============================================================================
-- 9. Locks
-- ============================================================================
\echo '9. CURRENT LOCKS (Blocked queries)'
\echo '-----------------------------------------'
SELECT
bl.pid AS blocked_pid,
a.usename AS blocked_user,
ka.query AS blocking_query,
now() - ka.query_start AS blocking_duration,
a.query AS blocked_query
FROM pg_catalog.pg_locks bl
JOIN pg_catalog.pg_stat_activity a ON bl.pid = a.pid
JOIN pg_catalog.pg_locks kl ON bl.transactionid = kl.transactionid AND bl.pid != kl.pid
JOIN pg_catalog.pg_stat_activity ka ON kl.pid = ka.pid
WHERE NOT bl.granted
AND a.datname = 'jellyfin';
\echo ''
-- ============================================================================
-- 10. Slow Queries (if pg_stat_statements is enabled)
-- ============================================================================
\echo '10. SLOWEST QUERIES (Requires pg_stat_statements extension)'
\echo '-----------------------------------------'
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
) THEN
RAISE NOTICE 'pg_stat_statements is installed. Showing slow queries...';
ELSE
RAISE NOTICE 'pg_stat_statements is NOT installed. Run: CREATE EXTENSION pg_stat_statements;';
END IF;
END $$;
-- Only run if extension exists (wrapped in DO block to prevent error)
DO $$
DECLARE
query_rec RECORD;
BEGIN
IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements') THEN
FOR query_rec IN
SELECT
calls,
round(mean_exec_time::numeric, 2) as avg_time_ms,
round(max_exec_time::numeric, 2) as max_time_ms,
round(total_exec_time::numeric, 2) as total_time_ms,
round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 2) as percent_total,
left(query, 100) as query_preview
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat_statements%'
AND query NOT LIKE '%pg_catalog%'
ORDER BY mean_exec_time DESC
LIMIT 10
LOOP
RAISE NOTICE 'Calls: %, Avg: %ms, Max: %ms, Total: %ms, %%: %, Query: %',
query_rec.calls,
query_rec.avg_time_ms,
query_rec.max_time_ms,
query_rec.total_time_ms,
query_rec.percent_total,
query_rec.query_preview;
END LOOP;
END IF;
END $$;
\echo ''
-- ============================================================================
-- 11. Database Configuration
-- ============================================================================
\echo '11. KEY POSTGRESQL SETTINGS'
\echo '-----------------------------------------'
SELECT
name,
setting,
unit,
short_desc
FROM pg_settings
WHERE name IN (
'max_connections',
'shared_buffers',
'effective_cache_size',
'work_mem',
'maintenance_work_mem',
'random_page_cost',
'effective_io_concurrency',
'autovacuum',
'checkpoint_completion_target',
'wal_buffers',
'default_statistics_target'
)
ORDER BY name;
\echo ''
-- ============================================================================
-- 12. Recommendations
-- ============================================================================
\echo '12. RECOMMENDATIONS'
\echo '-----------------------------------------'
DO $$
DECLARE
conn_count int;
hit_ratio numeric;
max_conn int;
bloat_tables int;
BEGIN
-- Check connection count
SELECT count(*) INTO conn_count
FROM pg_stat_activity WHERE datname = 'jellyfin';
SELECT setting::int INTO max_conn
FROM pg_settings WHERE name = 'max_connections';
IF conn_count::numeric / max_conn > 0.8 THEN
RAISE WARNING 'High connection usage: % of % max connections', conn_count, max_conn;
RAISE NOTICE 'RECOMMENDATION: Increase max_connections in postgresql.conf';
END IF;
-- Check cache hit ratio
SELECT round(
sum(blks_hit) / nullif(sum(blks_hit) + sum(blks_read), 0) * 100, 2
) INTO hit_ratio
FROM pg_stat_database WHERE datname = 'jellyfin';
IF hit_ratio < 95 THEN
RAISE WARNING 'Low cache hit ratio: %%. Target: >95%%', hit_ratio;
RAISE NOTICE 'RECOMMENDATION: Increase shared_buffers and effective_cache_size';
END IF;
-- Check bloat
SELECT count(*) INTO bloat_tables
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
AND schemaname IN ('library', 'users', 'authentication');
IF bloat_tables > 0 THEN
RAISE WARNING 'Found % tables with significant dead tuples', bloat_tables;
RAISE NOTICE 'RECOMMENDATION: Run VACUUM ANALYZE or adjust autovacuum settings';
END IF;
RAISE NOTICE 'Diagnostic report complete.';
END $$;
\echo ''
\echo '========================================='
\echo 'Run sql/performance_indexes.sql to add missing indexes'
\echo 'See PERFORMANCE_TROUBLESHOOTING.md for detailed fixes'
\echo '========================================='
@@ -0,0 +1,117 @@
-- Query Performance Monitoring for Jellyfin PostgreSQL Database
-- Use these queries to identify slow operations and monitor index usage
\pset pager off
-- 1. Show slow queries currently running (taking more than 5 seconds)
SELECT
pid,
now() - query_start AS duration,
state,
wait_event_type,
wait_event,
LEFT(query, 100) AS query_preview
FROM pg_stat_activity
WHERE state != 'idle'
AND query NOT LIKE '%pg_stat_activity%'
AND (now() - query_start) > interval '5 seconds'
ORDER BY duration DESC;
-- 2. Show index usage statistics for BaseItems table
SELECT
schemaname,
relname,
indexrelname,
idx_scan AS index_scans,
idx_tup_read AS tuples_read,
idx_tup_fetch AS tuples_fetched,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
CASE
WHEN idx_scan = 0 THEN 'UNUSED'
WHEN idx_scan < 10 THEN 'RARELY USED'
WHEN idx_scan < 100 THEN 'OCCASIONALLY USED'
ELSE 'FREQUENTLY USED'
END AS usage_level
FROM pg_stat_user_indexes
WHERE relname = 'BaseItems'
AND schemaname = 'library'
ORDER BY idx_scan DESC, indexrelname;
-- 3. Find missing indexes (sequential scans on BaseItems)
SELECT
schemaname,
relname,
seq_scan AS sequential_scans,
seq_tup_read AS rows_read_sequentially,
idx_scan AS index_scans,
n_tup_ins AS rows_inserted,
n_tup_upd AS rows_updated,
n_tup_del AS rows_deleted,
CASE
WHEN seq_scan > 0 AND idx_scan = 0 THEN 'INDEX NEEDED'
WHEN seq_scan > idx_scan THEN 'MORE INDEXES MAY HELP'
ELSE 'INDEXES WORKING WELL'
END AS recommendation
FROM pg_stat_user_tables
WHERE relname = 'BaseItems'
AND schemaname = 'library';
-- 4. Show table and index sizes
SELECT
schemaname,
relname,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) AS total_size,
pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) AS table_size,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname) - pg_relation_size(schemaname||'.'||relname)) AS indexes_size
FROM pg_stat_user_tables
WHERE relname = 'BaseItems'
AND schemaname = 'library';
-- 5. Show query statistics from pg_stat_statements (if extension is enabled)
-- Requires: CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT
LEFT(query, 80) AS query_preview,
calls,
ROUND(total_exec_time::numeric, 2) AS total_time_ms,
ROUND(mean_exec_time::numeric, 2) AS avg_time_ms,
ROUND(max_exec_time::numeric, 2) AS max_time_ms,
rows
FROM pg_stat_statements
WHERE query LIKE '%BaseItems%'
AND query NOT LIKE '%pg_stat%'
ORDER BY mean_exec_time DESC
LIMIT 20;
-- 6. Check for bloat in BaseItems table
SELECT
schemaname,
relname,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) AS total_size,
n_dead_tup AS dead_tuples,
n_live_tup AS live_tuples,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_tuple_percent,
last_vacuum,
last_autovacuum,
CASE
WHEN ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) > 20 THEN 'VACUUM RECOMMENDED'
WHEN ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) > 10 THEN 'VACUUM SOON'
ELSE 'OK'
END AS recommendation
FROM pg_stat_user_tables
WHERE relname = 'BaseItems'
AND schemaname = 'library';
-- 7. Show cache hit ratio (should be > 99%)
SELECT
schemaname,
relname,
heap_blks_read AS disk_reads,
heap_blks_hit AS cache_hits,
ROUND(100.0 * heap_blks_hit / NULLIF(heap_blks_hit + heap_blks_read, 0), 2) AS cache_hit_ratio
FROM pg_statio_user_tables
WHERE relname = 'BaseItems'
AND schemaname = 'library';
-- 8. Reset statistics (useful after making changes to get fresh data)
-- UNCOMMENT TO USE:
-- SELECT pg_stat_reset();
-- SELECT pg_stat_statements_reset(); -- If pg_stat_statements is enabled
+296
View File
@@ -0,0 +1,296 @@
-- ============================================================================
-- Slow Query Analysis Script
-- Purpose: Identify and analyze slow-running queries (including 110-second ones)
-- Requirements: pg_stat_statements extension must be enabled
-- ============================================================================
\pset pager off
\timing on
\x auto
\echo '========================================='
\echo 'Jellyfin Slow Query Analysis'
\echo 'Generated: 2026-03-05'
\echo '========================================='
\echo ''
-- ============================================================================
-- 1. CHECK EXTENSION STATUS
-- ============================================================================
\echo '1. CHECKING pg_stat_statements EXTENSION'
\echo '-----------------------------------------'
SELECT
CASE
WHEN COUNT(*) > 0 THEN '✅ pg_stat_statements is installed'
ELSE '❌ pg_stat_statements is NOT installed - Run: CREATE EXTENSION pg_stat_statements;'
END AS status
FROM pg_extension
WHERE extname = 'pg_stat_statements';
\echo ''
-- ============================================================================
-- 2. TOP 20 SLOWEST QUERIES BY MAX EXECUTION TIME
-- ============================================================================
\echo '2. TOP 20 SLOWEST QUERIES (by max execution time)'
\echo '-----------------------------------------'
\echo 'These are the queries that have taken the longest time to execute'
\echo ''
SELECT
LEFT(query, 100) AS query_preview,
calls,
ROUND(mean_exec_time::numeric, 2) AS avg_ms,
ROUND(max_exec_time::numeric, 2) AS max_ms,
ROUND(min_exec_time::numeric, 2) AS min_ms,
ROUND(stddev_exec_time::numeric, 2) AS stddev_ms,
ROUND(total_exec_time::numeric, 2) AS total_ms,
ROUND((100.0 * total_exec_time / SUM(total_exec_time) OVER ())::numeric, 2) AS pct_total,
rows AS rows_returned
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat_statements%'
AND query NOT LIKE '%pg_catalog%'
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY max_exec_time DESC
LIMIT 20;
\echo ''
-- ============================================================================
-- 3. TOP 20 SLOWEST QUERIES BY AVERAGE EXECUTION TIME
-- ============================================================================
\echo '3. TOP 20 SLOWEST QUERIES (by average execution time)'
\echo '-----------------------------------------'
\echo 'These queries consistently take a long time'
\echo ''
SELECT
LEFT(query, 100) AS query_preview,
calls,
ROUND(mean_exec_time::numeric, 2) AS avg_ms,
ROUND(max_exec_time::numeric, 2) AS max_ms,
ROUND(total_exec_time::numeric, 2) AS total_ms,
ROUND((100.0 * total_exec_time / SUM(total_exec_time) OVER ())::numeric, 2) AS pct_total,
rows AS rows_returned,
CASE
WHEN calls > 0 THEN ROUND((rows::numeric / calls), 2)
ELSE 0
END AS avg_rows_per_call
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat_statements%'
AND query NOT LIKE '%pg_catalog%'
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
AND calls > 1 -- Only queries called multiple times
ORDER BY mean_exec_time DESC
LIMIT 20;
\echo ''
-- ============================================================================
-- 4. QUERIES WITH HIGHEST TOTAL TIME (Performance Impact)
-- ============================================================================
\echo '4. QUERIES WITH HIGHEST TOTAL TIME'
\echo '-----------------------------------------'
\echo 'These queries consume the most total database time'
\echo ''
SELECT
LEFT(query, 100) AS query_preview,
calls,
ROUND(mean_exec_time::numeric, 2) AS avg_ms,
ROUND(max_exec_time::numeric, 2) AS max_ms,
ROUND(total_exec_time::numeric, 2) AS total_ms,
ROUND((100.0 * total_exec_time / SUM(total_exec_time) OVER ())::numeric, 2) AS pct_total,
ROUND((total_exec_time / 1000 / 60)::numeric, 2) AS total_minutes
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat_statements%'
AND query NOT LIKE '%pg_catalog%'
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY total_exec_time DESC
LIMIT 20;
\echo ''
-- ============================================================================
-- 5. BASEITEMS QUERIES SPECIFICALLY (Main Problem Area)
-- ============================================================================
\echo '5. BASEITEMS TABLE QUERIES ANALYSIS'
\echo '-----------------------------------------'
\echo 'Focusing on BaseItems table (where 110-second queries occur)'
\echo ''
SELECT
LEFT(query, 150) AS query_preview,
calls,
ROUND(mean_exec_time::numeric, 2) AS avg_ms,
ROUND(max_exec_time::numeric, 2) AS max_ms,
ROUND(min_exec_time::numeric, 2) AS min_ms,
ROUND(total_exec_time::numeric, 2) AS total_ms,
rows AS rows_returned,
CASE
WHEN calls > 0 THEN ROUND((rows::numeric / calls), 2)
ELSE 0
END AS avg_rows_per_call,
CASE
WHEN max_exec_time > 100000 THEN '🔥 CRITICAL (>100s)'
WHEN max_exec_time > 10000 THEN '⚠️ SLOW (>10s)'
WHEN max_exec_time > 1000 THEN '⚡ MODERATE (>1s)'
ELSE '✅ FAST'
END AS performance_rating
FROM pg_stat_statements
WHERE query ILIKE '%BaseItems%'
AND query NOT LIKE '%pg_stat_statements%'
AND query NOT LIKE '%pg_catalog%'
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY max_exec_time DESC;
\echo ''
-- ============================================================================
-- 6. PEOPLES AND ITEMVALUES QUERIES
-- ============================================================================
\echo '6. PEOPLES AND ITEMVALUES QUERIES'
\echo '-----------------------------------------'
\echo 'Queries on high-sequential-scan tables'
\echo ''
SELECT
CASE
WHEN query ILIKE '%Peoples%' THEN 'Peoples'
WHEN query ILIKE '%ItemValues%' THEN 'ItemValues'
ELSE 'Other'
END AS table_name,
LEFT(query, 100) AS query_preview,
calls,
ROUND(mean_exec_time::numeric, 2) AS avg_ms,
ROUND(max_exec_time::numeric, 2) AS max_ms,
ROUND(total_exec_time::numeric, 2) AS total_ms,
rows AS rows_returned
FROM pg_stat_statements
WHERE (query ILIKE '%Peoples%' OR query ILIKE '%ItemValues%')
AND query NOT LIKE '%pg_stat_statements%'
AND query NOT LIKE '%pg_catalog%'
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY max_exec_time DESC
LIMIT 20;
\echo ''
-- ============================================================================
-- 7. QUERIES WITH MANY ROWS BUT FAST (Good Index Usage)
-- ============================================================================
\echo '7. EFFICIENT QUERIES (Many rows, fast execution)'
\echo '-----------------------------------------'
\echo 'These queries return many rows but execute quickly (good indexes!)'
\echo ''
SELECT
LEFT(query, 100) AS query_preview,
calls,
ROUND(mean_exec_time::numeric, 2) AS avg_ms,
rows AS total_rows_returned,
ROUND((rows::numeric / NULLIF(calls, 0))::numeric, 2) AS avg_rows_per_call,
ROUND((mean_exec_time / NULLIF((rows::numeric / NULLIF(calls, 0)), 0))::numeric, 4) AS ms_per_row
FROM pg_stat_statements
WHERE rows > 100000 -- Many rows returned
AND mean_exec_time < 100 -- But fast execution
AND calls > 10 -- Called frequently
AND query NOT LIKE '%pg_stat_statements%'
AND query NOT LIKE '%pg_catalog%'
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY rows DESC
LIMIT 10;
\echo ''
-- ============================================================================
-- 8. CURRENTLY RUNNING QUERIES (Real-time)
-- ============================================================================
\echo '8. CURRENTLY RUNNING QUERIES'
\echo '-----------------------------------------'
SELECT
pid,
usename,
application_name,
NOW() - query_start AS duration,
state,
wait_event_type,
wait_event,
LEFT(query, 100) AS query_preview
FROM pg_stat_activity
WHERE state != 'idle'
AND pid != pg_backend_pid()
AND query NOT LIKE '%pg_stat_activity%'
AND datname = current_database()
ORDER BY query_start ASC;
\echo ''
-- ============================================================================
-- 9. QUERY STATISTICS SUMMARY
-- ============================================================================
\echo '9. OVERALL QUERY STATISTICS SUMMARY'
\echo '-----------------------------------------'
SELECT
COUNT(*) AS total_unique_queries,
SUM(calls) AS total_calls,
ROUND(SUM(total_exec_time)::numeric / 1000 / 60, 2) AS total_exec_minutes,
ROUND(AVG(mean_exec_time)::numeric, 2) AS avg_query_time_ms,
ROUND(MAX(max_exec_time)::numeric, 2) AS slowest_query_ever_ms,
ROUND(MAX(max_exec_time)::numeric / 1000, 2) AS slowest_query_ever_seconds,
SUM(rows) AS total_rows_returned,
ROUND(AVG(rows::numeric / NULLIF(calls, 0))::numeric, 2) AS avg_rows_per_query
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat_statements%'
AND query NOT LIKE '%pg_catalog%'
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database());
\echo ''
-- ============================================================================
-- 10. RECOMMENDATIONS
-- ============================================================================
\echo ''
\echo '========================================='
\echo 'RECOMMENDATIONS'
\echo '========================================='
\echo ''
\echo 'Based on the analysis above:'
\echo ''
\echo '1. CRITICAL QUERIES (>100s):'
\echo ' - Review queries marked 🔥 CRITICAL'
\echo ' - Check EXPLAIN ANALYZE for these queries'
\echo ' - Consider query rewrite or additional indexes'
\echo ''
\echo '2. SLOW QUERIES (>10s):'
\echo ' - Review queries marked ⚠️ SLOW'
\echo ' - Check if indexes are being used'
\echo ' - Run: EXPLAIN (ANALYZE, BUFFERS) <query>'
\echo ''
\echo '3. HIGH TOTAL TIME QUERIES:'
\echo ' - Even fast queries with many calls add up'
\echo ' - Consider caching frequently called queries'
\echo ' - Optimize connection pooling'
\echo ''
\echo '4. TO RESET STATISTICS:'
\echo ' Run: SELECT pg_stat_statements_reset();'
\echo ' (Useful after making optimization changes)'
\echo ''
\echo '5. TO GET FULL QUERY TEXT:'
\echo ' This script shows only first 100-150 characters'
\echo ' Query full text from pg_stat_statements table directly'
\echo ''
\echo '========================================='