INSERT...SELECT copies data from one table into another entirely inside the database engine, in a single statement — genuinely more efficient than the common alternative of looping through rows in application code, reading each one, and issuing a separate insert per row.
The basic syntax
INSERT INTO archived_orders (id, customer_id, total, created_at)
SELECT id, customer_id, total, created_at
FROM orders
WHERE created_at < '2023-01-01';
The column lists on both sides need to match in count and compatible type, in the same order — MySQL doesn't match by column name across the two tables, only by position.
Copying an entire table's structure and data at once
INSERT INTO orders_backup
SELECT * FROM orders;
Using SELECT * only works cleanly when orders_backup already has an identical column structure to orders — for tables with even slightly different columns, listing explicit column names on both sides (as in the first example) avoids a column-count mismatch error.
Copying only rows matching a condition
INSERT INTO high_value_customers (customer_id, total_spent)
SELECT customer_id, SUM(total)
FROM orders
GROUP BY customer_id
HAVING SUM(total) > 10000;
The SELECT half can be any valid query — including joins, aggregates, and GROUP BY/HAVING clauses, as shown here — not just a simple flat table copy.
Handling duplicate keys during the copy
INSERT INTO archived_orders (id, customer_id, total)
SELECT id, customer_id, total FROM orders WHERE archived = 1
ON DUPLICATE KEY UPDATE total = VALUES(total);
ON DUPLICATE KEY UPDATE combined with INSERT...SELECT gives an "insert or update" (upsert) pattern for a data copy operation that might be re-run against overlapping data, rather than failing outright on a duplicate primary key or unique constraint.
Using Laravel's query builder for the same pattern
DB::statement('
INSERT INTO archived_orders (id, customer_id, total, created_at)
SELECT id, customer_id, total, created_at FROM orders WHERE created_at < ?
', [$cutoffDate]);
Eloquent has no built-in fluent method for INSERT...SELECT specifically — dropping to DB::statement() with a parameterized raw query (using ? placeholders, as shown, to avoid SQL injection) is the standard way to run this pattern from within a Laravel application.
Why this beats a PHP loop for large data migrations
Fetching thousands of rows into PHP, looping through them, and inserting each one individually involves considerable overhead — network round-trips between PHP and the database, PHP memory for holding the fetched rows, and per-row insert overhead — all of which INSERT...SELECT avoids entirely by keeping the whole operation inside the database engine itself.
A genuine caution: this runs as one large transaction by default
For a very large copy operation, running it as one enormous statement can hold locks and consume transaction log space for an extended period — for a genuinely huge migration, batching the operation with a LIMIT and a loop of smaller INSERT...SELECT statements (each committing separately) is sometimes the more production-safe approach than one massive single statement.