Wise Hustlers — Digital Product & App Development Studio Logo
Get Consultation
By Wise Hustler Admin9/17/20264 min read

Prisma select vs include: the query that fetches ten times what your page renders

Prisma select vs include: the query that fetches ten times what your page renders

Here is a query that looks completely ordinary:

const articles = await prisma.article.findMany({
  where: { isPublished: true },
  include: { author: { select: { name: true } } },
  orderBy: { createdAt: "desc" },
  take: 60,
});

It feeds a list page showing a title, a summary, a date and an author. Seven fields.

It fetches every column of every row. On a table where the article body lives in a text column of 10–17KB, that is roughly a megabyte of content pulled out of Postgres, deserialised into JavaScript objects, and then discarded during mapping — to render seven fields.

`include` does not mean "only this"

This is the misreading, and it is an easy one. include and select sound like alternatives. They are not symmetrical.

  • `select` is a whitelist. You get the fields you name and nothing else.
  • `include` is additive. You get all scalar fields of the model, plus the relations you name.

So include: { author: true } does not narrow anything. It widens the result. The nested select on author limits the author's columns, which is what makes the query look careful — while the parent model is still returning everything it has.

If you want both a restricted column set and a relation, the relation goes inside select:

const articles = await prisma.article.findMany({
  where: { isPublished: true },
  select: {
    slug: true,
    titleJson: true,
    summaryJson: true,
    createdAt: true,
    author: { select: { name: true } },
  },
  orderBy: { createdAt: "desc" },
});

You cannot use select and include at the same level — Prisma will reject it. The relation nests inside select, as above.

Why this hides so well

Over-fetching does not throw. It does not warn. Locally, with a seeded table of thirty short rows, it is genuinely free — you will never feel the difference.

It scales with the thing you are least likely to look at: the size of the columns you are not using. A table of short rows over-fetches harmlessly for years. Add one large text column — a markdown body, a JSON blob, a base64 field — and the same query quietly starts moving an order of magnitude more data, while the page it feeds looks unchanged.

The result is a "slow page" with no slow query in sight. Each query is fast by the database's own measure; the cost is in transfer and deserialisation.

What it was actually costing

The fix on the list page above was to name the seven fields it rendered. The effect was not a marginal gain: fetching every published row with select moved less data than fetching sixty rows without it. The pagination cap that was there to keep the page cheap had been the wrong lever the whole time — the expensive part was the columns, not the rows.

That is the useful shape of this bug. It makes people add limits to pages that did not need limiting.

Seeing it for yourself

Prisma can emit a query event with a duration, which you can log past a threshold:

const prisma = new PrismaClient({
  log: [{ emit: "event", level: "query" }],
});

prisma.$on("query", (e) => {
  if (e.duration >= 100) {
    console.warn(JSON.stringify({ query: e.query, durationMs: e.duration }));
  }
});

Useful, but be aware of the blind spot: this measures database time. An over-fetching query is often fast in the database and expensive everywhere after it. To catch that, look at the emitted SQL rather than the duration — if the SELECT names columns your page never renders, you have found it regardless of how quick it was.

A rule that holds up

Default to select on anything that returns a list. Reach for include when you genuinely want the whole model, which is more often on a detail page than a listing.

The wider the model gets over time, the more that default pays. A query written as select keeps costing what it costs; a query written as include gets more expensive every time somebody adds a column, and nothing in your code changes to tell you.

Related articles