Files
pgsql-jellyfin/docs/TV_SHOWS_SQL_QUERY_PATTERNS.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

293 lines
9.4 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 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