Mid-level 3 min · March 05, 2026

SQL SELECT — SELECT * Fails After Schema Migration

SELECT * in production caused 500 errors after a schema migration added a column.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • SELECT retrieves rows and columns from one or more tables — the foundation of every read operation in SQL
  • SELECT * fetches all columns; listing columns explicitly reduces network payload and clarifies intent
  • Aliases (AS) rename columns in the result — aliases cannot be used in WHERE of the same query
  • SQL execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY — SELECT runs near last
  • Biggest mistake: SELECT * in application code — fetches hidden columns, breaks code when schema changes
Plain-English First

Imagine a massive filing cabinet with thousands of folders — each folder is a row of data about a customer, a product, or an order. The SQL SELECT statement is your hand reaching into that cabinet and pulling out exactly the folders you want. You can grab all of them, only specific ones, or just certain pages from each folder. That's it. SELECT is how you ask a database a question and get a useful answer back.

Every app you use — Spotify, Amazon, your bank — stores data in a database. When Spotify shows you your last-played songs, or Amazon lists your order history, something had to go fetch that data. That something is a SQL SELECT statement. It's the most-used command in all of SQL, and understanding it deeply is the foundation of working with any database system, whether you're a developer, data analyst, or product manager.

What a Database Table Actually Looks Like

Before you can SELECT anything, you need to understand what you're selecting from. A database stores data in tables — think of a table exactly like a spreadsheet. It has columns (the categories of data, like 'first_name' or 'price') and rows (the actual individual records, like one specific customer or one product).

For example, an e-commerce site might have a table called 'products'. Each row is one product. Each column holds one type of information about that product — its name, price, how many are in stock, and so on.

The SELECT statement lets you look at that table's data. You decide WHICH columns to show and, optionally, WHICH rows to include. Everything flows from that one simple idea.

view_products_table.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- This is what our 'products' table looks like before we query it.
-- Think of this as the raw spreadsheet sitting in the database.

-- TABLE: products
-- +------------+---------------------+-------+-------+
-- | product_id | product_name        | price | stock |
-- +------------+---------------------+-------+-------+
-- |          1 | Wireless Headphones | 79.99 |   120 |
-- |          2 | USB-C Charging Cable|  9.99 |   500 |
-- |          3 | Laptop Stand        | 34.99 |    85 |
-- |          4 | Mechanical Keyboard |119.99 |    40 |
-- |          5 | Webcam HD 1080p     | 54.99 |   200 |
-- +------------+---------------------+-------+-------+

-- We'll use this table in every example below.
-- You can create it yourself with:
CREATE TABLE products (
    product_id   INT PRIMARY KEY,
    product_name VARCHAR(100),
    price        DECIMAL(6, 2),
    stock        INT
);

INSERT INTO products VALUES (1, 'Wireless Headphones',  79.99, 120);
INSERT INTO products VALUES (2, 'USB-C Charging Cable',  9.99, 500);
INSERT INTO products VALUES (3, 'Laptop Stand',         34.99,  85);
INSERT INTO products VALUES (4, 'Mechanical Keyboard', 119.99,  40);
INSERT INTO products VALUES (5, 'Webcam HD 1080p',      54.99, 200);
Output
Query OK, 5 rows affected
Why This Table?
Every example in this article uses this 'products' table. Run the CREATE and INSERT statements once in any SQL tool (MySQL Workbench, pgAdmin, DB Browser for SQLite, or even an online playground like sqlfiddle.com) and you can follow along interactively.
Production Insight
SELECT * is the leading cause of schema-migration-related API failures in production.
Every column you fetch crosses the network and consumes memory — list only what you need.
Add table aliases (c.customer_id not just customer_id) in multi-table queries for unambiguous columns.
Key Takeaway
SELECT * is exploratory SQL only — never ship it in application code.
Explicit column lists are self-documenting and immune to schema additions.
Table aliases prevent ambiguous column reference errors when querying multiple tables.

Your First SELECT — Fetching All Rows and Columns

