What exactly does SELECT * expand to, and what happens when two joined tables share a column name?
answer
- The asterisk is expanded, not executed
- Ask when the expansion happens
- Nothing is merged or deduplicated
- Two tables, two columns of the same name
- Order follows the CREATE TABLE declaration
basics
~20 sSELECT * expands to every column of every table in the FROM clause, in FROM order and then table-definition order. Joined tables that share a name yield two result columns with the same name, which breaks name-based client access.
solid answer
~40 s`*` is shorthand the engine expands at parse time into the full column list of everything in the `FROM` clause: tables in the order they appear, and within each table the order its columns were defined. Nothing merges. Joining `orders(order_id, customer_id, …)` with `customers(customer_id, name)` under `SELECT *` gives you **two** columns literally named `customer_id`; a result set may legally carry duplicate column names, but a driver asked for `rs.getString("customer_id")` will silently hand you one of them. You can narrow the star with a qualified form — `SELECT o.*, c.name` returns only the orders columns plus one. In standard SQL, an unqualified `*` must be the entire select list; a qualified `t.*` may be mixed with other items, though most engines are lenient about the former.
code
sql · 12 lines-- Two columns both named customer_id come back; the client picks one by name
SELECT *
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id;
-- Explicit and unambiguous
SELECT o.order_id,
o.total,
c.customer_id AS customer_key,
c.name AS customer_name
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id;go deeper
Know that * means every column of every table in FROM, that a join therefore returns both tables' columns, and that listing columns explicitly is the safer habit in code you commit.
Explain when the expansion happens, what fixes the column order, and why a join under * can hand your driver two identically named columns whose values come from different tables.
Demonstrate the failure mode in production: a schema addition or a new join silently reshapes results, name-based mapping picks the wrong duplicate, and nothing throws. Argue for explicit lists at every boundary the query crosses.
Own the standard: where SELECT * is acceptable (ad-hoc, throwaway), where it is banned (application queries, saved reports, anything with external consumers), and how that rule is enforced in review or lint.
## What the asterisk means `SELECT *` is not a runtime feature; it is a textual shorthand that the engine expands while analysing the statement. It expands to a reference to **every column of every table expression in the FROM clause**, at the moment the statement is compiled. Two rules fix the resulting order: 1. tables (or derived tables, or CTEs) in the order they appear in `FROM`; 2. within each table, the columns in the order the catalogue records them — the order they were declared in `CREATE TABLE`, with later `ALTER TABLE ... ADD COLUMN` columns normally appended at the end. The order is not alphabetical, not primary-key-first, and not "index order". It is definition order, which is exactly why it can change under you. ## The qualified form `t.*` expands to the columns of just that one table expression, using its alias if it has one. That gives you a middle ground: ```sql SELECT o.*, c.name AS customer_name FROM orders o JOIN customers c ON c.customer_id = o.customer_id; ``` Here you get all of `orders` plus a single, deliberately named column from `customers`. Per the standard's grammar, an unqualified `*` must stand alone as the whole select list, while `t.*` is one item among others. In practice PostgreSQL, MySQL and SQLite also accept `SELECT *, something_else`, so a query that works locally may not be strictly portable. ## Duplicate column names across a join The expansion does not deduplicate, rename, or coalesce anything. If both joined tables define `customer_id`, the result has two columns named `customer_id`, and if both define `created_at` you get two of those as well. SQL allows a result set to have duplicate column names — the columns are distinguished by position, not by name. That is where application code breaks. JDBC's `getString("created_at")`, a Python `DictCursor`, a pandas frame built from the result — all address columns by name, and each has its own rule for which duplicate wins (usually the first, sometimes the last, sometimes with a silent rename). The symptom is not an error; it is a value quietly coming from the wrong table. The fix is to stop asking for the star and to list and alias the columns you actually want: ```sql SELECT o.order_id, o.created_at AS order_created_at, c.created_at AS customer_created_at, c.name FROM orders o JOIN customers c ON c.customer_id = o.customer_id; ``` ## Why the star is fragile even without a join Because expansion happens against the catalogue as it stands when the statement is compiled, adding a column to a table changes the shape of every `SELECT *` against it: an extra column appears, usually at the end. Code that reads results positionally (`row[3]`) now reads the wrong thing, and code that maps a result set onto a fixed structure may fail outright. Explicit column lists make the contract between the query and its caller visible in the query text. ## A related asterisk that is not this one The `*` inside `COUNT(*)` is a different piece of grammar entirely — it is not a select list and it does not expand to columns; it simply means "count rows". Do not reason about it using the rules above. ## When the star is fine Interactive exploration, a quick look at a small table, and `SELECT * FROM some_cte` where the CTE already defines a deliberate, narrow column list are all reasonable. The star is a problem when it crosses a boundary — into application code, into a saved report, into anything whose callers you cannot re-read. ## Scope note There are also cost-side arguments about `SELECT *` — extra column transfer, losing an index-only access path — and hazards specific to view definitions and `INSERT ... SELECT`. Those belong to the access-path and schema discussions. The point here is narrower and purely about meaning: what the statement expands to, in what order, and what names come back.
- When is the star expanded — at parse time or when each row is produced?At parse/analysis time, against the catalogue as it stands then. That is why adding a column to the table changes the shape of the very next execution of a `SELECT *`, and why the expansion is fixed for the duration of one compiled statement rather than being re-evaluated per row.
- Is SELECT *, order_date FROM orders valid standard SQL?No. The standard's grammar lets an unqualified `*` be the entire select list only; anything mixed with other items must be the qualified form, `orders.*` or an alias's `o.*`. Several engines accept the mixed unqualified form anyway, so the query may run locally and fail elsewhere.
- Two joined tables both have a created_at column. Under SELECT *, how do you tell them apart?Only by position — the result carries two columns both named `created_at`, and the client library picks one when you address it by name. Stop using the star for that query and alias explicitly, for example `o.created_at AS order_created_at, c.created_at AS customer_created_at`.
saying these in an interview costs you the question
- Thinks the join merges same-named columns into one
- Believes SELECT * returns columns in alphabetical order
- Assumes duplicate result column names raise an error
- Claims SELECT *, extra_col is standard SQL
- Treats COUNT(*) as expanding to the column list