{"id":107854,"date":"2025-11-19T11:00:00","date_gmt":"2025-11-19T11:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=107854"},"modified":"2025-12-17T11:21:31","modified_gmt":"2025-12-17T11:21:31","slug":"monitoring-and-tuning-batched-deletion-processes","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/monitoring-and-tuning-batched-deletion-processes\/","title":{"rendered":"How to Monitor and Optimize Batched Deletion Processes in SQL Server"},"content":{"rendered":"\n<p><strong>Batched deletions are a common strategy in <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">SQL Server<\/a> to manage large datasets without overloading the system, but poorly tuned deletes can cause blocking, long-running transactions, and heavy log usage. Learn how to monitor and optimize these processes for smooth, efficient database performance.<\/strong><\/p>\n\n\n\n<p>In <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/purging-data-from-a-large-table-in-sql-server\/\">previous <\/a><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/replacing-a-column-in-a-large-active-sql-server-table\/\" target=\"_blank\" rel=\"noreferrer noopener\">articles<\/a> I showed patterns for working with large amounts of data on big tables while keeping <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/locking-objects-in-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">locking<\/a> at a minimum. These processes can allow migrations and <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/maintenance-plans\/maintenance-plans?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">maintenance<\/a> without requiring downtime but, in environments with unpredictable database workloads, there is a risk of heavy traffic starting at any time and disrupting a once smooth operation. In this article, I&#8217;ll demonstrate how to augment these processes to allow dynamic adjustment of the configuration.<\/p>\n\n\n\n<p>For most systems, the main limitation these techniques run into is the speed and throughput of <a href=\"https:\/\/builtin.com\/hardware\/i-o-input-output\" target=\"_blank\" rel=\"noreferrer noopener\">I\/O (input\/output)<\/a>. During periods of low traffic, a large batch size may perform great with no impact to production, but as traffic increases, the storage subsystem may not be able to keep up. <\/p>\n\n\n\n<p>I&#8217;ll show two workarounds to deal with this issue: lowering the batch size, and introducing a small delay in between batches. Both will allow the process to continue running with less I\/O demand but, if this still isn&#8217;t enough, we can easily stop the process and restart at a different time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-logging-tables-in-sql-server\">Logging Tables in SQL Server<\/h2>\n\n\n\n<p>Before we get into the configuration options, let\u2019s consider how we can get feedback on our process while it\u2019s running, so we&#8217;re better informed about the adjustments we want to make. Let\u2019s review the code for purging in batches explored in this <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/purging-data-from-a-large-table-in-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">article<\/a>:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SET NOCOUNT ON;\n--control the number of rows deleted per iteration\nDECLARE @BatchSize INT = 5000;\n \n--variable used to tell the process to stop\nDECLARE @Stop INT = 0;\n \nIF (OBJECT_ID ('tempdb..#ToProcess') IS NULL)\n  CREATE TABLE #ToProcess (Id INT NOT NULL PRIMARY KEY CLUSTERED);\nIF (OBJECT_ID ('tempdb..#Batch') IS NULL)\n  CREATE TABLE #Batch (Id INT NOT NULL PRIMARY KEY CLUSTERED);\n \n-----------------Gather Ids------------------------------------\nINSERT INTO #ToProcess (Id)\nSELECT Id\nFROM dbo.Posts\nWHERE CreationDate &lt; '2011';\n \n-----------------Main Loop------------------------------------\nWHILE (@Stop = 0)\nBEGIN\n  --Load up our batch table while deleting from the main set\n  DELETE TOP (@BatchSize) #ToProcess\n  OUTPUT DELETED.Id INTO #Batch (Id);\n \n  --Once the rowcount is less than the batchsize,\n  -- we can stop (after this loop iteration)\n  IF @@ROWCOUNT &lt; @BatchSize\n    SELECT @Stop = 1;\n \n--Perform the DELETE\n  DELETE FROM p\n  FROM dbo.Posts p\n    JOIN #Batch b ON p.Id = b.Id;\n \n  --Clear out Batch table\n  TRUNCATE TABLE #Batch\n \nEND;<\/pre><\/div>\n\n\n\n<p>Now, we&#8217;ll add a new table to hold our logging:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">IF (OBJECT_ID('dbo.PurgeLogging') IS NULL)\n\tCREATE TABLE dbo.PurgeLogging (\n\t LogId INT IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED\n\t,StartTime DATETIME2(7) NOT NULL DEFAULT GETDATE()\n\t,EndTime DATETIME2(7) NULL\n\t,Duration_ms AS \nDATEDIFF(MILLISECOND\n, StartTime\n, ISNULL(EndTime, GETDATE()))\t\n\t,RowsAffected INT NULL\n\t,[BatchSize] INT NOT NULL\n\t,DelayTime varchar(8) NOT NULL\n\t,ErrorCode INT NULL\n\t,ErrorMessage NVARCHAR(255) NULL\n\t)<\/pre><\/div>\n\n\n\n<p>This table will contain a row for every iteration of the loop, tracking the time it takes for the <a href=\"https:\/\/www.w3schools.com\/sql\/sql_delete.asp\" target=\"_blank\" rel=\"noreferrer noopener\"><code>DELETE<\/code> statement<\/a> to run (because that&#8217;s what will alert us to any blocking). Depending on the <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/measuring-replication-latency-in-sql-server-synchronous-availability-groups\/\" target=\"_blank\" rel=\"noreferrer noopener\">latency<\/a> demands of your system, even a 1-2 second iteration may be too slow, but other systems may function without issue as long as this is below the default timeout of 30 seconds. <\/p>\n\n\n\n<p>Besides the time tracking, we&#8217;ll also record the values of our <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/database-engine\/configure-windows\/server-configuration-options-sql-server?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">configuration<\/a> parameters and any errors that come up during the operation. We do this by inserting a record at the beginning of our loop:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">INSERT INTO dbo.PurgeLogging (StartTime,[BatchSize], DelayTime)\nSELECT GETDATE(), @BatchSize, @DelayTime\nSELECT @LogId = SCOPE_IDENTITY()<\/pre><\/div>\n\n\n\n<p>And by updating that record at the end of our loop:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">UPDATE dbo.PurgeLogging \n\tSET EndTime = GETDATE()\n\t, RowsAffected = @RowsAffected\n\t, ErrorMsg = @ErrorMsg\n\t, ErrorNum = @ErrorNum\nWHERE LogId = @LogId<\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-configuration-tables-in-sql-server\">Configuration Tables in SQL Server<\/h2>\n\n\n\n<p>In our previous script we used a static variable to hold the BatchSize, but we&#8217;ll now store this in a table along with other parameters to adjust the process. <\/p>\n\n\n\n<p>This allows us to make changes on the fly without stopping the script and rolling back a midflight <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/transactions\/\" target=\"_blank\" rel=\"noreferrer noopener\">transaction<\/a>. It also opens up the possibility of using an automated process to tweak these parameters based on system load.<\/p>\n\n\n\n<p>Let\u2019s look at our configuration table:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">IF (OBJECT_ID ('dbo.PurgeConfig') IS NULL)\n\tCREATE TABLE dbo.PurgeConfig (\n\t\tConfigId INT NOT NULL DEFAULT (1) PRIMARY KEY CLUSTERED \n\t\t, DelayTime varchar(8) NOT NULL\n\t\t, [BatchSize] INT NOT NULL\n\t\t, [Stop] bit NOT NULL DEFAULT (0)\n\t\t, CONSTRAINT CH_PurgeConfig_ConfigId CHECK (ConfigId=1)\n\t\t);<\/pre><\/div>\n\n\n\n<p>This table contains our parameters as well as a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/the-check-constraint\/#:~:text=The%20CHECK()%20constraint%20can,table%20that%20models%20commercial%20orders.\" target=\"_blank\" rel=\"noreferrer noopener\">check constraint<\/a> to ensure we only have one row stored at any time. We&#8217;ll update the running variables at the end of each loop (as well as at the beginning of the script):<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT @BatchSize = [BatchSize]\n\t\t, @DelayTime = [DelayTime]\n\t\t--ONLY UPDATE @Stop when it is 0\n\t\t, @Stop = CASE WHEN @Stop = 1 THEN 1 ELSE [Stop] END\nFROM dbo.PurgeConfig<\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-error-handling-in-sql-server\">Error Handling in SQL Server<\/h2>\n\n\n\n<p>While we are making improvements to the script, let\u2019s add some polish by introducing <a href=\"https:\/\/www.sqlshack.com\/how-to-implement-error-handling-in-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">error handling<\/a> inside the main loop. This can be as simple as adding a <em>TRY\/CATCH<\/em> block and some variables to store the error message and number. <\/p>\n\n\n\n<p>You might also opt to set <em>@Stop = 1<\/em> in this section if you want the script to stop any time it hits an error. In this example, I&#8217;m letting it continue because I can address the error and rerun the script later; any records that failed to delete will be scooped up by the insert to the <em>#ToProcess<\/em> table on the next run.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-putting-it-all-together\">Putting It All Together<\/h2>\n\n\n\n<p>Now let\u2019s look at what our script looks like with these new tables:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SET NOCOUNT ON;\n--control the number of rows deleted per iteration\nDECLARE @BatchSize INT;\n \n--variable used to tell the process to stop\nDECLARE @Stop INT = 0;\n\n--add a delay between iterations\nDECLARE @DelayTime VARCHAR(8);\n\n--logging variable for rowcount\nDECLARE @RowsAffected INT;\n\n--logging variable for the log record\nDECLARE @LogId INT;\n\n--logging variables for errors\nDECLARE @ErrorMsg nvarchar(4000)\n\t, @ErrorNum INT;\n\nIF (OBJECT_ID ('tempdb..#ToProcess') IS NULL)\n  CREATE TABLE #ToProcess (Id INT NOT NULL PRIMARY KEY CLUSTERED);\nIF (OBJECT_ID ('tempdb..#Batch') IS NULL)\n  CREATE TABLE #Batch (Id INT NOT NULL PRIMARY KEY CLUSTERED);\n\n--------------Logging Table----------------------------------- \nIF (OBJECT_ID('dbo.PurgeLogging') IS NULL)\n\tCREATE TABLE dbo.PurgeLogging (\n\t LogId INT IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED\n\t,StartTime DATETIME2(7) NOT NULL DEFAULT GETDATE()\n\t,EndTime DATETIME2(7) NULL\n\t,Duration_ms AS \n    DATEDIFF(MILLISECOND\n    , StartTime\n    , ISNULL(EndTime, GETDATE()))\t\n\t,RowsAffected INT NULL\n\t,[BatchSize] INT NOT NULL\n\t,DelayTime varchar(8) NOT NULL\n\t,ErrorCode INT NULL\n\t,ErrorMessage NVARCHAR(255) NULL\n\t);\n\n-----------------Configuration Table---------------------------\nIF (OBJECT_ID ('dbo.PurgeConfig') IS NULL)\nBEGIN\n\tCREATE TABLE dbo.PurgeConfig (\n\t\tConfigId INT NOT NULL DEFAULT (1) PRIMARY KEY CLUSTERED \n\t\t, DelayTime varchar(8) NOT NULL\n\t\t, [BatchSize] INT NOT NULL\n\t\t, [Stop] bit NOT NULL DEFAULT (0)\n\t\t, CONSTRAINT CH_PurgeConfig_ConfigId CHECK (ConfigId=1)\n\t\t);\n\n-----------------Add Initial Configuration---------------------\n\tINSERT INTO dbo.PurgeConfig (DelayTime, [BatchSize], [Stop])\n\tVALUES ('00:00:00', 5000, 0);\n\tSELECT @BatchSize = [BatchSize]\n\t\t\t, @DelayTime = [DelayTime]\n\t\t\t--ONLY UPDATE @Stop when it is 0\n\t\t\t, @Stop = CASE WHEN @Stop = 1 THEN 1 ELSE [Stop] END\n\tFROM dbo.PurgeConfig;\nEND\n\n-----------------Gather Ids------------------------------------\nINSERT INTO #ToProcess (Id)\nSELECT Id\nFROM dbo.Posts\nWHERE CreationDate &lt; '2011';\n\n-----------------Main Loop------------------------------------\nWHILE (@Stop = 0)\nBEGIN\n  --Create a logging record\n  INSERT INTO dbo.PurgeLogging (StartTime,[BatchSize], DelayTime)\n  SELECT GETDATE(), @BatchSize, @DelayTime;\n  SELECT @LogId = SCOPE_IDENTITY();\n\n  --Load up our batch table while deleting from the main set\n  DELETE TOP (@BatchSize) #ToProcess\n  OUTPUT DELETED.Id INTO #Batch (Id);\n  \n  --Once the rowcount is less than the batchsize,\n  -- we can stop (after this loop iteration)\n  IF @@ROWCOUNT &lt; @BatchSize\n    SELECT @Stop = 1;\n  \n  BEGIN TRY\n    --Perform the DELETE\n    DELETE FROM p\n    FROM dbo.Posts p\n      JOIN #Batch b ON p.Id = b.Id;\n    --Store the rowcount\n    SELECT @RowsAffected = @@ROWCOUNT;\n  END TRY\n  BEGIN CATCH\n    --if we have an error, store the message and log it\n    -- optionally you can stop the process here by setting @stop =1\n\t\tSELECT @ErrorMsg = ERROR_MESSAGE()\n\t\t, @ErrorNum = ERROR_NUMBER()\n\tEND CATCH\n\n  --Update the logging table\n  UPDATE dbo.PurgeLogging \n\tSET EndTime = GETDATE()\n    , RowsAffected = @RowsAffected\n    , ErrorMessage = @ErrorMsg\n    , ErrorCode = @ErrorNum\n  WHERE LogId = @LogId;\n\n  --Refresh running configuration from config table\n  SELECT @BatchSize = [BatchSize]\n\t\t, @DelayTime = [DelayTime]\n\t\t--ONLY UPDATE @Stop when it is 0\n\t\t, @Stop = CASE WHEN @Stop = 1 THEN 1 ELSE [Stop] END\n  FROM dbo.PurgeConfig;\n\n  --Clear out Batch table\n  TRUNCATE TABLE #Batch;\n\n  \u2013-Wait before running the next batch\n  WAITFOR DELAY @DelayTime\n \nEND;<\/pre><\/div>\n\n\n\n<p>After we kick off this script we can monitor the progress by querying the <em>PurgeLogging<\/em> table:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT TOP 100 * FROM dbo.PurgeLogging\nORDER BY LogId DESC<\/pre><\/div>\n\n\n\n<p>This results in each batch taking less than 200 milliseconds to complete:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"268\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-12-1024x268.png\" alt=\"An image showing each batch taking less than 200 milliseconds to complete.\" class=\"wp-image-107855\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-12-1024x268.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-12-300x79.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-12-768x201.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-12-1536x402.png 1536w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-12.png 1784w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Then, if we want to change the batch size or add a delay in between batches, we can update the <em>PurgeConfig <\/em>table like so:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">UPDATE PurgeConfig\n\tSET DelayTime = '00:00:02'\n\t, [BatchSize] = 100000<\/pre><\/div>\n\n\n\n<p>We can see in our logging table that the change takes effect seamlessly:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"247\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-13-1024x247.png\" alt=\"An image showing that the change takes effect seamlessly.\" class=\"wp-image-107856\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-13-1024x247.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-13-300x72.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-13-768x185.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-13-1536x371.png 1536w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-13.png 1794w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Notice that there&#8217;s now a second gap between the StartTime and EndTime of the previous row, which could help slower disks keep up.<\/p>\n\n\n\n<p>We can also see that each batch is now taking more than a second &#8211; if we feel this is too long, we can update our PurgeConfig table once again to lower the batch size:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"245\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-14-1024x245.png\" alt=\"Image showing updating the PurgeConfig table to lower the batch size.\" class=\"wp-image-107857\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-14-1024x245.png 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-14-300x72.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-14-768x184.png 768w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-14-1536x367.png 1536w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/11\/image-14.png 1791w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-conclusion\">Conclusion<\/h2>\n\n\n\n<p>Adding logging and dynamic configuration to this technique allows us to tune the process to the unique capabilities and requirements of any environment. By looking at the time each loop takes to execute, we can adjust the batch size to keep our impact to an acceptable amount. If our I\/O is saturated, we can add a delay in between batches to allow other processes to complete. <\/p>\n\n\n\n<p>This technique can be used to purge old data as I have shown here, but it can also be used for more advanced processes like <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/replacing-a-column-in-a-large-active-sql-server-table\/\" target=\"_blank\" rel=\"noreferrer noopener\">changing the datatype on a column while a table is being used<\/a>, or deleting from multiple tables with <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/uniqueness-keys-and-identity\/#:~:text=and%20other%20services.-,FOREIGN%20KEY%20and%20REFERENCES%20Clauses,-There%20is%20a\" target=\"_blank\" rel=\"noreferrer noopener\">foreign key<\/a> relationships.<\/p>\n\n\n\n<section id=\"my-first-block-block_1178a62a61c97a3f37a495fcfe933ece\" 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\">10 tools for every stage of SQL Server development<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            SQL Toolbelt Essentials includes 10 ingeniously simple tools that cover your entire database development lifecycle.                                    <\/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: 10 tools for every stage of SQL Server development\">Learn more &amp; try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: Batched Deletions in SQL Server<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What is a batched delete in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <div>A batched delete in SQL Server removes rows in small chunks instead of one large transaction to reduce blocking, log growth, and performance impact.<\/div>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. Why use batched deletes in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p><span style=\"font-size: 1rem\">Batched deletes in SQL Server improve stability, avoid long locks, and prevent transaction log issues during large data purges.<\/span><\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. How do I pick a batch size in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p><span style=\"font-size: 1rem\">Start small (e.g., 1,000 rows), monitor performance, and adjust until you balance speed with minimal blocking.<\/span><\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. How do batched deletes affect the transaction log in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p><span style=\"font-size: 1rem\">Deletes are fully logged. Smaller batches help control log growth and make backups faster.<\/span><\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">5. Do batched deletes in SQL Server cause fragmentation?<\/h3>\n            <div class=\"faq-answer\">\n                <p><span style=\"font-size: 1rem\">Yes. Plan index maintenance and update statistics after large deletes.<\/span><\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">6. How can I prevent blocking?<\/h3>\n            <div class=\"faq-answer\">\n                <p><span style=\"font-size: 1rem\">Prevent blocking in SQL Server by using indexed predicates, short transactions, smaller batches, and consider snapshot isolation.<\/span><\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">7. What should I monitor when using batched delete in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p><span style=\"font-size: 1rem\">Track rows per batch, duration, waits, blocking, CPU\/IO, and log usage.<\/span><\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">8. Can partitioning speed up deletes in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p><span style=\"font-size: 1rem\">Yes! Partition switching is much faster and less disruptive than row-by-row deletes.<\/span><\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">9. How do I resume after a failure?<\/h3>\n            <div class=\"faq-answer\">\n                <p><span style=\"font-size: 1rem\">Save the last processed key and restart from there to avoid duplicates.<\/span><\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">10. How do I confirm efficiency?<\/h3>\n            <div class=\"faq-answer\">\n                <p><span style=\"font-size: 1rem\">Check execution plans for index seeks, avoid scans, and tune predicates.<\/span><\/p>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>Batched deletions are a common strategy in SQL Server to manage large datasets without overloading the system, but poorly tuned deletes can cause blocking, long-running transactions, and heavy log usage. Learn how to monitor and optimize these processes for smooth, efficient database performance. In previous articles I showed patterns for working with large amounts of&#8230;&hellip;<\/p>\n","protected":false},"author":344195,"featured_media":105376,"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":[159137],"class_list":["post-107854","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\/107854","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\/344195"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=107854"}],"version-history":[{"count":4,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/107854\/revisions"}],"predecessor-version":[{"id":108131,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/107854\/revisions\/108131"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/105376"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=107854"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=107854"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=107854"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=107854"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}