Skip to content

PageFinder2: conditions on one single-value field share its join, so ranges use one index range - #378

Open
adrianbj wants to merge 1 commit into
processwire:devfrom
adrianbj:feature/pagefinder-range-join
Open

adrianbj wants to merge 1 commit into
processwire:devfrom
adrianbj:feature/pagefinder-range-join

Conversation

@adrianbj

@adrianbj adrianbj commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Moved to PageFinder2, as Ryan asked; PageFinder is unchanged. This is on top of current dev.

Each condition on a field joins its table again. date>=2024-01-01, date<2025-01-01 joins field_date twice. num>=40000, num<41000 checks the < in a LEFT JOIN, because < also matches pages with no value (counted as 0). MySQL and MariaDB can't combine conditions on different aliases into one index range. So when neither bound is selective on its own, they read every page and look up both joins for each one.

When a field's table has one row per page (a primary key of just pages_id), every join of it reaches the same row. So in PageFinder2:

  • mergeSingleRowJoins() merges the inner joins of such a table into one, with all their conditions, and renames the merged aliases wherever the query uses them.
  • mergeBlankJoins() handles the LEFT JOIN that whereEmptyValuePossible() adds for "no value". When the same table is also inner joined, that LEFT JOIN is dropped and its conditions use the inner join: the page must have the row anyway, and it's the same row.

Both rewrites give the same results. Fields with several rows per page, such as FieldtypeMulti, keep their joins as before, as do OR-groups and !=. $blankJoins is reset at the start of each getQuery(), and PageFinder2's subqueries use other instances, so reused instances don't mix them up.

Numbers

PageFinder2 on MariaDB 13.0.2 with 20k pages. Each row compares against a PageFinder2 subclass with both merges turned off, in the same process, alternating, as the median of 10 batches:

before after
num>=40000, num<41000 (171 pages) 31.4 ms 2.2 ms
same, sort=-num 30.0 ms 2.2 ms
num>40000, num<=40500 28.9 ms 1.0 ms
same range + title%=a 30.4 ms 2.2 ms
same range, sort=title, limit=10 31.3 ms 2.9 ms
num>=40000, num>=40500 (two joins, as for a date range) 39.8 ms 28.7 ms
body~=mine, body~=kalo 21.0 ms 14.2 ms
num<500, num<500, num!=100, OR-groups unchanged unchanged

Every selector returned the same pages both ways. The only difference was the order of pages with equal values under sort=-num, which the query doesn't specify.

Tests

PageFinder2.test.php (new, with its own fixtures) gets testRangeOnSingleValueField(). It uses a temporary FieldtypeInteger field, which finish() removes. It checks:

  • ranges with either bound first, <=/>, and two lower bounds: the right pages, with one join and no LEFT JOIN;
  • a lone < still matches a page with no value, and still checks for one;
  • < with !=, and OR-groups, give the same pages as before.

WireTests: SQLite and PostgreSQL 111/113 each (MarkupRSS's fresh-install check and WireHttp), MariaDB 111/115 (the same four environmental failures as before).

#377 also adds a PageFinder2.test.php, with the same fixtures. Whichever of the two merges second, I'll rebase it and combine the files.

-Adrian (via Claude Code)

🤖 Generated with Claude Code

…a range reads one index range

Each condition on a field joined its table again, i.e. date>=a, date<b joined
field_date twice, and num>=a, num<b checked the < in a LEFT JOIN that allows
for no value (counted as 0). MySQL/MariaDB can't combine conditions on
different aliases into one index range, so they read every page: about 30 ms
for a range of 171 pages among 20k.

When a table has one row per page (a primary key of just pages_id), its inner
joins all reach the same row, so PageFinder2 merges them into one, and replaces
a LEFT JOIN for no value with the inner join when there is one. The results
are the same; the range now takes about 2 ms.
@adrianbj
adrianbj force-pushed the feature/pagefinder-range-join branch from 670ea51 to 96704fa Compare September 29, 2026 16:10
@adrianbj adrianbj changed the title PageFinder: conditions on one single-value field share its join, so ranges use one index range PageFinder2: conditions on one single-value field share its join, so ranges use one index range Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant