7eb2b445cb
- 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.
4.3 KiB
4.3 KiB
Correlated Subquery Performance Fix
Problem Identified
Query with 165-second execution time was caused by correlated subquery pattern in BaseItemRepository.cs:
// BEFORE (SLOW - Correlated Subquery)
var tempQuery = dbQuery
.GroupBy(e => e.PresentationUniqueKey)
.Select(e => e.FirstOrDefault())
.Select(e => e!.Id);
dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
Generated SQL (Problematic):
SELECT b."Id", b."Album", [70+ columns...]
FROM library."BaseItems" AS b
WHERE b."Id" IN (
SELECT (
SELECT b1."Id"
FROM library."BaseItems" AS b1
WHERE b1."Type" = $3
AND b1."IsVirtualItem" = $1
AND (b1."TopParentId" = ANY ($2))
AND (b0."PresentationUniqueKey" = b1."PresentationUniqueKey")
LIMIT 1
)
FROM library."BaseItems" AS b0
)
Performance Impact:
- 165 seconds max execution time
- 15.7 seconds average
- 108,209 rows returned requiring 108,209+ subquery executions
- Caused exponential performance degradation
Solution Applied
AFTER (FAST - DistinctBy)
// Use DistinctBy() to generate PostgreSQL DISTINCT ON
dbQuery = dbQuery.DistinctBy(e => e.PresentationUniqueKey);
Generated SQL (Optimized):
SELECT DISTINCT ON (b."PresentationUniqueKey")
b."Id", b."Album", [70+ columns...]
FROM library."BaseItems" AS b
LEFT JOIN library."UserData" AS u ON b."Id" = u."ItemId"
WHERE b."Type" = $3
AND b."IsVirtualItem" = $1
AND (b."TopParentId" = ANY ($2))
Expected Performance:
- <50ms execution time (600-3300x faster)
- Single table scan instead of correlated lookups
- Proper index usage
Locations Fixed
Fixed 5 locations in Jellyfin.Server.Implementations/Item/BaseItemRepository.cs:
- Line 603-605 (PRIMARY CULPRIT):
GroupBy(PresentationUniqueKey)→DistinctBy(PresentationUniqueKey) - Line 600-602:
GroupBy(new { PresentationUniqueKey, SeriesPresentationUniqueKey })→DistinctBy(...) - Line 608-610:
GroupBy(SeriesPresentationUniqueKey)→DistinctBy(SeriesPresentationUniqueKey) - Line 1660-1666:
GroupBy(PresentationUniqueKey)in masterQuery →DistinctBy(PresentationUniqueKey) - Line 1828-1834:
GroupBy(PresentationUniqueKey)in masterQuery →DistinctBy(PresentationUniqueKey)
Verification Steps
1. Build the Solution
dotnet build Jellyfin.sln -c Release
2. Re-run Performance Diagnostics
. .\scripts\db-config.ps1
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "sql\diagnostics.sql"
Expected Results:
- Max query time should drop from 165s to <1s
- ItemValues index usage should improve from 50% to >90%
- Sequential scans should reduce significantly
3. Check Top Slow Queries
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "sql\query-analysis.sql" > query_analysis_after_fix.txt
Expected Results:
- No queries with >100s execution time
- Average query time <100ms for BaseItems queries
Technical Background
Why GroupBy().FirstOrDefault() is Slow
EF Core translates this pattern to:
WHERE Id IN (SELECT (SELECT ... LIMIT 1) FROM ...)
This creates a correlated subquery where:
- Outer query scans BaseItems
- For EACH row, inner subquery executes
- PostgreSQL can't optimize this pattern
- Indexes are bypassed
Why DistinctBy() is Fast
EF Core translates DistinctBy() to PostgreSQL's native DISTINCT ON:
SELECT DISTINCT ON (PresentationUniqueKey) ...
This:
- Performs single table scan
- Uses indexes effectively
- PostgreSQL native optimization applies
- Returns first row per unique key efficiently
References
- docs/database-query-optimization.md: Original documentation of this anti-pattern
- sql/query-analysis.sql: Query analysis tool that identified the 165s query
- sql/diagnostics.sql: Comprehensive database diagnostics
- critical_query_165s.txt: Full extracted query showing correlated subquery structure
Date Applied
March 6, 2025
Diagnostics Results (Before Fix):
- Max query time: 165 seconds
- ItemValues index usage: 50.25%
- Sequential scans: 595,439 (4.7B rows)
- Cache hit ratio: 97.42%
Next Steps:
- Build solution
- Re-run diagnostics
- Compare before/after performance
- Document improvements