Postgres perf, path migration, and shutdown race fixes

- Major query performance boost: replaced correlated subqueries with DistinctBy() in BaseItemRepository, added episode deduplication index, and increased default command timeout to 120s.
- Fixed LibraryMonitor shutdown race: added disposal checks and exception handling in FileRefresher to prevent ObjectDisposedException.
- Added PowerShell and SQL utilities for fixing Linux paths in config files and database; new scripts for WebDir and path migration.
- Auto-fix for WebDir in startup.json on load; new helper scripts and docs.
- Added automated weekly DB performance monitoring scripts and reporting.
- Expanded documentation and README for all fixes, scripts, and migration steps.
- Updated SQL scripts for index creation and diagnostics; improved output handling.
- Updated db-config defaults and added new diagnostic/report files.
This commit is contained in:
2026-03-05 08:56:42 -05:00
parent 0eb44818ff
commit 7eb2b445cb
44 changed files with 3504 additions and 23 deletions
+1
View File
@@ -1,6 +1,7 @@
-- Performance Indexes for BaseItems Table
-- These indexes improve performance of grouping queries in BaseItemRepository
-- Run on PostgreSQL database
\pset pager off
-- Connect to your Jellyfin database first:
-- \c jellyfin_testdata
+1
View File
@@ -2,6 +2,7 @@
-- These are the essential performance indexes that were in the original schema dump
-- but missing from the InitialCreate migration
-- Run this if migration doesn't apply automatically
\pset pager off
\echo 'Adding base performance indexes...';
+23
View File
@@ -0,0 +1,23 @@
-- PostgreSQL Performance Fix for Episode Queries
-- Run this script to add the missing index for PresentationUniqueKey
-- This will significantly speed up episode deduplication queries
\pset pager off
-- Create the index (CONCURRENTLY means it won't lock the table during creation)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_baseitems_presentationuniquekey_episodes
ON library."BaseItems" ("PresentationUniqueKey", "TopParentId", "IsVirtualItem")
WHERE "Type" = 'MediaBrowser.Controller.Entities.TV.Episode' AND "PresentationUniqueKey" IS NOT NULL;
-- Verify the index was created
SELECT
schemaname,
tablename,
indexname,
indexdef
FROM pg_indexes
WHERE schemaname = 'library'
AND indexname = 'idx_baseitems_presentationuniquekey_episodes';
-- Check index size
SELECT
pg_size_pretty(pg_relation_size('library.idx_baseitems_presentationuniquekey_episodes'::regclass)) AS index_size;
+1
View File
@@ -1,6 +1,7 @@
-- Combined Performance Indexes Script
-- This combines both base and supplementary indexes into one script
-- Run this on a fresh database after InitialCreate migration
\pset pager off
\echo '========================================';
\echo 'Creating All Performance Indexes';
+1
View File
@@ -1,5 +1,6 @@
-- PostgreSQL Performance Diagnostics for Jellyfin
-- Run this to diagnose current performance issues
\pset pager off
\timing on
\x auto
+146
View File
@@ -0,0 +1,146 @@
-- PostgreSQL Path Fix for Windows
-- This script converts Linux paths stored in the database to Windows paths
-- Run this if you migrated from Linux or have incorrect paths in your database
\pset pager off
-- IMPORTANT: Backup your database before running this script!
-- Step 1: Check what paths exist in your database
SELECT 'ImageInfo Paths' AS table_name, COUNT(*) AS count,
COUNT(CASE WHEN "Path" LIKE '/var/lib/jellyfin/%' THEN 1 END) AS linux_paths,
COUNT(CASE WHEN "Path" LIKE '/var/%' OR "Path" LIKE '/usr/%' THEN 1 END) AS all_linux_paths
FROM library."ImageInfos"
UNION ALL
SELECT 'BaseItems Paths' AS table_name, COUNT(*) AS count,
COUNT(CASE WHEN "Path" LIKE '/var/lib/jellyfin/%' THEN 1 END) AS linux_paths,
COUNT(CASE WHEN "Path" LIKE '/var/%' OR "Path" LIKE '/usr/%' THEN 1 END) AS all_linux_paths
FROM library."BaseItems";
-- Step 2: Show sample paths that will be updated
(SELECT '"ImageInfos"' AS table_name, "Id"::text, "Path" AS current_path
FROM library."ImageInfos"
WHERE "Path" LIKE '/var/lib/jellyfin/%' OR "Path" LIKE '/var/%' OR "Path" LIKE '/usr/%'
LIMIT 10)
UNION ALL
(SELECT '"BaseItems"' AS table_name, "Id"::text, "Path" AS current_path
FROM library."BaseItems"
WHERE "Path" LIKE '/var/lib/jellyfin/%' OR "Path" LIKE '/var/%' OR "Path" LIKE '/usr/%'
LIMIT 10);
-- Step 3: Preview the path conversion (does NOT modify data)
-- This shows you what the new paths will look like
SELECT
"Id",
"Path" AS old_path,
REGEXP_REPLACE(
REGEXP_REPLACE(
"Path",
'^/var/lib/jellyfin',
'C:/ProgramData/jellyfin/data',
'g'
),
'/',
'\',
'g'
) AS new_path
FROM library."ImageInfos"
WHERE "Path" LIKE '/var/lib/jellyfin/%'
LIMIT 5;
-- ===========================================================================
-- STOP HERE AND REVIEW THE OUTPUT ABOVE BEFORE PROCEEDING!
-- ===========================================================================
-- If the preview looks correct, uncomment and run the UPDATE statements below
/*
-- Step 4: Update ImageInfo paths
-- Convert: /var/lib/jellyfin -> C:/ProgramData/jellyfin/data
-- Convert: / -> \
BEGIN;
UPDATE library."ImageInfos"
SET "Path" = REGEXP_REPLACE(
REGEXP_REPLACE(
"Path",
'^/var/lib/jellyfin',
'C:/ProgramData/jellyfin/data',
'g'
),
'/',
'\',
'g'
)
WHERE "Path" LIKE '/var/lib/jellyfin/%';
-- Show how many rows were updated
SELECT 'Updated ImageInfos: ' || ROW_COUNT() AS result;
COMMIT;
-- Step 5: Update BaseItems paths (if needed)
BEGIN;
UPDATE library."BaseItems"
SET "Path" = REGEXP_REPLACE(
REGEXP_REPLACE(
"Path",
'^/var/lib/jellyfin',
'C:/ProgramData/jellyfin/data',
'g'
),
'/',
'\',
'g'
)
WHERE "Path" LIKE '/var/lib/jellyfin/%';
SELECT 'Updated BaseItems: ' || ROW_COUNT() AS result;
COMMIT;
-- Step 6: Update any other Linux paths (e.g., /usr/share/jellyfin)
BEGIN;
UPDATE library."BaseItems"
SET "Path" = REGEXP_REPLACE(
REGEXP_REPLACE(
"Path",
'^/usr/share/jellyfin',
'C:/Program Files/Jellyfin',
'g'
),
'/',
'\',
'g'
)
WHERE "Path" LIKE '/usr/share/jellyfin/%';
SELECT 'Updated BaseItems (usr/share): ' || ROW_COUNT() AS result;
COMMIT;
-- Step 7: Verify the changes
SELECT 'Verification - ImageInfos' AS check_type,
COUNT(*) AS total_rows,
COUNT(CASE WHEN "Path" LIKE '/var/%' OR "Path" LIKE '/usr/%' THEN 1 END) AS remaining_linux_paths
FROM library."ImageInfos"
UNION ALL
SELECT 'Verification - BaseItems' AS check_type,
COUNT(*) AS total_rows,
COUNT(CASE WHEN "Path" LIKE '/var/%' OR "Path" LIKE '/usr/%' THEN 1 END) AS remaining_linux_paths
FROM library."BaseItems";
*/
-- ===========================================================================
-- IMPORTANT NOTES:
-- ===========================================================================
-- 1. This script assumes your Windows Jellyfin data is at C:/ProgramData/jellyfin/data
-- If it's different, update the path in the REGEXP_REPLACE statements
--
-- 2. The script converts forward slashes (/) to backslashes (\)
-- However, .NET's Path class accepts forward slashes on Windows too
--
-- 3. You may need to restart Jellyfin after running this script
--
-- 4. If you have custom metadata paths, you'll need to adjust the script accordingly
+1
View File
@@ -1,5 +1,6 @@
-- 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
+143
View File
@@ -0,0 +1,143 @@
-- ============================================================================
-- Optimize Indexes for Peoples and ItemValues Tables
-- Generated: 2026-03-05
-- Purpose: Reduce sequential scans on high-traffic tables
-- ============================================================================
\pset pager off
\echo '========================================='
\echo 'Optimizing Peoples and ItemValues Indexes'
\echo '========================================='
\echo ''
-- ============================================================================
-- 1. PEOPLES TABLE OPTIMIZATION
-- ============================================================================
-- Current Issue: 144,299 sequential scans reading 13.7 BILLION rows
-- Current Indexes: IX_Peoples_Name, PK_Peoples
-- Target: Reduce sequential scans by 80%+
\echo '1. Creating Peoples Table Indexes...'
\echo '-----------------------------------------'
-- Index for queries filtering by Name (case-insensitive searches)
-- Supports ILIKE/LIKE queries
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_peoples_name_lower
ON library."Peoples" (LOWER("Name"))
WHERE "Name" IS NOT NULL;
-- Composite index for common join patterns (People -> Items)
-- Supports queries that need both Id and Name
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_peoples_id_name
ON library."Peoples" ("Id", "Name")
WHERE "Name" IS NOT NULL;
-- Index for external ID lookups (TMDB, IMDB, etc.)
-- Supports metadata provider queries
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_peoples_providerid
ON library."Peoples" ("ProviderIds")
WHERE "ProviderIds" IS NOT NULL;
\echo 'Peoples indexes created successfully!'
\echo ''
-- ============================================================================
-- 2. ITEMVALUES TABLE OPTIMIZATION
-- ============================================================================
-- Current Issue: 506,048 sequential scans reading 3.7 BILLION rows
-- Current Indexes: IX_ItemValues_Type_CleanValue, IX_ItemValues_Type_Value
-- Target: Increase index usage from 82% to 95%+
\echo '2. Creating ItemValues Table Indexes...'
\echo '-----------------------------------------'
-- Index for CleanValue lookups without Type filter
-- Supports genre/tag searches that don't filter by type
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_cleanvalue
ON library."ItemValues" ("CleanValue")
WHERE "CleanValue" IS NOT NULL;
-- Index for Value lookups (with original casing)
-- Supports exact value matches
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_value
ON library."ItemValues" ("Value")
WHERE "Value" IS NOT NULL;
-- Composite index for ItemValuesMap joins
-- Optimizes: ItemValues JOIN ItemValuesMap ON ItemValues.Id = ItemValuesMap.ItemValueId
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_id_type_cleanvalue
ON library."ItemValues" ("Id", "Type", "CleanValue");
-- Index for reverse lookups (find all items with specific values)
-- Supports queries like "find all genres" or "find all tags"
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_type_id
ON library."ItemValues" ("Type", "Id")
WHERE "Type" IS NOT NULL;
-- Partial index for common value types (Genre, Tags, Studio, etc.)
-- Reduces index size while covering most common queries
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_common_types
ON library."ItemValues" ("Type", "CleanValue", "Id")
WHERE "Type" IN (0, 1, 2, 3, 4, 5, 6, 7, 8); -- Common metadata types
\echo 'ItemValues indexes created successfully!'
\echo ''
-- ============================================================================
-- 3. UPDATE STATISTICS
-- ============================================================================
\echo '3. Updating Table Statistics...'
\echo '-----------------------------------------'
-- Update statistics so query planner knows about new indexes
ANALYZE VERBOSE library."Peoples";
ANALYZE VERBOSE library."ItemValues";
\echo ''
\echo '========================================='
\echo 'Optimization Complete!'
\echo '========================================='
\echo ''
-- ============================================================================
-- 4. VERIFY NEW INDEXES
-- ============================================================================
\echo '4. Verifying New Indexes...'
\echo '-----------------------------------------'
-- Show all indexes on Peoples table
\echo ''
\echo 'Peoples Table Indexes:'
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE schemaname = 'library'
AND tablename = 'Peoples'
ORDER BY indexname;
\echo ''
\echo 'ItemValues Table Indexes:'
-- Show all indexes on ItemValues table
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE schemaname = 'library'
AND tablename = 'ItemValues'
ORDER BY indexname;
\echo ''
\echo '========================================='
\echo 'Next Steps:'
\echo '1. Use Jellyfin normally for a few hours'
\echo '2. Run: sql\diagnostics.sql'
\echo '3. Check if sequential scans decreased'
\echo '4. Run: sql\query-analysis.sql for slow query details'
\echo '========================================='
+1
View File
@@ -1,6 +1,7 @@
-- PostgreSQL Performance Indexes for Jellyfin
-- Run this script to improve query performance
-- Safe to run multiple times (uses IF NOT EXISTS)
\pset pager off
\timing on
\echo 'Creating performance indexes for Jellyfin PostgreSQL database...'
+1
View File
@@ -2,6 +2,7 @@
-- Based on actual schema analysis
-- Run this script to add supplementary performance indexes
-- Safe to run multiple times
\pset pager off
\timing on
\echo '========================================';
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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 '========================================='