Skip to content

Avoid per-row queries for evolution condition variables and natures - #1690

Open
santichausis wants to merge 1 commit into
PokeAPI:masterfrom
santichausis:perf/evolution-detail-lookups
Open

santichausis wants to merge 1 commit into
PokeAPI:masterfrom
santichausis:perf/evolution-detail-lookups

Conversation

@santichausis

Copy link
Copy Markdown
Contributor

Problem

PokemonEvolutionSerializer.get_condition_expression and get_allowed_natures run one query per PokemonEvolution row. Milcery → Alcremie has 63 evolution rows, each with a condition_expression, so /evolution-chain/452/ takes 65 queries (63 of them to pokemon_v2_evolutionvariable).

Fix

Both tables are tiny (4 evolution variables, 25 natures). Each is loaded once, lazily, through a cached_property on the serializer instance and filtered in Python, keeping the previous ordering. ListSerializer reuses a single child instance for every row, so this is one query per list of evolution details instead of one per row, and no state is shared across requests.

Verification

  • Built the DB from the CSVs and serialized all 540 evolution chains before/after: byte-identical JSON for every chain, chain 452 drops from 65 to 3 queries, and no chain needs more. OpenAPI schema unchanged.
  • Added a test that builds chains with 1 and 5 evolution rows (each with a condition_expression and nature_bitmask), asserts the query count is the same, and checks the resolved variables and natures. This is the first API test coverage for these two fields. It uses cachalot_disabled(), because inside the test transaction cachalot serves identical repeated queries from memory, which hides per-row queries. On master this test fails with 13 vs 5 queries.
  • manage.py test pokemon_v2 (64 tests), ruff, and ty (no new diagnostics) pass.
  • In production cachalot/Redis caches these queries, so the gain is mainly for cold-cache requests and fewer cache round-trips.
  • Touches the same file as Fix N+1 query in evolution chain serializer #1688; whichever lands second will need a trivial rebase.

Comment thread pokemon_v2/serializers.py
# a ListSerializer reuses one child instance for every row, so these load once per
# list of evolution details instead of once per row
@cached_property
def _all_natures(self) -> list[Nature]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

would it be possible to use prefetch_related instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

prefetch_related only works across model relations, and neither of these is one: nature_bitmask is an IntegerField and condition_expression is a CharField whose tokens are matched against EvolutionVariable.symbol. There's no FK/M2M from PokemonEvolution to Nature or EvolutionVariable to prefetch through. Getting there would mean adding M2M tables plus a migration and build/CSV changes, which felt out of scope for a perf fix. The cached_property does the equivalent of a prefetch here: one query per table per list of evolution details, filtered in Python.

If you'd prefer, I can instead preload both tables once in EvolutionChainDetailSerializer.build_chain (next to the batched evolution query from #1688) and pass them down via the serializer context, which makes it one query per request. Happy to go either way.

Also rebased onto master now that #1688 is in. With both changes, serializing all 540 evolution chains from the real data goes from 1106 to 895 queries, with byte-identical output.

get_condition_expression and get_allowed_natures ran a query per
PokemonEvolution row. Milcery -> Alcremie has 63 rows, each with a
condition_expression, so its evolution chain took 65 queries.

Both tables are tiny (4 evolution variables, 25 natures), so load each
once, lazily, on the serializer instance. A ListSerializer reuses its
child instance for every row, so this is one query per list instead of
one per row, without sharing state across requests.
@santichausis
santichausis force-pushed the perf/evolution-detail-lookups branch from 003f0ff to 7f96c55 Compare September 30, 2026 11:53
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.

2 participants