The simplest SELECT statement says: 'Give me everything from this table.' The syntax is two words followed by two more:

SELECT * FROM table_name;

The asterisk (*) is shorthand for 'all columns'. FROM tells SQL which table to look in. The semicolon ends the statement — think of it as a period at the end of a sentence.

This is the 'blast radius' query. It's great for exploring a table you've never seen before. You'll use it constantly when you first connect to a new database just to see what's there.

That said, SELECT * is a starting point, not a habit. In real applications you almost always want specific columns — we'll get there in the next section.

select_all_products.sqlSQL
1
2
3
4
5
6
-- SELECT: the command that retrieves data
-- *: a wildcard meaning 'every column in this table'
-- FROM: tells SQL which table to look in
-- products: our table name

SELECT * FROM products;
Output
+------------+---------------------+--------+-------+
| product_id | product_name | price | stock |
+------------+---------------------+--------+-------+
| 1 | Wireless Headphones | 79.99 | 120 |
| 2 | USB-C Charging Cable| 9.99 | 500 |
| 3 | Laptop Stand | 34.99 | 85 |
| 4 | Mechanical Keyboard | 119.99 | 40 |
| 5 | Webcam HD 1080p | 54.99 | 200 |
+------------+---------------------+--------+-------+
5 rows in set
Watch Out: Don't Use SELECT * in Production Code
SELECT * fetches every column, even ones you don't need. On a table with 50 columns and millions of rows, this slows your app down significantly and wastes network bandwidth. Always name your columns explicitly in production queries.
Production Insight
SELECT * on a table with BLOB or TEXT columns fetches all binary data even if you only need the ID.
On a 10M row table, SELECT * vs SELECT id, name is the difference between 8 seconds and 0.2 seconds.
Always check EXPLAIN to see how many bytes the query is expected to return.
Key Takeaway
Fewer columns = less data transferred = faster queries, especially with large TEXT/BLOB columns.
Listing columns is also your query's documentation — it shows readers exactly what data is needed.

Selecting Specific Columns — Only Get What You Need

Instead of the wildcard *, you can list exactly which columns you want, separated by commas. This is called a column list, and it's how 99% of real-world queries are written.

Why does this matter? Imagine your products table has 30 columns — including internal cost prices, supplier IDs, and audit timestamps. If you're building a page that just shows customers the product name and price, why fetch all 30 columns? You'd be doing extra work for nothing.

Naming your columns also makes your query self-documenting. Anyone reading SELECT product_name, price FROM products immediately knows what this query is for. SELECT * tells them nothing.

You can list columns in any order — SQL returns them in the order you specify, not the order they're stored in the table.

select_specific_columns.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
-- Fetch only the product name and price — ideal for a public-facing product listing page.
-- Notice: we list column names separated by commas.
-- The columns appear in the OUTPUT in the order we list them here.

SELECT product_name, price
FROM products;

-- -------------------------------------------------
-- You can also flip the column order — SQL doesn't care.
-- This returns price FIRST, then product_name.

SELECT price, product_name
FROM products;
Output
-- First query output:
+---------------------+--------+
| product_name | price |
+---------------------+--------+
| Wireless Headphones | 79.99 |
| USB-C Charging Cable| 9.99 |
| Laptop Stand | 34.99 |
| Mechanical Keyboard | 119.99 |
| Webcam HD 1080p | 54.99 |
+---------------------+--------+
5 rows in set
-- Second query output (columns flipped):
+--------+---------------------+
| price | product_name |
+--------+---------------------+
| 79.99 | Wireless Headphones |
| 9.99 | USB-C Charging Cable|
| 34.99 | Laptop Stand |
| 119.99 | Mechanical Keyboard |
| 54.99 | Webcam HD 1080p |
+--------+---------------------+
5 rows in set
Pro Tip: Column Order in SELECT Is Presentation, Not Storage
SQL never changes the order data is stored. When you reorder columns in SELECT, you're only changing how results are displayed. The underlying table stays exactly the same — useful to know when you're debugging 'why did the data change' questions.
Production Insight
Column aliases defined in SELECT cannot be referenced in WHERE — the order of SQL execution bites constantly.
Put the expression in a subquery or CTE if you need to filter on an alias.
Aliases in GROUP BY work in MySQL and PostgreSQL but not SQL Server — know your database.
Key Takeaway
Aliases rename columns in the output only — they don't create new columns for WHERE or HAVING to use.
Use a CTE or subquery to filter on calculated values: WITH enriched AS (...) SELECT * FROM enriched WHERE alias > x.

