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

9.4 KiB
Raw Blame History

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_idx on (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
  • ⚠️ 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)

-- 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)

-- 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 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)

-- 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

  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