I've been an ORM hater for a while now. I've seen a lot of projects start off with an ORM for its abstraction and readability and whatever other magical healing powers that have been sold in the name of ORMs, and eventually descend into madness of JOINs because people just kept adding relationships to entities without thinking about what the actual cost of it is going to be.

Maybe a topic for another blog/rant.

Since then, I've been writing raw queries only, with lightweight utils for DB access, transaction management, connection pooling, etc. Keeps things simple. You see the queries, so you know exactly what is going to be executed at the DB. No hiding behind abstractions which will internally make 55 table JOINs.

This also means I'm pretty mindful of the queries I write and DB design. I've heard the whole "don't do SELECT * because it'll fetch everything" argument, and I don't generally buy it. Generally is the operating word here. There are obviously exceptions to this. I mean, I get why people say that.

If you have a table with 500 columns and you fetch 10k rows, yes, SELECT * vs SELECT id, email makes a difference. But most of the time, SELECT * is operationally safe.

Is what I always thought.

But a recent bug changed my mind, and I'm going to stick to selecting specific columns from now on.

The bug

A user said a page was slow. It was working fine earlier. We tested the same page from our dev setup, but it seemed fine to us. At least nothing noticeable.

We checked the same page with a large amount of data and it was still reasonable. The user is in the same region as us, so load time should've been similar. But the user's PostHog recordings showed that his page load was indeed sometimes 3-4s slower than ours.

Weird.

We did an audit of all the APIs on that particular page. Nothing significant had been changed in the last month or so. Same DB queries and all. Still, we started checking DB queries using EXPLAIN and also timing them, specifically on this user's IDs, since DB is generally the usual culprit.

Basically, we were running the same queries this user's actions would've run. But that also ended up giving us the same results, so I started checking some other things.

That's when I realized that we had added a column, specifically a LONGTEXT column, to one of the tables the API reads from. This column was for storing a lot of text data. Each row would have at least 10 KB to 100 KB of data in this column.

If you're fetching 50 rows, that's an additional 500 KB to 5 MB of data that gets fetched. And we have SELECT * in most of the places in our codebase.

Because this column had been added, one of the APIs was now fetching that column too. And because this endpoint was returning the fetched row shape pretty directly, up to 5 MB of extra data was also getting sent to this user.

The query didn't get slower because the WHERE clause changed. The response got heavier because SELECT * pulled in a new large column, and our API accidentally carried that column all the way back to the client.

And then there was the VPN

Now, like I said, we also tested with a large dataset. Our load times were fine, but this user's page was exceptionally slower.

This turned out to be because this user was using a VPN, which made the issue much worse for them. When we tested it out, it seemed okay, or at least reasonably fast, to us.

The user was in India, the VPN exit was in the US, and our servers are in Stockholm. So the larger API response had to travel over a worse network path than the one we were testing on. The DB query itself was fine. There was some extra data moving from DB to backend, but that wasn't the visible problem. The painful part was sending a much larger response back to a client behind that VPN path.

Yeah, tough luck.

And this is where my previous assumption about SELECT * started looking a little shaky. Yes, in day-to-day use, maybe for most of the tables, it will not cause any issues for you. Performance-wise, you probably don't need to worry about it.

But there seem to be a lot of edge cases where SELECT * will cause you a lot of headaches. These edge cases will not show up every day. They'll probably show up once in a while as you grow, when you've completely forgotten that some API is doing a SELECT * on some table that got a massive column added to it.

As a defensive practice, maybe it's better to just SELECT the exact columns you need from the database. This is just one of the cases we saw.

And if you have APIs that return DB rows pretty directly, maybe it's worth doing a quick review of both sides: what columns the query fetches, and what fields the API actually returns. Especially if you've vibecoded the whole thing.

My lesson

I think my lesson right now is essentially no SELECT *s at stable app/API boundaries. Not because SELECT * is inherently bad or because it's going to make your API slow every time you use it.

It's more that you're giving yourself another way for a completely unrelated schema change to unexpectedly affect your APIs, especially if fetched DB rows map too directly to response payloads.

In our case, the API was fine. The query plan was fine. The amount of data we had in the DB was fine. Even our testing was fine.

It was just SELECT * + a new large column + an API returning fetched columns too directly + one user using a VPN. And unless you hit that exact combination of things together, you probably won't find the problem.

We didn't.

And that's what makes this particularly painful to debug.

I'm going to keep thinking about this and maybe edit this article if I come up with some other cases where SELECT * can cause a problem. But for production code paths that feed APIs, I'm done with it.

No more SELECT *.

AI is increasing the value of experience in software engineering today. Ironically, it may also be laying the groundwork for that advantage to disappear tomorrow.

Your database has been solving concurrency problems longer than most of us have been writing code. Maybe it's time to trust it.