MERGEINTO<target> t
USING<source | subquery> s
ON (<join condition>)
WHENMATCHEDTHENUPDATESET t.col = s.col [WHERE ...] [DELETEWHERE ...]
WHENNOTMATCHEDTHENINSERT (cols) VALUES (s.cols) [WHERE ...];
Example
MERGEINTO customers c
USING (SELECT customer_id, MAX(order_date) AS last_dt
FROM orders GROUPBY customer_id) o
ON (c.customer_id = o.customer_id)
WHENMATCHEDTHENUPDATESET c.last_order_date = o.last_dt
WHENNOTMATCHEDTHENINSERT (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 — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
UPDATE<target> t
SET col = <value | (SELECT ... FROM<src> s WHERE s.key = t.key)>
WHEREEXISTS (SELECT 1 FROM<src> s WHERE s.key = t.key [AND ...]);
Example
UPDATE customers c
SET c.status = 'ACTIVE'WHEREEXISTS (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.
EXISTS (<subquery>) -- TRUE as soon as the first row is foundNOTEXISTS (<subquery>)
Example
SELECTCASEWHENEXISTS (SELECT 1 FROM orders WHERE customer_id = :id)
THEN 1 ELSE 0 ENDINTO 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 — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
OPEN c;
LOOPFETCH c BULKCOLLECTINTO l_rows LIMIT 10000;
EXITWHEN l_rows.COUNT = 0; -- not c%NOTFOUND here — that drops the last partial batch-- ... process l_rows ...ENDLOOP;
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 — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
FORALL i IN<low> .. <high> [SAVEEXCEPTIONS]
<single DML statement referencing collection(i)>;
FORALL i ININDICESOF<collection> ... -- sparse collectionFORALL i INVALUESOF<index_collection> ... -- subset chosen by an index collection
Example
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 — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
FORALL i IN 1 .. <n>SAVEEXCEPTIONS<dml>;
-- 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
FOR r IN (SELECT customer_id, tier FROM customers WHERE tier = 'VIP') LOOP
l_tier(r.customer_id) := r.tier;
ENDLOOP;
Note: Since 10g the compiler fetches 100 rows per fetch automatically (PLSQL_OPTIMIZE_LEVEL >= 2, the default) — fine for reading. DML inside the loop is still row by row — that is what FORALL is for.
Deep dive:deep-dives/cursor-for-loop.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
Note: A promise to Oracle: same input → same output. Lets it skip repeated calls within the same fetch and build a function-based index. Never mark a function that reads a table, SYSDATE or package state.
Deep dive:deep-dives/deterministic.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
CREATEORREPLACEFUNCTION customer_tier (p_id NUMBER) RETURNVARCHAR2RESULT_CACHEIS
v_tier customers.tier%TYPE;
BEGINSELECT tier INTO v_tier FROM customers WHERE customer_id = p_id;
RETURN v_tier;
END;
/
Note: The result is stored in the SGA keyed by parameter values and shared by all sessions; invalidated automatically when a table it read changes. Good for small, stable lookup tables — not for orders.
Deep dive:deep-dives/result_cache.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
Note: 12c+. Compiles the function for calls from SQL — makes the context switch cheaper (typically 2–4x). It does not eliminate it: JOIN/CASE are still better.
Deep dive:deep-dives/pragma-udf.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
WITHFUNCTION<name> (<params>) RETURN<type>ISBEGIN ... END;
SELECT ... FROM ...;
Example
WITHFUNCTION vat (p NUMBER) RETURNNUMBERISBEGINRETURN ROUND(p * 0.17, 2); END;
SELECT order_id, amount, vat(amount) AS vat_amount
FROM orders
WHERE order_date >= DATE'2026-01-01';
/
Note: 12c+. An inline function for a single query — no schema object, and the context switch is as cheap as PRAGMA UDF. In SQL*Plus/SQLcl terminate with /. Inside a PL/SQL block — only via dynamic SQL (OPEN FOR / EXECUTE IMMEDIATE).
Deep dive:deep-dives/with-function.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
DECLARETYPE t_tier_map ISTABLEOF customers.tier%TYPEINDEXBYPLS_INTEGER;
l_tier t_tier_map;
BEGINFOR r IN (SELECT customer_id, tier FROM customers WHERE tier = 'VIP') LOOP
l_tier(r.customer_id) := r.tier;
ENDLOOP;
IF l_tier.EXISTS(:some_id) THEN
DBMS_OUTPUT.PUT_LINE(l_tier(:some_id));
ENDIF;
END;
Note: A hash in session memory (PGA), keyed access in O(1). Every session holds its own copy — in a package global this multiplies memory by the number of sessions. Use in SQL (TABLE()) — only if the type is declared in a package spec (12c+).
Deep dive:deep-dives/associative-array.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
-- Iterate a sparse collection (non-contiguous keys):DECLARE
i PLS_INTEGER := l_tier.FIRST;
BEGINWHILE i ISNOTNULLLOOP
DBMS_OUTPUT.PUT_LINE(i || ' => ' || l_tier(i));
i := l_tier.NEXT(i);
ENDLOOP;
l_tier.DELETE; -- frees the memoryEND;
Note:FOR i IN 1 .. c.COUNT is correct only for a dense collection; for sparse — FIRST/NEXT. A package-level collection — DELETE after use, otherwise the memory stays for the life of the session.
PROCEDURE apply_discounts (p_rows INOUTNOCOPY t_order_rows) ISBEGINFOR i IN 1 .. p_rows.COUNT LOOP
p_rows(i).amount := p_rows(i).amount * 0.9;
ENDLOOP;
END;
Note:OUT/IN OUT are passed by copy by default (copy in, copy out) — on a 100K-row collection that hurts. NOCOPY requests pass-by-reference; a hint, not a guarantee. Side effect: if the procedure fails midway, partial changes remain. IN is always by reference — no need.
Deep dive:deep-dives/nocopy.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
LOOPFETCH c BULKCOLLECTINTO l_ids LIMIT 50000;
EXITWHEN l_ids.COUNT = 0;
FORALL i IN 1 .. l_ids.COUNT
UPDATE orders SET status = 'PROCESSED'WHERE order_id = l_ids(i);
COMMIT; -- once per batchENDLOOP;
Note: Every COMMIT = a wait on the log writer (log file sync). COMMIT inside a loop over an open cursor risks ORA-01555 and prevents a full rollback. COMMIT WRITE NOWAIT BATCH — only for jobs allowed to lose the last transaction on a crash.
Deep dive:deep-dives/commit.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
BEGINSAVEPOINT before_batch;
FORALL i IN 1 .. l_ids.COUNT
UPDATE orders SET status = 'PROCESSED'WHERE order_id = l_ids(i);
post_batch_checks(l_ids);
EXCEPTIONWHENOTHERSTHENROLLBACKTO before_batch; -- undoes this batch only, not the whole job
log_error('batch', SQLCODE, SQLERRM);
RAISE;
END;
Note: Undoes part of a transaction without ending it; locks of the undone part are released. Oracle rolls back only the failing statement by itself — DML a procedure completed before it raised stays in the transaction (unless the exception escapes the top-level call). SAVEPOINT + ROLLBACK TO in the handler is what makes a procedure atomic.
Deep dive:deep-dives/savepoint.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
EXECUTEIMMEDIATE'UPDATE orders SET status = ''CLOSED'' WHERE status = :1 AND order_date < :2'USING p_status, p_before;
DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT);
-- Dynamic PL/SQL block:EXECUTEIMMEDIATE'BEGIN pricing_pkg.reprice_all(:yr); END;'USING p_year;
Note: Binds are positional, not by name (:1 twice = two binds in USING). Object/column names cannot be bound — that is what DBMS_ASSERT is for. DDL via EXECUTE IMMEDIATE performs an implicit COMMIT.
Deep dive:deep-dives/execute-immediate.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
OPEN<ref_cursor>FOR<sql_string> [USING<bind>, ...];
FETCH<ref_cursor> [BULKCOLLECT] INTO ... [LIMIT<n>];
CLOSE<ref_cursor>;
Example
DECLARE
c SYS_REFCURSOR;
TYPE t_ids ISTABLEOF orders.order_id%TYPE;
l_ids t_ids;
BEGINOPEN c FOR'SELECT order_id FROM ' || DBMS_ASSERT.SQL_OBJECT_NAME(p_table) ||
' WHERE status = :1'USING p_status;
LOOPFETCH c BULKCOLLECTINTO l_ids LIMIT 10000;
EXITWHEN l_ids.COUNT = 0;
-- ... process ...ENDLOOP;
CLOSE c;
END;
Note: A dynamic SELECT with a large result — OPEN FOR + BULK COLLECT LIMIT, not EXECUTE IMMEDIATE ... BULK COLLECT (which loads everything into memory).
Deep dive:deep-dives/open-for.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
-- Column count unknown at write time: describe at runtimeDECLARE
c INTEGER := DBMS_SQL.OPEN_CURSOR;
l_cnt INTEGER;
l_desc DBMS_SQL.DESC_TAB;
BEGIN
DBMS_SQL.PARSE(c, 'SELECT * FROM ' || DBMS_ASSERT.SQL_OBJECT_NAME(p_table), DBMS_SQL.NATIVE);
DBMS_SQL.DESCRIBE_COLUMNS(c, l_cnt, l_desc);
FOR i IN 1 .. l_cnt LOOP
DBMS_OUTPUT.PUT_LINE(l_desc(i).col_name || ' type=' || l_desc(i).col_type);
ENDLOOP;
DBMS_SQL.CLOSE_CURSOR(c);
END;
Note: The heavy tool. Only when the number/type of columns or binds is unknown in advance. For everything else — EXECUTE IMMEDIATE / OPEN FOR: faster and more readable.
Deep dive:deep-dives/dbms_sql.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
BEGIN
...
EXCEPTIONWHEN<exception> [OR<exception>] THEN ...
WHENOTHERSTHEN ... RAISE;
END;
RAISE; -- re-raises the current error with the original stackRAISE<exception>; -- raises a named exception
Example
BEGINSELECT tier INTO v_tier FROM customers WHERE customer_id = p_id;
EXCEPTIONWHEN NO_DATA_FOUND THEN
v_tier := 'NONE'; -- expected case — a business decisionWHEN TOO_MANY_ROWS THEN
RAISE_APPLICATION_ERROR(-20001, 'duplicate customer ' || p_id);
WHENOTHERSTHEN
log_error('get_tier ' || p_id, SQLCODE, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
RAISE;
END;
Note:WHEN OTHERS must end with RAISE or RAISE_APPLICATION_ERROR (otherwise warning PLW-06009 — rightly so). A handler in an inner block catches only what happened in that block.
Deep dive:deep-dives/exception.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
IF p_amount < 0 THEN
RAISE_APPLICATION_ERROR(-20010, 'amount must be >= 0, got ' || p_amount);
ENDIF;
Note: Turns a business error into ORA-20010 that the caller (PL/SQL, JDBC, .NET) sees with the message. TRUE as the third parameter keeps the previous error's stack.
DECLARE
e_row_locked EXCEPTION;
PRAGMAEXCEPTION_INIT(e_row_locked, -54); -- ORA-00054: resource busy (NOWAIT)BEGINSELECT order_id INTO v_id FROM orders WHERE order_id = :id FORUPDATENOWAIT;
EXCEPTIONWHEN e_row_locked THEN
DBMS_OUTPUT.PUT_LINE('busy, retry later');
END;
Note: Gives a name to an ORA error that has no predefined exception (-54, -24381, -1555, your own -20xxx). WHEN e_row_locked is more readable and safer than WHEN OTHERS THEN IF SQLCODE = -54.
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE RETURNVARCHAR2-- "ORA-06512: at "PKG", line 42 ..."
DBMS_UTILITY.FORMAT_ERROR_STACK RETURNVARCHAR2-- full message (2000 chars, more than SQLERRM)
DBMS_UTILITY.FORMAT_CALL_STACK RETURNVARCHAR2-- who called whom (works without an error too)
Note: The backtrace points to the line where the error was raised — only if called in the first handler that catches it. RAISE preserves it; RAISE_APPLICATION_ERROR without TRUE resets it. 12c+ also has UTL_CALL_STACK for structured access.
Note: The log survives even when the main transaction rolls back. Only for logging/audit — not for business logic: the autonomous transaction does not see the caller's uncommitted changes and can block against it.
SELECT ... FROM ... WHERE ...
FORUPDATE [OF<col>] [NOWAIT | WAIT<seconds> | SKIPLOCKED];
-- In a cursor: UPDATE ... WHERE CURRENT OF <cursor>;
Example
-- Work queue: several workers in parallel, each takes 1000 free rowsSELECT order_id BULKCOLLECTINTO l_ids
FROM orders
WHERE status = 'NEW'AND ROWNUM <= 1000
FORUPDATESKIPLOCKED;
-- ... process ...FORALL i IN 1 .. l_ids.COUNT
UPDATE orders SET status = 'DONE'WHERE order_id = l_ids(i);
COMMIT; -- releases the locks
Note: The default (no NOWAIT/SKIP LOCKED) = wait forever. COMMIT closes a FOR UPDATE cursor (ORA-01002 on the next fetch) — so each batch = a fresh SELECT. FETCH FIRST is not supported with FOR UPDATE — use ROWNUM.
Deep dive:deep-dives/for-update.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
INSERT/*+ APPEND */INTO orders_archive
SELECT * FROM orders WHERE order_date < DATE'2025-01-01';
COMMIT; -- required before reading the table in the same session (ORA-12838)
Note: Writes above the HWM straight to the datafiles, bypassing the buffer cache, minimal undo. With NOLOGGING on the table — minimal redo too (then back up right after!). Locks the table against other DML until COMMIT; does not fill holes in existing blocks.
Deep dive:deep-dives/append-hint.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
ALTERSESSIONENABLEPARALLELDML; -- or hint: /*+ ENABLE_PARALLEL_DML */ (12c+)INSERT/*+ APPEND PARALLEL(t, n) */INTO t SELECT/*+ PARALLEL(s, n) */ ... FROM s;
UPDATE/*+ PARALLEL(t, n) */ t SET ...;
DELETE/*+ PARALLEL(t, n) */FROM t WHERE ...;
COMMIT;
Example
ALTERSESSIONENABLEPARALLELDML;
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;
Note: Without ENABLE PARALLEL DML — only the SELECT is parallel, the INSERT is not. Too high a degree starves a shared server — coordinate with the DBA. COMMIT is required before further DML/SELECT in the same session.
Deep dive:deep-dives/parallel-dml.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
DBMS_PARALLEL_EXECUTE.CREATE_TASK(<task_name>);
DBMS_PARALLEL_EXECUTE.CREATE_CHUNKS_BY_ROWID(<task>, <owner>, <table>, <by_row>, <chunk_size>);
-- or CREATE_CHUNKS_BY_NUMBER_COL / CREATE_CHUNKS_BY_SQL
DBMS_PARALLEL_EXECUTE.RUN_TASK(<task>, <sql_stmt>, DBMS_SQL.NATIVE, parallel_level => <n>);
-- sql_stmt must contain :start_id and :end_id
DBMS_PARALLEL_EXECUTE.TASK_STATUS(<task>); RESUME_TASK(<task>); DROP_TASK(<task>);
Example
BEGIN
DBMS_PARALLEL_EXECUTE.CREATE_TASK('reprice');
DBMS_PARALLEL_EXECUTE.CREATE_CHUNKS_BY_ROWID('reprice', USER, 'ORDERS', TRUE, 100000);
DBMS_PARALLEL_EXECUTE.RUN_TASK('reprice',
'UPDATE orders SET amount = pricing_pkg.reprice(order_id, amount)
WHERE rowid BETWEEN :start_id AND :end_id',
DBMS_SQL.NATIVE, parallel_level => 8);
-- Monitor: SELECT status, COUNT(*) FROM user_parallel_execute_chunks-- WHERE task_name = 'reprice' GROUP BY status;
DBMS_PARALLEL_EXECUTE.DROP_TASK('reprice');
END;
Note: Each chunk = a separate transaction with its own COMMIT, run as a scheduler job. A failed chunk does not stop the others — RESUME_TASK reruns only those. Requires the CREATE JOB privilege.
Note: DDL — immediate, no per-row undo/redo, implicit COMMIT, no ROLLBACK, no triggers. Without UPDATE INDEXES — global indexes become UNUSABLE. DELETE of millions of rows = hours + huge undo; TRUNCATE = seconds.
Deep dive:deep-dives/truncate.md — full syntax, worked examples on the course schema, what breaks at scale, pitfalls.
-- Archive: a whole partition becomes a standalone table, the partition is left empty. Zero copying.CREATETABLE orders_archive_2024_12 FOREXCHANGEWITHTABLE orders; -- 12.2+: identical structure guaranteedALTERTABLE orders EXCHANGEPARTITION p_2024_12
WITHTABLE orders_archive_2024_12 INCLUDINGINDEXESWITHOUTVALIDATION;
Note: Metadata swap only — 40M rows in a millisecond. Works the other way too: loading a staging table into a partition. The table must match in structure and local indexes.
DBMS_APPLICATION_INFO.SET_MODULE('nightly_batch', 'load');
load_orders;
DBMS_APPLICATION_INFO.SET_ACTION('reprice');
reprice_orders;
DBMS_APPLICATION_INFO.SET_MODULE(NULL, NULL);
-- What is running now:SELECT sid, module, action, sql_id FROM v$session WHERE module = 'nightly_batch';
Note: This is what lets the DBA (and AWR/ASH) attribute SQL to a job and a step. Zero cost. SET_SESSION_LONGOPS — a progress bar in V$SESSION_LONGOPS for long loops.
DBMS_UTILITY.GET_TIME RETURNNUMBER-- hundredths of a second, elapsed (wall clock)
DBMS_UTILITY.GET_CPU_TIME RETURNNUMBER-- hundredths of a second, CPU only
Note: The absolute value is meaningless (wraps / overflows) — only differences. elapsed ≫ cpu = waiting (I/O, locks, log file sync); elapsed ≈ cpu = the code itself. For human-readable timestamps — SYSTIMESTAMP and subtraction.
-- One-time (DBA): CREATE DIRECTORY prof_dir AS '...'; @?/rdbms/admin/dbmshptab.sql
DBMS_HPROF.START_PROFILING(location => '<dir>', filename => '<file>');
... the code being measured ...
DBMS_HPROF.STOP_PROFILING;
runid := DBMS_HPROF.ANALYZE('<dir>', '<file>'); -- loads into DBMSHP_FUNCTION_INFO / DBMSHP_PARENT_CHILD_INFO
Example
BEGIN
DBMS_HPROF.START_PROFILING('PROF_DIR', 'nightly.trc');
nightly_batch;
DBMS_HPROF.STOP_PROFILING;
END;
/
SELECT function, calls, function_elapsed_time/1e6 AS sec
FROM dbmshp_function_info
WHERE runid = :runid
ORDERBY function_elapsed_time DESCFETCHFIRST 10 ROWSONLY;
Note: Hierarchical profiler — per function: who called it, how many times, how long (including SQL as __static_sql_exec_line42). Answers "where do the 4 hours go" without guessing. Requires READ/WRITE on the DIRECTORY.
Deep dive:deep-dives/dbms_hprof.md — setup, full example with output, call-tree query, plshprof, 12.2+ table mode, pitfalls.
SELECT * FROMTABLE(DBMS_XPLAN.DISPLAY_CURSOR(<sql_id>, <child_no>, '<format>'));
-- format 'ALLSTATS LAST': actual statistics — needs /*+ GATHER_PLAN_STATISTICS */ or statistics_level=ALL
EXPLAIN PLAN FOR<sql>;
SELECT * FROMTABLE(DBMS_XPLAN.DISPLAY); -- estimated plan only
Example
-- 1. The job's heaviest SQL (see DBMS_APPLICATION_INFO)SELECT sql_id, executions, elapsed_time/1e6 AS sec, SUBSTR(sql_text, 1, 60) AS txt
FROM v$sql WHERE module = 'nightly_batch'ORDERBY elapsed_time DESCFETCHFIRST 5 ROWSONLY;
-- 2. The plan that actually ranSELECT * FROMTABLE(DBMS_XPLAN.DISPLAY_CURSOR('7h35uxf5uhmm1', NULL, 'ALLSTATS LAST'));
Note:DISPLAY_CURSOR shows what actually ran (from the shared pool); EXPLAIN PLAN — what the optimizer guesses, which can differ (binds, statistics). E-Rows vs A-Rows: a big gap = wrong statistics. Requires SELECT on V$SQL/V$SQL_PLAN.
Deep dive:deep-dives/dbms_xplan.md — the functions, format string, three worked plans (pruning, misestimate, bind peeking), reading checklist, automatic plan capture from PL/SQL, pitfalls.