{"id":113333,"date":"2026-11-06T12:00:00","date_gmt":"2026-11-06T12:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=113333"},"modified":"2026-10-19T08:40:29","modified_gmt":"2026-10-19T08:40:29","slug":"5-common-sql-server-transaction-problems-and-how-to-solve-them","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/5-common-sql-server-transaction-problems-and-how-to-solve-them\/","title":{"rendered":"5 common SQL Server transaction problems (and how to solve them)"},"content":{"rendered":"\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>SQL Server transactions can produce results that seem wrong even when the code looks correct. Perhaps it&#8217;s a row count that doesn&#8217;t match, a query that waits when it shouldn&#8217;t, or an audit record that vanishes after a rollback. This article works through five such problems as hands-on tasks, each with a setup script, an explanation of why it happens, and a solution.<\/strong><\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-the-problems-and-their-solutions-covered-in-this-guide\">The problems (and their solutions) covered in this guide:<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/?p=113333#:~:text=recommend%20for%20each.-,The%20problem%3A%20a%20query%20blocks%20while%20a%20long%2Drunning%20transaction%20is%20still%20inserting%20rows,-Suppose%20we%20have\" target=\"_blank\" rel=\"noreferrer noopener\">A query blocks while a long-running transaction is still inserting rows<\/a><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/?p=113333#:~:text=rows%20multiple%20times.-,The%20problem%3A%20a%20transaction%20reads%20different%20values%20for%20the%20same%20row%20twice,-Suppose%20we%20have\" target=\"_blank\" rel=\"noreferrer noopener\">A transaction reads different values for the same row twice<\/a><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/?p=113333#:~:text=try%20for%20free-,The%20problem%3A%20a%20transaction%20gets%20a%20different%20row%20count%20without%20inserting%20or%20deleting%20anything%20itself,-Let%E2%80%99s%20consider%20a\" target=\"_blank\" rel=\"noreferrer noopener\">A transaction gets a different row count without inserting or deleting anything itself<\/a><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/?p=113333#:~:text=LEVEL%20READ%20COMMITTED-,The%20problem%3A%20a%20rolled%2Dback%20transaction%20wipes%20out%20the%20audit%20record%20with%20it,-We%20need%20to\" target=\"_blank\" rel=\"noreferrer noopener\">A rolled-back transaction wipes out the audit record with it<\/a><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/?p=113333#:~:text=and%20compliance%20requirements.-,The%20problem%3A%20a%20nested%20rollback%20undoes%20more%20than%20it%20should,-In%20the%20previous\" target=\"_blank\" rel=\"noreferrer noopener\">A nested rollback undoes more than it should<\/a><\/strong><\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>I encourage you to try identifying and solving the problem yourself, before reading my solution.<\/em> <em>Also, please note: each task should be tested independently and, before starting a new task, run the setup script<\/em> <em>associated with it.<\/em><\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"h-the-problems-and-their-solutions\">The problems (and their solutions)<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Without further ado, let\u2019s move onto the problems &#8211; and, most importantly, the solutions I recommend for each.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-problem-a-query-blocks-while-a-long-running-transaction-is-still-inserting-rows\">The problem: a query blocks while a long-running transaction is still inserting rows<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Suppose we have a table named <code>TestTable<\/code> with the following structure:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">IF DB_ID(N'TestDB') IS NULL\n    EXEC('CREATE DATABASE TestDB');\nGO\n\nUSE TestDB\nGO\n\nDROP TABLE IF EXISTS TestTable;\nGO\n-- Create TestTable\nCREATE TABLE TestTable\n(\n    ID INT IDENTITY(1,1),\n    Value INT\n)<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We are inserting a large amount of data into <code>TestTable<\/code> using the query below:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE TestDB\n\nGO\n\nBEGIN TRY\n\n&nbsp;&nbsp;&nbsp; BEGIN TRANSACTION\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -- Insert data into TestTable\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -- The value 500000 is used to facilitate the testing process.\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -- Please choose this value carefully for your server to avoid overloading it.\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DECLARE @i INT = 1\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; WHILE (@i &lt;= 500000)\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; BEGIN\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; INSERT INTO TestTable(Value)\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; VALUES(@i)\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; SET @i = @i + 1\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; END\n\n&nbsp;&nbsp;&nbsp; COMMIT\n\nEND TRY\n\nBEGIN CATCH\n\n&nbsp;&nbsp;&nbsp; IF @@TRANCOUNT &gt; 0\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ROLLBACK\n\n&nbsp;&nbsp;&nbsp; SELECT ERROR_MESSAGE();\n\nEND CATCH<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">As the <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/transactions\/\" target=\"_blank\" rel=\"noreferrer noopener\">transaction<\/a> runs for a long time, we want to periodically check how many rows have been <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/the-basics-of-inserting-data-into-a-sql-server-table\/\" target=\"_blank\" rel=\"noreferrer noopener\">inserted into the table<\/a> so far. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So, we open another query window, and run the <code>SELECT COUNT(*) FROM TestTable<\/code> query.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This query waits until the first query finishes and then returns the result:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"754\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-6-1024x754.png\" alt=\"The result the query returns v1\" class=\"wp-image-113336\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-6-1024x754.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-6-300x221.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-6-768x565.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-6.png 1110w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"862\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-7-1024x862.png\" alt=\"The result the query returns v2\" class=\"wp-image-113337\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-7-1024x862.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-7-300x252.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-7-768x646.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-7.png 1162w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">How can we rewrite the query to check the number of rows inserted so far without waiting for the transaction to complete? Let&#8217;s find out.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-solution-read-uncommitted-data-with-nolock-or-read-uncommitted-how-to-guide\">The solution: read uncommitted data with <code>NOLOCK<\/code> or <code>READ UNCOMMITTED<\/code> (how-to guide)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The reason the second query waits for the first transaction to complete before returning the result is that it uses the <code>READ COMMITTED<\/code> <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/questions-about-t-sql-transaction-isolation-levels-you-were-too-shy-to-ask\/\" target=\"_blank\" rel=\"noreferrer noopener\">transaction isolation level<\/a> by default (this example assumes the database is using the default locking behavior for the <code>READ COMMITTED<\/code> isolation level). <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This isolation level prevents <strong>dirty reads<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In simple terms, showing the row count while rows are still being inserted into the table may <em>not<\/em> be accurate because the transaction has yet to be completed (so can still be <a href=\"https:\/\/www.red-gate.com\/simple-talk\/podcasts\/rollback-vs-roll-forward-in-databases\/\" target=\"_blank\" rel=\"noreferrer noopener\">rolled back<\/a>.)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, since we want to check the number of inserted rows at a specific moment, we can allow dirty reads &#8211; bearing in mind that the result may include rows that can be rolled back later on. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To do this, in the second query, we can use the <code><strong>WITH (NOLOCK)<\/strong><\/code> <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/queries\/hints-transact-sql-table?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">table hint<\/a> (Solution 1), or set the transaction isolation level for the session to <code><strong>READ UNCOMMITTED<\/strong><\/code> (Solution 2):<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">-- Solution 1: Read uncommitted data without waiting for the transaction to complete\nSELECT COUNT(*) \nFROM TestTable WITH (NOLOCK)<\/pre><\/div>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"653\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-12-1024x653.png\" alt=\"image showing solution 1\" class=\"wp-image-113343\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-12-1024x653.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-12-300x191.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-12-768x490.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-12.png 1162w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">-- Solution 2: Set the isolation level to read uncommitted data\nSET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED\n\nSELECT COUNT(*) \nFROM TestTable\n\n-- Optional: reset the isolation level back to the default\nSET TRANSACTION ISOLATION LEVEL READ COMMITTED<\/pre><\/div>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"735\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-13-1024x735.png\" alt=\"image showing solution 2\" class=\"wp-image-113344\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-13-1024x735.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-13-300x215.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-13-768x551.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-13.png 1162w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">It&#8217;s worth mentioning that <code><strong>NOLOCK<\/strong><\/code> can also produce inaccurate results due to concurrent data modifications, such as missing or reading the same rows multiple times.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-problem-a-transaction-reads-different-values-for-the-same-row-twice\">The problem: a transaction reads different values for the same row twice<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Suppose we have <code>TestTable<\/code> with the following structure:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">IF DB_ID(N'TestDB') IS NULL\n    EXEC('CREATE DATABASE TestDB');\nGO\n\nUSE TestDB\nGO\n\nDROP TABLE IF EXISTS TestTable;\nGO\n\n-- Create TestTable\nCREATE TABLE TestTable\n(\n    ID INT PRIMARY KEY, \n    FirstName VARCHAR(40) NOT NULL,\n    LastName VARCHAR(40) NOT NULL,\n    Email VARCHAR(254) NOT NULL\n)\n\n-- Create a unique index on the Email column\nCREATE UNIQUE INDEX UIX_TestTable_Email \nON TestTable(Email)\n\n-- Insert sample data into TestTable\nINSERT INTO TestTable(ID, FirstName, LastName, Email)\nVALUES  (1, 'FirstName1', 'LastName1', 'user1@example.com'),\n        (2, 'FirstName2', 'LastName2', 'user2@example.com'),\n        (3, 'FirstName3', 'LastName3', 'user3@example.com');<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We are running the following transaction, which has many other statements between \u201cStatement 1\u201d and \u201cStatement 2\u201d. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this example, we are using <code><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/language-elements\/waitfor-transact-sql?view=sql-server-ver17\"><strong>W<\/strong><\/a><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/language-elements\/waitfor-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>AITFOR DELAY<\/strong><\/a><\/code> instead of these statements to simplify the demonstration:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE TestDB\nGO\n\n-- Query 1\nDECLARE @fname VARCHAR(40),\n        @lname VARCHAR(40),\n        @email VARCHAR(254) = 'user2@example.com'\n\nBEGIN TRANSACTION;\n\n    -- Statement 1: Get the first name by email\n    SET @fname = (SELECT FirstName FROM TestTable WHERE Email = @email)\n\n    -- Simulate other statements running between Statement 1 and Statement 2\n    WAITFOR DELAY '00:00:05'\n\n    -- Statement 2: Get the last name by email\n    SET @lname = (SELECT LastName FROM TestTable WHERE Email = @email)\n\nCOMMIT\n\nSELECT @fname + ' ' + @lname AS FullName<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">After \u201cStatement 1\u201d finishes and before \u201cStatement 2\u201d starts, \u201cQuery 2\u201d is executed in another query window:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE TestDB\nGO\n\n-- Query 2\nUPDATE TestTable\nSET Email = 'user4@example.com'\nWHERE Email = 'user2@example.com'\n\nUPDATE TestTable\nSET Email = 'user2@example.com'\nWHERE Email = 'user3@example.com'<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">When \u201cQuery 1\u201d finishes, we can see that the result of the last <code><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/the-basic-t-sql-select-statement\/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>SELECT<\/strong><\/a><\/code> statement is confusing as there&#8217;s no person with the full name returned by the query:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"475\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-8-1024x475.png\" alt=\"image showing that there's no person with the full name returned by the query.\" class=\"wp-image-113338\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-8-1024x475.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-8-300x139.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-8-768x356.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-8.png 1161w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">However, the final <code>SELECT<\/code> statement needs to return the full name of an existing person.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">How can we modify \u201cQuery 1\u201d to make it work correctly without changing \u201cStatement 1\u201d and \u201cStatement 2\u201d, which must assign the first and last names of the same person to the variables, respectively?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s find out.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-solution-use-repeatable-read-to-prevent-non-repeatable-reads-how-to\">The solution: use <code>REPEATABLE READ<\/code> to prevent non-repeatable reads (how-to)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The issue happens because the default <code>READ COMMITTED<\/code> isolation level protects each individual statement from reading uncommitted data. <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It does <em>not<\/em>, however, guarantee that two separate <code>SELECT<\/code> statements inside the same transaction will read the same version of the data.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this example, after \u201cStatement 1\u201d reads the first name for <code>user2@example.com<\/code>, another query can update the email values before \u201cStatement 2\u201d reads the last name. This results in both \u201cStatement 1\u201d and \u201cStatement 2\u201d being able to read data from different rows. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To solve this problem, we can use the <strong><code>REPEATABLE READ<\/code> isolation level<\/strong>. It keeps shared locks on the rows read by the transaction <em>until<\/em> the transaction is actually complete, so another query can&#8217;t update the rows between \u201cStatement 1\u201d and \u201cStatement 2\u201d.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As a result, the first name and last name will be read from the same person. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before testing the solution, recreate <code>TestTable<\/code> using the original setup script:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE TestDB\nGO\n\n-- Query 1\nSET TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n\nDECLARE @fname VARCHAR(40),\n        @lname VARCHAR(40),\n        @email VARCHAR(254) = 'user2@example.com'\n\nBEGIN TRANSACTION;\n\n    -- Statement 1: Get the first name by email\n    SET @fname = (SELECT FirstName FROM TestTable WHERE Email = @email)\n\n    -- Simulate other statements running between Statement 1 and Statement 2\n    WAITFOR DELAY '00:00:05'\n\n    -- Statement 2: Get the last name by email\n    SET @lname = (SELECT LastName FROM TestTable WHERE Email = @email)\n\nCOMMIT\n\nSELECT @fname + ' ' + @lname AS FullName;\n\n-- Optional: reset the isolation level back to the default\nSET TRANSACTION ISOLATION LEVEL READ COMMITTED<\/pre><\/div>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"496\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-14-1024x496.png\" alt=\"an image showing the solution to task 2\" class=\"wp-image-113346\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-14-1024x496.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-14-300x145.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-14-768x372.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-14.png 1163w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<section id=\"my-first-block-block_5660a68b7028e81cddcd434e1dfe75d3\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Fast, reliable and consistent SQL Server development&#8230;<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            &#8230;with SQL Toolbelt Essentials. 10 ingeniously simple tools for accelerating development, reducing risk, and standardizing workflows.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/products\/sql-toolbelt-essentials\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Learn more &amp; try for free: Fast, reliable and consistent SQL Server development...\">Learn more &amp; try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-problem-a-transaction-gets-a-different-row-count-without-inserting-or-deleting-anything-itself\">The problem: a transaction gets a different row count without inserting or deleting anything itself<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s consider a table named <code>TestTable<\/code> with the following structure:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">IF DB_ID(N'TestDB') IS NULL\n    EXEC('CREATE DATABASE TestDB');\nGO\n\nUSE TestDB\nGO\n\nDROP TABLE IF EXISTS TestTable;\nGO\n-- Create TestTable\nCREATE TABLE TestTable\n(\n    ID INT PRIMARY KEY, \n    Value INT\n)\n\n-- Insert sample data into TestTable\nINSERT INTO TestTable(ID, Value)\nVALUES  (1, 10),\n        (3, 30),\n        (5, 50);<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We are running the following transaction, which has many other statements between \u201cStatement 1\u201d and \u201cStatement 2\u201d. We use <code>WAITFOR DELAY<\/code> instead of these statements to simplify the demonstration:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE TestDB\nGO\n-- Query 1\nBEGIN TRANSACTION\n\n    -- Statement 1: Get the current row count\n    SELECT COUNT(*) AS CountOfRows \n    FROM TestTable\n\n    -- Simulate other statements running between Statement 1 and Statement 2\n    WAITFOR DELAY '00:00:05'\n\n    -- Statement 2: Get the current row count again\n    SELECT COUNT(*) AS CountOfRows \n    FROM TestTable\n\nCOMMIT<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">After \u201cStatement 1\u201d finishes and before \u201cStatement 2\u201d starts, \u201cQuery 2\u201d is executed in another query window:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE TestDB\nGO\n-- Query 2\n-- Insert new rows into TestTable from another session\nINSERT INTO TestTable(ID, Value)\nVALUES  (2, 20),\n        (4, 40),\n        (6, 60);<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">When \u201cQuery 1\u201d finishes, we can see that different row counts are returned for <code>TestTable<\/code> at the beginning and at the end of our transaction (although we didn&#8217;t add or remove any rows in the current transaction):<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"542\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-9-1024x542.png\" alt=\"\" class=\"wp-image-113340\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-9-1024x542.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-9-300x159.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-9-768x407.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-9.png 1112w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">However, we need to get the same row count for <code>TestTable<\/code> even when we don&#8217;t add or delete rows in the current transaction.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">How, then, can we modify \u201cQuery 1\u201d to make it work correctly? After all, \u201cStatement 1\u201d and \u201cStatement 2\u201d must return the same result if the row count hasn&#8217;t changed inside the current transaction.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s how to ensure that happens.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-solution-use-serializable-to-prevent-phantom-reads-how-to\">The solution: use <code>SERIALIZABLE<\/code> to prevent phantom reads (how-to)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The issue happens because, under the default <code>READ COMMITTED<\/code> isolation level, \u201cStatement 1\u201d and \u201cStatement 2\u201d are executed separately and another session can insert new rows between them. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As a result, the row count can be different at the beginning <em>and<\/em> at the end of the same transaction.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To avoid this, we can use the <strong><code>SERIALIZABLE<\/code> isolation level<\/strong>. This prevents SQL Server from allowing another transaction to insert new rows into the range read by the current transaction until it&#8217;s complete &#8211; meaning \u201cStatement 1\u201d and \u201cStatement 2\u201d will return the same row count.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before testing the solution, recreate <code>TestTable<\/code> using the original setup script:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE TestDB\nGO\n\n-- Query 1\nSET TRANSACTION ISOLATION LEVEL SERIALIZABLE;\n\nBEGIN TRANSACTION\n\n    -- Statement 1: Get the current row count\n    SELECT COUNT(*) AS CountOfRows \n    FROM TestTable\n\n    -- Simulate other statements running between Statement 1 and Statement 2\n    WAITFOR DELAY '00:00:05'\n\n    -- Statement 2: Get the current row count again\n    SELECT COUNT(*) AS CountOfRows \n    FROM TestTable\n\nCOMMIT;\n\n-- Optional: reset the isolation level back to the default\nSET TRANSACTION ISOLATION LEVEL READ COMMITTED<\/pre><\/div>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"554\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-15-1024x554.png\" alt=\"image showing the solution to task 3\" class=\"wp-image-113348\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-15-1024x554.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-15-300x162.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-15-768x415.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-15.png 1163w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-problem-a-rolled-back-transaction-wipes-out-the-audit-record-with-it\">The problem: a rolled-back transaction wipes out the audit record with it<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We need to audit user logins in the system. We have a <code>LoginAudit<\/code> table to store information about user logins:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">IF DB_ID(N'TestDB') IS NULL\n    EXEC('CREATE DATABASE TestDB');\nGO\n\nUSE TestDB\nGO\n\n-- Drop tables if they already exist\nDROP TABLE IF EXISTS LoginAudit\nDROP TABLE IF EXISTS UserData\nGO\n\n-- Create UserData table\nCREATE TABLE UserData\n(\n    ID INT PRIMARY KEY, \n    LoginName VARCHAR(254) NOT NULL,\n    PassHash BINARY(64) NOT NULL\n)\n\n-- Create a unique index on the LoginName column\nCREATE UNIQUE INDEX UIX_UserData_LoginName \nON UserData(LoginName)\n\n-- Insert sample user data\nINSERT INTO UserData(ID, LoginName, PassHash)\nVALUES (1, 'user1@example.com', HASHBYTES('SHA2_512', 'abc123'))\n\n-- Create LoginAudit table\nCREATE TABLE LoginAudit\n(\n    LoginName VARCHAR(254), \n    ActionType VARCHAR(10), \n    Result VARCHAR(7), \n    ActionDate DATETIME DEFAULT GETDATE()\n)<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">User authentication occurs within a transaction, which may be rolled back:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">-- Log in and audit\nDECLARE @LoginName VARCHAR(254) = 'user1@example.com',\n        @pass VARCHAR(50) = 'abc123',\n        @Result VARCHAR(10)\n\nIF OBJECT_ID('tempdb..#tmpAudit') IS NOT NULL\n    DROP TABLE #tmpAudit\n\n-- Create a temporary table to store audit information\nCREATE TABLE #tmpAudit\n(\n    LoginName VARCHAR(254), \n    ActionType VARCHAR(10), \n    Result VARCHAR(7)\n)\n\nBEGIN TRANSACTION\n\n    -- Check user credentials\n    IF EXISTS \n    (\n        SELECT ID \n        FROM UserData \n        WHERE LoginName = @LoginName \n          AND PassHash = HASHBYTES('SHA2_512', @pass)\n    )\n        SET @Result = 'Success'\n    ELSE \n        SET @Result = 'Fail'\n\n    -- Insert audit information into the temporary table\n    INSERT INTO #tmpAudit(LoginName, ActionType, Result)\n    VALUES(@LoginName, 'Log In', @Result)\n\n-- For simplicity, we manually roll back the transaction here to simulate a case where something went wrong.\n\nROLLBACK\n\n-- Insert audit information into LoginAudit table\nINSERT INTO LoginAudit(LoginName, ActionType, Result)\nSELECT LoginName, ActionType, Result\nFROM #tmpAudit\n\nSELECT @Result AS Result\n\nSELECT * \nFROM LoginAudit<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">When the transaction is rolled back, we can see that there are no audit records for this login:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"536\" height=\"372\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-10.png\" alt=\"Image showing that there are no audit records for this login\" class=\"wp-image-113341\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-10.png 536w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-10-300x208.png 300w\" sizes=\"auto, (max-width: 536px) 100vw, 536px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">So, how can we modify the code above to record user login information even when the transaction is rolled back? Let&#8217;s find out.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-solution-use-a-table-variable-to-survive-a-rollback-and-how-to-do-it\">The solution: use a table variable to survive a rollback (and how to do it)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>We can use a table variable instead of a temporary table inside the transaction to store audit information. This is because <code><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/language-elements\/rollback-transaction-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">ROLLBACK TRANSACTION<\/a><\/code> does <em>not<\/em> erase changes made to table variables. <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After the transaction is rolled back, we can still insert the audit information from the table variable into the <code>LoginAudit<\/code> table:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">-- Log in and audit\nDECLARE @LoginName VARCHAR(254) = 'user1@example.com',\n        @pass VARCHAR(50) = 'abc123',\n        @Result VARCHAR(10)\n\n-- Declare a table variable to store audit information\nDECLARE @tbvAudit AS TABLE\n(\n    LoginName VARCHAR(254), \n    ActionType VARCHAR(10), \n    Result VARCHAR(7)\n)\n\nBEGIN TRANSACTION\n\n    -- Check user credentials\n    IF EXISTS \n    (\n        SELECT ID \n        FROM UserData \n        WHERE LoginName = @LoginName \n          AND PassHash = HASHBYTES('SHA2_512', @pass)\n    )\n        SET @Result = 'Success'\n    ELSE \n        SET @Result = 'Fail'\n\n    -- Insert audit information into the table variable\n    INSERT INTO @tbvAudit(LoginName, ActionType, Result)\n    VALUES(@LoginName, 'Log In', @Result)\n-- For simplicity, we manually roll back the transaction here to simulate a case where something went wrong.\nROLLBACK\n\n-- Insert audit information into LoginAudit table\nINSERT INTO LoginAudit(LoginName, ActionType, Result)\nSELECT LoginName, ActionType, Result\nFROM @tbvAudit\n\nSELECT @Result AS Result\n\nSELECT * \nFROM LoginAudit<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We can now see the result in the <code>LoginAudit<\/code> table:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"712\" height=\"371\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-16.png\" alt=\"Image showing the result in the LoginAudit table\" class=\"wp-image-113350\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-16.png 712w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-16-300x156.png 300w\" sizes=\"auto, (max-width: 712px) 100vw, 712px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Please note that this example is simplified for demonstration purposes. In real systems, the password verification logic, authentication and audit logging should be designed carefully based on <a href=\"https:\/\/www.red-gate.com\/simple-talk\/data-security-privacy-compliance\/\" target=\"_blank\" rel=\"noreferrer noopener\">security and compliance<\/a> requirements.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-problem-a-nested-rollback-undoes-more-than-it-should\">The problem: a nested rollback undoes more than it should<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In the previous task, we used a table variable to keep audit information after rollback. Now, let\u2019s consider another case where we work with permanent tables inside the transaction and need to roll back only part of the work. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Suppose we have both the <code>LoginAudit<\/code> and <code>UserLogin<\/code> tables:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">IF DB_ID(N'TestDB') IS NULL\n    EXEC('CREATE DATABASE TestDB');\nGO\n\nUSE TestDB\nGO\n\n-- Drop tables if they already exist\nDROP TABLE IF EXISTS UserLogin\nDROP TABLE IF EXISTS LoginAudit\nGO\n\n-- Create LoginAudit table\nCREATE TABLE LoginAudit\n(\n    LoginName VARCHAR(254), \n    ActionType VARCHAR(10), \n    Result VARCHAR(7), \n    ActionDate DATETIME DEFAULT GETDATE()\n)\n\n-- Create UserLogin table\nCREATE TABLE UserLogin\n(\n    ID INT IDENTITY(1,1) PRIMARY KEY,\n    LoginName VARCHAR(254),\n    LoginDate DATETIME DEFAULT GETDATE()\n)<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We need to insert login audit information into <code>LoginAudit<\/code> first, and <em>then<\/em> insert login information into <code>UserLogin<\/code>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If something goes wrong during the insert into <code>UserLogin<\/code>, we need to roll back that insert <em>only<\/em>, but still also need to keep the audit record in <code>LoginAudit<\/code>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A developer wrote the following code using a <a href=\"https:\/\/learn.microsoft.com\/en-us\/previous-versions\/windows\/desktop\/ms716985(v=vs.85)\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>nested transaction<\/strong><\/a>, expecting that the <code>ROLLBACK<\/code> within it would <em>only<\/em> roll back the insert into <code>UserLogin<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">-- Log in and audit\nDECLARE @LoginName VARCHAR(254) = 'user1@example.com',\n        @SomethingWentWrong BIT = 1\n\nBEGIN TRANSACTION\n\n    -- Statement 1: Insert audit information\n    INSERT INTO LoginAudit(LoginName, ActionType, Result)\n    VALUES(@LoginName, 'Log In', 'Fail')\n\n    -- Nested transaction\n    BEGIN TRANSACTION\n\n        -- Statement 2: Insert login information\n        INSERT INTO UserLogin(LoginName)\n        VALUES(@LoginName)\n\n        -- For simplicity, we use this variable to simulate a case\n        -- where something went wrong while inserting login information.\n        IF @SomethingWentWrong = 1\n        BEGIN\n            ROLLBACK \n        END\n        ELSE\n        BEGIN\n            -- Commit the nested transaction\n            COMMIT \n\n            -- Commit the outer transaction\n            COMMIT \n        END\n\nSELECT *\nFROM LoginAudit\n\nSELECT *\nFROM UserLogin<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">However, when <code>ROLLBACK TRANSACTION<\/code> is executed, <strong>the<\/strong> <strong>whole transaction is rolled back<\/strong> &#8211; and the audit record isn&#8217;t saved either:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"654\" height=\"395\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-11.png\" alt=\"image showing that the whole transaction is rolled back with ROLLBACK TRANSACTION - and the audit record isn't saved, either.\" class=\"wp-image-113342\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-11.png 654w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-11-300x181.png 300w\" sizes=\"auto, (max-width: 654px) 100vw, 654px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">So, how can we modify the code to <em>only<\/em> roll back the insert into <code>UserLogin<\/code> and, at the same time, keep the audit record in <code>LoginAudit<\/code>? Let&#8217;s find out.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-solution-use-save-transaction-to-roll-back-to-a-savepoint-and-how-to-do-so\">The solution: use <code>SAVE TRANSACTION<\/code> to roll back to a savepoint (and how to do so)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The issue happens because <code>ROLLBACK TRANSACTION<\/code> inside a nested transaction does <em>not<\/em> just roll back the inner transaction &#8211; in SQL Server, it rolls back the outer transaction as well. <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The only exception is if a <strong><a href=\"https:\/\/www.sqlservercentral.com\/blogs\/savepoints-and-conditional-transactions\" target=\"_blank\" rel=\"noreferrer noopener\">savepoint<\/a><\/strong> is specified in the <code>ROLLBACK<\/code> statement. And, since in our case <em>both<\/em> the insert into <code>LoginAudit<\/code> <em>and<\/em> the insert into <code>UserLogin<\/code> are rolled back, we can use the savepoint as our solution.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After inserting the audit information into <code>LoginAudit<\/code>, we can create a savepoint by using <code><strong>SAVE TRANSACTION<\/strong><\/code>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then, if something goes wrong while inserting into <code>UserLogin<\/code>, we can roll back to that savepoint <em>only<\/em>, and commit the remaining part of the transaction:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">-- Log in and audit\nDECLARE @LoginName VARCHAR(254) = 'user1@example.com',\n        @SomethingWentWrong BIT = 1\n\nBEGIN TRANSACTION\n\n    -- Statement 1: Insert audit information\n    INSERT INTO LoginAudit(LoginName, ActionType, Result)\n    VALUES(@LoginName, 'Log In', 'Fail')\n\n    -- Create a savepoint after inserting audit information\n    SAVE TRANSACTION AuditInserted\n\n    -- Statement 2: Insert login information\n    INSERT INTO UserLogin(LoginName)\n    VALUES(@LoginName)\n\n    -- For simplicity, we use this variable to simulate a case\n    -- where something went wrong while inserting login information.\n    IF @SomethingWentWrong = 1\n    BEGIN\n        -- Roll back only the work done after the savepoint\n        ROLLBACK TRANSACTION AuditInserted\n    END\n\nCOMMIT \n\nSELECT *\nFROM LoginAudit\n\nSELECT *\nFROM UserLogin<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">As a result, the insert into <code>UserLogin<\/code> will be rolled back, but the audit record in <code>LoginAudit<\/code> will be saved:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"817\" height=\"528\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-17.png\" alt=\"an image showing that the audit record in LoginAudit is saved.\" class=\"wp-image-113352\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-17.png 817w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-17-300x194.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/09\/image-17-768x496.png 768w\" sizes=\"auto, (max-width: 817px) 100vw, 817px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">It&#8217;s worth noting that in real-world scenarios, some errors can leave a transaction in an <strong>uncommittable state<\/strong>, and rolling back to a savepoint becomes impossible. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Also be aware that, for simplicity, this example only demonstrates a failed operation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-conclusion\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Working with transactions can be tricky, and writing transaction-heavy SQL code requires a fundamental understanding of transaction management nuances, isolation levels, rollback mechanisms, and more. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this article, I&#8217;ve shed some light on a handful of common mistakes in transaction management (and offered potential solutions to them) &#8211; but there are many others. In general, a deep understanding of the theory is strongly recommended.<\/p>\n\n\n\n<section id=\"my-first-block-block_1d1ce0d68bd171bf52a187fe07b9890d\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Subscribe to the Simple Talk newsletter<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            All the latest articles, guides, podcasts, and more &#8211; delivered straight to your inbox.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/simple-talk\/subscribe\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Subscribe now: Subscribe to the Simple Talk newsletter\">Subscribe now<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. Why does NOLOCK return inaccurate results in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p><code dir=\"ltr\">NOLOCK<\/code> (or <code dir=\"ltr\">READ UNCOMMITTED<\/code>) reads uncommitted data that could still be rolled back, and can also produce inaccurate counts from concurrent modifications \u2014 missing rows or reading the same row multiple times. It&#8217;s a trade-off for avoiding waits, not a free performance win.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. Does ROLLBACK undo changes made to a table variable in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>No \u2014 the article demonstrates this directly: <code dir=\"ltr\">ROLLBACK<\/code> erases changes to a temp table but not to a table variable, which is why a table variable can be used to preserve audit information even when the surrounding transaction is rolled back.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. Does ROLLBACK inside a nested transaction only undo the inner transaction in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>No. A plain <code dir=\"ltr\">ROLLBACK<\/code> inside a nested transaction rolls back the entire outer transaction too. The only way to undo just part of the work is <code dir=\"ltr\">SAVE TRANSACTION<\/code> to create a named savepoint, then <code dir=\"ltr\">ROLLBACK TRANSACTION &lt;savepoint_name&gt;<\/code> to roll back to just that point.<\/p>\n            <\/div>\n            <\/section>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-useful-resources-to-help-develop-your-understanding-of-transactions\">Useful resources to help develop your understanding of transactions<\/h2>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/set-transaction-isolation-level-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">SET TRANSACTION ISOLATION LEVEL (Transact-SQL) &#8211; SQL Server | Microsoft Learn<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/queries\/hints-transact-sql-table?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">Table Hints (Transact-SQL) &#8211; SQL Server | Microsoft Learn<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/data-types\/table-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">table (Transact-SQL) &#8211; SQL Server | Microsoft Learn<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/language-elements\/declare-local-variable-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">DECLARE @local_variable (Transact-SQL) &#8211; SQL Server | Microsoft Learn<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/language-elements\/save-transaction-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">SAVE TRANSACTION (Transact-SQL) &#8211; SQL Server | Microsoft Learn<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/sql-server-transaction-locking-and-row-versioning-guide?view=sql-server-2017\" target=\"_blank\" rel=\"noreferrer noopener\">Transaction Locking and Row Versioning Guide &#8211; SQL Server | Microsoft Learn<\/a><\/li>\n<\/ul>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>Five practical SQL Server transaction problems \u2014 dirty reads, non-repeatable reads, phantom reads, rollback behavior, and nested transactions \u2014 with fixes.&hellip;<\/p>\n","protected":false},"author":335442,"featured_media":106871,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143523,53,143524],"tags":[4168,4170,4150,4151],"coauthors":[126372],"class_list":["post-113333","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-databases","category-featured","category-sql-server","tag-database","tag-database-administration","tag-sql","tag-sql-server"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/113333","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/335442"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=113333"}],"version-history":[{"count":30,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/113333\/revisions"}],"predecessor-version":[{"id":113900,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/113333\/revisions\/113900"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/106871"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=113333"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=113333"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=113333"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=113333"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}