Skip to content

Query: Always use INNER join to establish the outer query link - #173

Open
nilmerg wants to merge 1 commit into
mainfrom
always-use-inner-joins-for-sub-queries
Open

nilmerg wants to merge 1 commit into
mainfrom
always-use-inner-joins-for-sub-queries

Conversation

@nilmerg

@nilmerg nilmerg commented Aug 26, 2026 •

Copy link
Copy Markdown
Member

A subquery always needs to establish a link, be it part of a WHERE or solely used to eager load (using Query::derive) a relation. Using an INNER join allows the database to apply optimizations it wouldn't be able to otherwise.

Since this affects Query::derive(), any eager loaded relation will now return no result. But this should be usually not a problem, as a caller that iterates or calls first() without handling the never entered loop or null-return is already bugged, IMHO. I've checked this with Icinga DB Web and found exactly two cases: Icinga/icingadb-web#1408

Still attempting to prove this is actually of advantage, though…

@nilmerg nilmerg added this to the v1.0.0 milestone Aug 26, 2026
@nilmerg nilmerg self-assigned this Aug 26, 2026
@cla-bot cla-bot Bot added the cla/signed label Aug 26, 2026
@nilmerg
nilmerg force-pushed the fix/proper-reversal-of-relations-in-sub-queries-170 branch from 177dcc4 to bcd7fd8 Compare September 2, 2026 08:52
@nilmerg
nilmerg force-pushed the fix/proper-reversal-of-relations-in-sub-queries-170 branch 3 times, most recently from d3a89ab to c10c699 Compare September 21, 2026 14:23
@nilmerg
nilmerg force-pushed the always-use-inner-joins-for-sub-queries branch from a9e6101 to f8337a9 Compare September 21, 2026 14:29
@nilmerg
nilmerg force-pushed the fix/proper-reversal-of-relations-in-sub-queries-170 branch from c10c699 to cc5ee0c Compare September 21, 2026 14:51
@nilmerg
nilmerg force-pushed the always-use-inner-joins-for-sub-queries branch from f8337a9 to 8985dfa Compare September 21, 2026 14:51
@nilmerg
nilmerg force-pushed the fix/proper-reversal-of-relations-in-sub-queries-170 branch 2 times, most recently from 18b8c4a to 3d3229f Compare September 23, 2026 07:51
Base automatically changed from fix/proper-reversal-of-relations-in-sub-queries-170 to main September 23, 2026 07:53
A subquery always needs to establish a link, be it part of a
WHERE or solely used to eager load (using `Query::derive`)
a relation. Using an INNER join allows the database to apply
optimizations it wouldn't be able to otherwise.
@nilmerg
nilmerg force-pushed the always-use-inner-joins-for-sub-queries branch from 8985dfa to 5a1cb82 Compare September 23, 2026 08:57
@nilmerg
nilmerg marked this pull request as ready for review September 23, 2026 08:58
@nilmerg
nilmerg requested a review from lippserd September 23, 2026 08:58
@nilmerg

nilmerg commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

@lippserd Please share you opinion and experience regarding the advantage this might have.

@lippserd lippserd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been quite a while since I last benchmarked Icinga DB queries and filter combinations. If I remember correctly, that was against MySQL 5.6 and MySQL 8. Back then, LEFT joins turned out to be more flexible in terms of query plans, since MySQL wasn't forced into an INNER join straight away. But that was for regular joins, not subqueries. So to actually help here, I ran some tests.

Setup: MySQL 8.4 and 9.7, MariaDB 11.8 and 12.3, PostgreSQL 17 and 18, with our devlab data scaled up 300 times (about 31k hosts and 335k services). I generated the queries from Icinga DB Web's models, once with this change and once without it.

The good news first: the results are identical everywhere, so the change is safe.

In most cases it doesn't make a difference, because the databases already turn the LEFT join into an INNER join on their own: as soon as some condition rules out the extra NULL rows of the LEFT join, they know it can't matter, e.g., pk=fk. That covered almost everything I tried on services, hosts, comments and downtimes: host and service groups, custom vars, AND and OR combinations, negations and custom-var columns. Same plans, same timings.

Where it does help: filters combined with OR on the comments and downtimes lists. There, nothing rules out the NULL rows, so MariaDB and PostgreSQL keep the LEFT join. Since those tables have no index on host_id/service_id, they end up being scanned over and over. With your change they don't:

Page query MariaDB 12.3 PostgreSQL 18 MySQL 8.4
Comments: host.hostgroup.name=…|service.servicegroup.name=… 3.9 s → 14 ms 10 → 3 ms no change
Comments: host var OR service var with wildcard 99 s → 42 ms 144 → 27 ms no change
Downtimes: same filter >120 s → 58 ms 144 → 30 ms no change

The first filter is an ordinary one, and the same applies when it's used as a restriction. So this is a real improvement for those cases. I also checked whether adding the missing indexes would solve this without your change: they fix MariaDB's worst case, but don't help PostgreSQL, so your change is the better fix.

A nice side effect concerns the IS NOT NULL we add to negated filters. With the LEFT join it's essential: without it, service.vars.location!~*o* on downtimes returns 0 rows instead of 4893, on both MariaDB and PostgreSQL. With INNER it's no longer needed at all. Adding IS NOT NULL to the OR sub queries above (while keeping the LEFT join) gives exactly the same speed-up as your change. That confirms it's really just about telling the database that the NULL rows can't match.

Just one small request: could you relax the commit message a bit? "Allows the database to apply optimizations it wouldn't be able to otherwise" sounds like a general win, while in practice databases mostly do this already and the gain shows up only in cases like the above. Maybe something like:

A sub query always needs to establish a link to the outer query, so rows added by a LEFT join on that link can never match. Using an INNER join makes this explicit instead of relying on the database to figure it out, which it doesn't always do (e.g. MariaDB and PostgreSQL with sub queries combined by OR).

Separately, I noticed a few columns in Icinga DB that relations look up by but that have no index (e.g. comment/downtime host_id/service_id, notification_history host_id/service_id). I'll look into that in a separate PR for Icinga DB.

@nilmerg nilmerg modified the milestones: v1.0.0, v0.10.0 Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants