e51d3577ce
- 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.
305 lines
8.4 KiB
Markdown
305 lines
8.4 KiB
Markdown
# 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**:
|
||
```bash
|
||
# 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](Emby.Server.Implementations/Dto/DtoService.cs#L182):
|
||
```csharp
|
||
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:
|
||
```bash
|
||
# Browser DevTools → Network → GetItems request
|
||
# Check if query parameters include ItemCounts
|
||
```
|
||
|
||
**Fix**: Batch SetItemByNameInfo operations instead of per-item:
|
||
```csharp
|
||
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](Emby.Server.Implementations/Dto/DtoService.cs#L497):
|
||
```csharp
|
||
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**:
|
||
```bash
|
||
# Enable EF logging to see if GetChildCount() queries per-item
|
||
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"
|
||
```
|
||
|
||
**Potential Fix**: Cache folder child counts:
|
||
```csharp
|
||
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](Emby.Server.Implementations/Dto/DtoService.cs#L241):
|
||
```csharp
|
||
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](Emby.Server.Implementations/Dto/DtoService.cs#L264):
|
||
```csharp
|
||
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](Jellyfin.Server/Resources/Configuration/logging.json):
|
||
```json
|
||
{
|
||
"Serilog": {
|
||
"MinimumLevel": {
|
||
"Override": {
|
||
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Step 2: Clear Logs
|
||
```bash
|
||
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
|
||
```bash
|
||
# 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:
|
||
```javascript
|
||
// 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 |
|
||
|
||
---
|
||
|
||
## Recommended Testing
|
||
|
||
### 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
|
||
|
||
- Main Query Execution: [QUERY_EXECUTION_ANALYSIS.md](QUERY_EXECUTION_ANALYSIS.md)
|
||
- ItemsController: [Jellyfin.Api/Controllers/ItemsController.cs](Jellyfin.Api/Controllers/ItemsController.cs#L166)
|
||
- DtoService: [Emby.Server.Implementations/Dto/DtoService.cs](Emby.Server.Implementations/Dto/DtoService.cs#L160)
|
||
- BaseItemRepository: [Jellyfin.Server.Implementations/Item/BaseItemRepository.cs](Jellyfin.Server.Implementations/Item/BaseItemRepository.cs#L421)
|