PL/SQL & T-SQL (Oracle and SQL Server) Module 15 – Production PL/SQL: staging, concurrency, JSON and tests
Staging in Oracle: temporary tables and MERGE
Stage a feed in global or private temporary tables, upsert it with one MERGE that also deletes, and stop ORA-30926 and bad lines from failing the load.
What you will learn
- Load a feed into global and private temporary tables
- Merge a staged feed into a table, deleting rows with DELETE WHERE
- Prevent ORA-30926 by de-duplicating the source
- Log rejected rows with DML error logging
Before you start
What you’ll learn · Before you start
What you will learn
- Load a feed into global and private temporary tables
- Merge a staged feed into a table, deleting rows with DELETE WHERE
- Prevent ORA-30926 by de-duplicating the source
- Log rejected rows with DML error logging
Before you start
On this page
A shop’s nightly feed brings 3,000 shelf-count changes as a file. Most lines change items on the shelf, some bring new items, a few take an item below zero and a few break the table’s rules. Staging the feed first, in a temporary table, lets one MERGE statement apply every good line at once and set the bad ones aside.
You will stage a feed in global and private temporary tables, merge it with one statement that updates, inserts and deletes, stop the error that two lines for one row cause, and keep the lines a table refuses in an error table.
A global temporary table is defined once and holds each session’s own rows
The feed’s lines must wait in the database between the file and the MERGE, and two loads may run at the same moment. A staging table: A table that holds incoming data for a short time, so it can be checked, cleaned and de-duplicated in SQL before one statement moves it into the tables the application uses. holds them there, and Oracle offers two temporary kinds. A An Oracle table created once with CREATE GLOBAL TEMPORARY TABLE, whose definition every session shares while each session sees only the rows it inserted itself. is created once, at deployment, and its definition stays in the data dictionary like any table’s. Its rows belong to the session that inserted them: Oracle’s CREATE TABLE page says the data is visible only to that session. The ON COMMIT clause sets how long a session’s rows last:
| ON COMMIT | A session’s rows last until |
|---|---|
DELETE ROWS, the default |
its next commit, which truncates them |
PRESERVE ROWS |
the session ends, or truncates the table |
Step through two sessions that share one table, created ON COMMIT DELETE ROWS:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- One definition for everyone. CREATE GLOBAL TEMPORARY TABLE ran once, like any CREATE TABLE. The definition is in the data dictionary for every session, and no session has a row in it yet.
- A stages three lines. Session A inserts feed lines 1, 2 and 3. They go into rows that belong to A's session alone.
- B stages one line. Session B inserts its own line 1 into the same table name. The primary key raises nothing, because B's rows and their index entries are separate from A's.
- Each counts its own rows. COUNT(*) in session A returns 3 and in session B returns 1. Both read feed_stage_txn, and each sees only the rows it inserted.
- A commits. The table was created ON COMMIT DELETE ROWS, so A's commit truncates A's rows. B has not committed, and its line is still there.
- With PRESERVE ROWS instead. Created ON COMMIT PRESERVE ROWS, as feed_stage_ses is, the table keeps A's 3 lines after the commit, until A's session ends or truncates the table.
- The definition stays. When the sessions end, their rows go with them, but the table itself stays defined for the next session, until a DROP TABLE removes it.
Session B never saw A’s three lines, and A’s commit, which ends A’s transaction: A group of changes that the database keeps or undoes as one unit: a COMMIT makes all of them permanent, and a ROLLBACK removes all of them. More in “T-SQL transactions, @@TRANCOUNT and savepoints”, emptied
only A’s rows. The lab script runs the same statements in two windows, and the model prints what each session sees
under every setting. Press Edit, set B_COMMITS = True and run it again:
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1. Client: two SQL*Plus or SQLcl
-- windows, A and B, both connected as LAB. Run each numbered step in the window it names, in order.
-- Two global temporary tables are created once, like any table, and each session sees only its own rows in them;
-- then a private temporary table exists in one session only. session_rows_model.py prints the same counts.
-- The last step drops what the script made.
-- 1. Window A: two global temporary tables, one per ON COMMIT setting.
DROP TABLE IF EXISTS feed_stage_txn PURGE;
DROP TABLE IF EXISTS feed_stage_ses PURGE;
CREATE GLOBAL TEMPORARY TABLE feed_stage_txn (
line_no NUMBER(6) CONSTRAINT feed_stage_txn_pk PRIMARY KEY,
sku VARCHAR2(12) NOT NULL,
delta NUMBER(6) NOT NULL
) ON COMMIT DELETE ROWS;
CREATE GLOBAL TEMPORARY TABLE feed_stage_ses (
line_no NUMBER(6) CONSTRAINT feed_stage_ses_pk PRIMARY KEY,
sku VARCHAR2(12) NOT NULL,
delta NUMBER(6) NOT NULL
) ON COMMIT PRESERVE ROWS;
-- 2. Window A: stage three feed lines in each table.
INSERT INTO feed_stage_txn VALUES (1, 'RICE-5K', 4);
INSERT INTO feed_stage_txn VALUES (2, 'TEA-250', -3);
INSERT INTO feed_stage_txn VALUES (3, 'OIL-1L', 6);
INSERT INTO feed_stage_ses SELECT * FROM feed_stage_txn;
SELECT (SELECT COUNT(*) FROM feed_stage_txn) AS txn_rows, (SELECT COUNT(*) FROM feed_stage_ses) AS ses_rows FROM dual;
-- Expected: 3, 3
-- 3. Window B: the same tables, none of A's rows. Line 1 again is no clash: each session's index is its own.
SELECT COUNT(*) AS txn_rows FROM feed_stage_txn;
-- Expected: 0
INSERT INTO feed_stage_txn VALUES (1, 'DAL-1K', 2);
-- 4. Window A: commit. DELETE ROWS empties A's rows of the first table; PRESERVE ROWS keeps the second.
COMMIT;
SELECT (SELECT COUNT(*) FROM feed_stage_txn) AS txn_rows, (SELECT COUNT(*) FROM feed_stage_ses) AS ses_rows FROM dual;
-- Expected: 0, 3
-- 5. Window B: A's commit did not touch B's row.
SELECT COUNT(*) AS txn_rows FROM feed_stage_txn;
-- Expected: 1
ROLLBACK;
-- 6. Window A: a private temporary table. DROP DEFINITION makes it last until the end of this transaction, and
-- creating it does not commit, so it can be created in the middle of a transaction.
CREATE PRIVATE TEMPORARY TABLE ora$ptt_feed (line_no NUMBER(6), sku VARCHAR2(12), delta NUMBER(6))
ON COMMIT DROP DEFINITION;
INSERT INTO ora$ptt_feed VALUES (1, 'RICE-5K', 4);
SELECT COUNT(*) AS ptt_rows FROM ora$ptt_feed;
-- Expected: 1
-- 7. Window B: the name means nothing in another session.
SELECT COUNT(*) FROM ora$ptt_feed;
-- Expected: an error, because no such table exists in session B.
-- 8. Window A: the commit drops the table with its rows.
COMMIT;
SELECT COUNT(*) FROM ora$ptt_feed;
-- Expected: an error, because the table is gone.
-- 9. Window A: two definitions that a private temporary table refuses.
CREATE PRIVATE TEMPORARY TABLE feed_ptt (line_no NUMBER(6)) ON COMMIT DROP DEFINITION;
-- Expected: an error: the name lacks the prefix ORA$PTT_ that PRIVATE_TEMP_TABLE_PREFIX sets.
CREATE PRIVATE TEMPORARY TABLE ora$ptt_keyed (line_no NUMBER(6) PRIMARY KEY) ON COMMIT DROP DEFINITION;
-- Expected: an error: a private temporary table takes no PRIMARY KEY, UNIQUE, CHECK or NOT NULL constraint.
-- 10. Window A: clean up. A session stays bound to a PRESERVE ROWS table until it ends or truncates it, and DDL
-- waits for no session to be bound, so the table is truncated first.
TRUNCATE TABLE feed_stage_ses;
DROP TABLE feed_stage_txn PURGE;
DROP TABLE feed_stage_ses PURGE; """temp_tables.sql in the lab, in two SQL*Plus clients of LAB, A and B, step by step as the script numbers
them."""
from oracle_session import Lab
STEP = r"^-- \d+\. (?P<who>Window A|Window B|Both windows|Clean-up)\b"
WHO = {"Window A": ["A"], "Window B": ["B"], "Both windows": ["A", "B"], "Clean-up": ["A"]}
with Lab() as lab:
lab.steps("stage/temp_tables.sql", STEP, WHO, users={"A": "lab", "B": "lab"}, errors=4) Output
A> 1. Window A: two global temporary tables, one per ON COMMIT setting.
Table dropped.
Table dropped.
Table created.
Table created.
A> 2. Window A: stage three feed lines in each table.
1 row created.
1 row created.
1 row created.
3 rows created.
TXN_ROWS SES_ROWS
---------- ----------
3 3
B> 3. Window B: the same tables, none of A's rows. Line 1 again is no clash: each session's index is its own.
TXN_ROWS
----------
0
1 row created.
A> 4. Window A: commit. DELETE ROWS empties A's rows of the first table; PRESERVE ROWS keeps the second.
Commit complete.
TXN_ROWS SES_ROWS
---------- ----------
0 3
B> 5. Window B: A's commit did not touch B's row.
TXN_ROWS
----------
1
Rollback complete.
A> 6. Window A: a private temporary table. DROP DEFINITION makes it last until the end of this transaction, and
A> creating it does not commit, so it can be created in the middle of a transaction.
Table created.
1 row created.
PTT_ROWS
----------
1
B> 7. Window B: the name means nothing in another session.
SELECT COUNT(*) FROM ora$ptt_feed
*
ERROR at line 1:
ORA-00942: table or view "LAB"."ORA$PTT_FEED" does not exist
Help: https://docs.oracle.com/error-help/db/ora-00942/
A> 8. Window A: the commit drops the table with its rows.
Commit complete.
SELECT COUNT(*) FROM ora$ptt_feed
*
ERROR at line 1:
ORA-00942: table or view "LAB"."ORA$PTT_FEED" does not exist
Help: https://docs.oracle.com/error-help/db/ora-00942/
A> 9. Window A: two definitions that a private temporary table refuses.
CREATE PRIVATE TEMPORARY TABLE feed_ptt (line_no NUMBER(6)) ON COMMIT DROP DEFINITION
*
ERROR at line 1:
ORA-00903: invalid table name
Help: https://docs.oracle.com/error-help/db/ora-00903/
CREATE PRIVATE TEMPORARY TABLE ora$ptt_keyed (line_no NUMBER(6) PRIMARY KEY) ON COMMIT DROP DEFINITION
*
ERROR at line 1:
ORA-14451: unsupported feature with temporary table
Help: https://docs.oracle.com/error-help/db/ora-14451/
A> 10. Window A: clean up. A session stays bound to a PRESERVE ROWS table until it ends or truncates it, and DDL
A> waits for no session to be bound, so the table is truncated first.
Table truncated.
Table dropped.
Table dropped.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 temp_tables_session.py
"""What two sessions see in a staging table of each kind, modelled in Python.
A model, not Oracle: it applies the rules of Oracle's CREATE TABLE page and Administrator's Guide. A global temporary
table has one definition that every session uses, and each session sees only the rows it inserted: ON COMMIT DELETE
ROWS truncates a session's rows at each commit, ON COMMIT PRESERVE ROWS keeps them until the session ends. A private
temporary table belongs, definition and rows, to the session that created it: ON COMMIT DROP DEFINITION drops it at
the commit, ON COMMIT PRESERVE DEFINITION when the session ends.
Session A stages A_ROWS lines and session B stages B_ROWS lines in a table of the same name; then A commits. Press
Edit, set B_COMMITS = True (B commits as well), and run it again.
"""
A_ROWS = 3
B_ROWS = 1
B_COMMITS = False
SETTINGS = [
("global", "ON COMMIT DELETE ROWS"),
("global", "ON COMMIT PRESERVE ROWS"),
("private", "ON COMMIT DROP DEFINITION"),
("private", "ON COMMIT PRESERVE DEFINITION"),
]
MOMENTS = ["before A commits", "after A commits", "A in a new session"]
def after_commit(kind, setting, rows):
"""What a session holds right after its own commit: a row count, or no table at all."""
if setting.endswith("DELETE ROWS"):
return 0
if setting.endswith("DROP DEFINITION"):
return None
return rows
def in_new_session(kind):
"""A new session of the same user: a global table's definition is still there, a private one is not."""
return 0 if kind == "global" else None
def show(value):
return "no table" if value is None else str(value)
print(f"Session A stages {A_ROWS} lines and session B stages {B_ROWS}; then A commits"
+ (", and B commits too." if B_COMMITS else "; B does not commit."))
for kind, setting in SETTINGS:
b_after = after_commit(kind, setting, B_ROWS) if B_COMMITS else B_ROWS
a_values = [A_ROWS, after_commit(kind, setting, A_ROWS), in_new_session(kind)]
b_values = [B_ROWS, b_after, b_after]
print()
print(f"{setting} ({kind})")
print(f" {'':<20}{'A sees':>9}{'B sees':>9}")
for moment, a, b in zip(MOMENTS, a_values, b_values):
print(f" {moment:<20}{show(a):>9}{show(b):>9}")
finished = True Output
Session A stages 3 lines and session B stages 1; then A commits; B does not commit.
ON COMMIT DELETE ROWS (global)
A sees B sees
before A commits 3 1
after A commits 0 1
A in a new session 0 1
ON COMMIT PRESERVE ROWS (global)
A sees B sees
before A commits 3 1
after A commits 3 1
A in a new session 0 1
ON COMMIT DROP DEFINITION (private)
A sees B sees
before A commits 3 1
after A commits no table 1
A in a new session no table 1
ON COMMIT PRESERVE DEFINITION (private)
A sees B sees
before A commits 3 1
after A commits 3 1
A in a new session no table 1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 session_rows_model.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
A commit between staging and merging
Wrong: stage the feed in an ON COMMIT DELETE ROWS table, commit, then run the MERGE: it finds no rows and merges nothing, without any error. Right: stage and merge in one transaction, or create the table ON COMMIT PRESERVE ROWS. Oracle’s COMMIT page adds that DDL commits implicitly, so a CREATE TABLE between the two steps empties the stage too.
SQL Server goes the other way: Microsoft’s CREATE TABLE page describes a #feed table that the code creates when it
needs one and that is dropped when its procedure or session ends, as the
T-SQL temporary tables lesson shows. In Oracle the table is created once,
and only its rows come and go.
Checkpoint
Check: Global temporary tables and their ON COMMIT setting
One MERGE updates, inserts and deletes in one statement
The shelf holds RICE-5K at 10, TEA-250 at 2 and DAL-1K at 0. The staged feed adds 4 to RICE-5K, takes 3 from TEA-250
and brings 6 of a new item, OIL-1L. A MERGE joins the feed to the table with its ON clause: a line whose SKU is on the
shelf takes the WHEN MATCHED branch and updates the row, and any other line takes WHEN NOT MATCHED and inserts one.
That is an upsert: An update of a row when its key exists and an insert when it does not, done as one unit of work, for example with MERGE or with UPDATE followed by INSERT. More in “T-SQL MERGE, OUTPUT and safe upserts”, done in one set-based: Code that describes a whole set of rows in one statement, such as one UPDATE or one query, instead of a loop that handles one row or one value per pass. More in “T-SQL control flow: IF, WHILE and CASE” statement instead of
3,000 single-row ones. A DELETE WHERE clause inside the update branch removes rows as well, and Oracle’s MERGE page
sets two rules for it: it touches only rows this MERGE has just updated, and it reads their updated values, not the old
ones. Step through the three lines:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- RICE-5K comes in. The first feed line adds 4 to RICE-5K. The ON clause looks for a shelf row with the same SKU and finds one, holding 10.
- Matched, so it is updated. A match takes the WHEN MATCHED branch. The UPDATE adds the delta to the shelf count, 10 + 4 = 14.
- The delete check keeps it. DELETE WHERE then reads the row as the UPDATE left it. The count is 14, above 0, so the row stays.
- TEA-250 drops below zero. The second line takes 3 from TEA-250, which holds 2. It matches too, and the UPDATE leaves 2 - 3 = -1.
- Deleted by DELETE WHERE. DELETE WHERE reads the updated count, -1, not the 2 the row held before, and -1 is at most 0, so the MERGE deletes the TEA-250 row.
- OIL-1L is new. No shelf row has OIL-1L, so the line takes the WHEN NOT MATCHED branch and is inserted with its delta, 6.
- DAL-1K is left alone. DAL-1K already holds 0, but no feed line names it. The ON clause never joins it, so DELETE WHERE never sees it, and the row stays.
TEA-250 was deleted because its updated count, -1, met the condition, while DAL-1K stayed at 0 because no line joined
it. The script runs the MERGE on a staged feed, and the model replays it line by line. Press Edit, add the line
("DAL-1K", 0) to FEED and run it again: DAL-1K is joined now, keeps its count of 0, and DELETE WHERE removes it.
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1. Client: SQL*Plus or SQLcl.
-- A shop's shelf counts take a feed of changes: a positive delta for goods received, a negative one for goods
-- taken off the shelf. The feed is staged in a global temporary table, then one MERGE updates the SKUs that exist,
-- inserts the new ones, and deletes a row whose count falls to zero or below. merge_model.py prints the same result.
-- The script rolls its changes back and drops what it made.
SET SERVEROUTPUT ON
DROP TABLE IF EXISTS shelf_counts PURGE;
DROP TABLE IF EXISTS shelf_feed PURGE;
CREATE TABLE shelf_counts (
sku VARCHAR2(12) CONSTRAINT shelf_counts_pk PRIMARY KEY,
item_name VARCHAR2(60) NOT NULL,
on_shelf NUMBER(6) NOT NULL
);
INSERT INTO shelf_counts VALUES ('RICE-5K', 'Rice 5 kg', 10);
INSERT INTO shelf_counts VALUES ('TEA-250', 'Tea 250 g', 2);
INSERT INTO shelf_counts VALUES ('DAL-1K', 'Dal 1 kg', 0);
COMMIT;
-- The staging table: one definition for every session, rows for this transaction only.
CREATE GLOBAL TEMPORARY TABLE shelf_feed (
line_no NUMBER(6) CONSTRAINT shelf_feed_pk PRIMARY KEY,
sku VARCHAR2(12) NOT NULL,
item_name VARCHAR2(60),
delta NUMBER(6) NOT NULL
) ON COMMIT DELETE ROWS;
INSERT INTO shelf_feed VALUES (1, 'RICE-5K', NULL, 4);
INSERT INTO shelf_feed VALUES (2, 'TEA-250', NULL, -3);
INSERT INTO shelf_feed VALUES (3, 'OIL-1L', 'Oil 1 litre', 6);
MERGE INTO shelf_counts t
USING shelf_feed s
ON (t.sku = s.sku)
WHEN MATCHED THEN
UPDATE SET t.on_shelf = t.on_shelf + s.delta
DELETE WHERE t.on_shelf <= 0 -- reads the count after the update
WHEN NOT MATCHED THEN
INSERT (sku, item_name, on_shelf) VALUES (s.sku, s.item_name, s.delta);
SELECT sku, on_shelf FROM shelf_counts ORDER BY sku;
-- Expected:
-- DAL-1K 0 not in the feed, so the MERGE never touched it, although its count is 0
-- OIL-1L 6 inserted
-- RICE-5K 14 updated: 10 + 4
-- TEA-250 is gone: updated to 2 - 3 = -1, then deleted by DELETE WHERE
ROLLBACK;
DROP TABLE shelf_feed PURGE;
DROP TABLE shelf_counts PURGE; """stock_merge.sql in the lab: SQL*Plus runs it as LAB on a freshly loaded practice schema."""
from oracle_session import Lab
with Lab() as lab:
lab.sqlplus("merge/stock_merge.sql") Output
$ sqlplus -s lab@//localhost/FREEPDB1 @merge/stock_merge.sql Table dropped. Table dropped. Table created. 1 row created. 1 row created. 1 row created. Commit complete. Table created. 1 row created. 1 row created. 1 row created. 3 rows merged. SKU ON_SHELF ------------ ---------- DAL-1K 0 OIL-1L 6 RICE-5K 14 Rollback complete. Table dropped. Table dropped.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stock_merge_session.py
"""The shelf-count MERGE of stock_merge.sql, modelled in Python.
A model, not Oracle: it applies the rules of Oracle's MERGE page. A feed line whose SKU matches a row updates it; a
line with no match inserts a row. DELETE WHERE then removes a row only if this MERGE has just updated it, and it
reads the count after the update, not before. A row that is not in the feed is never deleted, whatever its count.
Press Edit, change FEED (add a line for DAL-1K, or a delta that empties RICE-5K), and run it again.
"""
SHELF = {"RICE-5K": 10, "TEA-250": 2, "DAL-1K": 0}
FEED = [("RICE-5K", 4), ("TEA-250", -3), ("OIL-1L", 6)]
shelf = dict(SHELF)
print("before: " + ", ".join(f"{sku} {n}" for sku, n in sorted(shelf.items())))
for line_no, (sku, delta) in enumerate(FEED, 1):
if sku in shelf:
old = shelf[sku]
shelf[sku] = old + delta # WHEN MATCHED THEN UPDATE
action = f"matched: {old} {'+' if delta >= 0 else '-'} {abs(delta)} = {shelf[sku]}"
if shelf[sku] <= 0: # DELETE WHERE t.on_shelf <= 0
del shelf[sku]
action += ", then deleted by DELETE WHERE"
else:
shelf[sku] = delta # WHEN NOT MATCHED THEN INSERT
action = f"no match: inserted with {delta}"
print(f"line {line_no} {sku:<8} {action}")
print("after: " + ", ".join(f"{sku} {n}" for sku, n in sorted(shelf.items())))
untouched = [sku for sku in SHELF if sku in shelf and sku not in dict(FEED)]
print(f"not in the feed, kept as they were: {', '.join(untouched) or 'none'}")
finished = True Output
before: DAL-1K 0, RICE-5K 10, TEA-250 2 line 1 RICE-5K matched: 10 + 4 = 14 line 2 TEA-250 matched: 2 - 3 = -1, then deleted by DELETE WHERE line 3 OIL-1L no match: inserted with 6 after: DAL-1K 0, OIL-1L 6, RICE-5K 14 not in the feed, kept as they were: DAL-1K
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 merge_model.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Two more rules on the MERGE page matter before the first run. The statement needs INSERT and UPDATE on the target
and SELECT on the source, plus DELETE on the target for a DELETE WHERE clause. And a column that the ON clause reads,
such as sku here, cannot be updated by the same MERGE.
Key idea
DELETE WHERE judges a row by the value its UPDATE has just written, and only rows that the MERGE matched and updated can be deleted.
Checkpoint
Check: MERGE with UPDATE, INSERT and DELETE WHERE
Private temporary tables: a stage without a deployment Pro: opens with a Pro, Premium or Ultimate pass
A private temporary table gives one session a scratch stage that no deployment has to create. This part covers its naming rule, its two lifetimes and what it refuses, and reproduces each refusal in the lab.
In this part:
- A private temporary table lives in one session only
- What the lab run refused, and why
about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Repeated keys and refused lines: ORA-30926 and LOG ERRORS Pro: opens with a Pro, Premium or Ultimate pass
A MERGE that works on a clean feed can still fail a whole load on a messy one. This part reproduces ORA-30926 with a corrected price, matches the de-duplication to the kind of feed, and keeps refused lines in an error table while the good ones go in.
In this part:
- Two feed lines for one row stop the MERGE with ORA-30926
- LOG ERRORS keeps the good lines and records the rest
- What the error table held after the third run
3 runnable examples (Python, SQL) · about 10 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: stage, merge and log
Each question gives a staging table, a feed or a MERGE. Work out what Oracle keeps, changes or refuses, then check:
Practice that adapts to you
Practise this lesson
6 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): when does a global temporary table lose its rows?
It depends on its ON COMMIT setting, and it never loses its definition. Created ON COMMIT DELETE ROWS, the default, a session’s rows are truncated at that session’s next commit, so they last one transaction. Created ON COMMIT PRESERVE ROWS, they last until the session ends or truncates the table. Either way a session only ever sees and loses its own rows: another session’s commit or rollback changes nothing in them. The table itself stays defined until a DROP TABLE removes it.
Two longer questions follow, on a nightly load that must resume after its session dies and on a MERGE that kept succeeding after the stock table changed shape:
-
Mid to senior
A nightly job stages a 200,000-line supplier file in a global temporary table created ON COMMIT PRESERVE ROWS, checks it in three steps that each commit, then merges it. Twice this month the session died in step 3, and the rerun had to fetch and stage the file again, missing its window. Redesign it so a rerun resumes.
-
Mid to senior
Stock moved from one row per SKU to one row per SKU and warehouse, and each warehouse now sends its own nightly feed with one line per SKU. The MERGE, ON (t.sku = s.sku), was not changed. It succeeds every night, yet both warehouses show the counts of whichever feed ran last. Explain why, and fix it.
Flashcards · 4 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Who sees the rows of a global temporary table, and how long do they last?
It is created once at deployment and its definition stays in the data dictionary, but its data is visible only to the session that inserted it. ON COMMIT DELETE ROWS, the default, truncates that session's rows at its next commit; ON COMMIT PRESERVE ROWS keeps them until the session ends.
-
A load stages its feed, commits, then merges. What does the MERGE find?
No rows, and no error: the commit truncated the session's rows in an ON COMMIT DELETE ROWS table. Stage and merge in one transaction, or create the table ON COMMIT PRESERVE ROWS, and remember that DDL commits implicitly, so a CREATE TABLE between the steps empties the stage too.
-
How does one MERGE apply a staged feed?
Its ON clause joins the feed to the table: a line whose key is present takes WHEN MATCHED and updates the row, any other takes WHEN NOT MATCHED and inserts one. That is an upsert in one set-based statement instead of thousands of single-row ones.
-
Which rows can a MERGE's DELETE WHERE clause remove, and by which values does it judge them?
Only rows this MERGE has just matched and updated, judged by the values the UPDATE has just written, not the old ones: a count updated to -1 is deleted, while a row no line joined stays.
Key takeaways
- A global temporary table is created once, and each session sees only its own rows, which last until its next commit with ON COMMIT DELETE ROWS or until the session ends with PRESERVE ROWS.
- One MERGE updates matching rows, inserts the others and, with DELETE WHERE, deletes rows it has just updated, judged by their new values.
Terms in this lesson
- private temporary table
- An Oracle temporary table whose definition and rows exist only in the session that created it, until its transaction or its session ends. Its name starts with the prefix that PRIVATE_TEMP_TABLE_PREFIX sets, ORA$PTT_ by default.
- staging table
- A table that holds incoming data for a short time, so it can be checked, cleaned and de-duplicated in SQL before one statement moves it into the tables the application uses.
References
- SQL Language Reference 26ai: CREATE TABLE (Oracle)
- Administrator's Guide 26ai: Managing Tables (Oracle)
- Database Reference 26ai: PRIVATE_TEMP_TABLE_PREFIX (Oracle)
- Database New Features Guide 18c (Oracle)
- SQL Language Reference 26ai: COMMIT (Oracle)
- SQL Language Reference 26ai: MERGE (Oracle)
- Database Error Messages: ORA-30926 (Oracle)
- SQL Language Reference 26ai: INSERT (Oracle)
- PL/SQL Packages and Types Reference 26ai: DBMS_ERRLOG (Oracle)
- Database Error Messages: ORA-01400 (Oracle)
- Database Error Messages: ORA-02290 (Oracle)
- Database Concepts 26ai: Data Integrity (Oracle)
- CREATE TABLE (Transact-SQL) (Microsoft)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress