# Deep Dive — INSERT SELECT > The database copies rows faster than any loop can: one statement, one pass, and with `APPEND` it never even touches the buffer cache. Used in: [Chapter 1 — SQL First](../course-plan.md), [Chapter 9 — Huge Tables](../course-plan.md) · Appendix entry: [INSERT SELECT](../appendix.md#insert-select) · Related: [APPEND HINT](append-hint.md), [PARALLEL DML](parallel-dml.md), [MERGE](merge.md), [EXCHANGE PARTITION](exchange-partition.md) --- ## 1. What It Is and When `INSERT ... SELECT` inserts the result of a query into a table. Anything you would do with a cursor loop that only inserts — archiving, copying, transforming, splitting into several tables — is one of these statements. Variants: - `INSERT INTO t SELECT ...` — single target. - `INSERT ALL INTO t1 ... INTO t2 ... SELECT ...` — every row goes to every target (multi-table insert). - `INSERT FIRST WHEN c1 THEN INTO t1 WHEN c2 THEN INTO t2 ELSE INTO t3 SELECT ...` — each row goes to the first matching target. - `CREATE TABLE t AS SELECT ...` (CTAS) — when the target does not exist yet; direct-path by default, `NOLOGGING` optional. ## 2. Syntax ```sql INSERT [hint] INTO [(, ...)] SELECT ... FROM ... [WHERE ...] [LOG ERRORS [INTO ] [()] [REJECT LIMIT { | UNLIMITED }]]; -- multi-table INSERT [ALL | FIRST] [WHEN THEN] INTO [()] VALUES () [WHEN THEN] INTO [()] VALUES () [ELSE INTO ...] SELECT ... FROM ...; -- new table CREATE TABLE [NOLOGGING] [PARALLEL n] AS SELECT ... ; ``` `RETURNING INTO` is **not** available with `INSERT ... SELECT` — only with single-row `INSERT ... VALUES`. ## 3. Examples on the Course Schema ### 3a. Archive — always list the columns ```sql INSERT INTO orders_archive (order_id, customer_id, order_date, amount, status) SELECT order_id, customer_id, order_date, amount, status FROM orders WHERE order_date < DATE '2025-01-01'; ``` `INSERT INTO orders_archive SELECT * FROM orders` works until someone adds a column to one table and not the other. Listing columns turns that into a compile error instead of shifted data. ### 3b. Direct path + parallel for the big one ```sql ALTER SESSION ENABLE PARALLEL DML; INSERT /*+ APPEND PARALLEL(a, 8) */ INTO orders_archive a (order_id, customer_id, order_date, amount, status) SELECT /*+ PARALLEL(o, 8) */ order_id, customer_id, order_date, amount, status FROM orders o WHERE order_date < DATE '2025-01-01'; COMMIT; -- required before reading orders_archive in this session (ORA-12838) ``` See [APPEND HINT](append-hint.md) and [PARALLEL DML](parallel-dml.md) for the mechanics and restrictions. ### 3c. Split one scan into two tables — INSERT FIRST ```sql INSERT FIRST WHEN status = 'CANCELLED' THEN INTO orders_cancelled (order_id, customer_id, order_date, amount) VALUES (order_id, customer_id, order_date, amount) WHEN order_date < DATE '2025-01-01' THEN INTO orders_archive (order_id, customer_id, order_date, amount, status) VALUES (order_id, customer_id, order_date, amount, status) SELECT order_id, customer_id, order_date, amount, status FROM orders WHERE status = 'CANCELLED' OR order_date < DATE '2025-01-01'; ``` One scan of `orders`, two targets. The loop version would scan once and insert row by row into two tables; the two-statement version would scan twice. ### 3d. Transform on the way in — sequences and expressions ```sql INSERT INTO order_summary (summary_id, customer_id, order_month, total_amount, order_count) SELECT order_summary_seq.NEXTVAL, customer_id, TRUNC(order_date, 'MM'), SUM(amount), COUNT(*) FROM orders WHERE order_date >= DATE '2025-01-01' GROUP BY customer_id, TRUNC(order_date, 'MM'); ``` `NEXTVAL` in the select list is allowed (not inside `DISTINCT`, `GROUP BY`, `ORDER BY` or subqueries — wrap the aggregate in an inline view if needed). ### 3e. New table from a query — CTAS ```sql CREATE TABLE orders_2024 NOLOGGING PARALLEL 8 AS SELECT /*+ PARALLEL(o, 8) */ * FROM orders o WHERE order_date >= DATE '2024-01-01' AND order_date < DATE '2025-01-01'; ``` Direct-path, no undo for the rows, minimal redo with `NOLOGGING`. Back it up afterwards. ### 3f. Do not stop on bad rows — LOG ERRORS ```sql EXEC DBMS_ERRLOG.CREATE_ERROR_LOG('ORDERS_ARCHIVE'); INSERT INTO orders_archive (order_id, customer_id, order_date, amount, status) SELECT order_id, customer_id, order_date, amount, status FROM orders_stage LOG ERRORS INTO err$_orders_archive ('stage load') REJECT LIMIT UNLIMITED; ``` ## 4. What Breaks at Scale - **Conventional insert of 40M rows** goes through the buffer cache block by block, generates full undo and redo, and maintains every index row by row. Direct path (`APPEND`) skips the cache and the undo; `NOLOGGING` skips most redo. - **Indexes.** For a load that adds a large fraction of the table, it is usually faster to mark indexes `UNUSABLE`, load, then `REBUILD ... NOLOGGING PARALLEL`. Direct path maintains indexes in bulk at the end anyway, but a rebuild of a big index is cheaper than a bulk merge into it. - **One transaction.** A 40M-row conventional insert is one transaction: undo sizing, and a failure at row 39M rolls back everything. Direct path needs almost no undo. Chunking (partition by partition, or [DBMS_PARALLEL_EXECUTE](dbms_parallel_execute.md)) gives restartability. - **Triggers** fire per row for conventional and direct-path alike — and an enabled trigger silently turns `APPEND` into a conventional insert. ## 5. Pitfalls 1. `INSERT INTO t SELECT *` without a column list — positional mapping breaks the day a column is added. 2. `ORA-12838` — reading a table in the same session after a direct-path insert, before `COMMIT`. 3. `NOLOGGING` on a database in `FORCE LOGGING` mode (every Data Guard setup) does nothing — and where it does work, the table is unrecoverable until the next backup. 4. `NEXTVAL` inside `GROUP BY`/`DISTINCT` queries: `ORA-02287`. Aggregate in an inline view, `NEXTVAL` outside. 5. `INSERT ALL` with `WHEN` clauses evaluates every `WHEN` for every row; `INSERT FIRST` stops at the first match. Choose deliberately. 6. Sequence gaps and `CACHE`: a 40M-row insert with `CACHE 20` hits the sequence 2M times. `ALTER SEQUENCE ... CACHE 10000` for bulk loads. 7. A `SELECT` with `ORDER BY` in an `INSERT ... SELECT` sorts for nothing — unless you are deliberately clustering rows for an index (`ORDER BY` the index columns can help compression and clustering factor). ## 6. Related - [APPEND HINT](append-hint.md) — direct path and its restrictions. - [PARALLEL DML](parallel-dml.md) — using all the cores. - [MERGE](merge.md) — when some rows already exist. - [EXCHANGE PARTITION](exchange-partition.md) — when the "copy" can be a metadata swap instead.