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.
163 lines
4.3 KiB
Markdown
163 lines
4.3 KiB
Markdown
# Correlated Subquery Performance Fix
|
|
|
|
## Problem Identified
|
|
|
|
**Query with 165-second execution time** was caused by correlated subquery pattern in BaseItemRepository.cs:
|
|
|
|
```csharp
|
|
// 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):
|
|
```sql
|
|
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)**
|
|
```csharp
|
|
// Use DistinctBy() to generate PostgreSQL DISTINCT ON
|
|
dbQuery = dbQuery.DistinctBy(e => e.PresentationUniqueKey);
|
|
```
|
|
|
|
### Generated SQL (Optimized):
|
|
```sql
|
|
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`:
|
|
|
|
1. **Line 603-605** (PRIMARY CULPRIT): `GroupBy(PresentationUniqueKey)` → `DistinctBy(PresentationUniqueKey)`
|
|
2. **Line 600-602**: `GroupBy(new { PresentationUniqueKey, SeriesPresentationUniqueKey })` → `DistinctBy(...)`
|
|
3. **Line 608-610**: `GroupBy(SeriesPresentationUniqueKey)` → `DistinctBy(SeriesPresentationUniqueKey)`
|
|
4. **Line 1660-1666**: `GroupBy(PresentationUniqueKey)` in masterQuery → `DistinctBy(PresentationUniqueKey)`
|
|
5. **Line 1828-1834**: `GroupBy(PresentationUniqueKey)` in masterQuery → `DistinctBy(PresentationUniqueKey)`
|
|
|
|
---
|
|
|
|
## Verification Steps
|
|
|
|
### 1. Build the Solution
|
|
```powershell
|
|
dotnet build Jellyfin.sln -c Release
|
|
```
|
|
|
|
### 2. Re-run Performance Diagnostics
|
|
```powershell
|
|
. .\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
|
|
```powershell
|
|
& $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:
|
|
```sql
|
|
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`:
|
|
```sql
|
|
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:**
|
|
1. Build solution
|
|
2. Re-run diagnostics
|
|
3. Compare before/after performance
|
|
4. Document improvements
|