5565dc3e05
- Add scripts and SQL for base and supplementary indexes (total 23) - Integrate index checks and auto-creation into Jellyfin startup - Update csproj to include SQL files in build/publish output - Add diagnostics, publish/copy scripts, and troubleshooting docs - Provide detailed guides for migration, verification, and merging - Ensures robust, automated, and well-documented index setup
366 lines
11 KiB
SQL
366 lines
11 KiB
SQL
-- 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 '========================================='
|