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

4.3 KiB

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:

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):

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:

dbQuery = dbQuery.DistinctBy(e => e.PresentationUniqueKey);

Generated SQL (OPTIMIZED - PostgreSQL):

SELECT DISTINCT ON (b0."PresentationUniqueKey") 
    b0."Id", b0."SortName", ...
FROM library."BaseItems" AS b0
ORDER BY b0."PresentationUniqueKey"

Generated SQL (OPTIMIZED - SQL Server):

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
  • Jellyfin.Server.Implementations/Item/BaseItemRepository.cs - Line 572-603 (ApplyGroupingFilter method)

Additional Recommendations

When loading items with multiple relationships (providers, images, user data), consider using split queries:

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:

opt.CommandTimeout(60);  // 60 seconds

3. Database Indexing

Ensure proper indexes exist on:

  • BaseItems.PresentationUniqueKey
  • BaseItems.SeriesPresentationUniqueKey
  • BaseItems.IsVirtualItem
  • BaseItems.TopParentId

Check with:

SELECT * FROM pg_indexes WHERE tablename = 'BaseItems';

4. Query Logging Configuration

To debug slow queries, enable Entity Framework Core query logging in logging.json:

{
    "Serilog": {
        "MinimumLevel": {
            "Override": {
                "Microsoft.EntityFrameworkCore.Database.Command": "Debug"
            }
        }
    }
}

See src/Jellyfin.Database/readme.md for more details on query logging.