Nobody has started this yet — be first.
Business impact
GET /api/contacts is the single most-hit endpoint in this API -- it's the main contact list view. Right now, listing N contacts issues roughly N + 1 SQL queries against the database (one to fetch the contacts, then one more per contact to fetch that contact's groups). On a dev machine with 5 contacts this is invisible. On a production account with a few thousand contacts, this endpoint's database load -- and its latency -- grows linearly with the size of the contact list, with no cap. This is the textbook "worked fine in the demo, fell over in production" bug: it degrades gradually as data grows, so it tends to surface first as a vague "the app feels slow today" complaint and, later, as a full database-connection-pool exhaustion incident that pages whoever is on-call -- not as a clean error anyone can immediately trace back to one line of code.
Problem
Contact.groups (in app/models.py) is configured with lazy="select" -- SQLAlchemy's default lazy-loading strategy, meaning each contact's .groups collection is only fetched from the database the moment it's first accessed, with its own separate query. Serializing a list of contacts (which reads .groups on every single one, to nest each contact's groups in the JSON response) therefore issues one additional query per contact on top of the base list query.
Current behavior
A single GET /api/contacts call listing 8 contacts, each in their own group, issues 9 SELECT statements instead of a small, constant number -- verified by counting actual SQL statements, since the JSON response body is byte-for-byte identical either way.
Expected behavior
Listing contacts should issue a small, constant number of queries regardless of how many contacts are returned (practically: one query for the contacts, plus one batched query that loads every returned contact's groups in a single round trip) -- not one additional query per contact.
Steps to reproduce
cd flask/contact_book source .venv/bin/activate SECRET_KEY=dev-secret pytest practice_tickets/tests/test_ticket_06_n_plus_one_groups.py -v
Why this matters
This is the classic ORM "N+1 query" problem, and it's specifically about which loading strategy a relationship uses, not about the query being "wrong" in the SQL-correctness sense -- every individual query it runs is perfectly valid, there's just far more of them than there needs to be. SQLAlchemy relationships can be configured with different lazy= loading strategies ("select" = lazy, one query per access; "selectin" = one extra batched query using an IN (...) clause covering every object in the parent result set, regardless of how many there are; "joined" = a single JOINed query, etc.) -- picking the wrong one is invisible in code review unless you specifically check for it, since nothing about the Python code that reads contact.groups looks any different either way.
Suggested approach
Look at the lazy= argument on the Contact.groups relationship in app/models.py, and compare it against the lazy= argument on the Group.contacts relationship right below it in the same file (they were originally configured to match). Consider what "batch every related object across an entire result set into one extra query" is called as a SQLAlchemy loading strategy.
Acceptance criteria
Verification
SECRET_KEY=dev-secret pytest practice_tickets/tests/test_ticket_06_n_plus_one_groups.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b fix/contacts-list-n-plus-one-groupsFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/contacts-list-n-plus-one-groupsSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inBy default DevelopmentConfig uses a local SQLite file, no Postgres server required -- point DATABASE_URL at a real Postgres instance instead if you want one, or use Docker Compose instead (cp .env.example .env then docker compose up --build, which starts a healthchecked Postgres 16 container plus the app served by gunicorn behind entrypoint.sh, which waits for Postgres, runs flask db upgrade, then starts gunicorn -- migrations are always applied on boot). The API is then available at http://localhost:5000 either way.
The existing suite runs under TestingConfig (an in-memory SQLite database created fresh per test, so it never touches your dev database):
source .venv/bin/activate
export SECRET_KEY=dev-secret
pytest -v
Work the tickets in practice_tickets/tickets/ (TICKET-01 through TICKET-07); each names one dedicated test in practice_tickets/tests/ -- a separate pytest package from the project's normal tests/, which plain pytest runs by default:
SECRET_KEY=dev-secret pytest practice_tickets/tests/test_ticket_01_name_length_boundary.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 7, clean pass/fail summary
All 7 dedicated tests fail out of the box -- that is the starting point, not a setup mistake. Fixing TICKET-02 (search operator flip) and TICKET-04 (CSV row-number off-by-one) correctly also turns 4 currently-failing tests in the project's own tests/ suite back to green.
Level 3
Optimize performance
Same behaviour, better characteristics.