Tag: Transactions

  • ACID Transactions in SQL Server: BEGIN, COMMIT, ROLLBACK Explained

    ACID Transactions in SQL Server: BEGIN, COMMIT, ROLLBACK Explained

    You’ve been using BEGIN TRANSACTION, COMMIT, and ROLLBACK since Chapter 4’s stored procedure pattern, largely as boilerplate to copy. This lesson is about the actual guarantee underneath that boilerplate — four properties that guarantee your data stays correct even when things go wrong mid-operation, not just “it undoes things on error.”

    ACID, via a bank transfer(four guarantees, one story)AAtomicityall or nothingCConsistencyvalid state → valid stateIIsolationno peeking, uncommittedDDurabilitysurvives a crashwatch Atomicity work on a transfer:Account 1$5000 → $4000UPDATE #1Account 2$2000 → $3000UPDATE #2$1000 mid-transfer💥 crash here?ROLLBACK — both untouched,$0 lost. that’s Atomicity.

    The Four Properties

    Property Guarantees
    Atomicity All statements succeed together, or none do — no half-finished transaction is ever visible
    Consistency The database moves from one valid state to another — constraints (Fundamentals Ch.6) are never violated, even mid-transaction from another session’s view
    Isolation Concurrent transactions don’t see each other’s uncommitted changes — the specific property Lesson 2 explores in depth
    Durability Once committed, changes survive a crash — guaranteed by the transaction log’s write-ahead logging, from the architecture covered in the Performance Tuning course

    The Classic Transfer

    BEGIN TRY
        BEGIN TRANSACTION;
    
        UPDATE dbo.BankAccount SET balance = balance - 1000 WHERE account_id = 1;
        UPDATE dbo.BankAccount SET balance = balance + 1000 WHERE account_id = 2;
    
        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
        THROW;
    END CATCH;

    What Happens Without a Transaction

    A crash between the two UPDATEs leaves $1000 debited but never credited — money vanishes

    Without wrapping both updates in one transaction, exactly this failure is possible — not hypothetically, but as a genuine risk any time two related writes happen as separate statements. That’s the specific problem transactions exist to prevent, and it’s Atomicity specifically doing the protecting here: SQL Server guarantees that if the crash happens after the first UPDATE but before the second, the entire transaction rolls back on recovery, leaving neither account touched.

    Proving It Yourself

    -- Deliberately introduce a failure between the two updates to watch Atomicity work
    BEGIN TRY
        BEGIN TRANSACTION;
        UPDATE dbo.BankAccount SET balance = balance - 1000 WHERE account_id = 1;
        SELECT 1/0; -- deliberate error, simulates a crash mid-transfer
        UPDATE dbo.BankAccount SET balance = balance + 1000 WHERE account_id = 2;
        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
    END CATCH;
    
    SELECT balance FROM dbo.BankAccount WHERE account_id IN (1,2); -- both unchanged, no money lost
    Practice tip: Run this deliberately-failing version and confirm both balances are unchanged, then remove the SELECT 1/0; line and confirm the transfer completes correctly. Seeing both outcomes, not just reading about them, is what makes ACID feel real rather than theoretical.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.

  • SQL Server Isolation Levels and Deadlocks: READ COMMITTED, SNAPSHOT, and Prevention

    SQL Server Isolation Levels and Deadlocks: READ COMMITTED, SNAPSHOT, and Prevention

    The “I” in ACID (previous lesson) said concurrent transactions don’t see each other’s uncommitted changes — but how much isolation, exactly, is itself a tunable setting with real tradeoffs. This lesson covers what each level actually permits, and how misunderstanding isolation is precisely how deadlocks catch people off guard.

    Isolation levels & the deadlock cyclestricter → more blockingREAD UNCOMMITTEDdirty reads allowed= NOLOCK hintREAD COMMITTEDthe defaultno dirty readsSERIALIZABLEstrictest, most blockingacts one-at-a-timeSNAPSHOTrow-versioning insteadreaders never block writersthen two sessions deadlock like this:Session 1HOLDS: 🔒 Account AWANTS: Account BSession 2HOLDS: 🔒 Account BWANTS: Account ADEADLOCK!💥chosen as VICTIM — error 1205, rolled backcommits successfullythe real fix: always lockresources in the SAME ordereverywhere — breaks the cycle 📌

    The Isolation Levels

    Level Notes
    READ UNCOMMITTED Fastest, allows dirty reads (seeing another transaction’s uncommitted, possibly-about-to-be-rolled-back changes) — rarely appropriate; this is what the SQL-hint WITH (NOLOCK) effectively opts a single query into
    READ COMMITTED (default) Never reads uncommitted data — SQL Server’s out-of-the-box behavior for every connection unless explicitly changed
    SERIALIZABLE Strictest, most blocking — behaves as if transactions ran one at a time, at real concurrency cost
    SNAPSHOT Row versioning — readers never block writers, and writers never block readers, at the cost of tempdb overhead for storing row versions
    -- Setting isolation level for a session
    SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
    
    -- Must be enabled at the DATABASE level first, or SNAPSHOT requests are rejected
    ALTER DATABASE CURRENT SET ALLOW_SNAPSHOT_ISOLATION ON;

    How a Deadlock Forms

    Transaction 1: locks A Transaction 2: locks B T1 wants B — blocked T2 wants A — blocked

    Each transaction holds a lock the other needs — a circular wait. SQL Server automatically detects this and kills one transaction (the “deadlock victim,” chosen by lowest rollback cost by default, meaning the transaction that’s done the least work so far is usually the one sacrificed).

    -- Reproducing this exact scenario needs two sessions running simultaneously:
    -- Session 1:
    BEGIN TRANSACTION;
    UPDATE dbo.BankAccount SET balance = balance - 100 WHERE account_id = 1; -- locks account 1
    -- (pause here, run Session 2's first line, then continue)
    UPDATE dbo.BankAccount SET balance = balance + 100 WHERE account_id = 2; -- wants account 2, blocked
    
    -- Session 2 (run its first line while Session 1 is paused above):
    BEGIN TRANSACTION;
    UPDATE dbo.BankAccount SET balance = balance - 50 WHERE account_id = 2; -- locks account 2
    UPDATE dbo.BankAccount SET balance = balance + 50 WHERE account_id = 1; -- wants account 1 → deadlock

    The Real Fix

    Always access shared tables/rows in the same consistent order across every transaction in your application (e.g. always update the lower account_id first). If every transaction acquires locks in the same sequence, the circular-wait condition can’t form — in the reproduction above, if both sessions updated account 1 before account 2 every time, neither would ever end up waiting on a lock the other already held while itself holding something the other needed.

    Common mistake: Treating deadlocks as a database bug to “fix” with more indexes or a bigger server. Deadlocks are fundamentally an application logic problem — the fix lives in the order your code acquires locks, not in the database’s configuration or hardware.
    Practice tip: If you have two query windows available, try reproducing the deadlock above exactly as written, pausing Session 1 right after its first UPDATE to give Session 2 time to run. Watch SQL Server pick a victim and return error 1205 to one of the two sessions — seeing the actual deadlock error is worth more than reading the theory.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.

  • Diagnosing Blocking in SQL Server: Finding Who’s Blocking Whom

    Diagnosing Blocking in SQL Server: Finding Who’s Blocking Whom

    A deadlock (previous lesson) is SQL Server actively resolving an impossible situation by killing one transaction. Ordinary blocking is different and far more common: one session simply waiting its turn for a lock another session holds, which resolves on its own once the first session finishes — no error, no victim, just a delay. Not every case of one session waiting on another is a bug; moderate, short-lived blocking is normal under real concurrent load. Here’s how to tell when it’s actually a problem.

    Who’s blocking whom?(one session waiting on another’s lock)Session 61BLOCKED — waitingwait_type: LCK_M_Ublocking_session_id = 55Session 55HOLDS the lockstill running…the usual root cause — a transaction held open too long:BEGIN TRAN…calls a payment API, waits on network…COMMITlocks held this whole time —everyone else just queues up 😩short blocking under loadis normal — the fix is aSHORTER window, not more RAM 📌

    Finding the Blocker

    SELECT
        blocking.session_id AS blocking_session,
        blocked.session_id AS blocked_session,
        blocked.wait_type,
        blocked.wait_time,
        blocked_text.text AS blocked_query
    FROM sys.dm_exec_requests blocked
    JOIN sys.dm_exec_sessions blocking ON blocking.session_id = blocked.blocking_session_id
    CROSS APPLY sys.dm_exec_sql_text(blocked.sql_handle) blocked_text
    WHERE blocked.blocking_session_id <> 0;

    This is the same blocking_session_id column flagged as the single most actionable field in Chapter 8’s DMV lesson — this query is that pointer, fully realized into a real diagnostic report.

    The Real Root Cause, Most of the Time

    A transaction held open far longer than necessary (e.g. waiting on user input mid-transaction)

    It becomes a genuine problem when a transaction holds locks far longer than necessary. The fix is almost always “keep transactions as short as possible” — not “add more indexes” or “increase timeout,” which just makes users wait longer for a symptom instead of fixing the actual cause.

    -- The specific anti-pattern that causes most real-world blocking incidents:
    BEGIN TRANSACTION;
    UPDATE dbo.Order SET status = 'processing' WHERE order_id = 500;
    -- ...application code here calls an external API, waits on a user click,
    -- or does anything else slow, all while the transaction (and its locks) stays open...
    COMMIT TRANSACTION; -- doesn't happen until that slow thing finishes
    
    -- The fix: do all slow, non-database work BEFORE or AFTER the transaction,
    -- never DURING it. Keep the window between BEGIN and COMMIT as short as possible.
    Common mistake: Opening a transaction, then calling out to an external service (payment gateway, email API, another microservice) before committing. Any latency or hang in that external call directly extends how long your locks are held, potentially blocking every other session that needs the same rows — this single anti-pattern is behind a large share of real production blocking incidents.

    Chapter 9, End to End

    These three lessons form one continuous story: ACID (Lesson 1) is the guarantee; isolation levels (Lesson 2) are the tunable dial controlling how strictly “Isolation” is enforced, with deadlocks as the sharp edge of getting concurrent access patterns wrong; and blocking (this lesson) is the everyday, non-error version of the same underlying mechanism — locks doing their job, just visible when they hold longer than expected.

    Practice tip: Open a transaction, run an UPDATE, and deliberately leave it open (don’t COMMIT or ROLLBACK yet) in one query window. In a second window, run the blocking-detection query above and confirm you can see your own first session listed as the blocker. This hands-on confirmation is exactly the diagnostic workflow you’d use on a real, unfamiliar production incident.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.