PL/SQL & T-SQL (Oracle and SQL Server) Module 16 – Across both engines: porting and concurrency
Oracle vs SQL Server: reads, locks and deadlocks
Why a reader waits on SQL Server and not on Oracle, what each engine undoes after a deadlock, and how isolation levels and retries change in a port.
What you will learn
- Predict whether a reader waits for an open change on Oracle and on SQL Server
- Explain what each engine rolls back when it breaks a deadlock
- Map Oracle isolation levels and FOR UPDATE options to SQL Server settings and hints
- Design retry logic that leaves the same balances on both engines
Before you start
What you’ll learn · Before you start
What you will learn
- Predict whether a reader waits for an open change on Oracle and on SQL Server
- Explain what each engine rolls back when it breaks a deadlock
- Map Oracle isolation levels and FOR UPDATE options to SQL Server settings and hints
- Design retry logic that leaves the same balances on both engines
Before you start
On this page
A report reads account 3 while a payment of 260.25 out of it is still open in another session. On Oracle the report
gets 760.25, the last committed balance, at once; on SQL Server, under its default settings, the same SELECT waits
for the payment, and with a 3-second lock timeout it fails with error 1222. Ported code meets this at every read, and
again at every deadlock: Oracle undoes one statement, SQL Server the whole transaction.
You will predict when each engine’s readers wait and what each undoes after a deadlock, then port isolation levels, lock options and retries.
A reader waits for a writer on SQL Server, not on Oracle
Oracle keeps queries consistent with multiversion read consistency: Oracle's way of giving each query the data as it was committed at one moment: a query that meets a changed row rebuilds the committed version from undo data, so readers take no locks and never wait for writers.. A
change writes the old values as undo data: The old values Oracle records whenever a transaction changes data. A rollback uses them to undo the change, and a query uses them to rebuild the version of a row that was committed when the query began., and a query that meets a changed row rebuilds
the version committed when the query began. Oracle’s concepts guide states the result as two rules: a reader never
blocks a writer, and a writer never blocks a reader. The exception it names is SELECT … FOR UPDATE, which locks the
rows it reads.
SQL Server’s default is READ COMMITTED with locking: READ_COMMITTED_SNAPSHOT is off, a reader takes a shared lock on
each row, and that lock cannot be granted while a writer holds its exclusive lock. The reader waits until the writer’s
transaction ends, or until its lock timeout: How long a statement waits for a lock that another session holds before it gives up. In SQL Server it is SET LOCK_TIMEOUT, -1 by default, which waits for ever; past the limit the statement fails with error 1222. runs out: -1, the default, waits for ever,
and SET LOCK_TIMEOUT 3000 ends the statement after 3 seconds with error 1222. Step through both engines:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Both writers change account 3. Each writer takes 260.25 out of account 3 and does not commit. Each now holds a row lock, and the new balance, 500.00, is visible only inside its own transaction.
- Oracle reads the committed row. The Oracle reader's SELECT meets the changed row and rebuilds, from undo data, the version that was committed when the query began. It returns 760.25 at once and takes no lock.
- SQL Server's reader waits. Under READ COMMITTED with locking, the SQL Server default, the reader asks for a shared lock on the row. It cannot have one while the writer holds its exclusive lock, so the SELECT waits.
- The lock timeout ends it. The reader ran SET LOCK_TIMEOUT 3000, so after 3 seconds the SELECT fails with error 1222 and the session goes on. With the default of -1 it would wait until the writer's transaction ended.
- RCSI reads like Oracle. With READ_COMMITTED_SNAPSHOT on, the same SQL Server reader reads the row version kept for it and returns 760.25 at once, as Oracle's does. The option is off by default in SQL Server.
- Both writers roll back. Both writers roll back, and account 3 holds 760.25 again. A NOLOCK reader on SQL Server would have reported 500.00, a balance that never existed; Oracle has no way to read it at all.
Both engines’ scripts, the writer first:
SQL (oracle_writer.sql) · readers/oracle_writer.sql
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1 with the practice schema.
-- Client: SQL*Plus, window 1. The writer: it takes 260.25 out of account 3 (760.25) and keeps the change open
-- for ten seconds. Start it, then run oracle_reader.sql in a second window within five seconds.
-- a row lock on account 3, not committed
UPDATE accounts SET balance = balance - 260.25 WHERE account_id = 3;
EXEC DBMS_SESSION.SLEEP(10)
-- puts the practice data back
ROLLBACK;
-- 760.25 again
SELECT balance FROM accounts WHERE account_id = 3; SQL (oracle_reader.sql) · readers/oracle_reader.sql
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1 with the practice schema.
-- Client: SQL*Plus, window 2, while oracle_writer.sql holds its change. The plain query returns at once with the
-- committed balance, rebuilt from undo data; only a query that asks for the row lock has to wait.
-- 760.25 at once: no waiting
SELECT balance FROM accounts WHERE account_id = 3;
-- asks for the lock: after 3 s it fails, with ORA-00054 and ORA-40097 on 26ai
SELECT balance FROM accounts WHERE account_id = 3 FOR UPDATE WAIT 3; """oracle_writer.sql and oracle_reader.sql in the lab: SQL*Plus runs them as LAB on a freshly loaded practice schema,
in two windows, the second started 3 seconds after the first. The lock error names a transaction by numbers that
change on every run, shown as <n>."""
from oracle_session import Lab
with Lab(masks=[(r'by \d+ \("usn<<16 \| slot"\)', 'by <n> ("usn<<16 | slot")'),
(r'and \d+ \("sequence"\)', 'and <n> ("sequence")'),
(r"transaction ID(\s+)\d+\.\d+\.\d+", r"transaction ID\1<n>.<n>.<n>")]) as lab:
lab.parallel([("window 1", "readers/oracle_writer.sql", 0), ("window 2", "readers/oracle_reader.sql", 3)],
errors=1) Output
[window 1] $ sqlplus -s lab@//localhost/FREEPDB1 @readers/oracle_writer.sql
[window 1] 1 row updated.
[window 1]
[window 1]
[window 1] PL/SQL procedure successfully completed.
[window 1]
[window 1]
[window 1] Rollback complete.
[window 1]
[window 1]
[window 1] BALANCE
[window 1] ----------
[window 1] 760.25
[window 2] $ sqlplus -s lab@//localhost/FREEPDB1 @readers/oracle_reader.sql
[window 2] BALANCE
[window 2] ----------
[window 2] 760.25
[window 2]
[window 2] SELECT balance FROM accounts WHERE account_id = 3 FOR UPDATE WAIT 3
[window 2] *
[window 2] ERROR at line 1:
[window 2] ORA-00054: Failed to acquire a lock (Type: "TX", Name: "Transaction",
[window 2] Description: "Lock held by a transaction to allow other transactions to wait
[window 2] for it") because it is currently held by another session. The resource being
[window 2] locked can be identified by <n> ("usn<<16 | slot") and <n> ("sequence")
[window 2] ORA-40097: Failure to acquire row lock, currently held by transaction ID
[window 2] <n>.<n>.<n>
[window 2] Help: https://docs.oracle.com/error-help/db/ora-00054/
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 readers_oracle_session.py
SQL (mssql_writer.sql) · readers/mssql_writer.sql
-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab with the practice schema, READ_COMMITTED_SNAPSHOT OFF
-- (the SQL Server default). Client: sqlcmd, window 1. The writer: it takes 260.25 out of account 3 and keeps the
-- change open for ten seconds. Start it, then run mssql_reader.sql in a second window within five seconds.
SET NOCOUNT ON;
BEGIN TRANSACTION;
UPDATE dbo.accounts SET balance = balance - 260.25 WHERE account_id = 3; -- an X lock on account 3's key
WAITFOR DELAY '00:00:10';
ROLLBACK TRANSACTION; -- puts the practice data back
SELECT balance FROM dbo.accounts WHERE account_id = 3; -- 760.25 again
GO SQL (mssql_reader.sql) · readers/mssql_reader.sql
-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab with the practice schema. Client: sqlcmd, window 2,
-- while mssql_writer.sql holds its X lock. READ COMMITTED with locking: the reader's shared lock waits for the
-- writer. SET LOCK_TIMEOUT 3000 ends the wait after 3 seconds with error 1222; the session itself goes on.
SET NOCOUNT ON;
SET LOCK_TIMEOUT 3000; -- wait at most 3000 ms for any lock
BEGIN TRY
SELECT balance FROM dbo.accounts WHERE account_id = 3; -- waits, then fails
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS error_number, ERROR_MESSAGE() AS message; -- 1222, Lock request time out ...
END CATCH;
SET LOCK_TIMEOUT -1; -- back to the default: wait for ever
SELECT is_read_committed_snapshot_on FROM sys.databases WHERE name = DB_NAME(); -- 0: readers use locks
GO """mssql_writer.sql and mssql_reader.sql in the lab: sqlcmd runs them as sa in the database lab, on a freshly loaded
practice schema, in two windows, the second started 3 seconds after the first."""
from mssql_session import Lab
with Lab() as lab:
lab.parallel([("window 1", "readers/mssql_writer.sql", 0), ("window 2", "readers/mssql_reader.sql", 3)]) Output
[window 1] $ sqlcmd -S localhost -U sa -C -d lab -i readers/mssql_writer.sql [window 1] balance [window 1] ------- [window 1] 760.25 [window 2] $ sqlcmd -S localhost -U sa -C -d lab -i readers/mssql_reader.sql [window 2] balance [window 2] ------- [window 2] error_number message [window 2] ------------ -------------------------------------- [window 2] 1222 Lock request time out period exceeded. [window 2] is_read_committed_snapshot_on [window 2] ----------------------------- [window 2] 0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 readers_mssql_session.py
The model replays that timeline for five readers and both endings of the writer’s transaction. Press Edit, set
END_AT = 3, and run it again:
"""One writer, one reader, two engines.
Session 1 takes 260.25 out of account 3 (760.25) at second 0 and ends its
transaction at second 5. Session 2 reads the balance at second 1. Each rule
is how one engine and setting answers that read: the rules come from
Oracle's Data Concurrency and Consistency chapter and Microsoft's SET
TRANSACTION ISOLATION LEVEL, SET LOCK_TIMEOUT and table hints pages.
Change END_AT or the timeout of a rule and run it again.
"""
from decimal import Decimal
COMMITTED = Decimal("760.25") # account 3 before session 1's UPDATE
CHANGED = COMMITTED - Decimal("260.25") # the value session 1 has not committed
READ_AT = 1 # second at which session 2 reads
END_AT = 5 # second at which session 1 commits or rolls back
# (label, rule, lock timeout in seconds or None for "wait for ever")
RULES = [
("Oracle, READ COMMITTED", "versions", None),
("SQL Server, locking RC", "locks", None),
("SQL Server, locking, 3 s", "locks", 3),
("SQL Server, RCSI on", "versions", None),
("SQL Server, NOLOCK", "dirty", None),
]
def answer(rule, timeout, outcome):
"""Return (second of the answer, what session 2 gets)."""
if rule == "versions": # undo data or row versions rebuild the committed row
return READ_AT, f"{COMMITTED}"
if rule == "dirty": # reads the change nobody has committed
note = "not committed yet" if outcome == "commit" else "never committed"
return READ_AT, f"{CHANGED} ({note})"
# Locking READ COMMITTED: the shared lock waits for session 1's exclusive lock.
if timeout is not None and READ_AT + timeout < END_AT:
return READ_AT + timeout, "error 1222, nothing read"
after = CHANGED if outcome == "commit" else COMMITTED
return END_AT, f"{after}"
for outcome in ("commit", "rollback"):
print(f"Session 1 ends with {outcome.upper()} at second {END_AT}:")
print(f" {'session 2 reads with':<26}{'waits':>6} answer")
for label, rule, timeout in RULES:
second, got = answer(rule, timeout, outcome)
print(f" {label:<26}{second - READ_AT:>4} s {got}")
print()
waiting = [label for label, rule, _ in RULES if rule == "locks"]
summary = f"{len(waiting)} of {len(RULES)} readers wait, all of them SQL Server with locking reads."
print(summary) Output
Session 1 ends with COMMIT at second 5: session 2 reads with waits answer Oracle, READ COMMITTED 0 s 760.25 SQL Server, locking RC 4 s 500.00 SQL Server, locking, 3 s 3 s error 1222, nothing read SQL Server, RCSI on 0 s 760.25 SQL Server, NOLOCK 0 s 500.00 (not committed yet) Session 1 ends with ROLLBACK at second 5: session 2 reads with waits answer Oracle, READ COMMITTED 0 s 760.25 SQL Server, locking RC 4 s 760.25 SQL Server, locking, 3 s 3 s error 1222, nothing read SQL Server, RCSI on 0 s 760.25 SQL Server, NOLOCK 0 s 500.00 (never committed) 2 of 5 readers wait, all of them SQL Server with locking reads.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 reader_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
Only the two locking readers wait, and only NOLOCK answers 500.00 before anything is committed, a balance the
rollback ending shows was never real. With END_AT = 3 the timed reader gets its answer after 2 seconds.
NOLOCK as the port of an Oracle read
Wrong: WITH (NOLOCK) on ported reports that now wait. It reads changes that may still be rolled back, which
Oracle never allows. Right: READ_COMMITTED_SNAPSHOT on, so readers get the last committed version; the
SQL Server isolation lesson covers the switch.
Checkpoint
Check: Readers and writers on each engine
A deadlock costs Oracle one statement and SQL Server the transaction
A A cycle of sessions in which each waits for a lock held by the next, so none can move until the database breaks the cycle. forms the same way on both engines. Session 1 moves 100.00 from account 1 to account 4, session 2 moves 50.00 from account 4 to account 1, and each locks its first row before it asks for the other’s. Both engines find the cycle; they break it differently.
Oracle rolls back one statement, the one that detected the deadlock, and returns ORA-00060 to its session. That
transaction’s earlier changes and their locks stay, so the other session keeps waiting until this one commits or
rolls back, and either session may be the one that gets the error. SQL Server’s deadlock monitor picks a victim, the
session with the lower DEADLOCK_PRIORITY or else the cheaper transaction to roll back, ends its batch, rolls back
its whole transaction, frees its locks and returns error 1205:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- Session 1 locks account 1. Session 1 takes 100.00 out of account 1 and keeps the row lock until its transaction ends. It has not touched account 4 yet.
- Session 2 locks account 4. Session 2 takes 50.00 out of account 4, in the opposite order, and holds that row. Nothing is wrong yet; each session has the one row it asked for.
- Session 1 waits for account 4. Session 1 now needs account 4 for its credit and waits for session 2. A wait is normal; it ends when session 2 commits or rolls back.
- Session 2 waits too, a cycle. Session 2 needs account 1, which session 1 holds. Each session waits for the other, and neither can ever go on by itself. Both engines find the cycle, and they break it differently.
- Oracle: one statement undone. On Oracle one waiting statement fails with ORA-00060; here it is session 2's. Only that UPDATE is rolled back. Its first UPDATE stays, account 4 stays locked, and session 1 goes on waiting.
- Oracle: the code must end it. Nothing moves until session 2 commits or rolls back. After its ROLLBACK, session 1 gets account 4 and commits, so account 1 holds 2400.00 and account 4 41350.50. Session 2 may now run its transfer again.
- SQL Server: the same cycle. Back to the cycle, now on SQL Server. Each session holds one account and waits for the other's. Session 2 runs with DEADLOCK_PRIORITY LOW, so the deadlock monitor will pick it as the victim.
- SQL Server: the victim's undo. The monitor rolls back session 2's whole transaction and frees its locks, so account 4 is back at 41250.50. Session 2 keeps nothing of its transfer; its CATCH block receives error 1205 and finds the transaction doomed.
- SQL Server: session 1 goes on. Session 1 gets account 4 at once and commits, so account 1 holds 2400.00 and account 4 41350.50. Session 2's CATCH block rolls back the doomed transaction and may run the transfer again.
The same cycle in two windows, Oracle’s first:
SQL (oracle_session_1.sql) · deadlock/oracle_session_1.sql
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1 with the practice schema.
-- Client: SQL*Plus, window 1. Session 1 moves 100.00 from account 1 to account 4: it locks account 1, waits five
-- seconds, then needs account 4. Start it, then oracle_session_2.sql in a second window within five seconds.
-- Oracle chooses no victim: either session may receive ORA-00060, so both scripts handle it the same way.
SET SERVEROUTPUT ON
SET LINESIZE 120
DECLARE
e_deadlock EXCEPTION;
PRAGMA EXCEPTION_INIT(e_deadlock, -60);
v_one accounts.balance%TYPE;
v_four accounts.balance%TYPE;
BEGIN
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; -- session 1 locks account 1
DBMS_SESSION.SLEEP(5);
UPDATE accounts SET balance = balance + 100 WHERE account_id = 4; -- needs session 2's row
COMMIT;
DBMS_OUTPUT.PUT_LINE('session 1 committed');
EXCEPTION
WHEN e_deadlock THEN
SELECT balance INTO v_one FROM accounts WHERE account_id = 1;
SELECT balance INTO v_four FROM accounts WHERE account_id = 4;
DBMS_OUTPUT.PUT_LINE('session 1: ORA-00060; this transaction still sees account 1 at ' || v_one
|| ' and account 4 at ' || v_four); -- the first UPDATE is still in place
ROLLBACK; -- frees account 1 for session 2
END;
/ SQL (oracle_session_2.sql) · deadlock/oracle_session_2.sql
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1 with the practice schema.
-- Client: SQL*Plus, window 2. Session 2 moves 50.00 from account 4 to account 1, in the opposite order to session 1,
-- so a cycle forms. The session that receives ORA-00060 loses only that statement: its first UPDATE and the lock
-- that came with it stay until it commits or rolls back, and the other session waits until then.
SET SERVEROUTPUT ON
SET LINESIZE 120
DECLARE
e_deadlock EXCEPTION;
PRAGMA EXCEPTION_INIT(e_deadlock, -60);
v_one accounts.balance%TYPE;
v_four accounts.balance%TYPE;
BEGIN
UPDATE accounts SET balance = balance - 50 WHERE account_id = 4; -- session 2 locks account 4
DBMS_SESSION.SLEEP(5);
UPDATE accounts SET balance = balance + 50 WHERE account_id = 1; -- needs session 1's row: a cycle
COMMIT;
DBMS_OUTPUT.PUT_LINE('session 2 committed');
EXCEPTION
WHEN e_deadlock THEN
SELECT balance INTO v_one FROM accounts WHERE account_id = 1;
SELECT balance INTO v_four FROM accounts WHERE account_id = 4;
DBMS_OUTPUT.PUT_LINE('session 2: ORA-00060; this transaction still sees account 1 at ' || v_one
|| ' and account 4 at ' || v_four); -- 2500 and 41200.5 when session 2 is hit
ROLLBACK; -- frees account 4 for session 1
END;
/
-- Put the practice data back after both windows have finished: undo whichever transfer committed.
-- UPDATE accounts SET balance = 2500.00 WHERE account_id = 1;
-- UPDATE accounts SET balance = 41250.50 WHERE account_id = 4;
-- COMMIT; """oracle_session_1.sql and oracle_session_2.sql in the lab: SQL*Plus runs them as LAB on a freshly loaded practice
schema, in two windows, the second started 2 seconds after the first."""
from oracle_session import Lab
with Lab() as lab:
lab.parallel([("window 1", "deadlock/oracle_session_1.sql", 0), ("window 2", "deadlock/oracle_session_2.sql", 2)]) Output
[window 1] $ sqlplus -s lab@//localhost/FREEPDB1 @deadlock/oracle_session_1.sql [window 1] session 1: ORA-00060; this transaction still sees account 1 at 2400 and account 4 at 41250.5 [window 1] [window 1] PL/SQL procedure successfully completed. [window 2] $ sqlplus -s lab@//localhost/FREEPDB1 @deadlock/oracle_session_2.sql [window 2] session 2 committed [window 2] [window 2] PL/SQL procedure successfully completed.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 deadlock_oracle_session.py
SQL (mssql_session_1.sql) · deadlock/mssql_session_1.sql
-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab with the practice schema. Client: sqlcmd, window 1.
-- Session 1 moves 100.00 from account 1 to account 4: it locks account 1, waits five seconds, then needs account 4.
-- Start it, then mssql_session_2.sql in a second window within five seconds; session 2 asks to be the victim.
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRANSACTION;
UPDATE dbo.accounts SET balance = balance - 100.00 WHERE account_id = 1; -- session 1 locks account 1
WAITFOR DELAY '00:00:05';
UPDATE dbo.accounts SET balance = balance + 100.00 WHERE account_id = 4; -- waits until session 2 is the victim
COMMIT TRANSACTION;
PRINT N'session 1 committed';
UPDATE dbo.accounts SET balance = balance + 100.00 WHERE account_id = 1; -- put the practice data back
UPDATE dbo.accounts SET balance = balance - 100.00 WHERE account_id = 4;
GO SQL (mssql_session_2.sql) · deadlock/mssql_session_2.sql
-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab with the practice schema. Client: sqlcmd, window 2.
-- Session 2 moves 50.00 from account 4 to account 1, in the opposite order, with DEADLOCK_PRIORITY LOW so that SQL
-- Server picks it as the victim. Error 1205 rolls back its whole transaction and frees account 4 for session 1;
-- inside TRY...CATCH the transaction is doomed until ROLLBACK runs.
SET NOCOUNT ON;
SET XACT_ABORT ON;
SET DEADLOCK_PRIORITY LOW; -- the lower priority is the victim
BEGIN TRY
BEGIN TRANSACTION;
UPDATE dbo.accounts SET balance = balance - 50.00 WHERE account_id = 4; -- session 2 locks account 4
WAITFOR DELAY '00:00:05';
UPDATE dbo.accounts SET balance = balance + 50.00 WHERE account_id = 1; -- needs session 1's row: a cycle
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
PRINT CONCAT(N'session 2: error ', ERROR_NUMBER(), N', XACT_STATE() ', XACT_STATE()); -- 1205 and -1
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION; -- ends the doomed transaction
PRINT CONCAT(N'session 2: @@TRANCOUNT ', @@TRANCOUNT, N' after ROLLBACK'); -- 0: nothing of the transfer is left
END CATCH;
GO """mssql_session_1.sql and mssql_session_2.sql in the lab: sqlcmd runs them as sa in the database lab, on a freshly
loaded practice schema, in two windows, the second started 2 seconds after the first."""
from mssql_session import Lab
with Lab() as lab:
lab.parallel([("window 1", "deadlock/mssql_session_1.sql", 0), ("window 2", "deadlock/mssql_session_2.sql", 2)]) Output
[window 1] $ sqlcmd -S localhost -U sa -C -d lab -i deadlock/mssql_session_1.sql [window 1] session 1 committed [window 2] $ sqlcmd -S localhost -U sa -C -d lab -i deadlock/mssql_session_2.sql [window 2] session 2: error 1205, XACT_STATE() -1 [window 2] session 2: @@TRANCOUNT 0 after ROLLBACK
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 deadlock_mssql_session.py
In the lab runs, Oracle gave ORA-00060 to session 1, which still saw its own debit, 2,400, until its handler rolled
back; SQL Server chose session 2, set to the lower DEADLOCK_PRIORITY, and its CATCH block saw 1205 with
XACT_STATE() -1.
The model applies both rules to the two transfers. Press Edit, change both 50.00 amounts of SESSION_2 to 500.00,
and run it again:
"""The same deadlock on both engines, and what each leaves behind.
Session 1 moves 100.00 from account 1 to account 4; session 2 moves 50.00
from account 4 to account 1, in the opposite order, so each ends up waiting
for the row the other holds. Session 2 receives the deadlock error on its
second UPDATE. The rules come from Oracle's Data Concurrency and Consistency
chapter (ORA-00060 rolls back one statement) and Microsoft's deadlocks guide
(error 1205 rolls back the victim's transaction and releases its locks).
"""
from decimal import Decimal
START = {1: Decimal("2500.00"), 4: Decimal("41250.50")}
SESSION_1 = [(1, Decimal("-100.00")), (4, Decimal("100.00"))]
SESSION_2 = [(4, Decimal("-50.00")), (1, Decimal("50.00"))]
def after_error(engine):
"""State right after session 2's second UPDATE fails."""
if engine == "Oracle":
kept = SESSION_2[:1] # only the failing statement is rolled back
open_tx, locks = "still open", "account 4"
else:
kept = [] # the whole transaction is rolled back
open_tx, locks = "rolled back (XACT_STATE() -1 in CATCH)", "none"
return kept, open_tx, locks
def run(changes, balances):
"""Apply a list of (account, amount) changes to a copy of balances."""
out = dict(balances)
for account, amount in changes:
out[account] += amount
return out
for engine, error in (("Oracle", "ORA-00060"), ("SQL Server", "error 1205")):
kept, open_tx, locks = after_error(engine)
pending = run(kept, START)
waits = "waits for account 4" if locks != "none" else "gets account 4 and commits"
print(f"{engine}: session 2 receives {error}")
print(f" changes of session 2 still in place: {len(kept)} of {len(SESSION_2)}")
print(f" account 4 as session 2 sees it: {pending[4]}")
print(f" session 2's transaction: {open_tx}; locks it holds: {locks}")
print(f" session 1 {waits}")
# The response that ends the same way on both engines: roll back, run the unit again.
final = run(SESSION_1 + SESSION_2, START)
total = sum(final.values())
print("After ROLLBACK and a second run of session 2, on both engines:")
print(f" account 1 {final[1]}, account 4 {final[4]}, total {total} (as before: {sum(START.values())})") Output
Oracle: session 2 receives ORA-00060 changes of session 2 still in place: 1 of 2 account 4 as session 2 sees it: 41200.50 session 2's transaction: still open; locks it holds: account 4 session 1 waits for account 4 SQL Server: session 2 receives error 1205 changes of session 2 still in place: 0 of 2 account 4 as session 2 sees it: 41250.50 session 2's transaction: rolled back (XACT_STATE() -1 in CATCH); locks it holds: none session 1 gets account 4 and commits After ROLLBACK and a second run of session 2, on both engines: account 1 2450.00, account 4 41300.50, total 43750.50 (as before: 43750.50)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 deadlock_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
After ORA-00060 session 2 still holds account 4 (41200.50); after 1205 it holds nothing, and its rerun leaves 2450.00 and 41300.50 on both engines.
Key idea
ORA-00060 undoes one statement and leaves the transaction and its locks to the code; error 1205 undoes the victim’s whole transaction before the code sees the error.
The three main differences:
| What happens | Oracle | SQL Server |
|---|---|---|
| a read meets an open change | gets the committed version | waits, unless RCSI is on |
| a deadlock error undoes | one statement: ORA-00060 | the transaction: error 1205 |
SERIALIZABLE meets a newer change |
fails with ORA-08177 | holds locks, others wait |
FOR UPDATE NOWAIT, WAIT and SKIP LOCKED become session settings and table hints in T-SQL, and a deadlock handler
written for one engine can leave wrong balances on the other.
Checkpoint
Check: What a deadlock error undoes
Isolation levels and row-lock options, ported from Oracle to SQL Server Pro: opens with a Pro, Premium or Ultimate pass
Code that sets an isolation level or a row-lock option in Oracle needs another spelling in T-SQL, and sometimes another level. This part maps each one on two timelines and in scripts for both engines.
In this part:
- Choosing the SQL Server level for an Oracle SERIALIZABLE transaction, on two timelines
- NOWAIT, WAIT and SKIP LOCKED in T-SQL, with the scripts for both engines
5 runnable examples (Oracle, Python, SQL (mssql_claims.sql), SQL (mssql_t1.sql), SQL (mssql_t2.sql), SQL (oracle_claims.sql), SQL (oracle_t1.sql), SQL (oracle_t2.sql), SQL Server) · 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).
Retry logic that leaves the same balances on both engines Pro: opens with a Pro, Premium or Ultimate pass
A deadlock handler that is right on one engine can lose or create money on the other. This part replays four common handlers on the lab’s transfer and builds one retry unit that both engines can run.
In this part:
- Four deadlock handlers, replayed on the lab’s transfer
- One retry unit for both engines, step by step
1 diagram · 3 runnable examples (Oracle, Python, SQL (transfer_retry_mssql.sql), SQL (transfer_retry_oracle.sql), SQL Server) · about 7 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: predict what each engine does
Each question freezes one moment of the timelines above: decide what the waiting session sees or keeps.
Practice that adapts to you
Practise this lesson
9 questions about 6 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): on SQL Server a report can wait for a writer, on Oracle it cannot; why?
Oracle answers a query from the data committed when the query began and rebuilds changed rows from undo data, so the
reader takes no lock and never meets the writer’s. SQL Server’s default READ COMMITTED takes a shared lock per row,
which waits for a writer’s exclusive lock until the writer’s transaction ends or the lock timeout ends the statement
with error 1222. With READ_COMMITTED_SNAPSHOT on, SQL Server readers use row versions and stop waiting.
Each longer question is a port that misbehaves; sketch your fix before you open it:
-
Mid to senior
A transfer procedure ported from PL/SQL to T-SQL kept its old deadlock handler: catch the error, pause, run the failed UPDATE again, commit. Since the port, a few customers a week receive credits that nobody debited. Explain what changed and redesign the handler so that it is correct on both engines.
-
Mid to staff
An Oracle batch runs at SERIALIZABLE and retries on ORA-08177. Its port to SQL Server sets SERIALIZABLE too. It never fails now, but order entry waits for minutes while the batch runs. Explain why, and choose the SQL Server setup that keeps the batch's Oracle behaviour.
-
Mid to senior
Three queries make up a month-end report: the ledger total, the total per account and the number of postings. On Oracle it runs in SET TRANSACTION READ ONLY and the numbers always agree. On SQL Server with READ_COMMITTED_SNAPSHOT on, they sometimes disagree by one posting. Explain why, and port the report.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Why do Oracle's readers and writers not block each other?
A change writes the old values as undo data, and a query that meets a changed row rebuilds the version committed when the query began. Oracle states it as two rules: a reader never blocks a writer and a writer never blocks a reader. The exception is SELECT ... FOR UPDATE, which locks the rows it reads.
-
What happens to the same read on SQL Server's default settings?
READ_COMMITTED_SNAPSHOT is off, so the reader takes a shared lock on each row, which cannot be granted while a writer holds its exclusive lock: it waits until the writer's transaction ends or until its lock timeout runs out, -1 by default, and SET LOCK_TIMEOUT 3000 ends the statement after 3 seconds with error 1222.
-
Why is WITH (NOLOCK) the wrong answer for a ported report that now waits?
It reads changes that may still be rolled back, a balance that was never real, which Oracle never allows. Turning READ_COMMITTED_SNAPSHOT on gives readers the last committed version instead.
-
How does a deadlock form on both engines?
Each session locks its first row before it asks for the other's, so two transfers in opposite directions close a cycle. Both engines find the cycle; they differ in how they break it.
-
What does Oracle undo when it breaks a deadlock?
One statement, the one that detected the deadlock, returning ORA-00060 to its session. That transaction's earlier changes and their locks stay, so the other session keeps waiting until this one commits or rolls back, and either session may be the one that gets the error.
-
What does SQL Server undo when it breaks a deadlock?
The victim's whole transaction: the deadlock monitor picks the session with the lower DEADLOCK_PRIORITY or else the cheaper transaction to roll back, ends its batch, rolls the transaction back, frees its locks and returns error 1205 before the code sees anything.
-
What does SERIALIZABLE do on each engine when it meets a newer change?
Oracle fails with ORA-08177; SQL Server holds locks instead, so other sessions wait.
-
What becomes of FOR UPDATE NOWAIT, WAIT and SKIP LOCKED in a port?
They become session settings and table hints in T-SQL, and a deadlock handler written for one engine can leave wrong balances on the other, because the two undo different amounts of work.
Key takeaways
- Oracle rebuilds the committed version of a changed row from undo data, so its readers and writers do not block each other.
- SQL Server’s default READ COMMITTED makes a reader wait for a writer’s exclusive lock until the lock timeout
(error 1222);
NOLOCKis never the cure. - ORA-00060 undoes only the statement that detected the deadlock and keeps the transaction and its locks, while error 1205 undoes the victim’s whole transaction and frees its locks.
Terms in this lesson
- lock timeout
- How long a statement waits for a lock that another session holds before it gives up. In SQL Server it is SET LOCK_TIMEOUT, -1 by default, which waits for ever; past the limit the statement fails with error 1222.
- multiversion read consistency
- Oracle's way of giving each query the data as it was committed at one moment: a query that meets a changed row rebuilds the committed version from undo data, so readers take no locks and never wait for writers.
- undo data
- The old values Oracle records whenever a transaction changes data. A rollback uses them to undo the change, and a query uses them to rebuild the version of a row that was committed when the query began.
References
- Data concurrency and consistency (Oracle AI Database Concepts 26ai) (Oracle)
- Transactions (Oracle AI Database Concepts 26ai) (Oracle)
- SET TRANSACTION (Oracle AI Database SQL Language Reference 26ai) (Oracle)
- SELECT (Oracle AI Database SQL Language Reference 26ai) (Oracle)
- SQL processing for application developers (Oracle AI Database Development Guide 26ai) (Oracle)
- ORA-00060 deadlock detected while waiting for resource (Oracle)
- ORA-08177 can't serialize access for this transaction (Oracle)
- ORA-30006 resource busy; acquire with WAIT timeout expired (Oracle)
- DBMS_RANDOM (Oracle AI Database PL/SQL Packages and Types Reference 26ai) (Oracle)
- Transaction locking and row versioning guide (Microsoft)
- Deadlocks guide (Microsoft)
- SET TRANSACTION ISOLATION LEVEL (Transact-SQL) (Microsoft)
- SET LOCK_TIMEOUT (Transact-SQL) (Microsoft)
- Table hints (Transact-SQL) (Microsoft)
- SET XACT_ABORT (Transact-SQL) (Microsoft)
- SET DEADLOCK_PRIORITY (Transact-SQL) (Microsoft)
- Database engine events and errors (1000 to 1999) (Microsoft)
- Database engine events and errors (3000 to 3999) (Microsoft)
Report a problem with this lesson
Kept only in this browser. Your Learn progress