Column Aliases — Giving Your Results Friendlier Names

Sometimes a column name in the database is technical or unclear. 'product_name' is fine, but what if a column were called 'prd_nm_v2' or something equally cryptic? Aliases let you rename columns in your output without touching the actual database.

You create an alias using the AS keyword after the column name. The alias only exists for the duration of that query — the table in the database is completely unchanged.

Aliases are also essential when you use calculated columns. If you multiply price by 1.2 to add tax, the result column has no name by default — SQL might call it something ugly like '(price * 1.2)'. An alias gives it a clean, readable name like 'price_with_tax'.

This is hugely useful when your app reads column names from query results, because your code can rely on the alias staying consistent even if the underlying column name changes.

select_with_aliases.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- AS gives a column a temporary friendly name in the output.
-- 'product_name AS name' means: show this column but call it 'name'
-- 'price AS price_usd' — clearer for international apps

SELECT
    product_name  AS name,
    price         AS price_usd,
    stock         AS units_available
FROM products;

-- -------------------------------------------------
-- Calculated column with alias:
-- We add 20% tax to the price and label the result cleanly.
-- Without the alias, the column header would be '(price * 1.20)'

SELECT
    product_name         AS name,
    price                AS base_price,
    price * 1.20         AS price_with_tax   -- calculated column with alias
FROM products;
Output
-- First query:
+---------------------+-----------+-----------------+
| name | price_usd | units_available |
+---------------------+-----------+-----------------+
| Wireless Headphones | 79.99 | 120 |
| USB-C Charging Cable| 9.99 | 500 |
| Laptop Stand | 34.99 | 85 |
| Mechanical Keyboard | 119.99 | 40 |
| Webcam HD 1080p | 54.99 | 200 |
+---------------------+-----------+-----------------+
-- Second query (with tax calculation):
+---------------------+------------+----------------+
| name | base_price | price_with_tax |
+---------------------+------------+----------------+
| Wireless Headphones | 79.99 | 95.988 |
| USB-C Charging Cable| 9.99 | 11.988 |
| Laptop Stand | 34.99 | 41.988 |
| Mechanical Keyboard | 119.99 | 143.988 |
| Webcam HD 1080p | 54.99 | 65.988 |
+---------------------+------------+----------------+
5 rows in set
Interview Gold: AS Is Optional But Never Skip It
SQL actually lets you write 'price price_usd' without the AS keyword and it still works. But always use AS anyway — it makes your intent unmistakably clear to anyone reading the query, and many style guides require it.
Production Insight
Column aliases with spaces must use double quotes in PostgreSQL: SELECT price AS "Unit Price". MySQL uses backticks. Mixing conventions across team members causes constant syntax errors.
Pick one quoting convention for your team and enforce it in a linter.
Key Takeaway
Use snake_case aliases without spaces — avoids quoting inconsistencies across database dialects.
Always alias calculated columns — raw expressions like price * 1.2 are unreadable in results.

Filtering Rows with WHERE — The Real Power of SELECT

So far every query returned all 5 rows. In real life, tables have thousands or millions of rows, and you rarely want all of them. The WHERE clause lets you set conditions so SQL only returns rows that match.

Think of WHERE like a security guard at the exit of a warehouse. Every row tries to leave. The guard checks each one against your condition. Only rows that pass the check make it into your results.

