Database Design, Indexes, and Query Thinking for Frontend Leads
A practical lesson on how to think about data access patterns, query cost, and the database trade-offs that shape product performance.
Learn how to reason about database shape, index choice, and query cost so you can discuss performance with backend teams, avoid expensive UI patterns, and design screens that still work when the data grows.
Core concepts
If a page always shows the newest 20 items for one workspace, that is a very different access pattern from a reporting screen that needs counts grouped by week. The database should be shaped around those repeated requests, not around abstract purity.
For a frontend lead, the useful question is not 'What does the schema look like?' but 'What does the UI ask for most often, and what is the cheapest way to answer that question consistently?'
- List views usually need a filtered slice, sorted in a stable order, with a small limit.
- Detail views usually need one primary entity plus a few related pieces of data.
- Dashboards often need aggregates, counts, or time-bucketed summaries rather than raw rows.
The best index is the one that matches a real query you run often. If the query filters by `workspace_id` and sorts by `created_at`, an index that follows that shape can remove a lot of unnecessary work. If the index does not match the query shape, the database may still have to do expensive work anyway.
This is why database conversations should be tied to product behavior. A beautiful schema that no query can use is not helpful, and a missing index on a hot list view can make the whole product feel slow.
- Indexes usually help on columns that are frequently filtered, joined, or sorted.
- Too many indexes slow down inserts and updates because every write must keep them in sync.
- A single good composite index often beats several partial guesses.
A frontend lead should get comfortable asking what the database actually has to do. Does it scan a tiny range or half a million rows? Can it stop after 20 results, or does it need to inspect most of the table first? Those are the questions that turn vague 'it feels slow' complaints into actionable debugging.
You do not need to be a database specialist to be useful here. You just need to recognize when a screen is asking for too much data, when a filter is missing an index, or when a pagination pattern will become expensive as the dataset grows.
- Filtering and sorting on the same indexed columns is often the best-case path.
- `SELECT *` is expensive when the UI only needs a few fields.
- Offset pagination gets slower as the offset grows because the database still has to walk past skipped rows.
Infinite scroll, cursor pagination, and page numbers all have different trade-offs. Cursor pagination usually scales better for long lists because it gives the database a stable anchor, while offset pagination can become painful when a user jumps deep into the result set.
Search pages are especially dangerous because they tempt teams to add many filters and cross-table joins at once. A lead should push the team to separate the common case from the rare advanced case, so the everyday experience stays fast.
- Prefer cursor pagination for very large or fast-changing lists.
- Keep the common filter path fast, and defer expensive advanced filters when possible.
- Return only the fields the screen needs instead of shipping a giant generic payload.
Visual model
Practical examples
GET /workspaces/ws_123/activity?limit=20
-- logical query shape --
WHERE workspace_id = 'ws_123'
ORDER BY created_at DESC
LIMIT 20200 OK
{
"items": [
{ "id": "act_1", "type": "comment", "createdAt": "2026-06-06T10:00:00Z" },
{ "id": "act_2", "type": "mention", "createdAt": "2026-06-06T09:58:00Z" }
],
"nextCursor": "act_2",
"hasMore": true
}GET /admin/users?team=platform&status=active&sort=lastLoginDesc200 OK
{
"items": [
{ "id": "u_1", "name": "Ploy", "team": "platform", "lastLoginAt": "2026-06-06T08:00:00Z" }
],
"hasMore": false
}GET /orders?page=500&limit=20200 OK
{
"items": [],
"nextCursor": null,
"hasMore": false
}