The by-name second pass is unmeasured: ten materialisations per restricted search #145
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two numbers from the review of #139, neither measured, both about the same
clause.
The second pass is emitted twice per search, and once per exempted kind
crates/jellyfin-db/src/query/filters.rs::behind_the_nameputs an uncorrelatedIN (SELECT BaseItems.Id …)inside each kind'sEXISTS.SQLitebuilds atransient index over that subquery per occurrence rather than sharing one, so a
restricted account's
/Search/Hints— five exempted kinds, andquery/search.rs::candidate_filterscopies the flag into the candidate cut —emits the clause set twice. Ten materialisations of the reachable set before a
single by-name row is tested.
Upstream emits the five clauses once, and for a different reason: its hints query
carries
ItemIds, andLibraryManager.AddUserToQueryskipsTopParentIdsentirely when
ItemIdsis non-empty — so the exemption lives only inSqlSearchProvider's access filter. This port has no equivalent split.Nothing here is a difference: the answers agree, measured against the
reference for both restricted accounts. It is a cost, and it is unknown.
The
ORmay cost an index seek on a plain listing(BaseItems.Type IN (…) OR BaseItems.TopParentId IN (…))is an OR-union whereTopParentId IN (…)alone was a seek. It reaches/Items?includeItemTypes=MusicArtistand the by-name listings, not only search. Upstream emits the identical
OR, soagreeing with it is not in question — what is unknown is what it costs here.
Where to start
The 56k-item anchor library, and
cargo xtask benchrather than a hand timing.Two numbers:
/Search/Hintsfor a restricted account before and after0e5a0dd..HEAD, and one plain by-name listing. If the search number is bad, the
shape to try is upstream's — narrow the candidates first and drop
top_parent_idsonce the ids are named, which removes the exemption's reason to exist on the
second query.
If it turns out to cost nothing, that is the result: write the number down in
crates/jellyfin-db's tests beside the clause and close this.House rules
Tests run under
cargo nextest; upstream's source is the checkout inupstream/at the pin. The anchor library and the scripts that stand a pair up on it are
outside the repository;
README.mdsays where.Measured, and it was not nothing. A restricted account's
/Search/Hintsfor asingle letter took 112 seconds on the anchor. Both numbers this issue asked
for are below; the first is now fixed, the second is a no.
The second pass was a list, and the list was the whole library
filters.rs::behind_the_nameasked whether anything behind a name is in reachby listing the reachable set:
That set is every item in every library the account may see.
EXPLAIN QUERY PLANover the statement the pair actually emitted shows ten of them — fivekinds in the cut, five again in the narrowing the cut is joined to — each its
own
LIST SUBQUERYoverIX_BaseItems_TopParentId_Id, and each a transientindex
SQLitebuilds before a single by-name row is tested./Search/Hintsemits two such statements, the count and the page, so twenty per request.
The same clause is in
people.rs::people_narrowing, which is what/Personsis, so it was paying the same price by a different route.
What it cost
Anchor library, an account with two of the three libraries in reach, one
request at a time over a warm connection, the pair on one host and one program
data directory:
/Search/Hints?searchTerm=the&limit=20/Search/Hints?searchTerm=e&limit=20/Items?includeItemTypes=MusicArtist&Limit=50/Items?includeItemTypes=Person&Limit=50/Persons?Limit=50&NameStartsWith=AAn account that restricts nothing is never asked, and its figures do not move:
the same search 65 ms against 69 ms, the same artist page 19 ms against 17 ms.
The fix
The reachable set is asked about one row, never listed.
narrowing::holdstakes the same narrowing and correlates it on the column naming the item, so
what was a transient index over a library is a seek on the primary key:
And the libraries are the cut's alone.
search.rs::remaining_filtersnowdrops
top_parent_ids: narrowing to them a second time narrows the cut toitself, and the second copy brought the whole second pass with it. That is
upstream's shape — its hints query names the ids the search found and
AddUserToQueryleavesTopParentIdsoff entirely — arrived at from theother end.
Ten emissions become five, and each of the five is cheap.
The
ORcosts nothing(BaseItems.Type IN (…) OR BaseItems.TopParentId IN (…))does not lose theseek. Its plan is a
MULTI-INDEX OR: one index search per branch,IX_BaseItems_Type_CleanNameandIX_BaseItems_TopParentId_Type_IsVirtualItem_DateCreated.Dropping the exemption outright — which answers a different question, 67 artists
where the right answer is 194 — saves 2 ms of a 20 ms by-name page, and ours is
ahead of upstream on that page either way. Nothing to do here.
What is asserted
filters.rs— the second pass asks the reachable set about a row rather thanlisting it.
search.rs— a ranked search narrows to the libraries once.portable.jsonagainst the pair: 62 responses, 55 agreed, accountingbyte-identical before and after.
docs/benchmarks.mdcarries the table.Landed in
229f900. Filed while measuring: #173 — the published ffmpeg cannot run onthe host, so
cargo xtask benchcannot start either server.