Files
pgsql-jellyfin/docs/QUERY_ISSUES_REMEDIATION.md
T
wjones e51d3577ce Implement N+1 query optimization and response caching strategies
- Added a comprehensive quick start guide for N+1 optimization in QUICK_START.md, detailing the problem, fixes, and deployment steps.
- Created RESPONSE_CACHING_STRATEGY.md to outline caching strategies for Jellyfin API endpoints, including implementation details and performance projections.
- Developed TECHNICAL_REFERENCE.md to document changes made in DtoService.cs, including method modifications and performance characteristics.
- Introduced a PowerShell script (convert_sql_identifiers.ps1) to convert SQL identifiers from PascalCase to lowercase/snake_case for consistency in database schema.
2026-07-09 16:08:11 -04:00

8.4 KiB
Raw Blame History

Query Execution - Quick Reference & Remediation Guide

The Query Flow in 30 Seconds

Browser loads page
    ↓
Multiple API calls (likely different item types/filters)
    ↓
ItemsController.GetItems() × N
    ↓
Build InternalItemsQuery (with DtoOptions.EnableUserData=true by default)
    ↓
BaseItemRepository.GetItemsAsync()
    ├─ Query 1: Get filtered item IDs
    └─ Query 2: Load full items with eager loading:
       ├─ .Include(e => e.UserData) ← UserData loaded HERE
       ├─ .Include(e => e.Provider)
       ├─ .Include(e => e.Images)
       ├─ .Include(e => e.TvExtras)
       ├─ .Include(e => e.LiveTvExtras)
       └─ .Include(e => e.AudioExtras)
    ↓
DtoService.GetBaseItemDtos() - For each item:
    ├─ GetBaseItemDtoInternal()
    │  └─ AttachUserSpecificInfo()
    │     └─ GetUserDataDto()
    │        └─ GetUserData() ← Accesses in-memory UserData (NO QUERY)
    └─ [Potential N+1] SetItemByNameInfo() if ItemCounts requested
    └─ [Potential N+1] GetChildCount() if ChildCount requested
    ↓
Return DTOs to browser

Identified Query Issues & Solutions

1. Multiple API Endpoints During Page Load

Status: ✓ Normal behavior, not a bug

What's Happening: The web UI calls multiple GetItems endpoints simultaneously for:

  • Movies library
  • Series library
  • Recently added
  • Resume items
  • Playlists

Each is a separate API call → separate database query.

Evidence to Confirm:

# Check network tab in browser DevTools
# Look for multiple /Items calls with different parameters

Solution: Consolidate UI requests or add caching if same queries repeat.


2. ItemCounts Field N+1 Pattern

Status: ⚠️ Actual N+1 detected

What's Happening at Emby.Server.Implementations/Dto/DtoService.cs:

for (int index = 0; index < accessibleItems.Count; index++)
{
    var item = accessibleItems[index];
    var dto = GetBaseItemDtoInternal(item, options, user, owner);
    
    if (options.ContainsField(ItemFields.ItemCounts))  // ← If requested
    {
        SetItemByNameInfo(dto, user);  // ← Called for EACH item
    }
}

Risk Level: HIGH if ItemCounts is frequently requested

To Check: Look for ItemCounts in API calls:

# Browser DevTools → Network → GetItems request
# Check if query parameters include ItemCounts

Fix: Batch SetItemByNameInfo operations instead of per-item:

if (options.ContainsField(ItemFields.ItemCounts))
{
    // Batch operation - collect all IDs first
    var itemIds = returnItems.Select(r => r.Id).ToList();
    var itemCountsMap = GetItemCountsForItems(itemIds, user);
    
    foreach (var dto in returnItems)
    {
        if (itemCountsMap.TryGetValue(dto.Id, out var counts))
        {
            dto.ItemCounts = counts;
        }
    }
}

3. ChildCount Field Potential N+1

Status: ⚠️ Partially optimized

What's Happening at Emby.Server.Implementations/Dto/DtoService.cs:

