Skip to content

Reduce number of database queries made by Scanner Query Rules#25181

Open
diox wants to merge 1 commit into
mozilla:masterfrom
diox:scanners-query-rules-optimize-queries
Open

Reduce number of database queries made by Scanner Query Rules#25181
diox wants to merge 1 commit into
mozilla:masterfrom
diox:scanners-query-rules-optimize-queries

Conversation

@diox

@diox diox commented Jul 22, 2026

Copy link
Copy Markdown
Member

Description

Optimize number of queries made by scanner query rules

Context

Scanner query rules run against a bunch of versions in the database. As we have millions of versions in the database, a rule is divided into chunks that each process up to 250 versions.

Before this change, assuming a chunk of 250 versions, each chunk would execute:

  • 251 queries for yara + 4 per match
  • 1,001 queries for narc + 4 per match (!)

After this change, the same chunk of 250 versions would execute:

  • 3 queries for yara + 1 for all matches
  • 5 queries for narc + 1 for all matches

The main query for each chunk now has a bunch of joins so it's more costly, but given the number of queries it saves the tradeoff should be worth it.

Fixes mozilla/addons#16346

Testing

We should have robust unit tests covering this. Scanner query rules can be created and triggered through the admin tools: http://olympia.test/en-US/admin/models/scanners/scannerqueryrule/

Before, using a chunk of 250 versions, each chunk would do:
- 251 queries for yara + 4 per match
- 1,001 queries for narc + 4 per match

After, same chunk of 250 versions:
- 3 queries for yara + 1 for all matches
- 5 queries for narc + 1 for all matches

The main query for each chunk now has a bunch of joins so it's more
costly, but given the number of queries it saves the tradeoff should
be worth it.
@diox

diox commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

FWIW locally using a bunch (~ 2000) of add-ons with real xpis stolen from prod, using rules that matches everything (yara rule xxx { condition: true } and narc .*), I'm getting between x2 and x4 faster completion.

Obviously though this is not a proper test, I don't have the volume, same workers setup, CPU/memory etc prod has.

@diox
diox marked this pull request as ready for review July 22, 2026 15:40
@diox
diox requested review from a team and eviljeff and removed request for a team July 22, 2026 15:53
Comment on lines +334 to +338
values_from_authors = [
author.display_name
for author in addon.authors.all()
if author.display_name is not None
]

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.

Isn't this less efficient than what it replaced?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ever so slightly if we look at it in isolation, but iterating over authors.all() allows to benefit from the prefetched queryset set up by the parent method. Ultimately add-on authors should all have a display name anyway (and the vast majority of add-ons only have one author)

Comment on lines +743 to +745
attach_trans_dict(
Addon, [version.addon for version in qs], field_names=['name', 'summary']
)

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.

optimization: collect all the add-ons from all the versions in a set so we're not attaching translations once for each add-on if there are multiple versions from the same add-on. Or is that already happening anyway by magic inside of attach_trans_dict (I skim read that function, it wasn't clear either way)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh, that's a good idea. attach_trans_dict doesn't appear to be checking if we already fetched translations for a given add-on so yes, it could be significant for add-ons with multiple versions.

Instead of collecting in a set() we could just check if translations property is set on the add-on, I'll do that.

Addon, [version.addon for version in qs], field_names=['name', 'summary']
)
results = []
for version in qs:

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.

I'm assuming django is clever enough to not re-fetch the version instances if it's been fetched on line 743

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, if it's the same queryset instance and we aren't using .iterator(), it's all cached on the queryset.

)
# Force waffle switch to be cached to not affect queries count.
waffle.switch_is_active('use-yara-x')
with self.assertNumQueries(3):

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.

To be certain we could re-run this query with an extra add-on to confirm it's still 3 queries. Similarly the other new tests in this file. (But with the different combinations of tests it's going to be very unlikely to get a false positive anyway)

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.

[Task]: Improve scalability of scanner query rules

2 participants