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.
This commit is contained in:
2026-07-14 10:32:56 -04:00
parent e51d3577ce
commit 8914f4dce9
29 changed files with 4123 additions and 31 deletions
+292
View File
@@ -0,0 +1,292 @@
# 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