Server Version#: 1.43.3.10828-00f62d37d
Player Version#: Plexamp for iOS 4.12.14 (iPhone 17 Pro Max / 254GB Available Space)
iPhone Wi-Fi iPerf Test to Plex Server:
Download: 902 Mbits/s Average
Upload: 858 Mbits/s Average
Opening Plexamp’s Library overview screen (Albums/Tracks/{main culprits} category list with item counts) takes 1-2 minutes to fully load. This is reproducible and confirmed independent of hardware — happens identically regardless of RAM allocation (tested at 4GB/8GB/12GB), disk type, or database journal mode (WAL vs delete).
Evidence — Plex’s own SLOW QUERY log warnings:
Warning - [Req#644] SLOW QUERY: It took 53810.000000 ms to retrieve 0 items.
Warning - [Req#19f] SLOW QUERY: It took 71050.000000 ms to retrieve 0 items.
Warning - [Req#642] SLOW QUERY: It took 14160.000000 ms to retrieve 21 items.
Debug - [Req#643] It took 20770.000000 ms to retrieve 157 items.
Debug - [Req#645] It took 24120.000000 ms to retrieve 835 items.
Debug - [Req#641] It took 61790.000000 ms to retrieve 303 items.
These map directly to the category counts on the Library screen (e.g. the 157-item query is the Album Genres count, 303 is Moods, etc.). Full breakdown of every category/endpoint/timing can be provided for further troubleshooting.
Root cause (confirmed via direct query capture)
I attached gdb to the running Plex Media Server process with a breakpoint on sqlite3_prepare_v2 to capture the actual SQL being executed. This is the real query behind the per-letter album count (and likely the same code path used for the overview counts):
select upper(substr(parents.title_sort, 1, 1)) as c, count(distinct metadata_items.id)
from metadata_items
join metadata_items as parents on metadata_items.parent_id = parents.id
join metadata_items as children on children.parent_id = metadata_items.id
where metadata_items.library_section_id = 1 and metadata_items.metadata_type = 9
group by c
The children self-join is never referenced in the SELECT list — seems like its only purpose is verifying each album has at least one track (a “has children” existence check). Implemented as a full JOIN rather than EXISTS, this multiplies every album row by its track count before aggregation. With ~8.8 tracks/album average, that turns ~165K rows of real work into on the order of ~1.4 million intermediate join rows just to compute a per-letter count.
Possible fix?
Would replacing join metadata_items as children on children.parent_id = metadata_items.id with WHERE EXISTS (SELECT 1 FROM metadata_items AS children WHERE children.parent_id = metadata_items.id) (or equivalent semi-join) in the music library aggregate/count query paths preserve identical filtering semantics without the row multiplication?
What I ruled out before making the conclusion it’s most likely a query bug and not environmental or config:
- Repeating the identical slow request back-to-back: no speedup (rules out cold cache)
- Raw SELECT COUNT(*) against the same tables: 12-63ms (rules out missing indexes/corruption)
- /playlists/all (structurally similar “list everything” endpoint, smaller table): 50ms on the same server
- RAM increasing 4GB to 12GB: no change
- journal_mode switched from delete to WAL: no change to this specific symptom
- Database verified clean via ChuckPa’s DBRepair (check/repair/reindex/FTS rebuild, all passed)
- Tested adding a covering index on the tags/taggings join tables which improved Genre/Style/Mood by 2-3x but regressed Albums/Tracks (confirmed live), consistent with the planner reacting to the underlying JOIN-fanout problem rather than a missing index
- Hypervisor/storage benchmarked directly: NVMe SSD sequential read/write ~2.7/2.5 GB/s, ~19K random 4K IOPS; RAM bandwidth ~8-13 GB/s — nowhere near a bottleneck for any SQLite query.
Why its unlikely due to just size of library:
- A raw SELECT COUNT(*) against the same 1.44M tracks / 165K albums completes in 12-63 milliseconds. Any database counts that many rows near-instantly — this isn’t a large table by modern standards. If size alone were the issue, this would be slow too. It isn’t.
- /playlists/all is fast (50ms) on the same server, same library, same hardware. If the library were too big for Plex to handle, everything touching a large dataset would be uniformly slow. It’s specifically the queries containing the unnecessary children self-join that are slow — a query-shape problem, not a data/volume problem.
- The captured query doesn’t need to touch 1.4 million rows to count 165K albums by first letter — it only does so because of the unnecessary JOIN. Replace it with EXISTS and the same “large” library would be counted in milliseconds, because EXISTS short-circuits on the first match instead of materializing every album × every track combination. The library size didn’t create this cost its the query shape that does.
- 71 seconds to return zero rows isn’t what a query scaling proportionally with library size looks like — it’s what a query does when it performs work proportional to the wrong thing (total track count) instead of the thing actually being asked for (26 letter buckets).
Can provide full debug logs or the complete timing table if useful to you guys.
Hypervisor:
- Proxmox VE 9.2.4 (kernel 7.0.14-5-pve)
- CPU: Intel Core i7-12700H (14 cores / 20 threads, 1 socket)
- RAM: 31GB total
- Storage backing the container: NVMe SSD (TEAM TM8FPK002T, 2TB)
LXC Container (Plex Music Server):
- Type: unprivileged LXC container, Debian
- Allocated: 8 CPU cores, 12GB RAM (raised from 4GB during this troubleshooting — no effect on the issue)
- Root disk: 500GB, local-lvm (LVM-thin on the NVMe SSD above)
- Music library storage: NFS mount (/mnt/nas/music), separate from the OS/database disk — the Plex database itself lives on local NVMe SSD, not network storage
- Network: bridged (vmbr0), static IP
