-- PostgreSQL Performance Diagnostics for Jellyfin -- Run this to diagnose current performance issues \pset pager off \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 '========================================='