Last week, I shared a meme in the MySQL Group on LinkedIn about using SELECT * in MySQL queries. Today, I’m backing up the punchline with something every joke desperately needs: a technical explanation.
We’ve all written queries like this:
SELECT *
FROM customers;
It’s quick. It’s convenient. It lets you postpone remembering whether the column is called customer_name, full_name, or name_final_v2. But when that shortcut makes it into application code, those saved keystrokes may come with a surprisingly high cost.
Everything Comes at a Cost
Suppose a page needs a customer’s name and email address:
SELECT *
FROM customers
WHERE customer_id = 42;
If the table also contains a biography, preferences stored as JSON, and a profile image stored as a BLOB or Base64-encoded text, you’ve asked MySQL to return those too.
Your application may throw the extra values away, but they still travel from the database to the client. The client still has to receive and process them. That’s like ordering a sandwich and having the deli deliver your lunch, along with everything else in the display case.
Instead, ask for what the application actually uses:
SELECT first_name, last_name, email
FROM customers
WHERE customer_id = 42;
Finding the Data is Only Part of the Trip
A query can find the right rows quickly and still leave your application waiting. Those results have to travel from MySQL to the database client, usually your application server, and then be decoded into something your code can use.
With large result sets or limited bandwidth, moving the data can become a bigger bottleneck than finding it. SELECT * makes that trip heavier when it includes columns your application never uses, especially large text, JSON, or binary values.
For a hypothetical example, returning 10,000 rows averaging 200 bytes each means about 2 MB of data. If unnecessary columns push that average to 5 KB, you’re moving about 50 MB for the same 10,000 rows, before protocol overhead and any compression.
Same number of rows. A lot more baggage.
Selecting only the columns you need reduces the payload. It won’t fix every slow application, but it avoids spending bandwidth and client processing time on data your application never needed in the first place. When measuring performance, time the complete fetch and consumption of the results, too. “MySQL found it quickly” and “my application received it quickly” are different accomplishments. Performance monitoring and profiling tools can help you measure network costs and identify where that time is going.
Also, LIMIT controls how many rows you get, not how much data each row contains. Ordering fewer sandwiches doesn’t help much if each one comes with the entire display case.
Same Query. Surprise Columns.
Consider this query:
SELECT *
FROM customers;
Today, it may return six columns.
Next month, someone adds a seventh visible column. When the query executes against the updated table, it now returns seven. MySQL is doing exactly what you asked. Unfortunately, your application may have interpreted “everything” as “these six specific things forever.”
That can cause trouble for code that unpacks rows into a fixed number of variables, maps values by position, or exports results into a format expecting a particular structure.
Explicit column lists make that dependency visible:
SELECT customer_id, first_name, last_name, email
FROM customers;
Adding an unrelated column to the table won’t expand this result set. Renaming or removing a selected column can still break the query, of course. Explicit columns aren’t magic, but they keep a routine schema update from sneaking extra guests into your result set.
Application caches and ORM mappings can complicate when the application sees a new column. A cached result may retain the old shape, and a model may expose only the fields it knows about. That doesn’t make the wildcard a stable contract. It adds another layer of behavior to understand.
One MySQL detail: * excludes invisible columns. It means all visible columns, because even “give me everything” has terms and conditions.
Your Index Had It Covered. Then You Asked for Everything.
A covering index contains all the columns a query needs, allowing MySQL to retrieve the requested values from the index without looking up the full table rows.
For example, suppose we have this index:
CREATE INDEX idx_orders_customer_status
ON orders (customer_id, status);
This query is a candidate for using that index as a covering index:
SELECT customer_id, status
FROM orders
WHERE customer_id = 42;
Change it to:
SELECT *
FROM orders
WHERE customer_id = 42;
If the table has additional columns outside that index, MySQL needs more than the index contains.
The index may still help find the matching rows. SELECT * does not automatically disable indexes or force a full table scan. It can, however, remove the opportunity to satisfy this query from that covering index. Think of it as finding the answer in a book’s index, then insisting someone bring you the entire bookshelf.
Use EXPLAIN to inspect the actual plan. Performance claims deserve evidence, even when the joke is excellent.
Joins Make “Everything” More Complicated
At first glance, this query looks innocent enough:
SELECT *
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id;
But that little * brings back every visible column from both tables, including both copies of customer_id. If both tables also have columns named created_at or status, those names appear twice too. Suddenly, your result set has more identity issues than a spy novel. Duplicate result names can make application mapping confusing, with behavior depending on the client library.
Be specific, and use aliases where they help:
SELECT
c.customer_id,
c.email,
o.order_id,
o.status AS order_status
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id;
Now a reader can see what the query is meant to provide without conducting a small archaeological expedition through the schema.
You Didn’t Mean to Publish That Column. Oops.
Imagine a query feeding an API response:
SELECT *
FROM users
WHERE user_id = 42;
Later, someone adds a visible internal_notes column.
If the application automatically serializes the whole row, that new field could end up in a response where it doesn’t belong. Similar accidents can happen in logs and exports. Selecting specific columns reduces that accidental exposure. It complements permissions and deliberate response mapping. It doesn’t replace them.
Your public profile endpoint probably doesn’t need to know that support described someone as “enthusiastic about refunds.”
Is SELECT * Ever OK?
Absolutely.
Exploring an unfamiliar table? Investigating a problem? Running a quick query in your SQL client? SELECT * is useful. It can also be appropriate when retrieving every visible column is an intentional requirement. The concern is making it the default in application code without considering what the application actually needs.
And COUNT(*)? Different job. It counts rows. It isn’t a request to ship every column back to your application. Please leave it out of this disciplinary meeting.
Your Query Doesn’t Need the Whole Buffet
Before committing a query, ask:
- Which columns does the caller actually use?
- How much unnecessary data would travel to the client?
- Would adding a column unexpectedly change the result?
- Could a narrower selection benefit from a covering index?
- Are unnecessary fields heading into responses, logs, or exports?
Then write those columns down.
Yes, it’s a little more typing. Your code editor should be able to help. It has been training for this moment.
Save SELECT * for the times you literally mean “everything.”
For the rest, give MySQL a itemized shopping list.
