Measure first: counting statements in async SQLAlchemy
A slow endpoint is a claim about time; a cause is a claim about work. The skill this section trains is turning "it is slow" into a number you can reproduce: how many SQL statements does one request run, and does that number grow with the data?
Count statements, not milliseconds
Wall-clock time depends on the machine, the cache and the network. The number of statements a request runs does not. Turn on statement logging (echo=True on the engine or the sqlalchemy.engine logger at INFO), call the endpoint for a small and a large customer, and compare. A count that grows with the number of rows is an N+1: one query for the list and one more for every row.
Relationships here never load implicitly
In this service relationships are declared with lazy="raise": touching an unloaded relationship raises instead of silently issuing SQL, because a lazy load inside async code cannot work. That protects you from hidden queries, but it also means the data is loaded wherever the query is written. Eager loading (selectinload for collections, joinedload for single parents) loads the related rows for all returned objects at once, in a constant number of statements.
Fix the query, then prove it: a test that records the statements of one request for a small and a large dataset and asserts that the count is equal.