How does INSERT ... SELECT match the query's result columns to the target table's columns?
answer
- names and aliases play no part in the matching
- count the expressions against the column list
- a same-typed pair in the wrong order still succeeds
- an empty source query is not an error
basics
~20 sStrictly by position: the first select-list expression fills the first column of the INSERT's column list, and so on. Names and aliases are ignored, so two type-compatible columns in the wrong order are inserted swapped without any error.
solid answer
~50 s`INSERT INTO archive (id, total) SELECT order_id, amount FROM orders WHERE created_at < DATE '2024-01-01'` replaces the VALUES clause with a query, and the match is positional against the INSERT's column list — the select list's aliases and source column names play no part. Counts must agree and each pair must be type-compatible; if they are, a swapped pair is inserted swapped, silently. Any query works as the source: joins, aggregates, UNION, a CTE, even a VALUES constructor. Columns you leave out of the target list still take their defaults, so `INSERT ... SELECT` and defaults compose normally. If the query returns no rows the statement succeeds and inserts nothing — that is not an error, and it is why an unnoticed over-restrictive WHERE shows up as a quietly empty load rather than a failure.
go deeper
Be able to write the form from memory — target table, column list, then a SELECT with no VALUES keyword — and to say that the values line up by position, not by name.
Explain the matching rules precisely: equal degree, type-compatible pairs, aliases ignored. Name the silent-swap case where two same-typed columns are inserted backwards without any error.
Bring the operational reading: an empty source is a successful run, so copy and archive jobs must assert on affected-row counts, and the two column lists deserve line-by-line review because the engine cannot catch a swap.
Own the pattern choice — when moving rows inside the database with INSERT ... SELECT beats round-tripping through an application, and what conventions keep such statements readable and safe as the schemas on both sides evolve.
## The form ```sql INSERT INTO order_archive (order_id, customer_id, total) SELECT o.id, o.customer_id, SUM(i.qty * i.unit_price) FROM orders o JOIN order_items i ON i.order_id = o.id WHERE o.closed_at < DATE '2024-01-01' GROUP BY o.id, o.customer_id; ``` The query takes the place of the `VALUES` clause. There is no `VALUES` keyword — the two forms are alternatives, and you cannot mix them in one statement. ## Matching is positional, and only positional The *n*-th expression of the select list is inserted into the *n*-th column of the INSERT's column list. The select list's column names and aliases are irrelevant: naming an expression `AS total` does not steer it toward a column called `total`. Two rules must hold: - **Degree.** The select list must have exactly as many expressions as the column list has columns. - **Type compatibility.** Each pair must be assignable; the engine applies the same assignment rules it would to a literal. Which means the dangerous case is a pair of same-typed columns in the wrong order: ```sql -- both INT: this succeeds, and every row is stored backwards INSERT INTO order_archive (order_id, customer_id) SELECT customer_id, id FROM orders; ``` Nothing is violated, so nothing complains. The defence is to keep the two lists visually aligned, one column per line, and to read them as a pair during review. ## Any query is a legal source The source is a full query expression, so it may include joins, `GROUP BY`, `DISTINCT`, `ORDER BY`, set operations, window functions, common table expressions, and even a bare `VALUES` constructor. That generality is the point: `INSERT ... SELECT` moves rows inside the database instead of pulling them to the client and pushing them back, and it can transform them on the way — computing a total, mapping a status, stamping a literal: ```sql INSERT INTO order_archive (order_id, total, archived_by) SELECT id, total, 'nightly-job' FROM orders WHERE closed_at < CURRENT_DATE - 30; ``` A constant in the select list is an ordinary expression and needs no special syntax. ## Defaults still apply Columns absent from the INSERT's column list behave exactly as they do in a VALUES insert: declared default, else NULL if nullable, else an error. So an archive table with `archived_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP` gets its stamp without the query producing one. ## Zero rows is a success If the source query returns no rows, the statement inserts nothing and succeeds with a row count of zero. This is correct and often exactly what you want, but it means a typo in the `WHERE` clause, a join that eliminates everything, or a source table that is empty in this environment all present as "the job ran fine". Jobs built on `INSERT ... SELECT` should check the reported row count rather than only the absence of an error. ## ORDER BY in the source You may write `ORDER BY` in the source query, and engines generally accept it, but a table has no row order, so it confers nothing on later reads of the target — a subsequent `SELECT` without its own `ORDER BY` may return the rows in any order. Treat any effect it seems to have as an artifact of a particular engine, not as a guarantee of the language. ## Degenerate but legal shapes `INSERT INTO t (a, b) SELECT * FROM ...` is legal but re-introduces exactly the positional fragility that a column list was supposed to remove, since `*` expands to whatever the source table currently has. Naming the source expressions costs nothing and keeps the statement stable. The source may also be a `VALUES` constructor used as a query, which is how you write a small literal load that is then filtered, joined, or ordered — the same table value constructor a multi-row `VALUES` clause uses. ## What interviewers look for Three things: that you say **positional, not by name**, that you name the silent-swap failure mode, and that you know an empty source is a success rather than an error. A candidate who adds that omitted columns still take defaults, and that any query — join, aggregate, CTE — can be the source, is comfortably above the bar.
- What happens if the source query returns no rows?The statement succeeds and inserts zero rows; an empty source is never an error. That is why a nightly load built on `INSERT ... SELECT` should assert on the reported affected-row count. A mistyped predicate, a join that filters everything out, or an empty source table all look identical to a run that legitimately had nothing to copy.
- Can you use both VALUES and SELECT in one INSERT?No — they are alternative sources for the same statement, so an INSERT has either a VALUES clause or a query, never both. If you want literal rows combined with queried ones, put the literals in a `VALUES` constructor inside the source query, for example as the second branch of a `UNION ALL`, or run two statements.
- How do you insert a constant alongside queried columns?Just write it in the select list: `INSERT INTO order_archive (order_id, total, archived_by) SELECT id, total, 'nightly-job' FROM orders`. A literal is an ordinary select-list expression, matched positionally like any other. The same works for `CURRENT_TIMESTAMP` or a CASE expression, though a value the table can supply itself is usually better left to a column default.
- Why is INSERT INTO t (a, b) SELECT * FROM s a bad habit even with an explicit target list?Because `*` expands to whatever columns `s` currently has, in its current order, so the statement's meaning changes when the source schema changes: the degree can stop matching, or worse, two type-compatible columns can swap and the insert still succeeds. Naming the source expressions makes the pairing readable and stable across migrations.
The two column lists are two rows of numbered slots pushed together: whatever sits in slot three of the query lands in slot three of the table, regardless of what either one is called.
saying these in an interview costs you the question
- Thinks columns are matched by name or by select-list alias
- Assumes an alias like AS total steers the value to a total column
- Believes a swapped pair of same-typed columns raises an error
- Says an empty source query makes the statement fail
- Thinks INSERT ... SELECT needs a VALUES keyword as well