- 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.
5.5 KiB
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:
- Query database for matching Series (with TvExtras JOIN)
- Load ALL results into memory (including episodes)
- Deduplicate by
SeriesPresentationUniqueKeyin C# - Apply paging
- 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
Typeindex - Composite
(IsFolder, Type)index - Index on
TvExtras.SeriesId - No filtering on
IsSeriescolumn
3. Inefficient Queries for Series
Line 2995-3000: IsPlayed special case
// 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
// 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#:
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 metadataLiveTvExtras- Live TV metadataAudioExtras- Audio metadataProvider- Provider IDsLockedFields- Locked metadataUserData- Watch historyImages- Item imagesTrailerTypes- 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:
- Full Series table scan with TvExtras JOIN
- Select into memory
- Deduplicate
- 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:
- Query for Episodes:
IncludeItemTypes=Episode(loads all 10,000 to deduplicate) - Filter by tags:
Tags=Action(must join parent Series tags) - Filter by played status:
IsPlayed=true(correlated subquery per Series) - Browse large library with series+episodes mixed query
- Any query requesting both Series AND Episodes
✅ FASTER:
- Query for just Series:
IncludeItemTypes=Series(no Episodes to load) - Query with TopParentId (uses composite index)
- Direct queries with no deduplication needed
- 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)
- Add index on
Typecolumn - Add index on
(IsFolder, Type) - Add index on
TvExtras.SeriesId - Optimize IsPlayed query to avoid correlated subquery
Medium Priority (Architecture)
- Implement caching for Series child counts
- Lazy-load TvExtras only when needed
- Create view for Series with precomputed episode counts
Low Priority (Long-term)
- Denormalize episode count into Series row
- Separate read replica for expensive queries
- 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.