You can filter by equality (=), comparisons (>, <, >=, <=), or inequality (<> or !=). You can combine multiple conditions using AND (both must be true) or OR (either one can be true).

The WHERE clause is evaluated before the columns are selected — SQL finds the matching rows first, then picks the columns you asked for. Understanding this order matters when you start writing more complex queries.

select_with_where_filter.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- Example 1: Find all products that cost more than $50
-- The WHERE clause checks each row's 'price' column against 50.
-- Only rows where price > 50 are returned.

SELECT product_name, price
FROM products
WHERE price > 50;

-- -------------------------------------------------
-- Example 2: Find products that are BOTH affordable AND well-stocked
-- AND means BOTH conditions must be true for a row to appear.

SELECT product_name, price, stock
FROM products
WHERE price < 50
  AND stock > 100;

-- -------------------------------------------------
-- Example 3: Find a specific product by its exact name
-- String values go inside single quotes — this is required in SQL.

SELECT product_id, product_name, price
FROM products
WHERE product_name = 'Laptop Stand';
Output
-- Example 1 output (products over $50):
+---------------------+--------+
| product_name | price |
+---------------------+--------+
| Wireless Headphones | 79.99 |
| Mechanical Keyboard | 119.99 |
| Webcam HD 1080p | 54.99 |
+---------------------+--------+
3 rows in set
-- Example 2 output (under $50 AND over 100 in stock):
+---------------------+-------+-------+
| product_name | price | stock |
+---------------------+-------+-------+
| USB-C Charging Cable| 9.99 | 500 |
+---------------------+-------+-------+
1 row in set
-- Example 3 output (exact match on name):
+------------+--------------+-------+
| product_id | product_name | price |
+------------+--------------+-------+
| 3 | Laptop Stand | 34.99 |
+------------+--------------+-------+
1 row in set
Watch Out: Single Quotes vs Double Quotes for Strings
In SQL, string values always go in single quotes: WHERE product_name = 'Laptop Stand'. Double quotes are used in some databases to quote column or table names, not values. Mixing them up causes syntax errors that confuse beginners for hours.
Production Insight
WHERE runs before SELECT in SQL's logical execution order — indexes on the WHERE column matter enormously.
A WHERE clause that matches 1% of rows reduces the result set before SELECT projects any data.
Always check EXPLAIN after adding a WHERE clause to confirm the database is using an index.
Key Takeaway
WHERE is where performance lives — the right index here turns seconds into milliseconds.
SQL executes FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY — WHERE is not the last step.
Always add WHERE before LIMIT — LIMIT without WHERE scans the full table then discards rows.
● Production incidentPOST-MORTEMseverity: high

SELECT * Broke the API After a Schema Migration

Symptom
POST /api/orders started returning 500 errors 10 minutes after a schema migration. The query itself hadn't been modified. Order creation worked fine; fetching orders was broken.
Assumption
The team assumed SELECT * would continue working because they were 'just reading data.' Adding columns shouldn't break reads.
Root cause
The application's ORM mapped SELECT * results to a fixed-field struct. The migration added a sensitive_notes column that the struct didn't have a field for. The ORM threw an unmapped column error and returned 500 to the client.
Fix
Changed SELECT * to explicit column list. Added a CI check that runs EXPLAIN on all critical queries against a schema snapshot to catch unmapped columns before deployment.
Key lesson
  • SELECT * is a maintenance liability — adding any column to the table can break downstream consumers
  • Always use explicit column lists in application code, not SELECT *
  • Run query schema tests in CI that catch column additions before they reach production
