# 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) ```sql -- 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_idx` on `(ParentId, Type)` - **HIT** #### Query 2: Load Full Series Entities (Lines 475-481) ```sql -- 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 - ⚠️ `UserData` index 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) ```sql -- 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_idx` on `(ParentId, Type)` - **HIT** **Performance Problem**: Loaded 10,000 rows just to deduplicate to 500 #### Query 2: Load Full Episodes (Lines 475-481) ```sql -- 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) ```sql -- 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 `SeriesId` in TvExtras (MISSING!) - ⚠️ ItemValuesMap indexes might be used if they exist **Performance Problem**: - No index on `TvExtras.SeriesId` forces 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) ```sql -- 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 ```sql -- 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**: ```sql -- 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 1. **In-memory deduplication**: Loads 10,000 rows to return 500 2. **Correlated subqueries**: IsPlayed × Series count = thousands of subqueries 3. **Missing indexes**: Type-only queries do full table scans 4. **Complex JOINs**: AsSingleQuery() creates massive JOIN chain 5. **Tag inheritance**: Must check two tables for every tag filter 6. **TvExtras required**: Every query must JOIN to TvExtras table **Net Result**: TV Shows queries are **10-100x slower** than equivalent Movie queries