Job orm-n-plus-one-fixed-in-one-endpoint · revised 11 October 2026
Fix one endpoint that runs hundreds of queries, and prove the query count
One endpoint stops issuing a query per row: its database query count drops to an agreed ceiling and a test fails if it climbs again, with identical response data.
You might be seeing
- The same SELECT repeats in the log with only the id changing, once per item on the page
- Response time grows roughly in line with the number of items returned
- The endpoint is quick in development with ten rows and slow in production with thousands
No passwords, keys, card details or admin invites needed to start.
What usually happened
The endpoint loads a list and then touches a related record for each item, so the ORM sends one query for the list and one more for every row. Lazy loading hides the pattern in code that looks harmless, and the query count rises with the data. The failure is the number of round trips in one request, not the cost of any single statement.
Who it’s for: A founder or engineering lead whose list page or API endpoint gets slower in step with the number of rows it returns.
Usually starts when: A profiler, log or debug toolbar shows one request making a separate query for every row, and response time grows with the size of the list.
The result: The named endpoint makes no more queries than the ceiling we agree, for small and large lists alike, returns the same response body as before, and has an automated test that fails if the count rises again.
Check whether this job fits
These questions decide whether the problem is a repeated lookup per row. Nothing here needs code or data.
Checks you can run yourself
Count the queries for one request
In a development or staging copy with fake data, load the endpoint with a debug panel or the framework's query log switched on and note the number of statements for one request. Do this only on a copy.
Look for: A count close to the number of rows plus one, with the same statement repeating. Send the count and one repeated statement with invented values.
What you get
- A pull request with the changed query code and the new test
- A before and after list of the queries issued, with the count for a small and a large data set
- A response comparison showing the data returned is identical for the agreed requests
- Notes on any relation deliberately left lazy, and why
Included
- One endpoint or view in one Django, Rails or SQLAlchemy application
- Count and list the queries the endpoint issues for a small and a large synthetic data set, and identify each repeated lookup
- Change the loading strategy for the relations involved, such as joining or batch-loading, and trim columns where that helps
- Add one automated test that asserts the query count for the endpoint and a response comparison with the original
Not included
- More than one endpoint; each further endpoint is a separate scope
- Rewriting the data model, adding caching or changing the front end
- Slow single statements; those are a plan and index question rather than a query count one
- Frameworks other than Django, Rails and SQLAlchemy-based applications
- Running anything against production data
How we know it’s done
Agreed with you before work starts. Each check produces evidence you keep.
For the agreed requests, the endpoint makes no more queries than the agreed ceiling for both the small and the large synthetic data set.
Evidence: The recorded query lists and counts for both data sets, before and after.
The response body for each agreed request is identical to the original, including item order.
Evidence: A response comparison for each request, attached to the pull request.
A new automated test asserts the query count and fails when run against the original code.
Evidence: Test output on the original code (failing) and on the changed code (passing).
Your maintainer reviews the evidence and merges the pull request under the repository's existing rules.
Evidence: Your written sign-off and the merged pull-request record.
Sign-off. You run the new test and compare the query lists, then sign off in writing and merge. Payment follows sign-off.
If it fails. If the agreed query ceiling is not met on the synthetic data, you do not pay for this fixed scope and you keep the query lists. If the cause is outside your code, we explain it and stop.
When it fits, and when we stop
It fits when
- The endpoint is in a Django, Rails or SQLAlchemy application whose version you can name
- The application runs locally or in staging with synthetic data, and its tests can be run
- You can name the endpoint and say which list or relation is slow
- An engineer on your side reviews and merges the pull request
We stop and tell you if
- The count is high because of many different statements rather than a repeated lookup, which needs a broader review
- The endpoint cannot run without production data or live third-party calls
- The repeated queries come from a library or plugin we cannot change, not from your code
- The slowness is in the response rendering or an external call, not in database queries
What could go wrong
The change is a pull request in your repository. Before merge, closing it leaves the code untouched; after merge, reverting the commit restores the previous loading behaviour. No data or schema is changed.
Scroll the table sideways to read it all.
| Risk | How we handle it |
|---|---|
| Eager loading fetches more data than the endpoint uses and trades round trips for memory. | We report the rows and columns loaded before and after, and trim the load where the response does not need it. |
| Filtering a prefetched relation afterwards sends the extra queries again. | The count test uses the same filters as the real requests, so a hidden re-query fails the check. |
| A change of loading strategy alters the order or duplicates of returned items. | The response comparison covers order and duplicates for each agreed request. |
A second reviewer checks that the response is identical, that the query-count test would fail on the old code, and that eager loading has not pulled in far more data than the endpoint needs. Your engineer reviews and merges.
How we deliver
We arrange the work and independent review, then show you the result against the agreed checks. You keep authority over your systems.
- Agree the endpoint, the framework version, the requests to measure and the query ceiling in writing
- Run the application with seed data and record the queries one request issues for a small and a large data set
- Group the repeated lookups by relation and choose the loading strategy for each, noting the trade-off in memory and join width
- Make the change, then compare the response body with the original for every agreed request
- Write the query-count test and confirm it fails against the original code
- Independent review of the diff and counts, then hand over the pull request
This is a one-off job, not emergency cover or a subscription. We confirm eligibility, the total price, a start window and a delivery date before you accept. Work starts only after agreed inputs, secure access and any permissions are in place. Hosting and database charges are excluded unless the written quote includes them. No charge or booking is created by an enquiry.
Need to keep it working?
Discuss a project that covers a defined number of endpoints, or a monthly database review, if the pattern appears elsewhere.
Ongoing work is separately scoped and quoted: no monitoring, response-time guarantee or automatic subscription is included in this job.
Explore an ongoing engineering lane, or mention the responsibility you need in your enquiry.
What you can check
This is a new service. We have not delivered this job for a client yet.
Other ways to get this done
- Django documents select_related and prefetch_related, and warns that filtering a prefetched relation can cause extra queries again. docs.djangoproject.com
- The Rails guide to Active Record querying describes includes, preload and eager_load, and strict_loading to catch lazy loads that remain. guides.rubyonrails.org
- SQLAlchemy documents selectinload and joinedload for collections and many-to-one relations, and raiseload to stop accidental lazy loading. docs.sqlalchemy.org
Questions
Is adding a cache a fix for this?
It can hide the pattern while leaving the code issuing a query per row. This job removes the repeated lookups so the endpoint is correct without a cache.
What if three different endpoints have the problem?
Each endpoint is its own scope and price. If several are involved, ask about the data-layer stabilisation project, which can include a defined number.
Will the test break whenever we add a field?
The test asserts a ceiling for the agreed requests, set with a little headroom that we discuss, so ordinary changes do not trip it but a lookup per row does.
Send an enquiry
Send us
- The endpoint path or view name and the framework and version
- How many queries a request makes, if your debug tooling shows it, and how long the request takes
- The model names involved, with no code and no data
- Do not send credentials, repository access or customer data in the first enquiry
Later, once you agree
- The repository through an authorised company-controlled route, and instructions to run the application and its tests locally
- Synthetic or anonymised seed data large enough to show the pattern
- The agreed query ceiling and the requests used to measure it
- The name of the person who reviews and merges the pull request
You own the database, the code and every credential. We work on a copy you prepare, such as a staging database restored from a backup with personal data removed or replaced, and we hand work back as a pull request or a script. We never ask for production passwords, and we do not connect to your production database. You apply any change to production yourself, after a restore point that you have tested.
Email fallback: open your mail app
If website submission is unavailable, review and send the fallback email yourself. An email fallback is not a website receipt. Or write to hello@syntheticindustry.ai with “orm-n-plus-one-fixed-in-one-endpoint” as the subject.