# Deep Dive — APPEND HINT
> Direct-path insert: write formatted blocks straight to the datafiles, above the high-water mark, with almost no undo. Five to ten times faster — with a list of conditions that silently turn it off.
Used in: [Chapter 9 — Huge Tables](../course-plan.md) · Appendix entry: [APPEND HINT](../appendix.md#append-hint) · Related: [PARALLEL DML](parallel-dml.md), [INSERT SELECT](insert-select.md), [FORALL](forall.md), [EXCHANGE PARTITION](exchange-partition.md), [TRUNCATE](truncate.md)
---
## 1. What It Is and When
A **conventional** insert finds free space in existing blocks (via the freelist / ASSM bitmaps), reads those blocks into the buffer cache, modifies them, generates undo for every row and redo for both data and undo, and maintains indexes row by row.
A **direct-path** insert formats new blocks in the session's PGA and writes them directly to the datafiles above the table's high-water mark (HWM). No buffer cache, no free-space search, undo only for the extent map (not the rows), and index maintenance done in bulk at the end. With `NOLOGGING` on the table, redo drops to almost nothing too.
Use it for: bulk loads and copies of millions of rows — archiving, staging, rebuilding. Not for: OLTP inserts, small batches, tables where free space below the HWM must be reused.
## 2. Syntax
```sql
INSERT /*+ APPEND */ INTO
[()] SELECT ... ; -- direct path for INSERT ... SELECT
INSERT /*+ APPEND_VALUES */ INTO VALUES (...); -- 11.2+: direct path for VALUES, i.e. FORALL
INSERT /*+ APPEND PARALLEL(, ) */ ... -- with ENABLE PARALLEL DML: parallel direct path
MERGE /*+ APPEND */ INTO ... -- direct path for the INSERT branch only
COMMIT; -- required before the table is readable in this session
```
Silently falls back to conventional insert when:
- the table has an **enabled trigger** or an **enabled foreign key** constraint (referential integrity);
- the table is in a **cluster**;
- the insert is in a **distributed transaction** or across a database link;
- the table has **deferred** constraints, or certain object/LOB configurations in old versions;
- `APPEND` is used on `INSERT ... VALUES` (use `APPEND_VALUES`).
Verify in the plan: `LOAD AS SELECT` = direct path; `LOAD TABLE CONVENTIONAL` = it fell back.
## 3. Examples on the Course Schema
### 3a. The archive copy
```sql
INSERT /*+ APPEND */ 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';
COMMIT;
```
### 3b. Checking that it really was direct path
```sql
EXPLAIN PLAN FOR
INSERT /*+ APPEND */ INTO orders_archive SELECT * FROM orders WHERE order_date < DATE '2025-01-01';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
```
```
| Id | Operation | Name |
| 0 | INSERT STATEMENT | |
| 1 | LOAD AS SELECT | ORDERS_ARCHIVE | ← direct path
| 2 | PARTITION RANGE ITERATOR | |
| 3 | TABLE ACCESS FULL | ORDERS |
```
If line 1 says `LOAD TABLE CONVENTIONAL`, find the trigger or FK:
```sql
SELECT trigger_name, status FROM user_triggers WHERE table_name = 'ORDERS_ARCHIVE';
SELECT constraint_name, status FROM user_constraints
WHERE table_name = 'ORDERS_ARCHIVE' AND constraint_type = 'R';
```
### 3c. The NOLOGGING sequence
```sql
-- 1. is it even possible?
SELECT force_logging FROM v$database; -- YES → NOLOGGING is ignored; stop here
-- 2. switch off logging for the load
ALTER TABLE orders_archive NOLOGGING;
INSERT /*+ APPEND */ INTO orders_archive SELECT * FROM orders WHERE order_date < DATE '2025-01-01';
COMMIT;
ALTER TABLE orders_archive LOGGING;
-- 3. the loaded blocks are NOT in the redo stream: back up now, or a media recovery loses them
-- (RMAN: BACKUP ... or at least note the SCN and back up the tablespace)
```
`NOLOGGING` affects only direct-path operations; conventional DML is always logged. And any standby database (Data Guard) requires `FORCE LOGGING`, which overrides this entirely.
### 3d. Direct path from PL/SQL — APPEND_VALUES with FORALL
```sql
DECLARE
CURSOR c IS SELECT * FROM orders WHERE order_date < DATE '2025-01-01';
TYPE t_rows IS TABLE OF c%ROWTYPE;
l_rows t_rows;
BEGIN
OPEN c;
LOOP
FETCH c BULK COLLECT INTO l_rows LIMIT 50000;
EXIT WHEN l_rows.COUNT = 0;
FORALL i IN 1 .. l_rows.COUNT
INSERT /*+ APPEND_VALUES */ INTO orders_archive VALUES l_rows(i);
COMMIT; -- must commit before the next FORALL touches the table again
END LOOP;
CLOSE c;
END;
```
Each `FORALL` is one direct-path load; each needs its `COMMIT` before the table can be inserted into again in the session (`ORA-12838`). This is only worth it when the rows had to pass through PL/SQL; otherwise 3a in one statement.
### 3e. Parallel direct path
```sql
ALTER SESSION ENABLE PARALLEL DML;
INSERT /*+ APPEND PARALLEL(a, 8) */ INTO orders_archive a
SELECT /*+ PARALLEL(o, 8) */ * FROM orders o WHERE o.order_date < DATE '2025-01-01';
COMMIT;
```
Eight PX servers each write their own extents. See [PARALLEL DML](parallel-dml.md).
### 3f. Indexes on a huge load — unusable, load, rebuild
```sql
ALTER INDEX orders_archive_cust_ix UNUSABLE;
ALTER SESSION SET skip_unusable_indexes = TRUE; -- default TRUE since 10g
INSERT /*+ APPEND PARALLEL(a, 8) */ INTO orders_archive a SELECT /*+ PARALLEL(o, 8) */ * FROM orders o WHERE ...;
COMMIT;
ALTER INDEX orders_archive_cust_ix REBUILD NOLOGGING PARALLEL 8;
ALTER INDEX orders_archive_cust_ix NOPARALLEL; -- don't leave the degree on the index
```
Direct path maintains indexes in bulk anyway, but for loads that double a table, rebuilding is cheaper than merging. Unique indexes cannot be skipped (`skip_unusable_indexes` does not apply to them) — keep the PK usable.
## 4. What Breaks at Scale
- **The HWM only grows.** Direct path never reuses free space below it. A table that is `DELETE`d and reloaded with `APPEND` every night doubles in allocated size until someone `TRUNCATE`s it. Pair `APPEND` with `TRUNCATE` (or partition maintenance), never with `DELETE`.
- **Exclusive table lock.** A direct-path insert takes a TM lock in exclusive mode on the table (or partition, with the partition-extended syntax `INSERT INTO orders PARTITION (p_2024_12)`): no other session can insert/update/delete that table until you commit. On a live table, load into a staging table and [EXCHANGE PARTITION](exchange-partition.md).
- **`ORA-12838`** — the session cannot read or modify the table again until `COMMIT`. A procedure that inserts with `APPEND` and then does `SELECT COUNT(*)` on the same table fails.
- **NOLOGGING + no backup = unrecoverable.** After a media failure, RMAN restores the datafile from before the load and recovery has no redo for those blocks: `ORA-01578`/`ORA-26040` "block was loaded using the NOLOGGING option". The DBA's rule is right: NOLOGGING load → backup immediately.
- **Silent fallback** turns your 10-minute load into a 2-hour one with no error. Always check the plan (3b) the first time, and after any schema change.
## 5. Pitfalls
1. `ORA-12838` — reading the table before commit.
2. Fallback to conventional because of a trigger or FK — no warning; check the plan.
3. `APPEND` on `INSERT ... VALUES` — ignored; `APPEND_VALUES`.
4. `DELETE` + `APPEND` cycles — table growth.
5. `NOLOGGING` on a `FORCE LOGGING` database — no effect; on a non-forced one — a backup gap.
6. The hint must be immediately after `INSERT` (or `MERGE`); `INSERT INTO t /*+ APPEND */` is a comment, not a hint.
7. Expecting undo to be small for the *indexes* — direct path generates undo/redo for index maintenance normally; on a heavily indexed table that can be most of the cost (3f).
8. Direct-path into a table with a `SELECT` from the same table in the same statement — allowed, but the exclusive lock applies at commit time; fine.
## 6. Related
- [PARALLEL DML](parallel-dml.md) — multiplying the throughput.
- [INSERT SELECT](insert-select.md) — the statement it decorates.
- [FORALL](forall.md) — `APPEND_VALUES`.
- [EXCHANGE PARTITION](exchange-partition.md) — loading a live table without locking it.
- [TRUNCATE](truncate.md) — resetting the HWM.