Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 25 minutes
  • Examples run with oracle-free 23.26.3.0.0, mssql 17.0.5005.3, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 10 of 25 minutes free
Free: A reader waits for a writer on SQL Server, not on Oracle · A deadlock costs Oracle one statement and SQL Server the transaction. With a pass: 2 in-depth parts and 3 interview answers, about 15 minutes.

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:

Interactive · step by step One open UPDATE, one reader on each engine Step through the two writers and the two readers, and watch which reader waits and what each one gets.
One open UPDATE, one reader on each engine, step 6 of 6. Showing Oracle writer (rolled back), SQL Server writer (rolled back), account 3 (760.25), account 3 (760.25), Oracle reader (760.25 at once), SQL Server reader (760.25 (RCSI)). In focus: Oracle writer, SQL Server writer, account 3, account 3.Oracle writerrolled backSQL Server writerrolled backaccount 3760.25account 3760.25Oracle reader760.25 at onceSQL Server reader760.25 (RCSI)

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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:

Oracle: the plain read answers at once, FOR UPDATE WAIT 3 does not

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, run in the lab Python · readers_oracle_session.py
"""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 Server: the reader waits 3 seconds, then error 1222

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, run in the lab Python · readers_mssql_session.py
"""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 open UPDATE, five ways to read account 3 Python · model/reader_model.py
"""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

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

  1. Question 1 of 2 Session 1 has an open UPDATE that takes account 3 from 760.25 to 500.00. On Oracle, session 2 runs SELECT balance FROM accounts WHERE account_id = 3. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It returns 760.25 at once

    Multiversion read consistency gives the query the data as committed when it began: 760.25. Only a SELECT ... FOR UPDATE would wait for session 1's row lock.

  2. Question 2 of 2 On SQL Server with READ_COMMITTED_SNAPSHOT off, session 2 runs SET LOCK_TIMEOUT 3000 and reads account 3 at second 1, while session 1's open UPDATE of that row lasts until second 10. At which second does session 2 receive error 1222?

    Type a number.

    A hint

    The timeout counts from the moment the read starts to wait.

    Show the answer: Question 2 of 2

    Answer: 4 seconds

    The shared lock waits from second 1; after 3000 ms, at second 4, the statement ends with error 1222, six seconds before session 1 would have finished.

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:

Interactive · step by step The same deadlock, broken two ways Step through the cycle, then Oracle's way out and SQL Server's, and compare what each session keeps.
The same deadlock, broken two ways, step 9 of 9. Showing Session 1 (commits), Session 2 (rolled back), account 1 (2400.00), account 4 (41350.50). In focus: Session 1, account 4.holdsSession 1commitsSession 2rolled backaccount 12400.00account 441350.50

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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:

Oracle: the transfer cycle and ORA-00060

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, run in the lab Python · deadlock_oracle_session.py
"""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 Server: the same cycle and error 1205

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, run in the lab Python · deadlock_mssql_session.py
"""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:

What each engine leaves after the deadlock error Python · model/deadlock_model.py
"""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

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

  1. Question 1 of 2 On Oracle, session 2 receives ORA-00060 on its second UPDATE, the credit to account 1. What is still in place in session 2's transaction?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Its first UPDATE, the debit of account 4, and the lock on that row

    ORA-00060 is a statement-level rollback. The debit and its lock stay until session 2 commits or rolls back, so session 1 keeps waiting for account 4 until then.

  2. Question 2 of 2 On SQL Server, session 2 is chosen as the deadlock victim. What happens to session 1, which was waiting for session 2's row?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It gets the row at once and can go on to commit

    The deadlock monitor ends the victim's batch, rolls back its transaction and releases its locks, so the waiting session continues at once.

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.

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.

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.

  1. Question 1 of 9 Why does a plain Oracle query not wait for a row that another session has changed?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: It rebuilds the committed version of the row from undo data and takes no lock

    Undo data holds the old values, so the query sees the row as committed when the query began.

  2. Question 2 of 9 In a new SQL Server database, how do READ COMMITTED readers meet a writer's open change?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: They wait, because READ_COMMITTED_SNAPSHOT is off by default and reads take shared locks

    With locking READ COMMITTED, a shared lock waits for the writer's exclusive lock until the writer's transaction ends.

  3. Question 3 of 9 On SQL Server with READ_COMMITTED_SNAPSHOT off and SET LOCK_TIMEOUT never run, a reader starts at second 0 on a row whose writer commits at second 60. For how many seconds does the reader wait?

    Type a number.

    Show the answer: Question 3 of 9

    Answer: 60 seconds

    The default timeout is -1, wait for ever, so the reader waits the full 60 seconds and then reads the committed value.

  4. Question 4 of 9 Session 1 changes account 3 from 760.25 to 500.00 and later rolls back. In between, a SQL Server report with NOLOCK read the row. What did the report show?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: 500.00, a balance that was never committed

    The model's rollback ending shows the same: only the NOLOCK reader answers 500.00.

  5. Question 5 of 9 In the reader model, END_AT is 3 and the timed reader starts at second 1 with a 3-second limit. The writer commits at second 3. What does that reader get?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: 500.00 at second 3, after waiting 2 seconds

    A lock timeout only matters when the writer holds the row longer than the limit.

  6. Question 6 of 9 Two Oracle sessions deadlock. Which one receives ORA-00060?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: Either one; Oracle rolls back a statement of the session that detects the deadlock

    Code on Oracle must therefore handle ORA-00060 in every session that can take part in the cycle.

  7. Question 7 of 9 After ORA-00060, session 2's handler logs the error and then waits for a user to decide. What does session 1 do meanwhile?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: It keeps waiting for account 4, which session 2 still holds

    On Oracle the victim's code must commit or roll back promptly, or the other session stays blocked.

  8. Question 8 of 9 Session A runs SET DEADLOCK_PRIORITY LOW and session B keeps the default; they deadlock on SQL Server. Which session is the victim?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: Session A

    DEADLOCK_PRIORITY decides first; with equal priorities SQL Server picks the transaction that is least expensive to roll back.

  9. Question 9 of 9 A web request on SQL Server ignores errors from its database call, and its transfer is chosen as a deadlock victim. What is the true state?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: The transfer was rolled back, but the request may still report success

    Handle error 1205 in every unit that can be a victim, or the caller is told about work that never happened.

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:

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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); NOLOCK is 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.