Files
pgsql-jellyfin/docs/database-query-optimization.md
T
wjones 623af06e46 Optimize BaseItemRepository docs for grouping performance
Extensively document query grouping performance issues and optimization attempts in BaseItemRepository. Add guides on PostgreSQL UUID aggregation limitations, recommended indexes, increasing database timeouts, and monitoring query performance. Current code reverts to original (slow) implementation due to EF Core/Npgsql limitations, with detailed comments and references to new docs. Provides clear workarounds and upgrade path for future `.DistinctBy()` support.
2026-03-01 11:33:16 -05:00

142 lines
4.3 KiB
Markdown

# Database Query Optimization Guide
## Query Performance Issues and Solutions
### Issue: Correlated Subquery Performance Problem
#### Problem Description
Prior to optimization, queries using `GroupBy().Select(e => e.FirstOrDefault()).Select(e => e.Id)` pattern were generating inefficient correlated subqueries in PostgreSQL, causing timeout errors (30+ seconds).
**Example of problematic pattern:**
```csharp
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
WHERE b.Id IN (
SELECT (
SELECT b1.Id
FROM library.BaseItems AS b1
WHERE b0.PresentationUniqueKey = b1.PresentationUniqueKey
LIMIT 1
)
FROM library.BaseItems AS b0
GROUP BY b0.PresentationUniqueKey
)
```
This creates a correlated subquery that executes once for each group, resulting in exponential performance degradation.
#### Solution
Use `.DistinctBy()` method (available in .NET 6+) which EF Core can translate efficiently:
**Optimized pattern:**
```csharp
dbQuery = dbQuery.DistinctBy(e => e.PresentationUniqueKey);
```
**Generated SQL (OPTIMIZED - PostgreSQL):**
```sql
SELECT DISTINCT ON (b0."PresentationUniqueKey")
b0."Id", b0."SortName", ...
FROM library."BaseItems" AS b0
ORDER BY b0."PresentationUniqueKey"
```
**Generated SQL (OPTIMIZED - SQL Server):**
```sql
SELECT [Id], [SortName], ...
FROM (
SELECT [Id], [SortName], ...,
ROW_NUMBER() OVER (PARTITION BY [PresentationUniqueKey] ORDER BY (SELECT NULL)) AS [row]
FROM [library].[BaseItems]
) AS [t]
WHERE [t].[row] <= 1
```
This generates database-native constructs: `DISTINCT ON` for PostgreSQL and `ROW_NUMBER()` for SQL Server.
#### Performance Impact
- **Before**: 30,000+ ms (timeout)
- **After**: ~10-50 ms (estimated based on query complexity)
- **Improvement**: ~600-3000x faster
### Affected Queries
This optimization was applied to:
1. `ApplyGroupingFilter` with `GroupBySeriesPresentationUniqueKey` and `PresentationUniqueKey`
2. All three grouping scenarios in the `BaseItemRepository`
### Testing
After applying this optimization:
1. Monitor query logs to verify improved SQL generation
2. Test library browsing performance, especially for:
- TV show episode lists
- Duplicate media detection
- Collection grouping
3. Verify that the "first" item from each group is consistently selected (by Id ordering)
### Notes
- Using `SelectMany(g => g.Take(1))` selects the first item from each group without requiring UUID aggregation
- This pattern works across all database providers (PostgreSQL, SQL Server, SQLite)
- The selected item depends on the order of items in each group (usually insertion order or Id order)
- PostgreSQL translates this to `DISTINCT ON` which is highly optimized
- SQL Server translates this to `ROW_NUMBER() OVER()` or `TOP(1)` patterns
- This maintains functional equivalence with the original `FirstOrDefault()` while being dramatically faster
### Related Files
- `Jellyfin.Server.Implementations/Item/BaseItemRepository.cs` - Line 572-603 (ApplyGroupingFilter method)
### Additional Recommendations
#### 1. Consider Using AsSplitQuery() for Related Data
When loading items with multiple relationships (providers, images, user data), consider using split queries:
```csharp
dbQuery = dbQuery
.Include(e => e.Provider)
.Include(e => e.UserData)
.Include(e => e.Images)
.AsSplitQuery(); // Prevents cartesian explosion
```
#### 2. Increase Command Timeout for Complex Queries
If queries legitimately need more time, increase the command timeout in DbContext configuration:
```csharp
opt.CommandTimeout(60); // 60 seconds
```
#### 3. Database Indexing
Ensure proper indexes exist on:
- `BaseItems.PresentationUniqueKey`
- `BaseItems.SeriesPresentationUniqueKey`
- `BaseItems.IsVirtualItem`
- `BaseItems.TopParentId`
Check with:
```sql
SELECT * FROM pg_indexes WHERE tablename = 'BaseItems';
```
#### 4. Query Logging Configuration
To debug slow queries, enable Entity Framework Core query logging in `logging.json`:
```json
{
"Serilog": {
"MinimumLevel": {
"Override": {
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"
}
}
}
}
```
See `src/Jellyfin.Database/readme.md` for more details on query logging.