# Appendix — Commands: Syntax + Example Each entry: syntax → example on the [course schema](course-plan.md#shared-example-schema) → one note → the chapter that uses it. Entries follow chapter order. --- ## MERGE **Syntax:** ```sql MERGE INTO t USING s ON () WHEN MATCHED THEN UPDATE SET t.col = s.col [WHERE ...] [DELETE WHERE ...] WHEN NOT MATCHED THEN INSERT (cols) VALUES (s.cols) [WHERE ...]; ``` **Example:** ```sql MERGE INTO customers c USING (SELECT customer_id, MAX(order_date) AS last_dt FROM orders GROUP BY customer_id) o ON (c.customer_id = o.customer_id) WHEN MATCHED THEN UPDATE SET c.last_order_date = o.last_dt WHEN NOT MATCHED THEN INSERT (customer_id, status, last_order_date) VALUES (o.customer_id, 'NEW', o.last_dt); ``` **Note:** Upsert in one statement; the source can be any query. A column used in `ON` cannot be updated. **Deep dive:** [deep-dives/merge.md](deep-dives/merge.md) — full syntax, worked examples on the course schema, what breaks at scale, pitfalls. ↩ [Chapter 1](course-plan.md) --- ## INSERT SELECT **Syntax:** ```sql INSERT INTO [(cols)] SELECT ... FROM ... [WHERE ...]; ``` **Example:** ```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'; ``` **Note:** Set-based, one operation. For large volumes — `/*+ APPEND */` (see [APPEND HINT](#append-hint)). **Deep dive:** [deep-dives/insert-select.md](deep-dives/insert-select.md) — full syntax, worked examples on the course schema, what breaks at scale, pitfalls. ↩ [Chapter 1](course-plan.md), [Chapter 9](course-plan.md) --- ## UPDATE WHERE EXISTS **Syntax:** ```sql UPDATE t SET col = s WHERE s.key = t.key)> WHERE EXISTS (SELECT 1 FROM s WHERE s.key = t.key [AND ...]); ``` **Example:** ```sql UPDATE customers c SET c.status = 'ACTIVE' WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND o.order_date >= ADD_MONTHS(SYSDATE, -12)); ``` **Note:** `WHERE EXISTS` prevents updating rows with no match (which would get NULL from a subquery in SET). When values from the source are needed too — `MERGE` reads better. **Deep dive:** [deep-dives/update-where-exists.md](deep-dives/update-where-exists.md) — full syntax, worked examples on the course schema, what breaks at scale, pitfalls. ↩ [Chapter 1](course-plan.md) --- ## EXISTS **Syntax:** ```sql EXISTS () -- TRUE as soon as the first row is found NOT EXISTS () ``` **Example:** ```sql SELECT CASE WHEN EXISTS (SELECT 1 FROM orders WHERE customer_id = :id) THEN 1 ELSE 0 END INTO v_has_orders FROM dual; ``` **Note:** `COUNT(*) > 0` scans every match; `EXISTS` stops at the first. For anti-joins (`NOT EXISTS` vs `NOT IN`) — `NOT IN` returns nothing if the source contains a NULL; `NOT EXISTS` is safe. **Deep dive:** [deep-dives/exists.md](deep-dives/exists.md) — full syntax, worked examples on the course schema, what breaks at scale, pitfalls. ↩ [Chapter 1](course-plan.md) --- ## FETCH FIRST **Syntax:** ```sql SELECT ... FROM ... [WHERE ...] [ORDER BY ...] [OFFSET ROWS] FETCH FIRST ROWS ONLY | FETCH FIRST PERCENT ROWS WITH TIES; ``` **Example:** ```sql SELECT order_id, amount FROM orders WHERE customer_id = :id ORDER BY order_date DESC FETCH FIRST 10 ROWS ONLY; ``` **Note:** 12c+. Cannot be combined with `FOR UPDATE` (ORA-02014) — use `ROWNUM <= n` there. **Deep dive:** [deep-dives/fetch-first.md](deep-dives/fetch-first.md) — full syntax, worked examples on the course schema, what breaks at scale, pitfalls. ↩ [Chapter 1](course-plan.md), [Chapter 8](course-plan.md) --- ## BULK COLLECT **Syntax:** ```sql SELECT ... BULK COLLECT INTO FROM ...; FETCH BULK COLLECT INTO [LIMIT ]; EXECUTE IMMEDIATE '' BULK COLLECT INTO ; RETURNING BULK COLLECT INTO ; ``` **Example:** ```sql DECLARE TYPE t_ids IS TABLE OF orders.order_id%TYPE; l_ids t_ids; BEGIN SELECT order_id BULK COLLECT INTO l_ids FROM orders WHERE customer_id = :id; -- small, known result — no LIMIT needed DBMS_OUTPUT.PUT_LINE(l_ids.COUNT); END; ``` **Note:** Brings the whole result into PGA at once. For large or unknown result sizes — always a cursor + [LIMIT](#limit). **Deep dive:** [deep-dives/bulk-collect.md](deep-dives/bulk-collect.md) — full syntax, worked examples on the course schema, what breaks at scale, pitfalls. ↩ [Chapter 2](course-plan.md) --- ## LIMIT **Syntax:** ```sql FETCH BULK COLLECT INTO LIMIT ; ``` **Example:** ```sql OPEN c; LOOP FETCH c BULK COLLECT INTO l_rows LIMIT 10000; EXIT WHEN l_rows.COUNT = 0; -- not c%NOTFOUND here — that drops the last partial batch -- ... process l_rows ... END LOOP; CLOSE c; ``` **Note:** 1,000–10,000 is the sensible range; beyond that the gain is marginal and PGA grows. If you do use `EXIT WHEN c%NOTFOUND` — only at the end of the loop, after processing. **Deep dive:** [deep-dives/limit.md](deep-dives/limit.md) — full syntax, worked examples on the course schema, what breaks at scale, pitfalls. ↩ [Chapter 2](course-plan.md) --- ## FORALL **Syntax:** ```sql FORALL i IN .. [SAVE EXCEPTIONS] ; FORALL i IN INDICES OF ... -- sparse collection FORALL i IN VALUES OF ... -- subset chosen by an index collection ``` **Example:** ```sql FORALL i IN 1 .. l_rows.COUNT UPDATE orders SET amount = l_rows(i).amount WHERE order_id = l_rows(i).order_id; DBMS_OUTPUT.PUT_LINE('total: ' || SQL%ROWCOUNT); -- total rows DBMS_OUTPUT.PUT_LINE('row 1: ' || SQL%BULK_ROWCOUNT(1)); -- per collection element ``` **Note:** `FORALL` is not a loop — it is one DML statement sent to the SQL engine with the whole collection. The body is exactly one statement. **Deep dive:** [deep-dives/forall.md](deep-dives/forall.md) — full syntax, worked examples on the course schema, what breaks at scale, pitfalls. ↩ [Chapter 2](course-plan.md) --- ## SAVE EXCEPTIONS **Syntax:** ```sql FORALL i IN 1 .. SAVE EXCEPTIONS ; -- If any element fails: ORA-24381 at the end, not in the middle. -- SQL%BULK_EXCEPTIONS.COUNT, SQL%BULK_EXCEPTIONS(j).ERROR_INDEX, .ERROR_CODE ``` **Example:** ```sql DECLARE e_bulk_errors EXCEPTION; PRAGMA EXCEPTION_INIT(e_bulk_errors, -24381); BEGIN FORALL i IN 1 .. l_rows.COUNT SAVE EXCEPTIONS INSERT INTO orders_archive VALUES l_rows(i); EXCEPTION WHEN e_bulk_errors THEN FOR j IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP log_error('archive idx ' || SQL%BULK_EXCEPTIONS(j).ERROR_INDEX, -SQL%BULK_EXCEPTIONS(j).ERROR_CODE, SQLERRM(-SQL%BULK_EXCEPTIONS(j).ERROR_CODE)); END LOOP; END; ``` **Note:** Without `SAVE EXCEPTIONS` — one failing row stops the whole batch and rolls it back. `ERROR_CODE` is positive — negate it for `SQLERRM`. **Deep dive:** [deep-dives/save-exceptions.md](deep-dives/save-exceptions.md) — full syntax, worked examples on the course schema, what breaks at scale, pitfalls. ↩ [Chapter 2](course-plan.md), [Chapter 7](course-plan.md) --- ## CURSOR FOR LOOP **Syntax:** ```sql FOR IN (