10.2 MERGE and Multitable Inserts
Key Takeaways
- The MERGE statement performs conditional upserts (inserting, updating, and optionally deleting target rows) based on match conditions evaluated against a source dataset in a single atomic SQL operation.
- Under strict Oracle SQL rules (ORA-38104), columns referenced in the MERGE statement's ON join condition cannot be modified in the WHEN MATCHED THEN UPDATE clause.
- The optional DELETE WHERE clause inside WHEN MATCHED removes only rows that were updated by the MERGE operation and satisfy the deletion predicate.
- Unconditional INSERT ALL inserts every source row into all listed target tables, while conditional INSERT ALL evaluates every WHEN condition for each row without short-circuiting.
- Conditional INSERT FIRST evaluates WHEN conditions sequentially and inserts the row only into the first matching table, short-circuiting and skipping all subsequent WHEN clauses and the ELSE clause.
10.2 MERGE and Multitable Inserts
Enterprise data processing frequently requires synchronizing operational tables with external feeds, data warehouses, or staging buffers. Traditionally, synchronizing a target table required executing multiple distinct SQL statements: a SELECT to identify existing keys, an UPDATE for existing records, and an INSERT for new records.
To optimize performance and simplify code, Oracle SQL provides advanced DML constructs:
- The
MERGEStatement: Combines conditional inserts, updates, and deletes into a single atomic statement (often called an "upsert"). - Multitable
INSERTStatements: Allows a single source query to populate multiple destination tables simultaneously usingINSERT ALLorINSERT FIRST.
Understanding the exact syntax, short-circuit execution rules, column update restrictions, and pivoting techniques for these statements is heavily emphasized on the Oracle Database SQL Certified Associate (1Z0-071) exam.
The MERGE Statement Architecture
The MERGE statement selects records from one or more source tables or subqueries and conditionally updates, deletes, or inserts data into a target table or updatable view based on a join condition.
+-----------------------------------------------------------------------------------+
| MERGE STATEMENT SYNTAX |
| |
| MERGE INTO target_table [alias_t] |
| USING source_table_or_subquery [alias_s] |
| ON (join_condition) |
| [WHEN MATCHED THEN |
| UPDATE SET alias_t.col1 = expr1 [, alias_t.col2 = expr2, ...] |
| [WHERE update_filter_condition] |
| [DELETE WHERE delete_filter_condition] |
| ] |
| [WHEN NOT MATCHED THEN |
| INSERT [(col1, col2, ...)] |
| VALUES (expr1, expr2, ...) |
| [WHERE insert_filter_condition] |
| ]; |
+-----------------------------------------------------------------------------------+
MERGE EXECUTION FLOW
|
+---------------+---------------+
| For each row in SOURCE dataset|
+---------------+---------------+
|
v
/---------------------------------\
/ Does source row match target \
< based on ON (join_condition)? >
\ /
\---------------------------------/
/ \
YES (MATCHED) NO (NOT MATCHED)
/ \
v v
+-----------------------+ +-----------------------+
| Execute WHEN MATCHED | | Execute WHEN NOT |
| UPDATE SET ... | | MATCHED INSERT ... |
+-----------+-----------+ +-----------------------+
|
v
/-----------------------\
/ Is DELETE WHERE \
< predicate specified >
\ and TRUE for row? /
\-----------------------/
/ \
YES NO
/ \
v v
+--------------------+ +---------------------------+
| Delete updated row | | Retain updated target row |
| from target table | +---------------------------+
+--------------------+
Concrete Example: Inventory Synchronization
Suppose an e-commerce platform receives a daily batch of product inventory updates in table STAGE_PRODUCTS that must be merged into the production table PROD_INVENTORY:
MERGE INTO prod_inventory tgt
USING stage_products src
ON (tgt.product_id = src.product_id)
WHEN MATCHED THEN
UPDATE SET
tgt.unit_price = src.unit_price,
tgt.stock_qty = tgt.stock_qty + src.delivered_qty,
tgt.last_updated = SYSDATE
WHERE src.delivered_qty > 0
DELETE WHERE tgt.stock_qty <= 0
WHEN NOT MATCHED THEN
INSERT (product_id, product_name, unit_price, stock_qty, last_updated)
VALUES (src.product_id, src.product_name, src.unit_price, src.delivered_qty, SYSDATE)
WHERE src.delivered_qty > 0;
Critical MERGE Rules and Restrictions (Tested on 1Z0-071)
Rule 1: Cannot Update Join Columns in the ON Clause (ORA-38104)
This is one of the most frequently tested rules on the certification exam:
The Join Column Immutability Rule: You CANNOT update any column in the
WHEN MATCHED THEN UPDATEclause that is referenced in theON (join_condition)clause! Attempting to do so raisesORA-38104.
-- INVALID MERGE: Raises ORA-38104
MERGE INTO employees tgt
USING new_roster src
ON (tgt.employee_id = src.emp_id)
WHEN MATCHED THEN
UPDATE SET
tgt.employee_id = src.emp_id, -- ERROR: TARGET.EMPLOYEE_ID is in the ON clause!
tgt.salary = src.salary;
Oracle Error:
ORA-38104: Columns referenced in the ON Clause cannot be updated: "TGT"."EMPLOYEE_ID".
Rule 2: Clauses are Optional (Oracle 10g and later)
- You do not have to include both
WHEN MATCHEDandWHEN NOT MATCHEDclauses. - A
MERGEstatement containing onlyWHEN MATCHED(pure update/delete) or onlyWHEN NOT MATCHED(pure insert) is 100% valid.
Rule 3: The DELETE WHERE Sub-Clause Behavior
- The
DELETE WHEREclause can appear only inside theWHEN MATCHED THEN UPDATEclause. It cannot stand alone without anUPDATE. - The
DELETE WHEREclause evaluates the row after theUPDATEhas been applied. - It deletes only rows in the target table that were matched and updated by that specific
MERGEstatement and satisfy theDELETE WHEREcondition. It will not delete untouched target rows that happen to match theDELETE WHEREpredicate!
Multitable INSERT Statements
Oracle SQL allows inserting rows into one or more target tables using a single INSERT statement driven by a single source SELECT query. There are two primary categories:
+-----------------------------------------------------------------------------------+
| MULTITABLE INSERT CLASSIFICATION |
| |
| 1. UNCONDITIONAL INSERT ALL: |
| - Inserts every row from the source query into ALL listed target tables. |
| - Commonly used for data replication and table pivoting. |
| |
| 2. CONDITIONAL INSERT ALL: |
| - Evaluates multiple WHEN conditions independently for each source row. |
| - A single source row can be inserted into MULTIPLE tables if multiple WHEN |
| conditions evaluate to TRUE. |
| |
| 3. CONDITIONAL INSERT FIRST: |
| - Evaluates WHEN conditions sequentially in order. |
| - Short-circuits: Inserts into the FIRST matching table and SKIPS all others! |
+-----------------------------------------------------------------------------------+
1. Unconditional INSERT ALL
In an unconditional INSERT ALL, every row returned by the subquery is inserted into every target table specified in the INTO clauses.
-- Unconditional Multitable Insert: Populates 3 audit/backup tables simultaneously
INSERT ALL
INTO orders_archive VALUES (order_id, customer_id, order_total, order_date)
INTO daily_sales_log VALUES (order_id, order_total, SYSDATE)
INTO customer_activity (cust_id, last_action_date) VALUES (customer_id, order_date)
SELECT order_id, customer_id, order_total, order_date
FROM current_orders
WHERE order_date = TRUNC(SYSDATE);
If the source query returns 100 rows, Oracle executes $100 \times 3 = 300$ total row insertions across the three destination tables.
2. Conditional INSERT ALL vs. Conditional INSERT FIRST
The difference in evaluation semantics between INSERT ALL and INSERT FIRST is a cornerstone of the 1Z0-071 exam.
+-----------------------------------------------------------------------------------+
| CONDITIONAL INSERT ALL vs INSERT FIRST EXECUTION |
| |
| Source Row: Salary = $15,000 |
| |
| Statement A (INSERT ALL): |
| WHEN salary >= 10000 THEN INTO high_sal_tab --> TRUE (Inserted!) |
| WHEN salary >= 5000 THEN INTO mid_sal_tab --> TRUE (Inserted!) |
| ELSE INTO low_sal_tab --> Evaluated ONLY if 0 matches |
| Result: Row inserted into BOTH high_sal_tab AND mid_sal_tab! |
| |
| Statement B (INSERT FIRST): |
| WHEN salary >= 10000 THEN INTO high_sal_tab --> TRUE (Inserted! STOP!) |
| WHEN salary >= 5000 THEN INTO mid_sal_tab --> SKIPPED (Short-circuited) |
| ELSE INTO low_sal_tab --> SKIPPED |
| Result: Row inserted ONLY into high_sal_tab! |
+-----------------------------------------------------------------------------------+
Concrete Comparison Code Example
-- CONDITIONAL INSERT ALL: All matching branches fire
INSERT ALL
WHEN salary >= 10000 THEN
INTO exec_salaries VALUES (employee_id, salary)
WHEN commission_pct IS NOT NULL THEN
INTO commission_staff VALUES (employee_id, salary, commission_pct)
ELSE
INTO standard_payroll VALUES (employee_id, salary)
SELECT employee_id, salary, commission_pct FROM employees;
- If an employee has
salary = 12000andcommission_pct = 0.20, bothWHENconditions evaluate toTRUE. The employee is inserted into bothEXEC_SALARIESandCOMMISSION_STAFF. - The
ELSEclause executes only for employees where neither condition is met.
-- CONDITIONAL INSERT FIRST: Short-circuits on first TRUE condition
INSERT FIRST
WHEN salary >= 10000 THEN
INTO tier1_salaries VALUES (employee_id, salary)
WHEN salary >= 5000 THEN
INTO tier2_salaries VALUES (employee_id, salary)
ELSE
INTO tier3_salaries VALUES (employee_id, salary)
SELECT employee_id, salary FROM employees;
- If an employee has
salary = 12000, the firstWHENcondition (salary >= 10000) isTRUE. The row is inserted intoTIER1_SALARIES. - Oracle immediately terminates evaluation for that row. The second
WHENcondition is never evaluated for that employee, preventing duplicate insertions across salary tiers.
3. Pivoting INSERT Statements
A classic application of unconditional INSERT ALL is pivoting (converting wide, non-normalized spreadsheets or flat records with multiple repeating quarterly columns into multiple normalized rows).
+-----------------------------------------------------------------------------------+
| PIVOTING WITH DML |
| |
| WIDE / DENORMALIZED TABLE (SALES_BY_QUARTER): |
| +---------+----------+----------+----------+----------+ |
| | EMP_ID | Q1_SALES | Q2_SALES | Q3_SALES | Q4_SALES | |
| | 101 | 5000 | 7500 | 6200 | 9000 | |
| +---------+----------+----------+----------+----------+ |
| |
| | (Unconditional INSERT ALL) |
| v |
| NORMALIZED TABLE (NORMALIZED_SALES): |
| +---------+---------+--------+ |
| | EMP_ID | QUARTER | AMOUNT | |
| +---------+---------+--------+ |
| | 101 | Q1 | 5000 | |
| | 101 | Q2 | 7500 | |
| | 101 | Q3 | 6200 | |
| | 101 | Q4 | 9000 | |
| +---------+---------+--------+ |
+-----------------------------------------------------------------------------------+
-- Pivoting 1 wide row into 4 normalized rows using INSERT ALL
INSERT ALL
INTO normalized_sales (emp_id, quarter_code, sales_amount) VALUES (emp_id, 'Q1', q1_sales)
INTO normalized_sales (emp_id, quarter_code, sales_amount) VALUES (emp_id, 'Q2', q2_sales)
INTO normalized_sales (emp_id, quarter_code, sales_amount) VALUES (emp_id, 'Q3', q3_sales)
INTO normalized_sales (emp_id, quarter_code, sales_amount) VALUES (emp_id, 'Q4', q4_sales)
SELECT emp_id, q1_sales, q2_sales, q3_sales, q4_sales
FROM sales_by_quarter;
Multitable INSERT Restrictions (Exam Checklist)
| Restriction | Description & Exam Impact |
|---|---|
| Target Object Types | Multitable INSERT can only target tables, not views or materialized views. |
| Remote Databases | Target tables cannot be remote objects accessed via Database Links (@dblink). |
| Sequences | If a sequence (seq.NEXTVAL) is referenced in multiple INTO clauses, Oracle generates only one sequence number per source row and distributes that identical number across all INTO clauses for that row. |
| TABLE Collection Expressions | Cannot use TABLE() collection expressions in multitable inserts. |
| Column Limit | The total number of target columns across all INTO clauses cannot exceed 999. |
A developer attempts to execute the following MERGE statement in Oracle Database: MERGE INTO departments d USING new_departments n ON (d.department_id = n.dept_id) WHEN MATCHED THEN UPDATE SET d.department_id = n.dept_id, d.department_name = n.dept_name, d.manager_id = n.mgr_id WHEN NOT MATCHED THEN INSERT (department_id, department_name, manager_id, location_id) VALUES (n.dept_id, n.dept_name, n.mgr_id, n.loc_id); Why does this statement fail to execute?
Examine the following multitable INSERT statement: INSERT FIRST WHEN score >= 90 THEN INTO honor_roll (student_id, score) VALUES (student_id, score) WHEN score >= 70 THEN INTO passing_students (student_id, score) VALUES (student_id, score) ELSE INTO remedial_students (student_id, score) VALUES (student_id, score) SELECT student_id, score FROM exam_submissions; Suppose student ID 501 has a SCORE of 95. What is the outcome for student 501?
A database developer needs to pivot monthly sales data stored in a denormalized table SALES_SUMMARY (columns: EMP_ID, JAN_SALES, FEB_SALES, MAR_SALES) into a normalized table MONTHLY_SALES (columns: EMP_ID, MONTH_NAME, AMOUNT). Which SQL construct achieves this transformation in a single statement?