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.
293 lines
9.4 KiB
Markdown
293 lines
9.4 KiB
Markdown
# 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
|