Subqueryload without per-row Row construction — ~26% on subqueryload by ollz272 · Pull Request #13404 · sqlalchemy/sqlalchemy · GitHub
Skip to content

Subqueryload without per-row Row construction — ~26% on subqueryload#13404

Open
ollz272 wants to merge 4 commits into
sqlalchemy:mainfrom
ollz272:perf/subquery-raw-tuples
Open

Subqueryload without per-row Row construction — ~26% on subqueryload#13404
ollz272 wants to merge 4 commits into
sqlalchemy:mainfrom
ollz272:perf/subquery-raw-tuples

Conversation

@ollz272

@ollz272 ollz272 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Description

_SubqCollections._load iterated the Query, which routes through
Query.__iter__ -> result.unique() and builds a Row object (plus runs the
uniquing filter) for every row. Execute the statement directly and group the
plain processed tuples from Result._raw_all_tuples() instead.

The legacy Query iteration always applied result.unique(), so the dedup is
replicated explicitly: the related object is keyed on identity (matching the ORM
use_id_for_hash uniquing filter) and the remaining key columns by value. The
Row-based unique() path is retained for the requires_uniquing case (e.g. a
joinedload nested inside the subqueryload).

Benchmarks

min ms, vs main. Python 3.14, in-memory SQLite, 50 repeats.

case before after Δ
subquery_o2m 16.728 12.366 −26.1%

Risk: low–medium — the explicit dedup must match unique() semantics; the
requires_uniquing fallback covers nested-eager cases. test_subquery_relations
and test_recursive_loaders pass.

Fixes: #13406

Checklist

This pull request is:

  • A documentation / typographical / small typing error fix
    • Good to go, no issue or tests are needed
  • A short code fix
    • please include the issue number, and create an issue if none exists, which
      must include a complete example of the issue. one line code fixes without an
      issue and demonstration will not be accepted.
    • Please include: Fixes: #<issue number> in the commit message
    • please include tests. one line code fixes without tests will not be accepted.
  • [ x ] A new feature implementation
    • please include the issue number, and create an issue if none exists, which must
      include a complete example of how the feature would look.
    • Please include: Fixes: #13406 in the commit message
    • please include tests.

Have a nice day!

I raised this in a discussion. Still having trouble with gerrit.. sorry!

ollz272 added 4 commits June 16, 2026 09:18
_SubqCollections._load iterated the Query, which routes through
Query.__iter__ -> result.unique() and builds a Row object plus runs the
uniquing filter for every row.  Execute the statement directly and group
the plain processed tuples from Result._raw_all_tuples() instead.

The legacy Query iteration always applied result.unique(), so replicate
that dedup explicitly: key the related object on identity (matching the
ORM use_id_for_hash uniquing filter) and the remaining columns by value.
The Row-based unique() path is retained for the requires_uniquing case
(e.g. a joinedload nested inside the subqueryload).
Add SubqueryloadRawTupleTest exercising _SubqCollections._load after it
switched from iterating the legacy Query (Row + result.unique()) to
consuming Result._raw_all_tuples() with an explicit dedup.

Covers:
- basic o2m collection grouping and per-collection order
- scalar/m2o dedup with distinct_target_key=False, asserting no
  'Multiple rows returned with uselist=False' SAWarning is escalated
- subqueryload combined with .distinct()
- order_by across a join producing duplicate subquery rows
- requires_uniquing fallback: joinedload of a collection nested inside a
  subqueryload (retained Row-based result.unique() path)
- populate_existing()
- empty collection / None scalar when no related rows
- subqueryload -> subqueryload and many-to-many chains
- bound-param / .params() filtered queries and param reuse
@CaselIT

CaselIT commented Jun 30, 2026

Copy link
Copy Markdown
Member

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.

use new _raw_all_tuples with subqueryloads

2 participants