<br />
<b>Warning</b>:  Constant WP_TEMP_DIR already defined in <b>/var/www/html/blogs/paul/wp-config.php</b> on line <b>92</b><br />
<br />
<b>Warning</b>:  Cannot modify header information - headers already sent by (output started at /var/www/html/blogs/paul/wp-config.php:92) in <b>/var/www/html/blogs/paul/wp-includes/feed-rss2.php</b> on line <b>8</b><br />
<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Paul S. Randal</title>
	<atom:link href="https://www.sqlskills.com/blogs/paul/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.sqlskills.com/blogs/paul/</link>
	<description>In Recovery...</description>
	<lastBuildDate>Wed, 25 Mar 2026 22:34:14 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>
	<item>
		<title>SQL101: Top Ten SQL Server Performance Tuning Best Practices</title>
		<link>https://www.sqlskills.com/blogs/paul/sql101-top-ten-sql-server-performance-tuning-best-practices/</link>
					<comments>https://www.sqlskills.com/blogs/paul/sql101-top-ten-sql-server-performance-tuning-best-practices/#comments</comments>
		
		<dc:creator><![CDATA[Paul Randal]]></dc:creator>
		<pubDate>Wed, 25 Mar 2026 20:11:58 +0000</pubDate>
				<category><![CDATA[Performance Tuning]]></category>
		<category><![CDATA[SQL101]]></category>
		<guid isPermaLink="false">https://www.sqlskills.com/blogs/paul/?p=5370</guid>

					<description><![CDATA[<p>There are a huge number of best practices around SQL Server performance tuning – I could easily write a whole book on the topic, especially when you consider the number of different database settings, server settings, coding practices, wait types, and so on that can affect performance. For this post I decided to step back [&#8230;]</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/sql101-top-ten-sql-server-performance-tuning-best-practices/">SQL101: Top Ten SQL Server Performance Tuning Best Practices</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p style="text-align: justify;">There are a huge number of best practices around SQL Server performance tuning – I could easily write a whole book on the topic, especially when you consider the number of different database settings, server settings, coding practices, wait types, and so on that can affect performance. For this post I decided to step back a bit from a list of specifics and list some general recommendations for how to <em>approach</em> performance tuning so that you maximize effort and minimize distractions.</p>
<h3 style="text-align: justify;">1) Don’t Assume the Symptom Is the Root Cause</h3>
<p style="text-align: justify;">Many DBAs and developers tend towards what I call ‘knee-jerk performance troubleshooting’, where a minimal amount of analysis and investigation is performed and the assumption is made that the most prevalent symptom of poor performance must be the root cause. When this happens, and effort is made to try to address the supposed root cause, it can lead to a lot of wasted time, and frustration that the mitigation efforts don’t help the situation.</p>
<p style="text-align: justify;">My favorite example of this, and a problem I’m sure you’ve all had, is when  average disk latency is high. The classic knee-jerk reaction is that it must be the I/O subsystem that has a problem, so the company spends money on a better I/O subsystem and the problem goes away for a little while and then comes back again, because the problem is not the hardware itself, but something happening within SQL Server.</p>
<p style="text-align: justify;">For a case like this, it’s generally better to take a mental step back and ask why is SQL Server overloading the I/O subsystem or more precisely, why is SQL Server doing so many physical reads. There are many reasons this could be happening, such as (but not limited to):</p>
<ul style="text-align: justify;">
<li>An inefficient query plan doing a large, parallel table scan instead of using a nonclustered index because of something like a missing index, or implicit conversion, or out-of-date statistics</li>
<li>Memory pressure on the buffer pool (meaning there isn’t enough space to hold the usual ‘working set’ of database pages) from the OS</li>
</ul>
<p style="text-align: justify;">It always pays to do some investigation instead of jumping to a quick conclusion on the root cause.</p>
<h3 style="text-align: justify;">2) Determine the Scope of the Problem</h3>
<p style="text-align: justify;">It’s extremely important to figure out what the scope of the problem is, as that determines how you’ll go about investigating the problem, what metrics to gather, and what scripts and tools to use. For instance, being asked to investigate stored procedure XYZ which takes twice as long to run as it usually does is very different from being asked to tune all long-running stored procedures.</p>
<p style="text-align: justify;">Stored procedure metrics can be obtained by running the query in Management Studio, and noting duration, CPU, and IO statistics.  That information can also be obtained from the plan cache, and you can also leverage the plan cache when you need to find the longest-running stored procedures.  The following query, adapted from the popular set of DMV scripts <a href="https://glennsqlperformance.com/resources/">here</a>, lists the slowest 25 procedures, based on average duration:</p>
<pre class="brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate">
SELECT
    TOP (25) &#x5B;p].&#x5B;name] AS &#x5B;SP Name],
    &#x5B;eps].&#x5B;min_elapsed_time],
    &#x5B;eps].&#x5B;total_elapsed_time] / &#x5B;eps].&#x5B;execution_count] AS &#x5B;avg_elapsed_time],
    &#x5B;eps].&#x5B;max_elapsed_time],
    &#x5B;eps].&#x5B;last_elapsed_time],
    &#x5B;eps].&#x5B;total_elapsed_time],
    &#x5B;eps].&#x5B;execution_count],
    ISNULL (&#x5B;eps].&#x5B;execution_count] /
        DATEDIFF (MINUTE, &#x5B;eps].&#x5B;cached_time], GETDATE ()), 0) AS &#x5B;Executions/Minute],
    FORMAT (&#x5B;eps].&#x5B;last_execution_time],
        &#039;yyyy-MM-dd HH:mm:ss&#039;, &#039;en-US&#039;) AS &#x5B;Last Execution Time],
    FORMAT (&#x5B;eps].&#x5B;cached_time],
        &#039;yyyy-MM-dd HH:mm:ss&#039;, &#039;en-US&#039;) AS &#x5B;Plan Cached Time]
    -- ,&#x5B;qp].&#x5B;query_plan] AS &#x5B;Query Plan] -- Uncomment if you want the query plan
FROM sys.procedures AS &#x5B;p] WITH (NOLOCK)
INNER JOIN sys.dm_exec_procedure_stats AS &#x5B;eps] WITH (NOLOCK)
    ON &#x5B;p].&#x5B;object_id] = &#x5B;eps].&#x5B;object_id]
CROSS APPLY sys.dm_exec_query_plan (&#x5B;eps]. &#x5B;plan_handle]) AS &#x5B;qp]
WHERE
    &#x5B;eps].&#x5B;database_id] = DB_ID ()
    AND DATEDIFF (MINUTE, &#x5B;eps].&#x5B;cached_time], GETDATE()) &gt; 0
ORDER BY &#x5B;avg_elapsed_time] DESC
OPTION (RECOMPILE);
</pre>
<p style="text-align: justify;">There are also tools like the <a href="https://www.sentryone.com/products/features/top-sql">Top SQL functionality</a> in SolarWinds SQL Sentry that can help identify highest impact and highest resource using queries.</p>
<h3 style="text-align: justify;">3) Define the Goal for Success</h3>
<p style="text-align: justify;">Once you have the scope of the problem, the next step is to determine the goal of the performance tuning effort, so you know when you&#8217;ve achieved success and can move on to another task. Don’t allow the goal to be something undefined and open-ended like ‘stored procedure XYZ needs to be faster’, it needs to be well-defined such as ‘stored procedure XYZ needs to run at the speed it did before, i.e. at 50% of the current elapsed time’.</p>
<p style="text-align: justify;">Sometimes the investigation will be a bit more involved if the scope is wider, requiring capturing metrics and information over time before any analysis and mitigation can start. For instance, one of the first consulting clients I worked with had a somewhat open-ended goal for me which was, paraphrasing, ‘tempdb runs out of space once a week, and we need it not to do that’ without any idea why. The investigation involved me setting up two SQL Agent jobs; one every 10 seconds to look for large uses of tempdb and log information to a table, and another once an hour to email me any results from the previous hour. The general code I wrote to find space-hogs in tempdb is below:</p>
<pre class="brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate">
-- InternalMB/Pages: worktables (cursor, spool) , workfiles (hash joins), sort
-- UserMB/Pages: everything else
--
SELECT
    GETDATE () AS &#x5B;Date],
    &#x5B;tsu].&#x5B;session_id] AS &#x5B;SessionID],
    &#x5B;tsu].&#x5B;exec_context_id] AS &#x5B;ExecContextID], -- anything over 0 means parallelism
    (&#x5B;tsu].&#x5B;user_objects_alloc_page_count] -
        &#x5B;tsu].&#x5B;user_objects_dealloc_page_count]) AS &#x5B;UserPages],
    ROUND (CONVERT (FLOAT, (&#x5B;tsu].&#x5B;user_objects_alloc_page_count] -
        &#x5B;tsu].&#x5B;user_objects_dealloc_page_count]) * 8) / 1024.0, 2) AS &#x5B;UserMB],
    (&#x5B;tsu].&#x5B;internal_objects_alloc_page_count] -
        &#x5B;tsu].&#x5B;internal_objects_dealloc_page_count]) AS &#x5B;InternalPages],
    ROUND (CONVERT (FLOAT, (&#x5B;tsu].&#x5B;internal_objects_alloc_page_count] -
        &#x5B;tsu].&#x5B;internal_objects_dealloc_page_count]) * 8) / 1024.0, 2) AS &#x5B;InternalMB],
    &#x5B;er].&#x5B;plan_handle] AS &#x5B;Plan],
    &#x5B;est].&#x5B;text] AS &#x5B;Text]
FROM sys.dm_db_task_space_usage &#x5B;tsu]
JOIN sys.dm_exec_requests &#x5B;er]
    ON &#x5B;er].&#x5B;session_id] = &#x5B;tsu].&#x5B;session_id]
CROSS APPLY sys.dm_exec_sql_text (&#x5B;er].&#x5B;sql_handle]) &#x5B;est]
/*
WHERE
    -- Optionally, filter by a size limit
    -- E.g., the 16384 is 128MB in 8KB pages
    ((&#x5B;user_objects_alloc_page_count] - &#x5B;user_objects_dealloc_page_count]) +
        (&#x5B;internal_objects_alloc_page_count] - &#x5B;internal_objects_dealloc_page_count])) &gt;= 16384
*/
ORDER BY
    ((&#x5B;user_objects_alloc_page_count] - &#x5B;user_objects_dealloc_page_count]) +
        (&#x5B;internal_objects_alloc_page_count] - &#x5B;internal_objects_dealloc_page_count])) DESC;
</pre>
<h3>4) Understand the Limitations</h3>
<p style="text-align: justify;">Before you start proposing or making changes, it’s important to know if there are any things you simply cannot do. Here are some examples:</p>
<ul style="text-align: justify;">
<li>If the application is written by a vendor, you’re not going to be able to make code changes to improve performance</li>
<li>If the application is written by a vendor, you might not even be able to add or change indexes without voiding the vendor’s support agreement</li>
<li>You might not be able to change a setting like MAXDOP or parameter sniffing for the whole server, which may mean using an <a href="https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-database-scoped-configuration-transact-sql">ALTER DATABASE SCOPED CONFIGURATION</a> option for just one database</li>
</ul>
<p style="text-align: justify;">Even if you can change code, there may be a lengthy testing process which prevents a change from being immediately implemented, so you may need to pursue alternative solutions (potentially short-term) to quickly fix the problem.</p>
<h3 style="text-align: justify;">5) Change One Thing at a Time</h3>
<p style="text-align: justify;">One of the most confusing things to do when performance tuning is to make multiple changes at the same time, as then you won’t know which change had an effect, or whether multiple changes cancelled each other out. Always change one thing at a time and keep a note of what you changed and what effect it had, if any. Also, if a change doesn’t have any effect then revert the change so that it doesn’t become a complication if the workload evolves at a later date.</p>
<h3>6) Do Not Test in Production</h3>
<p style="text-align: justify;">One of the worst things to do when performance tuning is to make changes directly in production, as that can lead to dire consequences for the workload and business if a change create a huge negative effect. This means you need a separate test/QA environment that can be used to evaluate changes under production workload conditions, or as close to it as possible. And that leads nicely into the next point…</p>
<h3 style="text-align: justify;">7) Understand How Test Compares to Production</h3>
<p style="text-align: justify;">If your test system doesn’t compare to production then you may not see the same change in performance in production as you do in test. Classic examples of this include:</p>
<ul style="text-align: justify;">
<li>A production system with a certain number of CPUs (e.g. four 8-core processors) and a lower powered test system to save money (e.g. four quad-core processors)</li>
<li>Along the same lines, test system having a lot less memory than production, or a different NUMA configuration, or a lower-rated storage subsystem</li>
<li>Test system only having a subset of the production data to test with</li>
<li>Test system not being able to simulate the production workload</li>
</ul>
<p style="text-align: justify;">All of these things can result in the test system producing different query plans, or the workload in test having very different characteristics than in production. This means you’ll be performance tuning for a different workload and environment and the efficacy of the changes may not translate to the production environment.</p>
<h3>8) Understand the Implications of the Change</h3>
<p style="text-align: justify;">After you’ve determined what the necessary change is, you need to consider what wider effect, if any, making that change will have. For example, if you need to change MAXDOP or the cost threshold for parallelism, that will flush the plan cache, and you might run the risk of parameter-sensitive queries recompiling with sub-optimal plans.</p>
<p style="text-align: justify;">Other changes might be more environmental, like offloading parts of a query workload to a readable secondary in an availability group. That can lead to index fragmentation issues on the primary database, which can be a performance problem of their own (as I described in <a href="https://sqlperformance.com/2015/03/sql-indexes/unexpected-fragmentation">this SQLPerformance.com post</a>).</p>
<p style="text-align: justify;">You don’t want to solve one performance problem and end up with an unexpected different problem to then have to solve.</p>
<h3>9) Create a Rollback Plan</h3>
<p style="text-align: justify;">It’s very important that you have a complete log of what’s been changed and have the ability to revert the changes if something goes wrong. This means preserving original copies of all code and schema and ideally having a script you can run to quickly roll back the changes.</p>
<p style="text-align: justify;">If this would be hard to do, and would really entail restoring the database from backups, one thing to consider is creating a database snapshot of the database and keeping it around for a few days. A database snapshot automatically keeps a pre-change copy of all changed data file pages since the time the database snapshot was created and allows you to effectively put the database back to that time with a one-line T-SQL command (internally SQL Server does this by pushing the pre-change pages back into the real database – called ‘reverting the database to the database snapshot’).</p>
<h3 style="text-align: justify;">10) Remove Diagnostic Elements from Production</h3>
<p style="text-align: justify;">Once you’ve finished the investigation and reached the performance tuning goal, make sure you remove all of the diagnostics that you implemented to help with the investigation, as they could cause performance problems themselves if left in place, especially Extended Event sessions as they can become ‘silent killers’ that use up a lot of CPU resources with no other clue that they are the problem.</p>
<p style="text-align: justify;">You can see which Extended Event sessions are running using the following code:</p>
<pre class="brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate">
SELECT
    &#x5B;ses].&#x5B;name] AS &#x5B;Session Name],
    CASE
        WHEN &#x5B;xs].&#x5B;address] IS NOT NULL THEN &#039;Running&#039;
        ELSE &#039;Stopped&#039;
    END AS &#x5B;State],
    &#x5B;xs].&#x5B;create_time] AS &#x5B;Start Time]
FROM sys.server_event_sessions AS &#x5B;ses]
LEFT OUTER JOIN sys.dm_xe_sessions AS &#x5B;xs]
    ON &#x5B;ses].&#x5B;name] = &#x5B;xs].&#x5B;name]
ORDER BY &#x5B;State], &#x5B;Start Time];
</pre>
<p style="text-align: justify;">And if you&#8217;re on SQL Server 2025 and using Extended Events, there&#8217;s a new MAX_DURATION option you can use to ensure a diagnostic session stops running after a certain amount of time.</p>
<h2>Summary</h2>
<p>You should always take a step-by-step approach to performance tuning rather than jumping right in and changing things haphazardly in production, and I hope this post has provided you with a simple framework you can put into practice. There’s a lot of code out there to help you with various investigations, plus free tools like <a href="https://www.solarwinds.com/free-tools/plan-explorer" target="_blank" rel="noopener">Plan Explorer</a> – I can’t recommend this enough! Happy tuning!</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/sql101-top-ten-sql-server-performance-tuning-best-practices/">SQL101: Top Ten SQL Server Performance Tuning Best Practices</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlskills.com/blogs/paul/sql101-top-ten-sql-server-performance-tuning-best-practices/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
		<item>
		<title>The Curious Case of&#8230; no buffer pool memory and no OS memory available</title>
		<link>https://www.sqlskills.com/blogs/paul/the-curious-case-of-no-buffer-pool-memory-and-no-os-memory-available/</link>
					<comments>https://www.sqlskills.com/blogs/paul/the-curious-case-of-no-buffer-pool-memory-and-no-os-memory-available/#comments</comments>
		
		<dc:creator><![CDATA[Paul Randal]]></dc:creator>
		<pubDate>Fri, 13 Mar 2026 20:01:07 +0000</pubDate>
				<category><![CDATA[The Curious Case of...]]></category>
		<guid isPermaLink="false">https://www.sqlskills.com/blogs/paul/?p=5366</guid>

					<description><![CDATA[<p>Jonathan had a client issue recently where SQL Server&#8217;s buffer pool had been forced down to a ridiculously small size, only a few hundred MB, but the OS also showed basically no free memory. Page Life Expectancy was zero! What was going on? From investigating SQL Server&#8217;s memory usage, the memory manager showed that target [&#8230;]</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/the-curious-case-of-no-buffer-pool-memory-and-no-os-memory-available/">The Curious Case of&#8230; no buffer pool memory and no OS memory available</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p style="text-align: justify;">Jonathan had a client issue recently where SQL Server&#8217;s buffer pool had been forced down to a ridiculously small size, only a few hundred MB, but the OS also showed basically no free memory. Page Life Expectancy was zero! What was going on?</p>
<p style="text-align: justify;">From investigating SQL Server&#8217;s memory usage, the memory manager showed that target and total memory were the same, at only 1.2GB, and lock pages in memory was correctly set. The next thing to check was for ballooning in VMware &#8211; a common cause of memory issues &#8211; but this wasn&#8217;t the problem either.</p>
<p style="text-align: justify;">Next, querying <em>sys.dm_os_sys_memory</em> showed almost 100GB in kernel non-paged pool memory. Jonathan immediately guessed that it was a kernel driver memory leak &#8211; based on some deliberate-error-inducing coding he&#8217;d done of kernel drivers &#8211; so the next step was to see what filter drivers were on the system.</p>
<p style="text-align: justify;">You can do this using the command fltmc on any Windows machine. For instance, in a command prompt using &#8216;Run as administrator&#8217; on a Windows 10 laptop, it shows:</p>
<pre>C:\WINDOWS\system32&gt;fltmc

Filter Name                     Num Instances    Altitude    Frame
------------------------------  -------------  ------------  -----
bindflt                                 1       409800         0
UCPD                                   11       385250.5       0
WdFilter                               11       328010         0
storqosflt                              0       244000         0
wcifs                                   1       189900         0
dbx                                     5       186500         0
CldFlt                                  5       180451         0
FileCrypt                               0       141100         0
luafv                                   1       135000         0
npsvctrig                               1        46000         0
RsFx0800                                1        41008.00      0
Wof                                     8        40700         0
FileInfo                               11        40500         0</pre>
<p style="text-align: justify;">And then you can investigate each driver using the <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/allocated-altitudes" target="_blank" rel="noopener">Allocated Filter Altitudes</a> list from Microsoft (as they assign a position in the filter driver stack to each filter driver), starting with those with the highest altitudes. From there, you can prove what&#8217;s taking up memory using the PoolMon tool from the Windows Driver Development Kit (see <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/using-poolmon-to-find-a-kernel-mode-memory-leak?source=recommendations" target="_blank" rel="noopener">Use PoolMon to Find a Kernel-Mode Memory Leak</a>).</p>
<p style="text-align: justify;">Turns out in the client&#8217;s case it was a driver from an old tool that hadn&#8217;t been un-installed &#8211; problem solved!</p>
<p style="text-align: justify;">Bottom line: don&#8217;t have the mindset that what manifests as a problem in SQL Server is always a SQL Server problem, as sometimes the issue is environmental and SQL Server is the unwitting victim of the root cause!</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/the-curious-case-of-no-buffer-pool-memory-and-no-os-memory-available/">The Curious Case of&#8230; no buffer pool memory and no OS memory available</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlskills.com/blogs/paul/the-curious-case-of-no-buffer-pool-memory-and-no-os-memory-available/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>The SQL Server Transaction Log, Part 3: The Circular Nature of the Log</title>
		<link>https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-3-the-circular-nature-of-the-log/</link>
					<comments>https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-3-the-circular-nature-of-the-log/#comments</comments>
		
		<dc:creator><![CDATA[Paul Randal]]></dc:creator>
		<pubDate>Mon, 09 Mar 2026 20:02:18 +0000</pubDate>
				<category><![CDATA[Transaction Log]]></category>
		<guid isPermaLink="false">https://www.sqlskills.com/blogs/paul/?p=5360</guid>

					<description><![CDATA[<p>(This post first appeared on SQLperformance.com four years ago as part of a blog series, before that website was mothballed later in 2022 and the series was curtailed. Reposted here with permission, with a few tweaks.) In the second part of this series, I described the structural hierarchy of the transaction log. As this post is chiefly [&#8230;]</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-3-the-circular-nature-of-the-log/">The SQL Server Transaction Log, Part 3: The Circular Nature of the Log</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p style="text-align: justify;"><em>(This post first appeared on SQLperformance.com four years ago as part of a blog series, before that website was mothballed later in 2022 and the series was curtailed. Reposted here with permission, with a few tweaks.)</em></p>
<p style="text-align: justify;">In the <a href="https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-2-log-architecture/" target="_blank" rel="noopener">second part</a> of this series, I described the structural hierarchy of the transaction log. As this post is chiefly concerned with the Virtual Log Files (VLFs) I described, I recommend you read the second part before continuing.</p>
<p style="text-align: justify;">When all is well, the transaction log will endlessly loop, reusing the existing VLFs. This behavior is what I call the <em>circular nature of the log</em>. Sometimes, however, something will happen to prevent this, and the transaction log grows and grows, adding more and more VLFs. In this post, I’ll explain how all this works, or sometimes doesn’t.</p>
<h2 style="text-align: justify;">VLFs and Log Truncation</h2>
<p style="text-align: justify;">All VLFs have a header structure containing metadata about the VLF. One of the most important fields in the structure is the status of the VLF, and the values we’re interested in are zero, meaning the VLF is <em>inactive</em>, and two, meaning the VLF is <em>active</em>. It’s important because an inactive VLF can be reused, but an active one cannot. Note that a VLF is wholly active or wholly inactive.</p>
<p style="text-align: justify;">A VLF will remain active while required log records are in it, so it can’t be reused and overwritten (I’ll cover log records themselves next time). Examples of reasons why log records may be required include:</p>
<ul style="text-align: justify;">
<li>There’s a long-running transaction the log records are part of, so they cannot be released until the transaction has committed or has finished rolling back</li>
<li>A log backup hasn’t yet backed up those log records</li>
<li>That portion of the log has not yet been processed by the Log Reader Agent for transactional replication or Change Data Capture</li>
<li>That portion of the log hasn’t yet been sent to an asynchronous database mirror or availability group replica</li>
</ul>
<p style="text-align: justify;">It’s important to note that if there are no reasons for a VLF to remain active, it won’t switch to being inactive again until a process called <em>log truncation</em> occurs – more on this below.</p>
<p style="text-align: justify;">Using a simple hypothetical transaction log with only five VLFs and VLF sequence numbers starting at 1 (remember from last time that in reality, they never do), when the transaction log is created, VFL 1 is immediately marked as active, as there always has to be at least one active VLF in the transaction log—the VLF where log blocks are currently being written to. Our example scenario is shown in Figure 1 below.</p>
<p style="text-align: justify;"><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure1.png"><img fetchpriority="high" decoding="async" class="alignnone wp-image-5361 size-large" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure1-1024x142.png" alt="" width="640" height="89" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure1-1024x142.png 1024w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure1-300x42.png 300w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure1-768x107.png 768w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure1.png 1030w" sizes="(max-width: 640px) 100vw, 640px" /></a></p>
<p style="text-align: justify;"><em class="caption">(Figure 1: Hypothetical, brand-new transaction log with 5 VLFs, sequence numbers 1 through 5 (my image))</em></p>
<p style="text-align: justify;">As more log records are created, and more log blocks are written to the transaction log, VLF 1 fills up, so VLF 2 has to become active for more log blocks to be written to, as shown in Figure 2 below.</p>
<p style="text-align: justify;"><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure2.png"><img decoding="async" class="alignnone wp-image-5362 size-large" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure2-1024x219.png" alt="" width="640" height="137" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure2-1024x219.png 1024w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure2-300x64.png 300w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure2-768x164.png 768w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure2.png 1030w" sizes="(max-width: 640px) 100vw, 640px" /></a></p>
<p style="text-align: justify;"><em class="caption">(Figure 2: Activity moves through the transaction log (my image))</em></p>
<p style="text-align: justify;">SQL Server tracks the start of the oldest uncommitted (active) transaction, and this LSN is persisted on disk every time a checkpoint operation occurs. The LSN of the most recent log record written to the transaction log is also tracked, but it’s only tracked in memory as there’s no way to persist it disk without running into various race conditions. That doesn’t matter as it’s only used during crash recovery, and SQL Server can work out the LSN of the “end” of the transaction log during crash recovery. Checkpoints and crash recovery are topics for future posts in the series.</p>
<p style="text-align: justify;">Eventually, VLF 2 will fill up, and VLF 3 will become active, and so on. The crux of the circular nature of the transaction log is that earlier VLFs in the transaction log become inactive so they can be reused. This is done by a process called <em>log truncation</em>, which is also commonly called <em>log clearing</em>. Unfortunately, both of these terms are terrible misnomers because nothing is actually truncated or cleared.</p>
<p style="text-align: justify;">Log truncation is simply the process of examining all the VLFs in the transaction log and determining which active VLFs can now be marked as inactive again, as none of their contents are still required by SQL Server. When log truncation is performed, there’s no guarantee any active VLFs can be made inactive—it entirely depends on what’s happening with the database.</p>
<p style="text-align: justify;">There are two common misconceptions about log truncation:</p>
<ol style="text-align: justify;">
<li>The transaction log gets smaller (the “truncation” misconception). No, it doesn’t – there’s no size change from log truncation. The only thing capable of making the transaction log smaller is an explicit DBCC SHRINKFILE.</li>
<li>The inactive VLFs are zeroed out in some way (the “clearing” misconception). No – nothing is written to the VLF when it’s made inactive except for a few fields in the VLF header.</li>
</ol>
<p style="text-align: justify;">Figure 3 below shows our transaction log where VLFs 3 and 4 are active, and log truncation was able to mark VLFs 1 and 2 inactive.</p>
<p style="text-align: justify;"><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure3.png"><img decoding="async" class="alignnone wp-image-5363 size-large" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure3-1024x219.png" alt="" width="640" height="137" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure3-1024x219.png 1024w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure3-300x64.png 300w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure3-768x165.png 768w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure3.png 1031w" sizes="(max-width: 640px) 100vw, 640px" /></a></p>
<p style="text-align: justify;"><em class="caption">(Figure 3: Log truncation marks earlier VLFs as inactive (my image))</em></p>
<p style="text-align: justify;">When log truncation occurs depends on which recovery model is in use for the database:</p>
<ul style="text-align: justify;">
<li>Simple model: log truncation occurs when a checkpoint operation completes</li>
<li>Full model or bulk-logged model: log truncation occurs when a log backup completes (as long as there isn’t a concurrent full or differential backup running, in which case log truncation is deferred until the data backup completes)</li>
</ul>
<p style="text-align: justify;">There are no exceptions to this.</p>
<h2 style="text-align: justify;">Circular Nature of the Log</h2>
<p style="text-align: justify;">To avoid the transaction log having to grow, log truncation must be able to mark VLFs inactive. The first physical VLF in the log must be inactive for the transaction log to have its circular nature.</p>
<p style="text-align: justify;">Consider Figure 4 below, which shows VLFs 4 and 5 are in use and log truncation has marked VLFs 1 through 3 as inactive. More log records are generated, more log blocks are written into VLF 5, and eventually, it fills up.</p>
<p style="text-align: justify;"><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure4.png"><img loading="lazy" decoding="async" class="alignnone wp-image-5364 size-large" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure4-1024x214.png" alt="" width="640" height="134" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure4-1024x214.png 1024w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure4-300x63.png 300w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure4-768x160.png 768w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure4.png 1034w" sizes="(max-width: 640px) 100vw, 640px" /></a></p>
<p style="text-align: justify;"><em class="caption">(Figure 4: Activity fills up the highest physical VLF in the transaction log (my image))</em></p>
<p style="text-align: justify;">At this point, the log manager for the database looks at the status of the first physical VLF in the transaction log, which in our example is VLF 1, with sequence number 1. VLF 1 is inactive, so the transaction log can wrap around and begin filling again from the start. The log manager changes the first VLF to active and increases its sequence number to be one higher than the current highest VLF sequence number. So it becomes VLF 6, and logging continues with log block being written into that VLF. This is the circular nature of the log, as shown below in Figure 5.</p>
<p style="text-align: justify;"><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure5.png"><img loading="lazy" decoding="async" class="alignnone wp-image-5365 size-large" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure5-1024x251.png" alt="" width="640" height="157" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure5-1024x251.png 1024w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure5-300x73.png 300w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure5-768x188.png 768w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/03/Figure5.png 1030w" sizes="(max-width: 640px) 100vw, 640px" /></a></p>
<p style="text-align: justify;"><em class="caption">(Figure 5: The circular nature of the transaction log and VLF reuse (my image))</em></p>
<h2 style="text-align: justify;">When Things Go Wrong</h2>
<p style="text-align: justify;">When the first physical VLF in the transaction log isn’t inactive, the transaction log cannot wrap around, so it will grow (as long as it’s configured to do so and there is sufficient disk space). This often happens because there’s something preventing log truncation from deactivating VLFs. If you find the transaction log for a database is growing, you can query SQL Server to find out if there’s a log truncation problem using this simple code below:</p>
<pre lang="tsql">SELECT [log_reuse_wait_desc]
FROM [master].[sys].[databases]
WHERE [name] = N'MyDatabase';
</pre>
<p style="text-align: justify;">If log truncation was able to deactivate one or more VLFs, then the result will be <em>NOTHING. Otherwise</em>, you’ll be given a reason why log truncation couldn’t deactivate any VLFs. There is a long list of possible reasons described <a href="https://learn.microsoft.com/en-us/sql/relational-databases/logs/the-transaction-log-sql-server?view=sql-server-ver17#factors-that-can-delay-log-truncation" target="_blank" rel="noopener">here</a> in the section <em>Factors that can delay log truncation.</em></p>
<p style="text-align: justify;">It’s important to understand the semantics of what the result is: it’s the reason log truncation couldn’t do anything <em>the last time it tried to run</em>. For instance, the result might be <em>ACTIVE_BACKUP_OR_RESTORE,</em> but you know that that long-running full backup has finished. This just means that the last time log truncation was attempted, the backup was still running.</p>
<p style="text-align: justify;">In my experience, the most common reason for log truncation being prevented is <em>LOG_BACKUP</em>; i.e., go perform a log backup! But there’s also an interesting, weird behavior with<em> LOG_BACKUP</em>. If you continually see the result <em>LOG_BACKUP</em> but you know log backups are happening successfully, it’s because there is very little activity in the database and the current VLF is the same as it was the last time a log backup was performed. So, <em>LOG_BACKUP</em> means “go perform a log backup” or “all of the log records backed up are from the current VLF, so it couldn’t be deactivated.” When the latter happens, it can be confusing.</p>
<h2 style="text-align: justify;">Circling Back&#8230;</h2>
<p style="text-align: justify;">Maintaining the circular nature of the transaction log is very important to avoid costly log growths and the need to take corrective action. Usually, this means ensuring log backups are happening regularly to facilitate log truncation and sizing the transaction log to be able to hold any large, long-running operations like index rebuilds or ETL operations without log growth occurring.</p>
<p style="text-align: justify;">In the next part of the series, I’ll cover log records, how they work, and some interesting examples.</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-3-the-circular-nature-of-the-log/">The SQL Server Transaction Log, Part 3: The Circular Nature of the Log</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-3-the-circular-nature-of-the-log/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>SQL101: Indexing Strategies for SQL Server Performance</title>
		<link>https://www.sqlskills.com/blogs/paul/sql101-indexing-strategies-for-sql-server-performance/</link>
					<comments>https://www.sqlskills.com/blogs/paul/sql101-indexing-strategies-for-sql-server-performance/#comments</comments>
		
		<dc:creator><![CDATA[Paul Randal]]></dc:creator>
		<pubDate>Wed, 04 Mar 2026 19:50:17 +0000</pubDate>
				<category><![CDATA[Indexes From Every Angle]]></category>
		<category><![CDATA[SQL101]]></category>
		<guid isPermaLink="false">https://www.sqlskills.com/blogs/paul/?p=5358</guid>

					<description><![CDATA[<p>One of the easiest ways to increase query performance in SQL Server is to make sure that it can quickly access the requested data, and this is done as efficiently as possible. In SQL Server, using one or more indexes can be exactly the fix you need. In fact, indexes are so important that SQL [&#8230;]</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/sql101-indexing-strategies-for-sql-server-performance/">SQL101: Indexing Strategies for SQL Server Performance</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p style="text-align: justify;">One of the easiest ways to increase query performance in SQL Server is to make sure that it can quickly access the requested data, and this is done as efficiently as possible. In SQL Server, using one or more indexes can be exactly the fix you need. In fact, indexes are so important that SQL Server can even warn you when it figures out that there’s an index missing that would benefit a query. This high-level post will explain what indexes are, why they’re so important, and a bit of the both the art and science of various indexing strategies.</p>
<h2 style="text-align: justify;">What Are Indexes?</h2>
<p style="text-align: justify;">An index is simply a way of organizing data. SQL Server supports a variety of index types (see <a href="https://docs.microsoft.com/en-us/sql/relational-databases/indexes/indexes?view=sql-server-ver15">here</a> for details) but this post will consider only the two most common ones, which are useful in a variety of ways and for a wide number of workloads: clustered and nonclustered non-columnstore indexes.</p>
<p style="text-align: justify;">A table without a clustered index is called a heap, where the data rows in the table are unordered. If there are no indexes on the heap that means finding a particular data value in the table requires reading all the data rows in the table (called a table scan). That is obviously very inefficient, and becomes more so the larger the table grows.</p>
<p style="text-align: justify;">A clustered index on a table arranges all the data rows in the table into a sorted order and places a navigational “tree” with the organized data so that it is easily navigated. The table is no longer a heap; it’s a clustered table. The order is defined by the clustered index key, which is comprised of one or more columns from the table. The structure of a clustered index is known as a B-tree, and this basic data structure allows a specific data row to be located (called a “seek”) based on the clustered index key value, without having to scan the whole table.</p>
<p style="text-align: justify;">A good example of a clustered index is a table that stores the details of a company’s employees, where the table has a clustered index using the Employee ID as the key. All the rows in the table are stored in the clustered index in order of Employee ID, so finding the details of a particular employee using their Employee ID is very efficient.</p>
<p style="text-align: justify;">A clustered index only allows efficient location of data rows based on the clustered index key. If it is necessary to be able to find data rows quickly using a different key value, then one or more additional indexes must be created, otherwise a table scan is required. For nonclustered indexes, each index row contains the nonclustered index key value and a locator for the corresponding data row (this is the data row’s physical location for a heap or the data row’s clustered index key for a clustered index).</p>
<p style="text-align: justify;">Continuing the Employee table example, if someone wants to find the details of a particular employee and only knows the employee’s name, a nonclustered index could be created with a composite key of the LastName, FirstName, and MiddleInitial table columns. That would allow the Employee ID for an employee to be found, and then retrieve all the employee’s details from the corresponding data row in the clustered index.</p>
<h2 style="text-align: justify;">Why Are Indexes So Important?</h2>
<p style="text-align: justify;">As you have no doubt gathered, the primary use of indexes is to allow the efficient retrieval of data from a table without having to perform a table scan. By limiting the amount of data that has to be accessed and then processed, there are a lot of benefits to overall workload performance:</p>
<ul style="text-align: justify;">
<li>Minimal amount of data has to be read from disk. This prevents undue pressure on the I/O subsystem from many queries reading inefficient or larger amounts of data, and help to prevent ‘churn’ in the buffer pool (the in-memory cache of data file pages) by not forcing data already in memory to be dropped from memory to make space for data be read from disk. In some cases, no data will have to be read from disk, if the required data is already in memory.</li>
<li>Minimal amount of data has to take up space in the buffer pool. This means more of the ‘working set’ of the workload can be held in memory, further reducing the need for physical reads.</li>
<li>Any reduction in the amount of physical reads that a query must perform will lead to a drop in execution time.</li>
<li>Any reduction in the amount of data that flows through the query plan will lead to a drop in execution time.</li>
</ul>
<p style="text-align: justify;">As well as indexes, there are other things that can help produce the benefits above, including:</p>
<ul style="text-align: justify;">
<li>Using proper join conditions.</li>
<li>Using search arguments to further narrow the data required.</li>
<li>Avoiding coding practices that force a table scan to be used, such as in advertently causing implicit conversions.</li>
<li>Making sure statistics are maintained correctly, so the query optimizer can choose the best processing strategies and indexes.</li>
<li>Taking into account the execution method of a query where a cached plan has been used, resulting in parameter sensitivity problems.</li>
</ul>
<p style="text-align: justify;">But these are all topics for future posts!</p>
<h2 style="text-align: justify;">The Art and Science of Indexing</h2>
<p style="text-align: justify;">There are two parts to index tuning a workload – there’s both an art and a science. The science is that for any query there is always a perfect index, but the art is realizing that index may not be in the best interests of the overall database or server workload and figuring out what the best overall solution is for your server takes analyzing the server’s workload and priorities.</p>
<p style="text-align: justify;">Clustered index key choice is more of a science than an art, and is a whole discussion by itself, but we usually say that a clustered index key should have multiple properties (in no particular order):</p>
<ol style="text-align: justify;">
<li>The clustered index key is the data row locator that is included in every index row in every nonclustered index. This means the narrower it is, the less space it will take up overall and that will help with data size.</li>
<li>Fixed-width. A clustered index key should be narrow but also use a fixed-width data type. When a variable-width data type is used then the data row and all nonclustered index rows will incur additional overhead.</li>
<li>If the clustered index key is not unique, then a special, hidden ‘uniquifier’ column is added to the clustered index key for all non-unique data rows, making the clustered index key up to four bytes longer for those rows.</li>
<li>If a clustered index key value changes, the data row must be deleted and reinserted internally, and all nonclustered index records containing that data row locator must be updated.</li>
<li>Ever-increasing. This property helps to prevent index fragmentation from occurring in the clustered index.</li>
<li>Non-nullable. The clustered index key should be unique by definition (see #3, above) so it implies that it cannot allow NULL values. In some SQL Server versions and in some structures, a nullable column would incur more overhead than a non-nullable column. Ideally, none of the columns that make up the clustered index key would allow NULL values.</li>
</ol>
<p style="text-align: justify;">As a generalization and because you can only have one clustered index, it’s usually nonclustered indexes (and multiple of them) that help queries run more efficiently.</p>
<p style="text-align: justify;">The science of constructing the best nonclustered index for a query involves:</p>
<ul style="text-align: justify;">
<li>Understanding the search arguments being used and the type of query (as there are different indexing strategies, for instance, when search arguments use AND or OR clauses, when aggregates are involved, and for different join types). The search arguments are basically which table columns are necessary to identify the required data rows. These will likely be part of the nonclustered index keys.</li>
<li>Understanding the ‘selectivity’ of the data in each of these key columns. This will dictate the order of the columns in the index key, with the most selective predicates leading the key definition.</li>
<li>Understanding the SELECT list for the query. Any of these columns may be candidates for being included in the index as non-key columns to avoid the query having to go to the data row to retrieve them (also known as “covering” a query).</li>
</ul>
<p style="text-align: justify;">And there’s also SQL Server’s missing indexes functionality that will recommend the best index for a query (it focuses on just the science of “query tuning” but not the art of “server tuning”).</p>
<p style="text-align: justify;">The art then becomes taking that index and figuring out whether and how it can be consolidated with other existing or also recommended indexes, so the table doesn’t become over-indexed.</p>
<p style="text-align: justify;">As a simple example, let’s say that a table has ten int columns named col1 through col10.</p>
<p style="text-align: justify;">The first query to index is <em>SELECT col2, col3 FROM table WHERE col6 = value.</em> A nonclustered index on col6 would avoid a table scan, but would require the query to go to the data row to get the values for col2 and col3. A more efficient nonclustered index would have col6 as the key and include col2 and col3 as non-key columns. This is called a covering index, because the index row has all the columns necessary for the index and removes the need to use the clustered index as well to get the additionally requested columns.</p>
<p style="text-align: justify;">The second query to index is <em>SELECT col4 FROM table WHERE col6 = value</em>. The science tells us that a nonclustered index on col6 that includes col4 is likely the best index for the query. But then there are two nonclustered indexes keyed on col6, each including different non-key columns. This is where the art comes in, as the best index for the overall workload is likely a single nonclustered index on col6 that includes col2, col3, and col4. Now you have one index with more uses and fewer overall indexes on the table.</p>
<p style="text-align: justify;">And the art can continue through multiple iterations.</p>
<p style="text-align: justify;">Let’s say a third query is created that is <em>SELECT col4, col5 from table where col6 = value AND col2 = value</em>. The science may say that the best nonclustered index is on (col6, col2) if col6 is more selective than col2, and including col4 and col5 as non-key columns. The art then has us look at consolidation and end up with a single nonclustered index on (col6, col2) that includes col3, col4, and col5. This satisfies all three queries with a single nonclustered index instead of three, so it takes up less space overall at the expense of being less efficient for each query than the individual “perfect” nonclustered indexes would be. However, there’s an added benefit to this consolidation – the fewer nonclustered indexes there are, there less amount of index maintenance needs to be done when a data row is inserted, deleted, or updated.</p>
<p style="text-align: justify;">Obviously, there’s point where you may over-consolidate as well, and that’s where experience in indexing design helps hone your art, so you’re not under-indexing, over-indexing, or over-consolidating.</p>
<h2 style="text-align: justify;">Summary</h2>
<p style="text-align: justify;">There’s a lot more to the art and science of designing an indexing strategy than can be covered in a post such as this but hopefully you now understand why having a good indexing strategy is so important. A deeper primer on indexing is Kimberly L. Tripp’s 7-hour Pluralsight course <a href="https://app.pluralsight.com/library/courses/sqlserver-indexing-for-performance/table-of-contents"><em>SQL Server: Indexing for Performance</em></a>.</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/sql101-indexing-strategies-for-sql-server-performance/">SQL101: Indexing Strategies for SQL Server Performance</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlskills.com/blogs/paul/sql101-indexing-strategies-for-sql-server-performance/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>March Madness: $699 Blackbelt Bundle!</title>
		<link>https://www.sqlskills.com/blogs/paul/march-madness-699-blackbelt-bundle/</link>
					<comments>https://www.sqlskills.com/blogs/paul/march-madness-699-blackbelt-bundle/#respond</comments>
		
		<dc:creator><![CDATA[Paul Randal]]></dc:creator>
		<pubDate>Sun, 01 Mar 2026 22:18:13 +0000</pubDate>
				<category><![CDATA[Career]]></category>
		<category><![CDATA[Training]]></category>
		<guid isPermaLink="false">https://www.sqlskills.com/blogs/paul/?p=5355</guid>

					<description><![CDATA[<p>Spring is already in the air here in Redmond so time for March Madness! Hundreds of dollars lower-than-ever prices on our signature 158-hour Blackbelt training bundle: US$699 for one-year access (or to upgrade) &#8211; $200 lower than ever before! &#8211; US$1,299 for Lifetime access (no expiration) &#8211; $300 lower than ever before! Both prices include [&#8230;]</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/march-madness-699-blackbelt-bundle/">March Madness: $699 Blackbelt Bundle!</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars.jpg"><img loading="lazy" decoding="async" class="alignnone wp-image-5180 size-thumbnail" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-150x150.jpg" alt="" width="150" height="150" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-150x150.jpg 150w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-300x300.jpg 300w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars.jpg 600w" sizes="(max-width: 150px) 100vw, 150px" /></a><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars.jpg"><img loading="lazy" decoding="async" class="alignnone wp-image-5180 size-thumbnail" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-150x150.jpg" alt="" width="150" height="150" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-150x150.jpg 150w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-300x300.jpg 300w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars.jpg 600w" sizes="(max-width: 150px) 100vw, 150px" /></a><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars.jpg"><img loading="lazy" decoding="async" class="alignnone wp-image-5180 size-thumbnail" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-150x150.jpg" alt="" width="150" height="150" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-150x150.jpg 150w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-300x300.jpg 300w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars.jpg 600w" sizes="(max-width: 150px) 100vw, 150px" /></a><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars.jpg"><img loading="lazy" decoding="async" class="alignnone wp-image-5180 size-thumbnail" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-150x150.jpg" alt="" width="150" height="150" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-150x150.jpg 150w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-300x300.jpg 300w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars.jpg 600w" sizes="(max-width: 150px) 100vw, 150px" /></a><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars.jpg"><img loading="lazy" decoding="async" class="alignnone wp-image-5180 size-thumbnail" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-150x150.jpg" alt="" width="150" height="150" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-150x150.jpg 150w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars-300x300.jpg 300w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2021/01/blackbelt-sqlserver2019-7stars.jpg 600w" sizes="(max-width: 150px) 100vw, 150px" /></a></p>
<p style="text-align: justify;">Spring is already in the air here in Redmond so time for March Madness!</p>
<p style="text-align: justify;">Hundreds of dollars lower-than-ever prices on our signature 158-hour Blackbelt training bundle:</p>
<ul>
<li>US$699 for <a href="https://www.sqlskills.com/product/sqlskills-blackbelt-bundle/" target="_blank" rel="noopener">one-year access</a> (or to upgrade) &#8211; $200 lower than ever before!</li>
<li>&#8211; US$1,299 for <a href="https://www.sqlskills.com/product/bbbct-lifetime/" target="_blank" rel="noopener">Lifetime access</a> (no expiration) &#8211; $300 lower than ever before!</li>
</ul>
<p style="text-align: justify;">Both prices include the 2022 updates and Q&amp;A sessions.</p>
<p style="text-align: justify;">If you have a one-year subscription and would like to upgrade it to Lifetime, purchase a second one-year and I’ll remove the expiration dates and make sure you get the 2022 Q&amp;As/Updates recordings with Lifetime access for free. It doesn’t matter if your original subscription has expired!</p>
<p style="text-align: justify;">Lifetime subscribers will also get the upcoming re-record of IECAG (clustering and AGs) for SQL Server 2025 for free, and Kimberly’s upcoming IESP (stored proc performance) for free.</p>
<p style="text-align: justify;">See our <a href="https://www.sqlskills.com/shop/" target="_blank" rel="noopener">shop</a> for details and <a href="mailto:paul@sqlskills.com?subject=December%20sale%20question%20from%20blog" target="_blank" rel="noopener">let me know</a> of any questions.</p>
<p style="text-align: justify;">Enjoy!</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/march-madness-699-blackbelt-bundle/">March Madness: $699 Blackbelt Bundle!</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlskills.com/blogs/paul/march-madness-699-blackbelt-bundle/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The Curious Case of&#8230; finding long IAM chains</title>
		<link>https://www.sqlskills.com/blogs/paul/the-curious-case-of-finding-long-iam-chains/</link>
					<comments>https://www.sqlskills.com/blogs/paul/the-curious-case-of-finding-long-iam-chains/#comments</comments>
		
		<dc:creator><![CDATA[Paul Randal]]></dc:creator>
		<pubDate>Wed, 25 Feb 2026 02:36:27 +0000</pubDate>
				<category><![CDATA[Inside the Storage Engine]]></category>
		<category><![CDATA[The Curious Case of...]]></category>
		<guid isPermaLink="false">https://www.sqlskills.com/blogs/paul/?p=5352</guid>

					<description><![CDATA[<p>In the previous Curious Case I described an issue Jonathan had at a client with very long IAM chains, and the circumstances leading to it. The question was how to prove that some allocation units had IAM chain lengths way out of proportion to the amount of data in the allocation unit, without tediously walking [&#8230;]</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/the-curious-case-of-finding-long-iam-chains/">The Curious Case of&#8230; finding long IAM chains</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p style="text-align: justify;">In the <a href="https://www.sqlskills.com/blogs/paul/the-curious-case-of-occasional-query-failure-on-a-tiny-table/" target="_blank" rel="noopener">previous Curious Case</a> I described an issue Jonathan had at a client with very long IAM chains, and the circumstances leading to it.</p>
<p style="text-align: justify;">The question was how to prove that some allocation units had IAM chain lengths way out of proportion to the amount of data in the allocation unit, without tediously walking through each IAM chain, starting with the first IAM page (whose ID is always stored in <em>sys.allocation_units</em> internal table).</p>
<p style="text-align: justify;">The answer was to do exactly that, but remove the tedium by writing some nifty code to do it, making use of the <a href="https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-db-page-info-transact-sql?view=sql-server-ver17" target="_blank" rel="noopener"><em>sys.dm_db_page_info</em></a> DMF that was added in SQL Server 2019 instead of having to use <em>DBCC PAGE</em> with the results <em>INSERT &#8230; EXEC</em>&#8216;d into a table.</p>
<p style="text-align: justify;">(DMF? Yes, Dynamic Management Function. Remember &#8211; they&#8217;re all DMOs &#8211; Dynamic Management Objects &#8211; and either views or functions &#8211; DMVs or DMFs. DMVs just look up information but DMFs have to do some work. They&#8217;re just collectively called DMVs for simplicity.)</p>
<p style="text-align: justify;">Specifically, the answer was for Jonathan to write the nifty code :-) and here it is. Give it a whirl and let me know if you find any indexes with massive IAM chains compared to the number of data or index pages.</p>
<pre class="brush: sql; title: ; toolbar: true; wrap-lines: true; notranslate">
;WITH IAM_PAGES AS
(
    SELECT
        1 AS &#x5B;IAM_Page_Ordinal],
        P.&#x5B;object_id],
        P.&#x5B;index_id],
        P.&#x5B;partition_number],
        IAU.&#x5B;total_pages],
        IAU.&#x5B;used_pages],
        IAU.&#x5B;data_pages],
        IAM_Page.&#x5B;file_id],
        IAM_Page.&#x5B;page_id],
        &#x5B;pfs_page_id],
        &#x5B;gam_page_id],
        &#x5B;sgam_page_id],
        &#x5B;next_page_file_id],
        &#x5B;next_page_page_id],
        &#x5B;is_iam_page]
    FROM sys.partitions P
    INNER JOIN sys.system_internals_allocation_units AS IAU
        ON P.&#x5B;hobt_id] = IAU.&#x5B;container_id]
    OUTER APPLY sys.fn_PageResCracker (IAU.&#x5B;first_iam_page]) AS IAM_Page
    OUTER APPLY sys.dm_db_page_info (
            DB_ID (), IAM_Page.&#x5B;file_id], IAM_Page.&#x5B;page_id], &#039;DETAILED&#039;) AS Page_Info
        WHERE IAM_Page.&#x5B;page_id] &lt;&gt; 0 AND OBJECT_SCHEMA_NAME (P.&#x5B;object_id]) &lt;&gt; N&#039;sys&#039;
UNION ALL
    SELECT           
        &#x5B;IAM_Page_Ordinal] + 1,
        IAMP.&#x5B;object_id],
        IAMP.&#x5B;index_id],
        IAMP.&#x5B;partition_number],
        IAMP.&#x5B;total_pages],
        IAMP.&#x5B;used_pages],
        IAMP.&#x5B;data_pages],
        Page_Info.&#x5B;file_id],
        Page_Info.&#x5B;page_id],
        Page_Info.&#x5B;pfs_page_id],
        Page_Info.&#x5B;gam_page_id],
        Page_Info.&#x5B;sgam_page_id],
        Page_Info.&#x5B;next_page_file_id],
        Page_Info.&#x5B;next_page_page_id],
        Page_Info.&#x5B;is_iam_page]
    FROM IAM_PAGES AS IAMP
    OUTER APPLY sys.dm_db_page_info (
            DB_ID (), IAMP.&#x5B;next_page_file_id], IAMP.&#x5B;next_page_page_id], &#039;DETAILED&#039;) AS Page_Info
        WHERE IAMP.&#x5B;next_page_page_id] &lt;&gt; 0
),
IAM_Counts AS
(
    SELECT
        &#x5B;object_id],
        &#x5B;index_id],
        &#x5B;partition_number],
        &#x5B;total_pages],
        &#x5B;used_pages],
        &#x5B;data_pages],
        COUNT (*) AS &#x5B;IAM_Page_Count]
    FROM IAM_PAGES
    GROUP BY &#x5B;object_id], &#x5B;index_id], &#x5B;partition_number],
        &#x5B;total_pages], &#x5B;used_pages], &#x5B;data_pages]
)
SELECT * FROM IAM_Counts
WHERE &#x5B;data_pages] &lt; &#x5B;iam_page_count]
--  AND &#x5B;object_id] = OBJECTD_ID (&#039;Schema.TableName&#039;)
OPTION (MAXRECURSION 0);
GO
</pre>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/the-curious-case-of-finding-long-iam-chains/">The Curious Case of&#8230; finding long IAM chains</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlskills.com/blogs/paul/the-curious-case-of-finding-long-iam-chains/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title>SQL101: Introduction to SQL Server Transactions</title>
		<link>https://www.sqlskills.com/blogs/paul/sql101-introduction-to-sql-server-transactions/</link>
					<comments>https://www.sqlskills.com/blogs/paul/sql101-introduction-to-sql-server-transactions/#comments</comments>
		
		<dc:creator><![CDATA[Paul Randal]]></dc:creator>
		<pubDate>Fri, 20 Feb 2026 22:30:38 +0000</pubDate>
				<category><![CDATA[SQL101]]></category>
		<category><![CDATA[Transaction Log]]></category>
		<guid isPermaLink="false">https://www.sqlskills.com/blogs/paul/?p=5351</guid>

					<description><![CDATA[<p>(The original version of this post first appeared on the now-deleted SentryOne blog at the start of 2022.) One of the most fundamental concepts in any relational database management system (RDBMS), such as SQL Server, is the transaction. During my consulting career, I&#8217;ve seen many instances of performance problems caused by developers not understanding how [&#8230;]</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/sql101-introduction-to-sql-server-transactions/">SQL101: Introduction to SQL Server Transactions</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>(The original version of this post first appeared on the now-deleted SentryOne blog at the start of 2022.)</em></p>
<p style="text-align: justify;">One of the most fundamental concepts in any relational database management system (RDBMS), such as SQL Server, is the transaction. During my consulting career, I&#8217;ve seen many instances of performance problems caused by developers not understanding how transactions work in SQL Server, so in this tutorial, I’ll explain what transactions are and why they’re necessary, plus some details of how they work in SQL Server. There are nuances to some of this when Accelerated Database Recovery (ADR) is in use &#8211; topics for future articles.</p>
<h2>What is a Transaction?</h2>
<p style="text-align: justify;">A transaction is a <em>unit of work</em> in the database. Every transaction has a defined starting point and a defined ending point. The ending point may be the transaction <em>committed</em> (i.e. completed successfully) or transaction finished <em>rolling back</em> (i.e. did not complete successfully), and I’ll discuss the meaning of those terms a little later.</p>
<p>The basic syntax for transactions is as follows:</p>
<ul>
<li style="text-align: justify;">BEGIN TRANSACTION (or BEGIN TRAN) starts the transaction.</li>
<li style="text-align: justify;">COMMIT TRANSACTION (or COMMIT TRAN) ends the transaction successfully.</li>
<li style="text-align: justify;">ROLLBACK TRANSACTION (or ROLLBACK TRAN) causes the transaction to end unsuccessfully so all operations performed in the transaction are reversed.</li>
</ul>
<p>You can also specify a transaction name but this is not required and I don&#8217;t see them used very often.</p>
<p style="text-align: justify;">It is important to think about what a <em>unit of work</em> is. It means all changes to the database within the confines of the transaction, typically DML data modification operations like insert statements, update statements, and delete statements. This might be a single T-SQL statement or multiple statements, depending on the kind of transaction being used. If it’s a single statement, it doesn’t necessarily mean a single change. Consider a table with 1,000 rows and someone runs an UPDATE statement with a WHERE clause. This is a single statement but will cause a change to all the rows in the table, at least 1,000 changes to the database within the transaction.</p>
<p style="text-align: justify;">Even if the UPDATE statement only operates on a single row in the table, there are still at least two changes to the database; the update of the row itself on a data file page and the update of the differential bitmap page to mark that portion of the database as changed so the next differential backup will back up the extent that the page is part of. There are plenty more examples I&#8217;ve seen over the years of where a single statement can cause many changes to the database depending on data types like varchar, table formats, whether nonclustered indexes exist, and so on.</p>
<h2>Why Are Transactions Necessary?</h2>
<p style="text-align: justify;">Transactions are part of how SQL Server implements the ACID properties of a database (Atomicity, Consistency, Isolation, and Durability), along with mechanisms like locking and logging.</p>
<p style="text-align: justify;">A transaction guarantees that its unit of work is either wholly present in the database or wholly not present. This is the atomicity in the ACID properties, and I’ll explain how this is done later. This means transactions are very useful for SQL Server developers to control whether a set of operations (e.g. implementing some business logic) completely succeeds or does not succeed at all, so there are no partially-executed sets of operations that would leave the database inconsistent from a business perspective.</p>
<p style="text-align: justify;">A classic example is moving money from a checking account to a deposit account. This involves a debit from the checking account and a credit to the deposit account. This must be implemented as a single transaction so that if the debit succeeds and the credit fails, the transaction as a whole fails and the debit is reversed when the transaction rolls back.</p>
<p style="text-align: justify;">In all cases, changes to the database are performed under locks held by the transaction and these locks are not released until the transaction ends. Using the default <em>isolation level</em>, which is called <em>read committed</em>, other transactions will not be able to see these changes until the transaction has committed (ended), hence the name of the isolation level. This is the isolation in the ACID properties.</p>
<p style="text-align: justify;">For instance, a change to a row will involve the row being exclusively locked by the transaction. Another transaction that wants to read the row will usually require a share lock on the row, and so will also be blocked. This behavior can be changed if the reading transaction changes to the <em>read uncommitted</em> isolation level (or uses the NOLOCK option on the SELECT statement), that doesn’t require share locks for reading rows, but introduces the possibility of anomalies occurring.</p>
<h2>Types of Transactions in SQL Server</h2>
<p style="text-align: justify;">There are three basic types of transactions in SQL Server:</p>
<ol style="text-align: justify;">
<li><em>Explicit</em> transactions, as the name suggests, must be explicitly started with a BEGIN TRANSACTION statement and explicitly ended with either a COMMIT TRANSACTION statement or a ROLLBACK TRANSACTION statement. In other words, the SQL Server developer controls when the unit of work is committed or not.</li>
<li><em>Autocommit</em> transactions are where the developer does not control the starting and ending points of the transaction. Each T-SQL statement is its own transaction that SQL Server begins and commits automatically under the covers. There is no concept of being able to make a change to a SQL Server database without a transaction being started, as SQL Server must have the ability to roll back the change if something goes wrong.</li>
<li><em>Implicit</em> transactions are when a transaction is automatically started by SQL Server as soon as a change is made to the database, but remains active until it is explicitly ended. At that point a new transaction is automatically started. This behavior is not the default and must be specifically enabled using a SET IMPLICIT_TRANSACTIONS statement, which is not normally done except to allow behavior compatibility with another RDBMS where this is the default behavior. I&#8217;ve seen this be a problem when developers don&#8217;t realize implicit transactions are enabled and don&#8217;t think they need to explicitly commit the transaction. More on that in the &#8216;common mistakes&#8217; section below.</li>
</ol>
<p style="text-align: justify;">With all three of these transaction types, if SQL Server encounters a problem the entire transaction will automatically roll back.</p>
<p style="text-align: justify;">There are also two more advanced kinds of transactions in SQL Server, which are beyond the scope of this article:</p>
<ol>
<li style="text-align: justify;"><em>Batch-scoped</em> transactions, which are only used during Multiple Active Result Sets sessions.</li>
<li style="text-align: justify;"><em>Distributed</em> transactions, which are used when a local transaction needs to coordinate with multiple SQL Server instances, for instance to run stored procedures with business logic on different servers. This is done using a Distributed Transaction Coordinator, or by the service itself on Azure Managed Instance.</li>
</ol>
<h2>How Does Commit Work in SQL Server?</h2>
<p>Consider a very simple example of an explicit transaction that inserts a record into a table, using the code:</p>
<pre>BEGIN TRANSACTION;
INSERT INTO
     MyDatabase.dbo.MyTable
VALUES (1, ‘Paul’, ‘Randal’);
COMMIT TRANSACTION;</pre>
<p style="text-align: justify;">The insert statement causes some locks to be acquired, which provides the isolation portion of the ACID properties of a database. These will only be released once the transaction has committed. When the COMMIT TRANSACTION statement is executed, I know that the insert is now durable. How does this actually happen?</p>
<p style="text-align: justify;">All changes to a database are <em>logged</em>. Simply put, this means that when a change is made to a data file page, a description of the change is generated, called a <em>log record</em> and entered into the database transaction log. Also when a transaction begins, there is a log record generated and another when a transaction commits. This means our simple explicit transaction will have three log records in the transaction log, all with the same transaction ID, one for each of the three statements executed. In fact, if I’d used an autocommit transaction instead of an explicit transaction (executing just the insert statement), SQL Server would have automatically started and committed the transaction and there would still be three log records in the transaction log for the transaction. One interesting fact you might not know is that SQL Server usually names transactions that it starts; in this case it would have been named simply &#8216;INSERT&#8217;.</p>
<p style="text-align: justify;">When a transaction commits, SQL Server has to make sure that all the log records for the transaction are in the transaction log on disk and not just in memory, so in the event of a crash, the transaction can be replayed, guaranteeing its durability. It does this by making sure that all the transaction log in memory up to the log record for the COMMIT TRANSACTION is flushed to disk before the commit is acknowledged back to the user or application. The sequence of operations when a commit occurs is:</p>
<ul style="text-align: justify;">
<li>Make sure the log is flushed to disk</li>
<li>If there is a synchronous database mirror or synchronous availability group replica, make sure the log is also written to disk for their log files on the remote servers</li>
<li>Release the locks the transaction is holding</li>
<li>Acknowledge the commit has happened</li>
</ul>
<p style="text-align: justify;">There is no need to also flush the changed data file pages to disk at this point, as the transaction has already been made durable by making sure the description of all the changes are on disk. The data file pages will be written out later by a checkpoint operation – a topic for a future article.</p>
<h2>How Does Rollback Work in SQL Server?</h2>
<p style="text-align: justify;">When a transaction must be rolled back, all operations that were part of the transaction must be essentially reversed so none of the data modifications from the transaction are present in the database. This is done using the transaction log, as the log records for a transaction are linked together in reverse order and this allows the transaction’s changes to be undone in reverse order.</p>
<p>Consider another simple example:</p>
<pre>BEGIN TRANSACTION;
INSERT INTO
     MyDatabase.dbo.MyTable
VALUES (1, ‘Paul’, ‘Randal’);
INSERT INTO
     MyDatabase.dbo.MyTable
VALUES (2, ‘Kimberly’, ‘Tripp’);</pre>
<p style="text-align: justify;">At this point there are three log records for the transaction. If I then decide to execute a rollback command, SQL Server does the following:</p>
<ol style="text-align: justify;">
<li>Find the most recent log record for the ‘forward’ part of the transaction, work out what operation will undo the change described by the log record, perform the operation, and generate a log record.</li>
<li>Find the previous log record, pointed to by the ‘previous log record’ LSN.</li>
<li>Repeat until the begin log record is reached. At this point the rollback has been completed, so another log record is generated that indicates that the transaction has successfully aborted.</li>
</ol>
<p style="text-align: justify;">This will generate three more log records for my example. As you can see, rolling back a transaction takes a lot of work under the covers.</p>
<p style="text-align: justify;">It is also possible to define a <em>savepoint</em> using the SAVE TRANSACTION statement and roll back to that named point in the transaction rather than rolling the entire transaction back.</p>
<h2>Common Mistakes That Can Cause Transaction Log Problems</h2>
<p>The first mistake is to forget to commit a transaction. This means everything that subsequently happens on that connection is part of the same transaction. As more changes are made, more log records are generated and more transaction log space is required. The space used to store log records from earlier in the transaction cannot be reused (i.e. allowing the log to <em>truncate</em>), as those log records must remain in case the transaction rolls back (and they’re needed for the mechanism I described above). The transaction log will likely grow… and grow… and grow, until someone finally commits the long-running transaction and allows the log to be brought back under control.</p>
<p>The second mistake is to inadvertently execute some code that does a lot more work than you thought, for instance performing an update on a very large table (e.g. a billion rows) and forgetting a WHERE clause. For every row that’s updated, there’s at least one log record generated so there will be at least a billion log records generated for the transaction and that will likely cause explosive transaction log growth. A DBA that doesn’t know how rollback works might be tempted to immediately cancel the update. But a knowledgeable DBA will know that rolling back a very long-running transaction will generate at least the same number of log records as have already been generated, taking a lot more time, and may decide that the prudent course of action is to let the update complete.</p>
<p>If you have a transaction log that is seemingly growing out of control, you can see why by running this code:</p>
<pre>SELECT
     [log_reuse_wait_desc]
FROM [master].[sys].[databases]
WHERE
    [name] = N'MyDatabase';</pre>
<p>If one of these two mistake scenario is the culprit, the output will look like this:</p>
<pre>log_reuse_wait_desc
-------------------
ACTIVE_TRANSACTION</pre>
<p>If not, you can read about the other possible values and what they mean in the Microsoft documentation <a href="https://docs.microsoft.com/en-us/sql/relational-databases/logs/the-transaction-log-sql-server?view=sql-server-ver15">here</a>, in the section <em>Factors that can delay log truncation</em>.</p>
<h2>Importance of Understanding Transactions in SQL Server</h2>
<p>Not only is it important to understand what transactions are and design your code to make sure that it is appropriately implementing your business logic, it is also important to understand some of the internals that I’ve described as making mistakes can cause problems for database administrators. In my experience, database administrators often need to know how transactions work and what the potential mistakes that can be made are so they can troubleshoot issues around the transaction log.</p>
<p>There are many more facets to using transactions, such as specifying isolation levels and designing efficient code, but I hope this initial primer has given you a good grounding in why transactions are needed and how they work. I know it&#8217;s a bit of a cliche, but with SQL Server, it&#8217;s definitely a case of the more you know, the further you&#8217;ll go!</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/sql101-introduction-to-sql-server-transactions/">SQL101: Introduction to SQL Server Transactions</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlskills.com/blogs/paul/sql101-introduction-to-sql-server-transactions/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>The SQL Server Transaction Log, Part 2: Log Architecture</title>
		<link>https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-2-log-architecture/</link>
					<comments>https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-2-log-architecture/#comments</comments>
		
		<dc:creator><![CDATA[Paul Randal]]></dc:creator>
		<pubDate>Thu, 19 Feb 2026 22:38:14 +0000</pubDate>
				<category><![CDATA[Transaction Log]]></category>
		<guid isPermaLink="false">https://www.sqlskills.com/blogs/paul/?p=5347</guid>

					<description><![CDATA[<p>(This post first appeared on SQLperformance.com four years ago as part of a blog series, before that website was mothballed later in 2022 and the series was curtailed. Reposted here with permission, with a few tweaks.) In the first part of this series I introduced basic terminology around logging so I recommend you read that [&#8230;]</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-2-log-architecture/">The SQL Server Transaction Log, Part 2: Log Architecture</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p style="text-align: justify;"><em>(This post first appeared on SQLperformance.com four years ago as part of a blog series, before that website was mothballed later in 2022 and the series was curtailed. Reposted here with permission, with a few tweaks.)</em></p>
<p style="text-align: justify;">In the <a href="https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-1-logging-basics/" target="_blank" rel="noopener">first par</a>t of this series I introduced basic terminology around logging so I recommend you read that before continuing with this post. Everything else I’ll cover in the series requires knowing some of the architecture of the transaction log, so that’s what I’m going to discuss this time. Even if you’re not going to follow the series, some of the concepts I’m going to explain below are worth knowing for everyday tasks that DBAs handle in production.</p>
<p style="text-align: justify;">Note: as I progress through this series and talk about aspects of the log, there are often little edge-cases or weird behaviors in niche circumstances that have been added or changed over the years. I&#8217;ll ignore those unless I specifically want to call them out, otherwise the posts would be riddled with rat-holes and mazes of twisty-turny little passages (yes, I loved &#8217;80s text-based adventure games :-) that would distract from the main things to learn about in each post.</p>
<h2>Structural Hierarchy</h2>
<p>The transaction log is internally organized using a three-level hierarchy as shown in figure 1 below.</p>
<p><a href="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/02/log1.png"><img loading="lazy" decoding="async" class="alignnone wp-image-5348 size-full" src="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/02/log1.png" alt="" width="704" height="431" srcset="https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/02/log1.png 704w, https://www.sqlskills.com/blogs/paul/wp-content/uploads/2026/02/log1-300x184.png 300w" sizes="(max-width: 704px) 100vw, 704px" /></a></p>
<p><em>(Figure 1: The three-level structural hierarchy of the transaction log (my image))</em></p>
<p>The transaction log contains a number of virtual log files, which contain log blocks, which store the actual log records.</p>
<h2>Virtual Log Files</h2>
<p style="text-align: justify;">The transaction log is split up into sections called <em>virtual log files</em>, commonly just called <em>VLFs</em>. This is done to make managing operations in the transaction log easier for the log manager in SQL Server. You can’t specify how many VLFs are created by SQL Server when the database is first created or the log file automatically grows, but you can influence it. The algorithm for how many VLFs are created is as follows:</p>
<ul style="text-align: justify;">
<li>Log file size less than 64MB: create 4 VLFs, each roughly 16 MB in size</li>
<li>Log file size from 64MB to 1GB : create 8 VLFs, each roughly 1/8 of the total size</li>
<li>Log file size greater than 1GB: create 16 VLFs, each roughly 1/16 of the total size</li>
</ul>
<p style="text-align: justify;">Prior to SQL Server 2014, when the log file auto grows, the number of new VLFs added to the end of the log file is determined by the algorithm above, based on the auto-grow size. However, using that algorithm, if the auto-grow size is small, and the log file undergoes many auto-growths, that can lead to a very large number of small VLFs (called<em> VLF fragmentation</em>) that can be a big performance issue for some operations (see <a href="https://sqlperformance.com/2013/02/system-configuration/transaction-log-configuration">here</a>).</p>
<p style="text-align: justify;">Due to this problem, in SQL Server 2014 the algorithm changed for auto-growth of the log file. If the auto-grow size is less than 1/8 of the total log file size, only 1 new VLF is created, otherwise the old algorithm is used. This drastically reduces the number of VLFs for a log file that has undergone a large amount of auto-growth and I explained an example of the difference in <a href="https://www.sqlskills.com/blogs/paul/important-change-vlf-creation-algorithm-sql-server-2014/">this blog post</a>.</p>
<p style="text-align: justify;">Each VLF has a <em>sequence number</em> that uniquely identifies it, and is used in a variety of places, which I’ll explain below and in future posts. You would think that the sequence numbers would start at 1 for a brand new database, but that is not the case.</p>
<p style="text-align: justify;">On a SQL Server 2019 instance, I created a new database, without specifying any file sizes, and then checked the VLFs using the code below:</p>
<pre>CREATE DATABASE NewDB;
GO

SELECT
    [file_id],
    [vlf_begin_offset],
    [vlf_size_mb],
    [vlf_sequence_number]
FROM
    sys.dm_db_log_info (DB_ID (N'NewDB'));
GO</pre>
<p style="text-align: justify;">Note that the <em>sys.dm_db_log_info</em> DMV was added in SQL Server 2016 SP2. Before that (and also today, because it still exists) you can use the undocumented <em>DBCC LOGINFO</em> command, but you can’t give it a select list – just do <em>DBCC LOGINFO (N’NewDB’)</em> and the VLF sequence numbers are in the <em>FSeqNo</em> column of the result set.</p>
<p>Anyway, the results from querying<em> sys.dm_db_log_info </em>were:</p>
<pre>file_id vlf_begin_offset vlf_size_mb vlf_sequence_number
------- ---------------- ----------- -------------------
2       8192             1.93        37
2       2039808          1.93        0
2       4071424          1.93        0
2       6103040          2.17        0</pre>
<p style="text-align: justify;">Note that the first VLF starts at offset 8,192 bytes into the log file. This is because all database files, including the transaction log, have a file header page that takes up the first 8KB, and stores various metadata about the file.</p>
<p style="text-align: justify;">So why does SQL Server pick 37 and not 1 for the first VLF sequence number? The answer is that it finds the highest VLF sequence number in the <em>model</em> database and then for any new database, the transaction log’s first VLF uses that number plus 1 for its sequence number. I don’t know why that algorithm was chosen back in the mists of time, but it’s been that way since at least SQL Server 7.0.</p>
<p>To prove it, I ran this code:</p>
<pre>SELECT
    MAX ([vlf_sequence_number]) AS [Max_VLF_SeqNo]
FROM
    sys.dm_db_log_info (DB_ID (N'model'));
GO</pre>
<p>And the results were:</p>
<pre>Max_VLF_SeqNo
-------------
36</pre>
<p>So there you have it.</p>
<p style="text-align: justify;">There’s more to discuss about VLFs and how they’re used, but for now it’s enough to know that each VLF has a sequence number, which increases by one for each VLF.</p>
<h2 style="text-align: justify;">Log Blocks</h2>
<p style="text-align: justify;">Each VLF contains a small metadata header and the rest of the space is filled with log blocks. Each log block starts out at 512 bytes and will grow in 512-byte increments to a maximum size of 60KB, at which point it must be written to disk. A log block might be written to disk before it reaches its maximum size if one of the following occurs:</p>
<ul style="text-align: justify;">
<li>A transaction commits, and delayed durability is not being used for this transaction, so the log block must be written to disk so the transaction is durable</li>
<li>Delayed durability is in use and the background ‘flush the current log block to disk’ 1ms timer task fires</li>
<li>A data file page is being written to disk by a checkpoint or the lazy writer, and there are one or more log records in the current log block that affect the page that’s about to be written (remember write-ahead logging must be guaranteed)</li>
</ul>
<p style="text-align: justify;">You can consider a log block as something like a variable-sized page that stores log records in the order that they’re created by transactions changing the database. There isn’t a log block for each transaction; the log records for multiple concurrent transactions can be intermingled in a log block. You might think this would present difficulties for operations that need to find all the log records for a single transaction, but it doesn’t, as I’ll explain when I cover how transaction rollbacks work in a later post.</p>
<p style="text-align: justify;">Furthermore, when a log block is written to disk, it’s entirely possible that it contains log records from uncommitted transactions. This also is not a problem because of the way crash recovery works – which is a good few posts in the series future.</p>
<h2>Log Sequence Numbers</h2>
<p style="text-align: justify;">Log blocks have an ID within a VLF, starting at 1 and increasing by 1 for each new log block in the VLF. Log records also have an ID within a log block, starting at 1 and increasing by 1 for each new log record in the log block. So all three elements in the structural hierarchy of the transaction log have an ID and they are pulled together into a tripartite identifier called a <em>log sequence number</em>, more commonly referred to simply as an <em>LSN</em>.</p>
<p style="text-align: justify;">An LSN is defined as &lt;VLF sequence number&gt;:&lt;log block ID&gt;:&lt;log record ID&gt; (4 bytes: 4 bytes: 2 bytes) and uniquely identifies a single log record. It’s an ever-increasing identifier, because the VLF sequence numbers increase forever.</p>
<h2 style="text-align: justify;">Groundwork Done!</h2>
<p style="text-align: justify;">While VLFs are important to know about, in my opinion the LSN is the most important concept to understand around SQL Server’s implementation of logging as LSNs are the cornerstone on which transaction rollback and crash recovery are built, and LSNs will crop up again and again as I progress through the series. In the next post I’ll cover log truncation and the circular nature of the transaction log, which is all to do with VLFs and how they get reused.</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-2-log-architecture/">The SQL Server Transaction Log, Part 2: Log Architecture</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlskills.com/blogs/paul/the-sql-server-transaction-log-part-2-log-architecture/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Achievement Unlocked: One Year Sober</title>
		<link>https://www.sqlskills.com/blogs/paul/achievement-unlocked-one-year-sober/</link>
					<comments>https://www.sqlskills.com/blogs/paul/achievement-unlocked-one-year-sober/#comments</comments>
		
		<dc:creator><![CDATA[Paul Randal]]></dc:creator>
		<pubDate>Tue, 17 Feb 2026 21:32:10 +0000</pubDate>
				<category><![CDATA[Personal]]></category>
		<guid isPermaLink="false">https://www.sqlskills.com/blogs/paul/?p=5346</guid>

					<description><![CDATA[<p>(This is from the Insider Newsletter that I sent out earlier today&#8230;) Back in early 2023 I wrote a long blog post about my struggles with alcohol and how I’d decided to stop. I made it a few months, and then several times on and off after that, in the classic struggle to actually stop [&#8230;]</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/achievement-unlocked-one-year-sober/">Achievement Unlocked: One Year Sober</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>(This is from the Insider Newsletter that I sent out earlier today&#8230;)</p>
<p style="text-align: justify;">Back in early 2023 I wrote a <a href="https://www.sqlskills.com/blogs/paul/the-curious-case-of-alcoholism/">long blog post</a> about my struggles with alcohol and how I’d decided to stop. I made it a few months, and then several times on and off after that, in the classic struggle to actually stop and stay stopped, until February last year, when it finally stuck.</p>
<p style="text-align: justify;">This Friday (2/20/26) I’ll be sober for a year.</p>
<p style="text-align: justify;">If you struggle yourself, or know anyone who struggles with alcohol problems, you’ll know that a full trip around the sun sober is a major milestone and a significant mental achievement.</p>
<p style="text-align: justify;">Why am I telling you this? I’m not bragging or looking for affirmation. I was public here about my struggle back in 2023 because I wanted people in the SQL community who were struggling to know that they’re not alone and it’s nothing to be ashamed of (and boy, was I surprised by the number of ‘me too!’ emails). And now I’m being public about how I failed to stop a few times but persevered and eventually hit on the combination of things that let me stop and stay stopped.</p>
<p style="text-align: justify;">Perseverance was the key. Eventually I figured out:</p>
<ul style="text-align: justify;">
<li>A. A. just wasn’t for me. I’m not a ‘group chat/extemporaneous sharing’ person. It was great back in 2023 for the structure it provided over the first couple of months, but later I found myself not looking forward to meetings.</li>
<li>I needed substitutes. I found Athletic IPA, which tastes just like the real thing, but non-alcoholic and is pretty ubiquitous (along with Heineken Zero) in bars and restaurants. Chardonnay used to be my go-to, but non-alcoholic wine…. &lt;yuck&gt; And I also found Free Spirits ‘bourbon’, so I can have my late-night ‘clinky drink’ while I’m reading. And often around 5-6pm I’ll go for a drive for a bit, as that was my usual ‘start time’.</li>
</ul>
<p style="text-align: justify;">So far, so good – this is working for me nicely!</p>
<p style="text-align: justify;">If you’re struggling to meet a goal, don’t give up. The horribly-clichéd proverb ‘if at first you don’t succeed, try, try again’ is very true. And if you’re struggling to give up alcohol, or any other addiction, don’t lose hope. You can do it. And there are people who care that you do.</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/achievement-unlocked-one-year-sober/">Achievement Unlocked: One Year Sober</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlskills.com/blogs/paul/achievement-unlocked-one-year-sober/feed/</wfw:commentRss>
			<slash:comments>24</slash:comments>
		
		
			</item>
		<item>
		<title>The Curious Case of… occasional query failure on a tiny table</title>
		<link>https://www.sqlskills.com/blogs/paul/the-curious-case-of-occasional-query-failure-on-a-tiny-table/</link>
					<comments>https://www.sqlskills.com/blogs/paul/the-curious-case-of-occasional-query-failure-on-a-tiny-table/#comments</comments>
		
		<dc:creator><![CDATA[Paul Randal]]></dc:creator>
		<pubDate>Tue, 17 Feb 2026 03:16:11 +0000</pubDate>
				<category><![CDATA[Indexes From Every Angle]]></category>
		<category><![CDATA[Inside the Storage Engine]]></category>
		<category><![CDATA[The Curious Case of...]]></category>
		<guid isPermaLink="false">https://www.sqlskills.com/blogs/paul/?p=5344</guid>

					<description><![CDATA[<p>This is a case that happened on a client system last year: occasionally a common query on a tiny table appeared to &#8216;hang&#8217; and had to be killed and re-run. What&#8217;s going on? The table in question only had a few million rows of data in it, with a maximum row size of 60 bytes, [&#8230;]</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/the-curious-case-of-occasional-query-failure-on-a-tiny-table/">The Curious Case of… occasional query failure on a tiny table</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p style="text-align: justify;">This is a case that happened on a client system last year: occasionally a common query on a tiny table appeared to &#8216;hang&#8217; and had to be killed and re-run. What&#8217;s going on?</p>
<p style="text-align: justify;">The table in question only had a few million rows of data in it, with a maximum row size of 60 bytes, and the query usually ran in a few seconds, but occasionally the query would &#8216;hang&#8217; and would either be killed or take tens of minutes to run. Troubleshooting instrumentation when the issue happened showed no out-of-the-ordinary waits occurring, no pressure on the server, and the query plan generated when the query took a long time was essentially the same.</p>
<p style="text-align: justify;">The only thing noticeable was that when the problem occurred, a column statistics update happened as part of query compilation, but with such a tiny table, how could that be the root cause of the issue? The calculated disk space for the row size and count worked out to be about 250MB, but with a statistics sample rate of only 4%, extended events showed an auto_stats event taking close to an hour!</p>
<p style="text-align: justify;">Further investigation showed that although the table only had a few hundred MB of data in it, it was taking up more than 25GB of disk space! Jonathan and I were stumped as to how that could be the case. True, there had been bugs in earlier versions of SQL Server, such as with LOB data types where only one page from each dedicated extent (of 8 pages) was actually used, but none were currently known, and even something like that wouldn&#8217;t account for a table taking up more than 100x more disk space than data in the table.</p>
<p style="text-align: justify;">More and more curious, all the space was in a nonclustered index. How could *that* be possible?</p>
<p style="text-align: justify;">I suggested looking at what the pages were that were allocated to the table. Bingo! 25GB of IAM pages (one-per-4GB allocation bitmaps) were allocated to that single nonclustered index.</p>
<p style="text-align: justify;">Further investigation showed that the nonclustered index had thousands of partitions, each with a handful of index pages and around 1,500 IAM pages. The nonclustered index leading key was a ROWVERSION column and the churn rate on the table was very high, so essentially the schema and usage pattern were creating a long IAM chain for each partition, with hardly any data.</p>
<p style="text-align: justify;">In a nutshell, this meant a statistics update on a column covered by the nonclustered index would have needed to read and process 25GB of IAM pages, looking for allocated extents to then process the index records from to produce the statistic.</p>
<p style="text-align: justify;">Solution? The initial super-quick fix was to drop and recreate the nonclustered index to remove the long IAM chains (rather than having an index rebuild have to go through the same, laborious task of reading all the IAM pages!), and then to implement regular index maintenance to prevent the IAM chains from becoming long in the first place.</p>
<p>PS I’ll blog the script we used to prove it next week.</p>
<p>The post <a href="https://www.sqlskills.com/blogs/paul/the-curious-case-of-occasional-query-failure-on-a-tiny-table/">The Curious Case of… occasional query failure on a tiny table</a> appeared first on <a href="https://www.sqlskills.com/blogs/paul">Paul S. Randal</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlskills.com/blogs/paul/the-curious-case-of-occasional-query-failure-on-a-tiny-table/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
	</channel>
</rss>