private static int GetChildCount(Folder folder, User user)
{
    if (folder is ICollectionFolder || folder is UserView)
    {
        return Random.Shared.Next(1, 10);  // ✓ NO QUERY
    }

    return folder.GetChildCount(user);  // ⚠️ POTENTIAL QUERY
}

Current Status:

  • ✓ Collection folders & user views: return random (optimized, no query)
  • ⚠️ Regular folders: call GetChildCount() which MAY query

To Check:

# Enable EF logging to see if GetChildCount() queries per-item
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"

Potential Fix: Cache folder child counts:

private static readonly Dictionary<Guid, int> _childCountCache = new();

private static int GetChildCount(Folder folder, User user)
{
    if (folder is ICollectionFolder || folder is UserView)
    {
        return Random.Shared.Next(1, 10);
    }

    if (_childCountCache.TryGetValue(folder.Id, out var count))
    {
        return count;
    }

    count = folder.GetChildCount(user);
    _childCountCache[folder.Id] = count;
    return count;
}

4. People Field Processing

Status: ⚠️ Per-item operation

What's Happening at Emby.Server.Implementations/Dto/DtoService.cs:

if (options.ContainsField(ItemFields.People))
{
    AttachPeople(dto, item, user);  // ← Called for EACH item
}

Risk Level: MEDIUM - depends on AttachPeople implementation

To Investigate:

  • Search for AttachPeople implementation
  • Check if it queries database per-item

5. MediaSources Retrieval

Status: ⚠️ Per-item operation

What's Happening at Emby.Server.Implementations/Dto/DtoService.cs:

if (item is IHasMediaSources
    && options.ContainsField(ItemFields.MediaSources))
{
    dto.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray();
}

Risk Level: MEDIUM - depends on GetStaticMediaSources implementation

To Investigate:

  • Check if GetStaticMediaSources queries database
  • If it does, batch retrieve all media sources

How to Identify Which N+1 Pattern Is Occurring

Step 1: Enable SQL Logging

Modify Jellyfin.Server/Resources/Configuration/logging.json:

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

Step 2: Clear Logs

rm /var/log/jellyfin/log_*.log
# or for development: clear logs folder

Step 3: Load Web UI Page

Navigate to the slow page and note the time.

Step 4: Analyze Logs

# Look for timestamp of the page load
grep "SELECT" /var/log/jellyfin/log_*.log | tail -50

# Look for patterns like:
# - Same query repeated multiple times
# - "SELECT COUNT(*)" followed by "SELECT * FROM [BaseItems]"
# - Queries with same WHERE clause but different parameters

Step 5: Correlate with Fields Requested

Check browser DevTools for API parameters:

// In browser console
fetch("/Items?userId=...&fields=ItemCounts,ChildCount&...").then(r => r.json()).then(console.log)

Performance Impact Summary

Issue Queries per Page Load User Impact Fix Priority
Multiple API endpoints 2-5 Slow page load Medium
ItemCounts N+1 +20-100 Very slow for large libraries High
ChildCount N+1 +10-50 Slow for folder listings Medium
People field +5-20 Slow if many items Low
MediaSources +5-20 Slow if many video items Low

Test Case 1: Verify UserData is Not N+1

✓ Enable EF logging
✓ Load library page with 50 items
✓ Search logs for "SELECT.*FROM.*UserData"
✓ Should see only 1-2 queries for UserData (not 50)

Test Case 2: Check ItemCounts Impact

✓ Note count of queries in normal page load
✓ Remove ItemCounts from request
✓ Reload and compare query count
✓ Difference = cost of ItemCounts feature

Test Case 3: Monitor API Calls

✓ Open DevTools Network tab
✓ Clear filters
✓ Load home page
✓ Observe and count /Items calls
✓ Note different query parameters

Action Items Priority

🔴 HIGH Priority

  • Investigate if ItemCounts field causes N+1 queries
  • Batch SetItemByNameInfo operations
  • Profile library page load with 1000+ items

🟡 MEDIUM Priority

  • Investigate ChildCount field N+1 potential
  • Add caching to GetChildCount
  • Verify AttachPeople doesn't query per-item

🟢 LOW Priority

  • Investigate MediaSources retrieval
  • Consider batching other per-item operations
  • Monitor performance over time

References