Production debug guideDiagnosing wrong results, missing rows, and alias confusion3 entries
Symptom · 01
Query returns more rows than expected
Fix
Check your WHERE clause — a missing or wrong condition returns all rows. Verify with SELECT COUNT(*) before the full query. Also check if a JOIN is multiplying rows (see sql-joins-explained).
Symptom · 02
Column alias in WHERE clause throws error
Fix
You cannot reference a SELECT alias in WHERE — SQL processes WHERE before SELECT. Either repeat the expression in WHERE or use a subquery: SELECT FROM (SELECT price 1.2 AS adjusted FROM products) t WHERE t.adjusted > 100.
Symptom · 03
SELECT * returns different columns after schema change
Fix
This is expected — SELECT * always returns whatever columns exist. Switch to an explicit column list immediately. Use INFORMATION_SCHEMA.COLUMNS to see current columns: SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'your_table'.
TechniqueWhat It DoesWhen To Use ItExample Syntax
SELECT *Fetches every column from the tableExploring an unfamiliar table interactivelySELECT * FROM products;
SELECT col1, col2Fetches only the named columnsAny production query or app-facing querySELECT product_name, price FROM products;
Column Alias (AS)Renames a column in the output onlyCleaning up cryptic names or calculated columnsSELECT price * 1.20 AS price_with_tax
WHERE with =Filters rows to an exact matchLooking up a specific record by name or IDWHERE product_name = 'Webcam HD 1080p'
WHERE with > / <Filters rows to a numeric rangeFinding products in a price range, dates, scoresWHERE price > 50
WHERE with ANDBoth conditions must be trueMultiple simultaneous filters on one queryWHERE price < 50 AND stock > 100
WHERE with OREither condition can be trueCatching multiple valid values in one queryWHERE price < 10 OR price > 100

Key takeaways

1
SELECT * is for exploration
never ship it in production code because it fetches unnecessary data and slows performance.
2
The WHERE clause acts as a row filter evaluated before column selection
SQL finds matching rows first, then picks the columns you listed.
3
Column aliases (AS) only rename output
the actual table and column names in the database are never changed by an alias.
4
String values in WHERE conditions must always be wrapped in single quotes
double quotes behave differently across database systems and cause unpredictable errors.

Common mistakes to avoid

3 patterns
×

Forgetting single quotes around string values in WHERE

Symptom
ERROR: column 'Laptop' does not exist — the database interprets unquoted strings as column names
Fix
String values in SQL require single quotes: WHERE product_name = 'Laptop Stand'. Numbers do not need quotes: WHERE price > 50. Double quotes are for identifiers (column and table names) in most databases.
×

Using SELECT * in application code

Symptom
Schema migrations break downstream applications — ORM throws unmapped column errors, network payload grows silently as new columns are added
Fix
Always use explicit column lists: SELECT id, name, price FROM products. SELECT * is acceptable for ad-hoc exploration but never in production code.
×

Confusing = with == for comparisons in WHERE

Symptom
SyntaxError or unexpected results — == is not valid SQL comparison syntax despite being standard in Python, JavaScript, and most programming languages
Fix
SQL comparison is = (single equals): WHERE status = 'active'. For inequality: WHERE status != 'active' or WHERE status <> 'active'. Double equals is a programming language convention, not SQL.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between SELECT * and SELECT with named columns?
Q02SENIOR
If I run SELECT price * 1.2 AS adjusted_price FROM products and then try...
Q03SENIOR
In what order does SQL actually execute the clauses of a SELECT statemen...
Q01 of 03JUNIOR

What is the difference between SELECT * and SELECT with named columns?

ANSWER
SELECT fetches all columns that exist in the table at query time — if the schema changes and new columns are added, SELECT automatically includes them. This makes it convenient for exploration but dangerous in application code: new columns might break ORM mappings, increase network payload, or expose sensitive data to layers that shouldn't see it. Named column lists are explicit, self-documenting, and immune to schema additions. They also perform better when the table has large columns (TEXT, BLOB) that aren't needed — those columns are fetched and transferred even if you never use them.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What does SELECT * mean in SQL?
02
Can I use SELECT without a WHERE clause?
03
Does SQL care about uppercase vs lowercase in my query keywords?
🔥

That's SQL Basics. Mark it forged?

3 min read · try the examples if you haven't

Previous
SQL INSERT UPDATE DELETE
4 / 16 · SQL Basics
Next
SQL WHERE Clause and Filtering