Files
pgsql-jellyfin/docs/TV_SHOWS_PERFORMANCE_QUICK_REFERENCE.md
T
wjones 8914f4dce9 Add SQL query patterns documentation and Linux package build scripts
- Created `TV_SHOWS_SQL_QUERY_PATTERNS.md` to document SQL query patterns for TV shows, including performance issues and missing indexes.
- Added `README.md` for Linux package building, detailing steps for creating Debian and Red Hat packages.
- Implemented build scripts for Debian and Red Hat, including service files and post-installation hooks.
- Added necessary scripts for managing Jellyfin service lifecycle on both Debian and Red Hat systems.
- Included package specifications and installation instructions for both distributions.
2026-07-14 10:32:56 -04:00

158 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TV Shows Query Performance - Quick Summary
## Problem Statement
TV Shows queries are **10-100x slower** than other item types due to architectural limitations in how Series are queried and deduplicated.
## Critical Findings
### 1. Query Architecture
**Current Flow**:
1. Query database for matching Series (with TvExtras JOIN)
2. Load ALL results into memory (including episodes)
3. Deduplicate by `SeriesPresentationUniqueKey` in C#
4. Apply paging
5. Load full entities with all navigations (second query)
**Example Problem**:
- User has 500 TV Series with 10,000 Episodes total
- Query to display Series list:
- Query 1: Returns 10,000 Episode rows (all must be loaded to deduplicate)
- In-memory: Deduplicates to 500 unique Series
- Query 2: Loads 500 Series entities
### 2. Missing Database Indexes
From migration `20260226170000_AddBasePerformanceIndexes.cs`:
**What EXISTS:**
- `baseitems_parentid_idx``(ParentId, Type)`
- `baseitems_topparentid_idx``(TopParentId, Type)`
- `baseitems_seriespresentationuniquekey_idx``(SeriesPresentationUniqueKey, ...)`
**What's MISSING:**
- Standalone `Type` index
- Composite `(IsFolder, Type)` index
- Index on `TvExtras.SeriesId`
- No filtering on `IsSeries` column
### 3. Inefficient Queries for Series
**Line 2995-3000**: IsPlayed special case
```csharp
// For each Series, runs a correlated subquery to check if ANY episode was played
WHERE ... (
SELECT 1 FROM BaseItems
WHERE TvExtras.SeriesPresentationUniqueKey = e.PresentationUniqueKey
AND UserData.Played = true
)
```
**Impact**: 1 Series × (number of episodes to check) subqueries
**Line 3427-3438**: Tag inheritance for episodes
```csharp
// For each episode, must check both episode tags AND parent series tags
&& (!e.TvExtras.SeriesId.HasValue ||
!context.ItemValuesMap.Any(f =>
f.ItemId == e.TvExtras.SeriesId.Value
AND f.ItemValue.Type == ItemValueType.Tags))
```
**Impact**: Additional ItemValuesMap joins per episode with tag filters
### 4. In-Memory Deduplication
**Location**: Line 814-850 in BaseItemRepository.cs
Uses reflection on anonymous types to deduplicate results in C#:
```csharp
private List<Guid> ApplyGroupingInMemory<T>(List<T> items, InternalItemsQuery filter)
{
// Reflects properties, uses DistinctBy()
filtered = items.DistinctBy(e => seriesKeyProp.GetValue(e));
return filtered.Select(...).ToList();
}
```
**Why not database?** EF Core limitations - `DistinctBy()` on complex types can't be translated to SQL consistently
**Impact**:
- All matching rows must be loaded from database
- Temporary memory spike with 10,000+ item objects
- Slow reflection-based deduplication
### 5. Table Joins Required
For Series queries, ApplyNavigations (line 780-809) eagerly loads:
- `TvExtras` - Series/Season/Episode metadata
- `LiveTvExtras` - Live TV metadata
- `AudioExtras` - Audio metadata
- `Provider` - Provider IDs
- `LockedFields` - Locked metadata
- `UserData` - Watch history
- `Images` - Item images
- `TrailerTypes` - Trailer types (if requested)
**This creates a massive JOIN chain** instead of separate queries
### 6. Code Architecture Decisions
**Line 914**: `AsSingleQuery()`
- Forces EF Core to use one giant query instead of splitting
- **Good**: Avoids N+1 on navigations
- **Bad**: One huge JOIN with poor optimization potential
**Line 455-460**: Select only IDs and keys, then load full entities
- Meant to optimize but actually forces:
1. Full Series table scan with TvExtras JOIN
2. Select into memory
3. Deduplicate
4. Second query for full entities
## Performance Comparison
| Type | Bottleneck | Speed Relative to Movies |
|------|-----------|------------------------|
| Movies | Minimal dedup, direct Type filter | 1x (baseline) |
| Music Albums | Moderate dedup by Album | 2-5x slower |
| TV Shows | Massive dedup, correlated subqueries, tag inheritance | 10-100x slower |
## When TV Shows Are Slower
**❌ SLOW**:
1. Query for Episodes: `IncludeItemTypes=Episode` (loads all 10,000 to deduplicate)
2. Filter by tags: `Tags=Action` (must join parent Series tags)
3. Filter by played status: `IsPlayed=true` (correlated subquery per Series)
4. Browse large library with series+episodes mixed query
5. Any query requesting both Series AND Episodes
**✅ FASTER**:
1. Query for just Series: `IncludeItemTypes=Series` (no Episodes to load)
2. Query with TopParentId (uses composite index)
3. Direct queries with no deduplication needed
4. Small libraries with few episodes per series
## Why This Matters
**Scale Impact**:
- **100 Series × 10 Episodes each**: ~1000 rows loaded, deduplicated to 100 (10x overhead)
- **500 Series × 50 Episodes each**: ~25,000 rows loaded, deduplicated to 500 (50x overhead)
- **2000 Series × 100 Episodes each**: ~200,000 rows loaded, deduplicated to 2000 (100x overhead)
**For remote databases**: Even worse due to network latency × 50-100 overhead
## Recommended Fixes (Priority Order)
### High Priority (Quick Wins)
1. Add index on `Type` column
2. Add index on `(IsFolder, Type)`
3. Add index on `TvExtras.SeriesId`
4. Optimize IsPlayed query to avoid correlated subquery
### Medium Priority (Architecture)
1. Implement caching for Series child counts
2. Lazy-load TvExtras only when needed
3. Create view for Series with precomputed episode counts
### Low Priority (Long-term)
1. Denormalize episode count into Series row
2. Separate read replica for expensive queries
3. Move deduplication fully to database with DISTINCT ON
## Documentation
See **TV_SHOWS_QUERY_PERFORMANCE_ANALYSIS.md** for complete details with code references and SQL examples.