The slow page wasn't the database
A dashboard page was slow to load, so everyone reached for the database. The real cost was the code drawing one dropdown 400 times. A story about N+1 queries and the fix that took them from 400 to 4.
Users kept telling us the Phone Numbers page was slow. Open it, wait, watch it hang for a second or two, then the list finally appears. The obvious suspect was the database: the table must be huge, the query must be missing an index, we must need to tune Postgres. Every one of those guesses was wrong.
The table was tiny and the query was instant#
The first thing I did was look at where the time actually went. The table behind the page had about 4,700 rows and took up 2.4 MB. The query that lists the numbers was a 2.3 ms sequential scan. Adding an index would have done nothing, because there was nothing to speed up. The database was not the problem.
So where did the second-and-a-half go?
The page was fetching a whole voice agent for every single number#
The slow part was the code that turned each phone number into a chunk of JSON for the frontend. For every number in the list, it called the function that builds the full detail payload for the voice agent attached to that number. That payload is the same one the agent’s edit screen uses: it pulls in the agent’s context, its settings for what happens after a call ends, its languages, its transfer rules, its actions, its integrations, its stored files, and its per-minute cost, which then goes and looks up a currency.
That is 8 to 10 database queries per number, just for the list page.
I measured it on a real account with 28 numbers: 400 SQL queries and 1.32 seconds to draw the page. 110 of those queries were just looking up the same currency over and over. This is the classic N+1: one query to get the list, then N more (a whole cascade, in this case) for each row. It never shows up when you test with two numbers on your laptop. It gets worse for every customer who has more numbers, and worse again under real traffic.
The fix was to ask what the page actually needed#
I went and looked at what the frontend actually reads off that agent. Across every component on the page, the card, the dialogs, the view that manages your number pool, it only ever used three things: the agent’s id, its name, and its owner’s name. That is it. We were fetching the entire agent, ten queries deep, to show a name in a dropdown.
So instead of calling the big detail function, the list now returns a small dictionary with just those three fields. Because they are plain fields on a record, the ORM fetches them for every row in the list in one batched go instead of one row at a time. The cascade disappears.
Verified on the same production data: 400 queries and 1.34 seconds became 4 queries and 8 milliseconds. Same page, same data, roughly 150 times faster, and the diff was a few lines.
The lesson#
When a page is slow, it is easy to blame the data: the table must be too big, an index must be missing, the database needs tuning. Usually that isn’t it. Before touching the database, count how many queries the page actually runs. Most of the time the data is fine, and the code is just asking for far more than the screen will ever show. Fetch the few fields you need instead of the whole record, and let the database load them for every row at once instead of one row at a time.