8914f4dce9
- 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.
9.4 KiB
9.4 KiB
TV Shows Actual SQL Query Patterns
Generated SQL Examples
Scenario 1: Get TV Shows List (When User Clicks TV Shows)
API Call: /Items?IncludeItemTypes=Series&ParentId=abc123
Query 1: Get Series IDs (Lines 455-460)
-- This is what gets generated with AsSingleQuery() and INCLUDE statements
SELECT
bi."Id",
bi."PresentationUniqueKey",
te."SeriesPresentationUniqueKey"
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
WHERE bi."ParentId" = 'abc123-uuid'::uuid
AND bi."Type" = 'Series'
ORDER BY bi."SortName"
LIMIT 1000;
-- Result: 500 rows (1 per Series, no duplicates because each Series has 1 row)
-- Then ApplyGroupingInMemory() does DistinctBy in C# (no-op here)
Indexes Used:
- ✅
baseitems_parentid_idxon(ParentId, Type)- HIT
Query 2: Load Full Series Entities (Lines 475-481)
-- After AsSingleQuery(), all INCLUDE statements are added to this query
SELECT
bi."Id",
bi."ParentId",
bi."Path",
bi."Name",
bi."SortName",
bi."IsFolder",
bi."Type",
bi."CommunityRating",
bi."IsLocked",
-- ... 50+ more columns ...
-- TvExtras included
te."SeriesId", te."SeasonId", te."SeriesName", te."SeasonName", te."SeriesPresentationUniqueKey",
-- Images included
img."Id" as "Images__Id", img."Path" as "Images__Path", img."ImageType" as "Images__ImageType",
-- UserData included
ud."Id" as "UserData__Id", ud."Played" as "UserData__Played", ud."IsFavorite" as "UserData__IsFavorite",
-- Provider included
pr."ProviderId" as "Provider__ProviderId", pr."ProviderValue" as "Provider__ProviderValue"
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
LEFT JOIN library."BaseItemImageInfos" img ON bi."Id" = img."ItemId"
LEFT JOIN library."UserData" ud ON bi."Id" = ud."ItemId" AND ud."UserId" = 'user-uuid'::uuid
LEFT JOIN library."BaseItemProviders" pr ON bi."Id" = pr."ItemId"
WHERE bi."Id" IN (
-- These are the 100 Series IDs from paging
'series-id-1', 'series-id-2', ... 'series-id-100'
)
ORDER BY bi."SortName";
-- Result: 1-100 rows (one per Series) with all related data as columns
Indexes Used:
- ⚠️
PRIMARY KEY (Id)for WHERE IN clause - ⚠️
UserDataindex on(UserId, ItemId)- partial HIT
Scenario 2: Get Recent Episodes (Slower - N+1 Example)
API Call: /Items?IncludeItemTypes=Episode&ParentId=abc123&SortBy=DateCreated
Query 1: Get Episode IDs (Lines 455-460)
-- Note: This returns EPISODES, not Series!
SELECT
bi."Id",
bi."PresentationUniqueKey",
te."SeriesPresentationUniqueKey" -- Each Episode linked to its Series
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
WHERE bi."ParentId" = 'season-id'::uuid -- Parent is Season
AND bi."Type" = 'Episode'
ORDER BY bi."DateCreated" DESC
LIMIT 1000;
-- Result: 10,000 rows (one per Episode!)
-- If you wanted to deduplicate by Series:
-- ApplyGroupingInMemory() → DistinctBy(SeriesPresentationUniqueKey)
-- Result: 500 unique Series, but 10,000 rows loaded from DB
Indexes Used:
- ✅
baseitems_parentid_idxon(ParentId, Type)- HIT
Performance Problem: Loaded 10,000 rows just to deduplicate to 500
Query 2: Load Full Episodes (Lines 475-481)
-- Loads full entities for paginated subset of 100 episodes
SELECT
bi."Id",
bi."Name",
bi."IndexNumber",
bi."ParentIndexNumber",
-- ... all columns ...
te."SeriesId", te."SeasonId", te."SeriesName",
-- ... images, userdata, provider ...
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
-- ... other JOINs ...
WHERE bi."Id" IN (
'episode-id-1', 'episode-id-2', ... 'episode-id-100'
);
-- Result: 100 rows (paged subset)
Scenario 3: Filter By Tag (Slow - Correlated Subquery)
API Call: /Items?IncludeItemTypes=Episode&Tags=Action
Query 1: Get Episode IDs with Tag Inheritance (Line 3427-3438)
-- COMPLEX: Must check Episode's tags AND parent Series' tags
SELECT
bi."Id",
bi."PresentationUniqueKey",
te."SeriesPresentationUniqueKey"
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
WHERE bi."Type" = 'Episode'
AND (
-- Episode has tag
EXISTS (
SELECT 1 FROM library."ItemValuesMap" ivm
INNER JOIN library."ItemValues" iv ON ivm."ItemValueId" = iv."ItemValueId"
WHERE ivm."ItemId" = bi."Id"
AND iv."Type" = 1 -- Tags
AND iv."CleanValue" = 'action'
)
-- OR parent Series has tag
OR EXISTS (
SELECT 1 FROM library."ItemValuesMap" ivm
INNER JOIN library."ItemValues" iv ON ivm."ItemValueId" = iv."ItemValueId"
WHERE ivm."ItemId" = te."SeriesId"
AND iv."Type" = 1 -- Tags
AND iv."CleanValue" = 'action'
)
)
ORDER BY bi."DateCreated" DESC;
-- Result: Uncertain number of Episodes with tag or tagged Series
Indexes Used:
- ❌ NO index on
SeriesIdin TvExtras (MISSING!) - ⚠️ ItemValuesMap indexes might be used if they exist
Performance Problem:
- No index on
TvExtras.SeriesIdforces table scan - Complex EXISTS conditions can't be optimized well
Scenario 4: Filter By IsPlayed=true on Series (Correlated Subquery)
API Call: /Items?IncludeItemTypes=Series&IsPlayed=true
Query 1: Get Series That Have Played Episodes (Line 2995-3000)
-- SPECIAL CASE for Series: Check if ANY episode was played
SELECT
bi."Id",
bi."PresentationUniqueKey",
te."SeriesPresentationUniqueKey"
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
WHERE bi."Type" = 'Series'
AND EXISTS (
-- Correlated subquery: For THIS series, check all episodes
SELECT 1 FROM library."BaseItems" episodes
LEFT JOIN library."UserData" ud ON episodes."Id" = ud."ItemId"
LEFT JOIN library."BaseItemTvExtras" epi_te ON episodes."Id" = epi_te."ItemId"
WHERE episodes."IsFolder" = false
AND episodes."IsVirtualItem" = false
AND epi_te."SeriesPresentationUniqueKey" = te."SeriesPresentationUniqueKey"
AND ud."Played" = true
AND ud."UserId" = 'user-uuid'::uuid
);
-- Result: Only Series with at least one played episode
-- PERFORMANCE: For each Series, this checks ALL episodes in library!
Indexes Used:
- ❌ Correlated subquery might not use indexes well
- ⚠️
UserData (UserId, Played)index exists but subquery is still expensive
Performance Problem:
- Multiplies query cost by number of Series × average episodes per Series
- 500 Series = 500 separate correlated subqueries
- If library has 10,000 episodes total: potential for 5,000,000 rows examined
Scenario 5: MISSING INDEXES Example
API Call: /Items?IncludeItemTypes=Series (No ParentId specified)
Query Without Index
-- No ParentId filter, just type filter
SELECT
bi."Id",
bi."PresentationUniqueKey",
te."SeriesPresentationUniqueKey"
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
WHERE bi."Type" = 'Series' -- ❌ NO INDEX ON TYPE ALONE!
ORDER BY bi."SortName";
-- Index lookup:
-- - baseitems_parentid_idx: (ParentId, Type) - CAN'T USE (no ParentId filter)
-- - baseitems_topparentid_idx: (TopParentId, Type) - CAN'T USE (no TopParentId filter)
-- - baseitems_sortname_idx: (SortName) - CAN'T USE (Type filter first)
--
-- Result: FULL TABLE SCAN of all BaseItems!
Optimization: Need index on Type or (Type, SortName)
Index Usage Breakdown
Current Indexes (from migration 20260226170000):
| Index | Used When | Misses |
|---|---|---|
baseitems_parentid_idx (ParentId, Type) |
ParentId + Type filter | No Type-only queries |
baseitems_topparentid_idx (TopParentId, Type) |
TopParentId + Type filter | Type-only queries |
baseitems_sortname_idx (SortName) |
ORDER BY SortName | Can't use if Type filter first |
baseitems_seriespresentationuniquekey_idx (SeriesPresentationUniqueKey, ...) |
Episode queries | Series queries without this key |
Missing Critical Indexes:
-- 1. Type filter without Parent/TopParent
CREATE INDEX CONCURRENTLY idx_baseitems_type
ON library."BaseItems" ("Type");
-- 2. Series query with sorting
CREATE INDEX CONCURRENTLY idx_baseitems_type_sortname
ON library."BaseItems" ("Type", "SortName");
-- 3. Check if Series (IsFolder + Type)
CREATE INDEX CONCURRENTLY idx_baseitems_isfolder_type
ON library."BaseItems" ("IsFolder", "Type");
-- 4. Tag inheritance for Episodes
CREATE INDEX CONCURRENTLY idx_baseitems_tvextras_seriesid
ON library."BaseItemTvExtras" ("SeriesId", "ItemId");
-- 5. IsPlayed queries
CREATE INDEX CONCURRENTLY idx_userdata_userid_played
ON library."UserData" ("UserId", "Played");
Summary: Why These Queries Are Slow
- In-memory deduplication: Loads 10,000 rows to return 500
- Correlated subqueries: IsPlayed × Series count = thousands of subqueries
- Missing indexes: Type-only queries do full table scans
- Complex JOINs: AsSingleQuery() creates massive JOIN chain
- Tag inheritance: Must check two tables for every tag filter
- TvExtras required: Every query must JOIN to TvExtras table
Net Result: TV Shows queries are 10-100x slower than equivalent Movie queries