<?xml version="1.0" encoding="UTF-8" standalone="no"?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0">

<channel>
	<title>MarlonRibunal.com</title>
	<atom:link href="https://marlonribunal.com/feed/?max-results=10" rel="self" type="application/rss+xml"/>
	<link>https://marlonribunal.com</link>
	<description>SQL, Code, Coffee, etc.</description>
	<lastBuildDate>Sun, 23 Aug 2026 20:50:12 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>
	<xhtml:meta content="noindex" name="robots" xmlns:xhtml="http://www.w3.org/1999/xhtml"/><item>
		<title>Demo for Parameter Sniffing and Memory Grant Feedback</title>
		<link>https://marlonribunal.com/demo-for-parameter-sniffing-and-memory-grant-feedback/</link>
					<comments>https://marlonribunal.com/demo-for-parameter-sniffing-and-memory-grant-feedback/#respond</comments>
		
		<dc:creator><![CDATA[Marlon Ribunal]]></dc:creator>
		<pubDate>Thu, 13 Aug 2026 11:05:00 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://marlonribunal.com/?p=2861</guid>

					<description><![CDATA[<p>Disclaimer: The following is built with Claude Code. Just need to justify my $100/mo subscription cost. This was meant for a private demo so I can learn more about parameter sniffing and memory grant, but I decided to open it &#8230; <a href="https://marlonribunal.com/demo-for-parameter-sniffing-and-memory-grant-feedback/">Continue reading <span class="meta-nav">&#8594;</span></a></p>
<p>The post <a href="https://marlonribunal.com/demo-for-parameter-sniffing-and-memory-grant-feedback/">Demo for Parameter Sniffing and Memory Grant Feedback</a> first appeared on <a href="https://marlonribunal.com">SQL, Code, Coffee, Etc.</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph"><em><strong>Disclaimer</strong>: The following is built with Claude Code. Just need to justify my $100/mo subscription cost. This was meant for a private demo so I can learn more about parameter sniffing and memory grant, but I decided to open it to public. And you may say &#8220;Marlon, this isn&#8217;t you, this is too good.&#8221; Again, I will say it again, originally I was to keep this a private learning doc for me to understand Parameter sniffing. I asked Claude Code to build me a test for param sniffing and memory grant feedback. The following is the result of that after few prompts. You may use this for your own demo or POC. Run at your own risk. The Github Repo info is at the bottom of this post.</em></p>



<p class="has-babed-8-color has-text-color wp-block-paragraph">The stored procedure runs in milliseconds. It has been running in milliseconds for the last two years. Then one morning, it suddenly takes four minutes to complete. No code deployment. No configuration change. Nothing obvious changed. Restart the SQL Server instance, and it goes back to being fast — at least until later in the day.</p>



<p class="wp-block-paragraph">You already know what the first response in the incident channel will be: &#8220;It&#8217;s parameter sniffing.&#8221;</p>



<p class="wp-block-paragraph">And technically, that answer may be correct. But it does not tell you enough to fix the problem.</p>



<p class="wp-block-paragraph">Parameter sniffing is not a single issue. There are different ways this can bite you, but two of the most common ones are easy to confuse.</p>



<p class="wp-block-paragraph">The first is a bad plan choice. SQL Server compiles a plan based on one set of parameter values, but that same plan performs poorly when reused for a different set of values. For example, SQL Server may choose an index seek with hundreds of thousands of key lookups when a scan would have been the better option.</p>



<p class="wp-block-paragraph">The second is a bad memory grant. SQL Server estimates it only needs enough memory for a small number of rows, but the actual query returns hundreds of thousands of rows. The result can be spills to tempdb, poor performance, and unnecessary memory pressure.</p>



<p class="wp-block-paragraph">These two problems can look similar from the outside, but they require different troubleshooting approaches.</p>



<p class="wp-block-paragraph">This is especially important with newer SQL Server features like Memory Grant Feedback. It can help correct inaccurate memory grants, but it does not change a fundamentally bad plan choice. If you do not identify which problem you actually have, you can apply a fix that was never designed to solve the issue.</p>



<p class="has-babed-8-color has-text-color wp-block-paragraph">The goal of this post is to separate these two behaviors, show how they are different, and walk through a demo you can reproduce yourself.</p>



<h2 class="wp-block-heading">Sniffing is a feature</h2>



<p class="wp-block-paragraph">Before getting into the problem, it is important to set the right context. A lot of discussions around parameter sniffing make it sound like a SQL Server defect that Microsoft should have fixed. That is not really the case.</p>



<p class="wp-block-paragraph">When SQL Server compiles a parameterized query, it uses the parameter values that caused the compilation to estimate the number of rows and build a plan. It looks at the statistics, checks the histogram, estimates the expected cardinality, and creates a plan based on that information. That plan is then stored in cache and reused for future executions, even if those executions use very different parameter values.</p>



<p class="wp-block-paragraph">The first part of this process is actually what makes SQL Server perform well. Without parameter sniffing, SQL Server would have to create plans based on generic estimates instead of the actual values being searched. You would end up with a plan that is average for everyone instead of a plan that is optimized for the majority of cases.</p>



<p class="wp-block-paragraph">The problem is not parameter sniffing itself. The problem is reusing a plan when the data distribution does not match the values that plan was originally optimized for.</p>



<p class="wp-block-paragraph">This is where data skew comes into play. If values in a column are evenly distributed, most parameter values will produce similar row counts, and the cached plan will usually work well. Parameter sniffing only becomes a problem when some values return a small number of rows while others return a significantly different number of rows.</p>



<p class="wp-block-paragraph">So the first question should not be, &#8220;How do I disable parameter sniffing?&#8221;</p>



<p class="wp-block-paragraph">The better question is, &#8220;How is the data distributed, and how much skew exists in this column?&#8221;</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:16px 0 0 16px;width:100%;text-align:left;background-color:#292d3e"><span style="background:#aaafcf;padding:0.3rem 0.5rem 0.2rem;border-radius:1rem;font-size:0.8em;line-height:1;height:1.25rem;text-align:center;display:inline-flex;align-items:center;justify-content:center;color:#292d3e">SQL</span></span><span role="button" tabindex="0" style="color:#babed8;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>SELECT   TOP (20) CustomerID, Rows = COUNT_BIG(*)
FROM     Sales.OrderLines_or_whatever
GROUP BY CustomerID
ORDER BY Rows DESC;
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki material-theme-palenight" style="background-color: #292D3E" tabindex="0"><code><span class="line"><span style="color: #F78C6C">SELECT</span><span style="color: #BABED8">   </span><span style="color: #F78C6C">TOP</span><span style="color: #BABED8"> (</span><span style="color: #F78C6C">20</span><span style="color: #BABED8">) CustomerID, </span><span style="color: #F78C6C">Rows</span><span style="color: #BABED8"> </span><span style="color: #89DDFF">=</span><span style="color: #BABED8"> </span><span style="color: #82AAFF">COUNT_BIG</span><span style="color: #BABED8">(</span><span style="color: #89DDFF">*</span><span style="color: #BABED8">)</span></span>
<span class="line"><span style="color: #F78C6C">FROM</span><span style="color: #BABED8">     Sales.OrderLines_or_whatever</span></span>
<span class="line"><span style="color: #F78C6C">GROUP BY</span><span style="color: #BABED8"> CustomerID</span></span>
<span class="line"><span style="color: #F78C6C">ORDER BY</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">Rows</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">DESC</span><span style="color: #BABED8">;</span></span>
<span class="line"></span></code></pre></div>



<p class="wp-block-paragraph">If the difference between the highest and lowest values is only within an order of magnitude, then data skew is probably not your problem. There is likely something else causing the bad plan choice.</p>



<h2 class="wp-block-heading">Building a demo; thanks, claude</h2>



<p class="wp-block-paragraph">I wanted to demonstrate both failure modes using actual query behavior, which means I needed data with a noticeable skew. The challenge is that standard sample databases are not always useful for demonstrating these types of problems.</p>



<p class="wp-block-paragraph">For this test, I used <strong>Claude Code</strong> to help create the setup script and build the test scenario. WideWorldImporters, Microsoft&#8217;s sample database, was used as the starting point because it contains real order data. However, the data distribution is fairly uniform. Order lines are spread across customers, dates, and stock items. That makes sense for a sample database, but it does not create the conditions needed to demonstrate parameter sensitivity.</p>



<p class="wp-block-paragraph">The skew in this demo is intentional and documented. The script creates <code>Demo.OrderLinesSkewed</code> using WideWorldImporters&#8217; approximately 231,000 existing order lines, then adds another 500,000 rows tied to a single customer. The goal is to create a simple scenario where one customer has a large percentage of the data while most other customers have significantly fewer rows.</p>



<p class="wp-block-paragraph">The customer values are not hardcoded. The script identifies the large customer and the smaller customers from the data and prints them out. This makes the demo more portable because WideWorldImporters installations may not all contain identical data.</p>



<p class="wp-block-paragraph">I prefer demos that are transparent about how the test conditions were created. The important part is not the test data itself. The important part is understanding the SQL Server behavior we are trying to demonstrate.</p>



<p class="wp-block-paragraph">There are three important details in the setup that make this demo work. Each one represents a common reason why performance demos like this can produce misleading results:</p>



<p class="wp-block-paragraph"><strong>The index is narrow on purpose.</strong>&nbsp;A single non-clustered index on&nbsp;<code>CustomerID</code>, no&nbsp;<code>INCLUDE</code>&nbsp;columns:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:16px 0 0 16px;width:100%;text-align:left;background-color:#292d3e"><span style="background:#aaafcf;padding:0.3rem 0.5rem 0.2rem;border-radius:1rem;font-size:0.8em;line-height:1;height:1.25rem;text-align:center;display:inline-flex;align-items:center;justify-content:center;color:#292d3e">SQL</span></span><span role="button" tabindex="0" style="color:#babed8;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>CREATE NONCLUSTERED INDEX IX_OrderLinesSkewed_CustomerID
    ON Demo.OrderLinesSkewed (CustomerID);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki material-theme-palenight" style="background-color: #292D3E" tabindex="0"><code><span class="line"><span style="color: #F78C6C">CREATE</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">NONCLUSTERED</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">INDEX</span><span style="color: #BABED8"> IX_OrderLinesSkewed_CustomerID</span></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">ON</span><span style="color: #BABED8"> Demo.OrderLinesSkewed (CustomerID);</span></span>
<span class="line"></span></code></pre></div>



<p class="wp-block-paragraph">TThe goal is to force SQL Server to make a real choice. It can either use the index and perform key lookups for each row, or decide that scanning the clustered index is the better option. Where SQL Server draws that line is the plan shape side of the problem.</p>



<p class="wp-block-paragraph">If the index is covering, that decision goes away. You may still see a memory grant issue, but you will not see the plan change between executions. The result is a demo that only shows one side of the problem and misses how parameter sensitivity can affect plan selection.</p>



<p class="wp-block-paragraph">The rows are intentionally wide. There is a <code>char(200)</code> filler column, and the stored procedure includes that column in the output. Memory grants are calculated using estimated rows multiplied by estimated row width. If the rows are too narrow, the memory grant behavior is not very interesting.</p>



<p class="wp-block-paragraph">There is also no <code>TOP</code> and no <code>ROW_NUMBER()</code> in this demo. This is an important detail because many demos around this topic accidentally hide the memory grant problem.</p>



<p class="wp-block-paragraph">For example, a query like <code>SELECT TOP (50) ... ORDER BY UnitPrice DESC</code> introduces a <code>Top N Sort</code>. The memory grant for a <code>Top N Sort</code> is based on the number of rows being returned, in this case 50, instead of the total number of rows flowing through the sort. Filtering a <code>ROW_NUMBER()</code> value against a constant can have a similar issue because the optimizer may rewrite it into a <code>Top</code>.</p>



<p class="wp-block-paragraph">In both cases, the memory grant no longer scales with the actual workload. The demo may still run, but it is no longer showing the behavior we are trying to analyze.</p>



<p class="wp-block-paragraph">If you build your own version of this test, check the execution plan and make sure the operator is a <code>Sort</code> and not a <code>Top N Sort</code>. The demo scripts capture the plan after each execution and flag <code>Top N Sort</code> because it changes the behavior being tested.</p>



<p class="wp-block-paragraph">Here&#8217;s the procedure. It is deliberately boring:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:16px 0 0 16px;width:100%;text-align:left;background-color:#292d3e"><span style="background:#aaafcf;padding:0.3rem 0.5rem 0.2rem;border-radius:1rem;font-size:0.8em;line-height:1;height:1.25rem;text-align:center;display:inline-flex;align-items:center;justify-content:center;color:#292d3e">SQL</span></span><span role="button" tabindex="0" style="color:#babed8;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>CREATE OR ALTER PROCEDURE Demo.usp_CustomerLinesByPrice
    @CustomerID int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT  ol.OrderLineID, ol.OrderID, ol.CustomerID, ol.StockItemID,
            ol.Description, ol.Quantity, ol.UnitPrice, ol.OrderDate,
            ol.Filler
    FROM    Demo.OrderLinesSkewed AS ol
    WHERE   ol.CustomerID = @CustomerID
    ORDER BY ol.UnitPrice DESC, ol.Description;
END
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki material-theme-palenight" style="background-color: #292D3E" tabindex="0"><code><span class="line"><span style="color: #F78C6C">CREATE</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">OR</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">ALTER</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">PROCEDURE</span><span style="color: #BABED8"> Demo.usp_CustomerLinesByPrice</span></span>
<span class="line"><span style="color: #BABED8">    @CustomerID </span><span style="color: #C792EA">int</span></span>
<span class="line"><span style="color: #F78C6C">AS</span></span>
<span class="line"><span style="color: #F78C6C">BEGIN</span></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">SET</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">NOCOUNT</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">ON</span><span style="color: #BABED8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">SELECT</span><span style="color: #BABED8">  ol.OrderLineID, ol.OrderID, ol.CustomerID, ol.StockItemID,</span></span>
<span class="line"><span style="color: #BABED8">            ol.Description, ol.Quantity, ol.UnitPrice, ol.OrderDate,</span></span>
<span class="line"><span style="color: #BABED8">            ol.Filler</span></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">FROM</span><span style="color: #BABED8">    Demo.OrderLinesSkewed </span><span style="color: #F78C6C">AS</span><span style="color: #BABED8"> ol</span></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">WHERE</span><span style="color: #BABED8">   ol.CustomerID </span><span style="color: #89DDFF">=</span><span style="color: #BABED8"> @CustomerID</span></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">ORDER BY</span><span style="color: #BABED8"> ol.UnitPrice </span><span style="color: #F78C6C">DESC</span><span style="color: #BABED8">, ol.Description;</span></span>
<span class="line"><span style="color: #F78C6C">END</span></span>
<span class="line"></span></code></pre></div>



<p class="wp-block-paragraph">The test is simple by design. One equality predicate against a skewed column. One sort operation where no index can fully support it. Those two things are enough to reproduce the behavior we want to analyze.</p>



<h2 class="wp-block-heading">Failure mode one: sniff small, run big</h2>



<p class="wp-block-paragraph">Compile the procedure for the minnow. Then call it for the whale.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:16px 0 0 16px;width:100%;text-align:left;background-color:#292d3e"><span style="background:#aaafcf;padding:0.3rem 0.5rem 0.2rem;border-radius:1rem;font-size:0.8em;line-height:1;height:1.25rem;text-align:center;display:inline-flex;align-items:center;justify-content:center;color:#292d3e">SQL</span></span><span role="button" tabindex="0" style="color:#babed8;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>EXEC sys.sp_recompile N'Demo.usp_CustomerLinesByPrice';
EXEC Demo.usp_CustomerLinesByPrice @CustomerID = @Minnow;  -- compiles here
EXEC Demo.usp_CustomerLinesByPrice @CustomerID = @Whale;   -- suffers here
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki material-theme-palenight" style="background-color: #292D3E" tabindex="0"><code><span class="line"><span style="color: #F78C6C">EXEC</span><span style="color: #BABED8"> sys.sp_recompile </span><span style="color: #C3E88D">N</span><span style="color: #89DDFF">&#39;</span><span style="color: #C3E88D">Demo.usp_CustomerLinesByPrice</span><span style="color: #89DDFF">&#39;</span><span style="color: #BABED8">;</span></span>
<span class="line"><span style="color: #F78C6C">EXEC</span><span style="color: #BABED8"> Demo.usp_CustomerLinesByPrice @CustomerID </span><span style="color: #89DDFF">=</span><span style="color: #BABED8"> @Minnow;  </span><span style="color: #676E95; font-style: italic">-- compiles here</span></span>
<span class="line"><span style="color: #F78C6C">EXEC</span><span style="color: #BABED8"> Demo.usp_CustomerLinesByPrice @CustomerID </span><span style="color: #89DDFF">=</span><span style="color: #BABED8"> @Whale;   </span><span style="color: #676E95; font-style: italic">-- suffers here</span></span>
<span class="line"></span></code></pre></div>



<p class="wp-block-paragraph">The plan compiled for the minnow is a good plan for the minnow: seek the non-clustered index, look up the handful of matching rows in the clustered index, sort them in a memory grant barely above the minimum. For a few hundred rows that&#8217;s exactly right.</p>



<p class="wp-block-paragraph">Then the whale arrives, and the same plan does it 500000 times.</p>



<p class="wp-block-paragraph">Two separate things have now gone wrong, and from here on I&#8217;m going to insist on naming them separately.</p>



<p class="wp-block-paragraph"><strong>The plan shape is wrong.</strong>&nbsp;Key lookups are fine in the hundreds and catastrophic in the hundreds of thousands. The logical read count tells the story: 1519167 reads to return 500000 rows. A clustered index scan would have read the table roughly once.</p>



<p class="wp-block-paragraph"><strong>The memory grant is wrong</strong>, and this is the part people find surprising. The grant is not recalculated per execution. It is&nbsp;<em>baked into the cached plan</em>&nbsp;at compile time, computed from the estimated row count and the estimated row width. Runtime reality does not get a vote. So the sort gets a grant sized for the minnow — 1 MB — while the engine&#8217;s own after-the-fact assessment of what it should have had is 0.53 MB.</p>



<p class="wp-block-paragraph">When a sort doesn&#8217;t have enough memory, it spills to tempdb. Not a warning, not a retry — it writes sort runs to disk and merges them, and your query goes from memory-speed to disk-speed while holding its locks the whole time. In the actual execution plan you&#8217;ll see a warning triangle on the Sort operator. In the Extended Events output you&#8217;ll see&nbsp;<code>sort_warning</code>&nbsp;fire.</p>



<p class="wp-block-paragraph">The gap between&nbsp;<code>GrantMB</code>&nbsp;and&nbsp;<code>IdealMB</code>&nbsp;is the fingerprint. Learn to read it.</p>



<h2 class="wp-block-heading">Failure mode two: sniff big, run small</h2>



<p class="wp-block-paragraph">Now the mirror image, which most write-ups skip, and which is the more interesting half.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:16px 0 0 16px;width:100%;text-align:left;background-color:#292d3e"><span style="background:#aaafcf;padding:0.3rem 0.5rem 0.2rem;border-radius:1rem;font-size:0.8em;line-height:1;height:1.25rem;text-align:center;display:inline-flex;align-items:center;justify-content:center;color:#292d3e">SQL</span></span><span role="button" tabindex="0" style="color:#babed8;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>EXEC sys.sp_recompile N'Demo.usp_CustomerLinesByPrice';
EXEC Demo.usp_CustomerLinesByPrice @CustomerID = @Whale;   -- compiles here
EXEC Demo.usp_CustomerLinesByPrice @CustomerID = @Minnow;  -- wastes memory here
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki material-theme-palenight" style="background-color: #292D3E" tabindex="0"><code><span class="line"><span style="color: #F78C6C">EXEC</span><span style="color: #BABED8"> sys.sp_recompile </span><span style="color: #C3E88D">N</span><span style="color: #89DDFF">&#39;</span><span style="color: #C3E88D">Demo.usp_CustomerLinesByPrice</span><span style="color: #89DDFF">&#39;</span><span style="color: #BABED8">;</span></span>
<span class="line"><span style="color: #F78C6C">EXEC</span><span style="color: #BABED8"> Demo.usp_CustomerLinesByPrice @CustomerID </span><span style="color: #89DDFF">=</span><span style="color: #BABED8"> @Whale;   </span><span style="color: #676E95; font-style: italic">-- compiles here</span></span>
<span class="line"><span style="color: #F78C6C">EXEC</span><span style="color: #BABED8"> Demo.usp_CustomerLinesByPrice @CustomerID </span><span style="color: #89DDFF">=</span><span style="color: #BABED8"> @Minnow;  </span><span style="color: #676E95; font-style: italic">-- wastes memory here</span></span>
<span class="line"></span></code></pre></div>



<p class="wp-block-paragraph">The plan compiled for the whale is a clustered index scan with a memory grant sized for half a million wide rows. Reused for the minnow, it returns a few hundred rows and finishes quickly.</p>



<p class="wp-block-paragraph">Nothing spills. Nothing is slow. This query will never appear in your &#8220;top ten by duration&#8221; report. It is not broken in any way a duration-based monitor can see.</p>



<p class="wp-block-paragraph">It is, however,&nbsp;<strong>greedy</strong>. It asked for 127.31 MB of workspace memory and touched 0.22 MB of it.</p>



<p class="wp-block-paragraph">Here&#8217;s why you should care about memory a query didn&#8217;t use:</p>



<ul class="wp-block-list">
<li>A memory grant is&nbsp;<em>reserved</em>&nbsp;for the lifetime of the query, used or not. It is not lazily allocated and it is not shared.</li>



<li>Workspace memory is a finite, instance-wide pool. There is only so much of it.</li>



<li>When the pool is exhausted, incoming queries queue on&nbsp;<code>RESOURCE_SEMAPHORE</code>&nbsp;waits — they sit there, having compiled successfully, waiting for permission to start.</li>
</ul>



<p class="wp-block-paragraph">So one procedure with a badly sniffed grant, called from enough sessions concurrently, will stall queries that have nothing to do with it. The victim is never the culprit. That&#8217;s what makes this one hard to trace back, and it&#8217;s why duration is a bad detector for half of all parameter sniffing problems.</p>



<p class="wp-block-paragraph">The detector that does work is a comparison, not a threshold:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:16px 0 0 16px;width:100%;text-align:left;background-color:#292d3e"><span style="background:#aaafcf;padding:0.3rem 0.5rem 0.2rem;border-radius:1rem;font-size:0.8em;line-height:1;height:1.25rem;text-align:center;display:inline-flex;align-items:center;justify-content:center;color:#292d3e">SQL</span></span><span role="button" tabindex="0" style="color:#babed8;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>SELECT  qs.execution_count,
        GrantMB = qs.last_grant_kb      / 1024.0,
        UsedMB  = qs.last_used_grant_kb / 1024.0,
        IdealMB = qs.last_ideal_grant_kb/ 1024.0,
        st.text
FROM    sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
WHERE   qs.last_grant_kb > 1024
  AND   qs.last_grant_kb > qs.last_used_grant_kb * 2
ORDER BY qs.last_grant_kb DESC;
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki material-theme-palenight" style="background-color: #292D3E" tabindex="0"><code><span class="line"><span style="color: #F78C6C">SELECT</span><span style="color: #BABED8">  qs.execution_count,</span></span>
<span class="line"><span style="color: #BABED8">        GrantMB </span><span style="color: #89DDFF">=</span><span style="color: #BABED8"> qs.last_grant_kb      </span><span style="color: #89DDFF">/</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">1024</span><span style="color: #BABED8">.</span><span style="color: #F78C6C">0</span><span style="color: #BABED8">,</span></span>
<span class="line"><span style="color: #BABED8">        UsedMB  </span><span style="color: #89DDFF">=</span><span style="color: #BABED8"> qs.last_used_grant_kb </span><span style="color: #89DDFF">/</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">1024</span><span style="color: #BABED8">.</span><span style="color: #F78C6C">0</span><span style="color: #BABED8">,</span></span>
<span class="line"><span style="color: #BABED8">        IdealMB </span><span style="color: #89DDFF">=</span><span style="color: #BABED8"> qs.last_ideal_grant_kb</span><span style="color: #89DDFF">/</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">1024</span><span style="color: #BABED8">.</span><span style="color: #F78C6C">0</span><span style="color: #BABED8">,</span></span>
<span class="line"><span style="color: #BABED8">        st.text</span></span>
<span class="line"><span style="color: #F78C6C">FROM</span><span style="color: #BABED8">    sys.dm_exec_query_stats </span><span style="color: #F78C6C">AS</span><span style="color: #BABED8"> qs</span></span>
<span class="line"><span style="color: #F78C6C">CROSS</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">APPLY</span><span style="color: #BABED8"> sys.dm_exec_sql_text(qs.sql_handle) </span><span style="color: #F78C6C">AS</span><span style="color: #BABED8"> st</span></span>
<span class="line"><span style="color: #F78C6C">WHERE</span><span style="color: #BABED8">   qs.last_grant_kb </span><span style="color: #89DDFF">&gt;</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">1024</span></span>
<span class="line"><span style="color: #BABED8">  </span><span style="color: #F78C6C">AND</span><span style="color: #BABED8">   qs.last_grant_kb </span><span style="color: #89DDFF">&gt;</span><span style="color: #BABED8"> qs.last_used_grant_kb </span><span style="color: #89DDFF">*</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">2</span></span>
<span class="line"><span style="color: #F78C6C">ORDER BY</span><span style="color: #BABED8"> qs.last_grant_kb </span><span style="color: #F78C6C">DESC</span><span style="color: #BABED8">;</span></span>
<span class="line"></span></code></pre></div>



<p class="wp-block-paragraph">Granted far above used means memory reserved and wasted. Ideal far above granted means the query spilled. Same three columns, two different diseases.</p>



<h2 class="wp-block-heading">Two problems, not one</h2>



<p class="wp-block-paragraph">This is the pivot of the whole post, so here it is in one table:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th></th><th>Wrong plan shape</th><th>Wrong memory grant</th></tr></thead><tbody><tr><td><strong>Symptom</strong></td><td>Huge logical reads, wrong operators</td><td>Spills to tempdb, or memory reserved and never touched</td></tr><tr><td><strong>Detect with</strong></td><td><code>last_logical_reads</code>, the plan itself</td><td><code>last_grant_kb</code>&nbsp;vs&nbsp;<code>last_used_grant_kb</code>&nbsp;vs&nbsp;<code>last_ideal_grant_kb</code></td></tr><tr><td><strong>Shows up in a duration report?</strong></td><td>Yes</td><td>Only half the time</td></tr><tr><td><strong><code>OPTION (RECOMPILE)</code></strong></td><td>Fixes it</td><td>Fixes it</td></tr><tr><td><strong>Memory grant feedback</strong></td><td><strong>Never fixes it</strong></td><td>Fixes it, over several executions</td></tr><tr><td><strong>PSPO (SQL Server 2022)</strong></td><td>Fixes it</td><td>Indirectly, by fixing the estimate</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Everything below refers back to this.</p>



<h2 class="wp-block-heading"><code>OPTION (RECOMPILE)</code></h2>



<p class="wp-block-paragraph">The blunt instrument, and the one that always works.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:16px 0 0 16px;width:100%;text-align:left;background-color:#292d3e"><span style="background:#aaafcf;padding:0.3rem 0.5rem 0.2rem;border-radius:1rem;font-size:0.8em;line-height:1;height:1.25rem;text-align:center;display:inline-flex;align-items:center;justify-content:center;color:#292d3e">SQL</span></span><span role="button" tabindex="0" style="color:#babed8;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>    ...
    ORDER BY ol.UnitPrice DESC, ol.Description
    OPTION (RECOMPILE);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki material-theme-palenight" style="background-color: #292D3E" tabindex="0"><code><span class="line"><span style="color: #BABED8">    ...</span></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">ORDER BY</span><span style="color: #BABED8"> ol.UnitPrice </span><span style="color: #F78C6C">DESC</span><span style="color: #BABED8">, ol.Description</span></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">OPTION</span><span style="color: #BABED8"> (</span><span style="color: #F78C6C">RECOMPILE</span><span style="color: #BABED8">);</span></span>
<span class="line"></span></code></pre></div>



<p class="wp-block-paragraph">Both columns of that table go green. The plan is built against the actual parameter value on every single execution, so the shape is right and the grant is right, every time, by construction. You get a bonus, too: because the value is a literal constant at compile time, the optimizer can do things it can&#8217;t do for a cached plan — fold constants, eliminate whole branches of a query, simplify predicates it would otherwise have to keep general.</p>



<p class="wp-block-paragraph">In the demo, scenario D calls the recompiling version with the minnow, then the whale, then the minnow again, and the grant tracks the actual row count in both directions: 1, 127.35, 127.34 MB.</p>



<p class="wp-block-paragraph">Now the cost, stated honestly, because &#8220;just add RECOMPILE&#8221; is bad advice delivered confidently.</p>



<p class="wp-block-paragraph"><strong>You pay a compilation on every execution.</strong>&nbsp;Compilation is CPU-expensive. On a procedure called four thousand times a minute, you have traded an intermittent memory problem for a permanent CPU problem, and the second one is harder to notice because it doesn&#8217;t spike — it just raises your floor.</p>



<p class="wp-block-paragraph"><strong>You lose the plan cache as a diagnostic surface.</strong>&nbsp;After a recompiling statement runs, there&#8217;s nothing in&nbsp;<code>sys.dm_exec_cached_plans</code>&nbsp;to look at. Your monitoring gets thinner exactly where you were having trouble.</p>



<p class="wp-block-paragraph"><strong>Statement-level, not procedure-level.</strong>&nbsp;<code>OPTION (RECOMPILE)</code>&nbsp;on one statement recompiles that statement.&nbsp;<code>CREATE PROCEDURE ... WITH RECOMPILE</code>&nbsp;recompiles every statement in the procedure on every call, including the nine that were fine. It is almost never what you want. If you inherited a procedure with&nbsp;<code>WITH RECOMPILE</code>&nbsp;in the header, that&#8217;s usually someone&#8217;s decade-old shotgun fix for a problem in one statement.</p>



<p class="wp-block-paragraph">The rule of thumb worth internalizing:&nbsp;<strong>RECOMPILE is priced per execution.</strong>&nbsp;A reporting procedure called forty times an hour should probably just use it and stop thinking about this. A hot OLTP path called forty times a second should not.</p>



<p class="wp-block-paragraph">And note something for later: after scenario D there is&nbsp;<strong>no cached plan at all</strong>. Hold that thought.</p>



<h2 class="wp-block-heading">Memory grant feedback</h2>



<p class="wp-block-paragraph">Now the feature everyone wants to talk about, and the one whose limits are routinely oversold.</p>



<p class="wp-block-paragraph">Memory grant feedback is a learning loop. After a query executes, the engine compares the memory it granted against the memory the query actually used. If the query spilled, the grant was too small — write a bigger number onto the cached plan for next time. If the grant was more than twice what was used, it was too big — write a smaller one. Over a handful of executions the grant converges on reality without anybody recompiling anything.</p>



<p class="wp-block-paragraph">What you need for it:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Feature</th><th>Version</th><th>Also requires</th></tr></thead><tbody><tr><td>Batch mode memory grant feedback</td><td>2017+</td><td>compatibility level 140</td></tr><tr><td>Row mode memory grant feedback</td><td>2019+</td><td>compatibility level 150</td></tr><tr><td>Persistence across cache eviction</td><td>2022+</td><td>Query Store enabled</td></tr><tr><td>Percentile grant feedback</td><td>2022+</td><td>compatibility level 160</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">That compatibility level column is where most people get stuck. A database restored from an older instance keeps its old compatibility level forever; WideWorldImporters ships at 130. You can be running SQL Server 2022 and getting none of this.</p>



<p class="wp-block-paragraph">Scenario C in the demo sets the loop up to succeed: compile for the minnow, then call the whale six times in a row with nothing recompiling in between. That last part matters. Feedback is written onto the cached plan, so anything that evicts the plan throws away everything the engine learned. This is also why the adjustment always lands on the&nbsp;<em>following</em>&nbsp;execution — execution&nbsp;<em>n</em>&nbsp;discovers the grant was wrong, execution&nbsp;<em>n+1</em>&nbsp;benefits.</p>



<p class="wp-block-paragraph">The trajectory:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Execution</th><th>GrantMB</th><th>UsedMB</th><th>IdealMB</th><th>State</th></tr></thead><tbody><tr><td>1 (whale)</td><td>1.50</td><td>1.50</td><td>1.5</td><td>NULL</td></tr><tr><td>2</td><td>47.31</td><td>47.31</td><td>47.31</td><td>NULL</td></tr><tr><td>3</td><td>79.80</td><td>79.80</td><td>79.80</td><td>NULL</td></tr><tr><td>4</td><td>106.81</td><td>106.81</td><td>106.81</td><td>NULL</td></tr><tr><td>5</td><td>127.37</td><td>127.37</td><td>131.02</td><td>NULL</td></tr><tr><td>6</td><td>127.36</td><td>127.36</td><td>153.65</td><td>NULL</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">That&nbsp;<code>State</code>&nbsp;column is&nbsp;<code>IsMemoryGrantFeedbackAdjusted</code>&nbsp;from the cached plan&#8217;s XML, and it&#8217;s the cleanest way to watch the loop work: it moves from&nbsp;<code>NoFirstExecution</code>&nbsp;through&nbsp;<code>YesAdjusting</code>&nbsp;to&nbsp;<code>YesStable</code>.</p>



<p class="wp-block-paragraph">Now the three caveats, which are the actual reason this section exists.</p>



<p class="wp-block-paragraph"><strong>It fixes the grant. It never fixes the plan shape.</strong>&nbsp;Look at the&nbsp;<code>PlanShape</code>&nbsp;column across all six of those executions in the demo output. It does not change. It cannot change — memory grant feedback adjusts a number attached to an existing plan; it does not trigger a recompilation and it has no opinion about operators. Those 1519167 logical reads from failure mode one are still there on execution six. The query stops spilling and gets faster. It does not get&nbsp;<em>good</em>. If you go into this expecting feedback to solve parameter sniffing, this is where you&#8217;ll be disappointed, and it won&#8217;t be the feature&#8217;s fault.</p>



<p class="wp-block-paragraph"><strong>It&#8217;s a learning loop, so somebody has to do the learning.</strong>&nbsp;The first caller always eats the bad grant. On SQL Server 2019 and earlier, so does the first caller after any cache eviction — a plan flush, memory pressure, a stats update, a failover. SQL Server 2022&#8217;s Query Store persistence exists precisely to stop throwing that lesson away, and it&#8217;s a good reason to have Query Store on.</p>



<p class="wp-block-paragraph"><strong>It gives up if you make it thrash.</strong>&nbsp;A workload that genuinely alternates between tiny and enormous will push the grant up, then down, then up again. Rather than oscillate forever, the engine notices the instability and switches feedback off for that query. There&#8217;s an Extended Event for it —&nbsp;<code>memory_grant_feedback_loop_disabled</code>. Percentile grant feedback in SQL Server 2022 is the answer to this case: instead of chasing the last execution, it sizes the grant from a percentile of recent executions, which is far more stable across a genuinely bimodal workload.</p>



<h2 class="wp-block-heading">Why RECOMPILE and memory grant feedback don&#8217;t combine</h2>



<p class="wp-block-paragraph">This falls straight out of the two sections above, and it&#8217;s the question that sent me down this path in the first place.</p>



<p class="wp-block-paragraph">Memory grant feedback writes its correction&nbsp;<strong>onto a cached plan</strong>.&nbsp;<code>OPTION (RECOMPILE)</code>&nbsp;<strong>doesn&#8217;t leave a cached plan</strong>. There is nothing for the feedback to attach to.</p>



<p class="wp-block-paragraph">You can watch this in the demo: after scenario D runs three times, query the cached plan view and you get nothing back. Compare with scenario C, where the plan is sitting right there accumulating adjustments.</p>



<p class="wp-block-paragraph">This is not a conflict you need to resolve, and it isn&#8217;t a bug. RECOMPILE already produces an accurate grant on every execution by construction — there&#8217;s nothing left for a feedback loop to improve. But it does mean the two are&nbsp;<strong>alternatives, not layers</strong>. Don&#8217;t reach for RECOMPILE while imagining that feedback is also working quietly underneath, and don&#8217;t diagnose the absence of feedback on a recompiling statement as something being broken.</p>



<h2 class="wp-block-heading">Parameter Sensitive Plan optimization</h2>



<p class="wp-block-paragraph">Which leaves the gap in that table from earlier: memory grant feedback never fixes plan shape, and RECOMPILE fixes plan shape but charges you per execution. Is there anything that fixes the shape without the compile?</p>



<p class="wp-block-paragraph">On SQL Server 2022, yes. Parameter Sensitive Plan optimization caches&nbsp;<strong>multiple plan variants</strong>&nbsp;for a single statement and dispatches between them based on the cardinality the incoming parameter implies. The minnow gets the seek-and-lookup plan, the whale gets the scan, neither one triggers a compilation, and both come out of cache.</p>



<p class="wp-block-paragraph">It&#8217;s on by default at compatibility level 160. Its limits are worth knowing: equality predicates only, at most three of them, and the column has to be skewed enough for the engine to consider it worth the trouble — PSPO is not applied to every parameterized query, only to ones where the optimizer sees a genuine sensitivity.</p>



<p class="wp-block-paragraph">The most convincing thing I can say about PSPO is not an argument, it&#8217;s a confession about the demo:&nbsp;<strong>the setup script has to turn PSPO off.</strong>&nbsp;On a 2022 instance at compatibility level 160, scenarios A and B don&#8217;t fail. The engine handles them. I had to explicitly disable the feature to show you the classic behaviour at all:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:16px 0 0 16px;width:100%;text-align:left;background-color:#292d3e"><span style="background:#aaafcf;padding:0.3rem 0.5rem 0.2rem;border-radius:1rem;font-size:0.8em;line-height:1;height:1.25rem;text-align:center;display:inline-flex;align-items:center;justify-content:center;color:#292d3e">SQL</span></span><span role="button" tabindex="0" style="color:#babed8;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>ALTER DATABASE SCOPED CONFIGURATION
    SET PARAMETER_SENSITIVE_PLAN_OPTIMIZATION = OFF;
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki material-theme-palenight" style="background-color: #292D3E" tabindex="0"><code><span class="line"><span style="color: #F78C6C">ALTER</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">DATABASE</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">SCOPED</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">CONFIGURATION</span></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">SET</span><span style="color: #BABED8"> PARAMETER_SENSITIVE_PLAN_OPTIMIZATION </span><span style="color: #89DDFF">=</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">OFF</span><span style="color: #BABED8">;</span></span>
<span class="line"></span></code></pre></div>



<p class="wp-block-paragraph">Scenario E turns it back on so you can see the contrast: same procedure, same two parameters, no recompilation between them, two different plan shapes.</p>



<h2 class="wp-block-heading">Things that look like fixes</h2>



<p class="wp-block-paragraph">A tour of the mitigations you&#8217;ll find in older Stack Overflow answers, and what they actually cost.</p>



<p class="wp-block-paragraph"><strong><code>OPTIMIZE FOR UNKNOWN</code>.</strong>&nbsp;Compiles using the density vector — the average rows per distinct value — instead of the histogram. You&#8217;ve traded a plan that is excellent for most callers and terrible for a few, for a plan that is mediocre for everyone. Sometimes that really is the right trade, especially when the terrible case is bad enough to cause an outage. But it should be a decision, not a reflex.</p>



<p class="wp-block-paragraph"><strong><code>OPTIMIZE FOR (@CustomerID = 12345)</code>.</strong>&nbsp;You&#8217;ve pinned the plan to a magic constant. It works until the data distribution moves, at which point it fails silently and nobody remembers that number is in there.</p>



<p class="wp-block-paragraph"><strong>Assigning parameters to local variables.</strong>&nbsp;The folk-remedy version of&nbsp;<code>OPTIMIZE FOR UNKNOWN</code>&nbsp;— the optimizer can&#8217;t sniff a local variable, so you get the density estimate. Same trade-off, but now it&#8217;s invisible, and the next developer will &#8220;clean up&#8221; the pointless variable assignment and reintroduce the bug.</p>



<p class="wp-block-paragraph"><strong>Updating statistics, or rebuilding the index.</strong>&nbsp;This evicts plans, so the symptom goes away, so it looks like a fix. It will look like a fix again next week, and the week after, forever. This is the single most common way a parameter sniffing problem survives for years: it is permanently one maintenance job away from being invisible.</p>



<p class="wp-block-paragraph"><strong>Restarting SQL Server.</strong>&nbsp;Same mechanism, more downtime, and it usually happens at 3am while someone types &#8220;resolved&#8221; into a ticket.</p>



<p class="wp-block-paragraph"><strong>Splitting the procedure into branches</strong>&nbsp;— an&nbsp;<code>IF</code>&nbsp;that routes big customers to one procedure and small ones to another, so each gets its own plan. This one genuinely works. It also hardcodes today&#8217;s understanding of your data into control flow, and it ages badly. Reach for it when PSPO isn&#8217;t available and RECOMPILE is too expensive, and leave a comment explaining why.</p>



<h2 class="wp-block-heading">So what should you actually do?</h2>



<ol class="wp-block-list">
<li><strong>Confirm the column is skewed.</strong>&nbsp;Group by the predicate column, compare the top and bottom. Within an order of magnitude? It isn&#8217;t parameter sniffing. Go look somewhere else.</li>



<li><strong>Work out which problem you have.</strong>&nbsp;Compare&nbsp;<code>last_grant_kb</code>,&nbsp;<code>last_used_grant_kb</code>, and&nbsp;<code>last_ideal_grant_kb</code>&nbsp;against&nbsp;<code>last_logical_reads</code>. Wrong grant, wrong shape, or both.</li>



<li><strong>Grant only, on 2019 or later, with steady traffic</strong>&nbsp;— check your compatibility level is 150+ and let memory grant feedback handle it. Turn on Query Store if you&#8217;re on 2022, so the lesson survives an eviction.</li>



<li><strong>Shape wrong, on 2022</strong>&nbsp;— check whether PSPO is on before you write any code. You may not have a problem.</li>



<li><strong>Shape wrong, low call rate</strong>&nbsp;—&nbsp;<code>OPTION (RECOMPILE)</code>&nbsp;on the statement. Measure the compile cost afterwards rather than assuming it&#8217;s fine.</li>



<li><strong>Shape wrong, high call rate, no PSPO available</strong>&nbsp;— branch the procedure, and write down why in a comment, because in three years the reason will not be obvious.</li>
</ol>



<p class="wp-block-paragraph">The thing I&#8217;d most like you to take away is the second step. Almost everything written about parameter sniffing collapses the two failure modes into one story, and once you&#8217;ve separated them the modern features stop looking mysterious. Memory grant feedback isn&#8217;t under-delivering — it&#8217;s doing exactly the one job it claims to do, and PSPO is the feature that does the other one.</p>



<h2 class="wp-block-heading">Run it yourself</h2>



<p class="wp-block-paragraph">Download the sql files from my repo (see link below):</p>



<pre class="wp-block-code has-babed-8-color has-text-color has-875-rem-font-size"><code>01-setup.sql     run once, builds the skewed table and the procedure
02-demo.sql      run the whole file, all five scenarios, records its own evidence
03-cleanup.sql   restores everything it changed
</code></pre>



<p class="wp-block-paragraph">Two things before you do. Turn on&nbsp;<strong>Query Options → Results → Grid → Discard results after execution</strong>&nbsp;in SSMS, because the procedure returns half a million wide rows about ten times over and you want to be timing the server rather than the grid. And know that&nbsp;<code>01-setup.sql</code>&nbsp;raises your database compatibility level and turns PSPO off — both are recorded before they&#8217;re changed and restored by the cleanup script, but point it at a scratch instance, not production.</p>



<p class="wp-block-paragraph">The demo captures every measurement into&nbsp;<code>Demo.DemoResults</code>&nbsp;as it goes, so you don&#8217;t have to sit and read execution plans between executions. The summary at the bottom flags spills and wasted grants for you.</p>



<p class="wp-block-paragraph">Dowload the demo scripts from my <a href="https://github.com/MarlonRibunal/sqlserver-demos" target="_blank" rel="noopener nofollow" title=""><strong>github repo</strong></a>.</p>



<p class="wp-block-paragraph"></p><p>The post <a href="https://marlonribunal.com/demo-for-parameter-sniffing-and-memory-grant-feedback/">Demo for Parameter Sniffing and Memory Grant Feedback</a> first appeared on <a href="https://marlonribunal.com">SQL, Code, Coffee, Etc.</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://marlonribunal.com/demo-for-parameter-sniffing-and-memory-grant-feedback/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>When Do You Reach For A #temp Table?</title>
		<link>https://marlonribunal.com/when-do-you-reach-for-a-temp-table/</link>
					<comments>https://marlonribunal.com/when-do-you-reach-for-a-temp-table/#comments</comments>
		
		<dc:creator><![CDATA[Marlon Ribunal]]></dc:creator>
		<pubDate>Tue, 11 Aug 2026 11:05:00 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://marlonribunal.com/?p=2869</guid>

					<description><![CDATA[<p>I needed to do some research because there is more to #temp tables than simply creating one and using it. As I started digging into the topic, I realized there are a lot of discussions around #temp tables and @table variables, especially around when one might be a better choice over the other. <a href="https://marlonribunal.com/when-do-you-reach-for-a-temp-table/">Continue reading <span class="meta-nav">&#8594;</span></a></p>
<p>The post <a href="https://marlonribunal.com/when-do-you-reach-for-a-temp-table/">When Do You Reach For A #temp Table?</a> first appeared on <a href="https://marlonribunal.com">SQL, Code, Coffee, Etc.</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph"><em><strong>Disclaimer:</strong> Rather than spending time writing a test scenario from scratch, I used Claude Code to generate the test scenarios for this article. I wanted a certain behavior to demonstrate a couple of differences between #temp and @table. You can download the test scripts from my public GitHub repo (see info at the bottom of this post).</em></p>



<p class="wp-block-paragraph"><code>#temp</code> tables can be useful in so many ways. I hadn&#8217;t really given them much thought beyond using them when a situation called for it, until I saw the invitation for  <strong><a href="https://www.jefftaylor.io/post/t-sql-tuesday-201-invitation-temp-tables-friend-or-foe" target="_blank" rel="noopener nofollow" title="">T-SQL Tuesday #201 Invitation: Temp Tables, Friend or Foe?</a></strong> This month&#8217;s T-SQL Tuesday is hosted by Jeff Taylor (<a href="https://www.jefftaylor.io/profile/jeff/profile" target="_blank" rel="noopener nofollow" title=""><strong>b</strong></a>). </p>


<div class="wp-block-image">
<figure class="alignleft size-full"><a href="https://www.jefftaylor.io/post/t-sql-tuesday-201-invitation-temp-tables-friend-or-foe"><img fetchpriority="high" decoding="async" width="420" height="420" src="https://marlonribunal.com/wp-content/uploads/2026/08/TSQL_Tuesday_Logo.png" alt="" class="wp-image-2872" srcset="https://marlonribunal.com/wp-content/uploads/2026/08/TSQL_Tuesday_Logo.png 420w, https://marlonribunal.com/wp-content/uploads/2026/08/TSQL_Tuesday_Logo-300x300.png 300w, https://marlonribunal.com/wp-content/uploads/2026/08/TSQL_Tuesday_Logo-150x150.png 150w" sizes="(max-width: 420px) 100vw, 420px" /></a></figure>
</div>


<p class="wp-block-paragraph">For this post, I needed to do some research because there is more to <code>#temp </code>tables than simply creating one and using it. As I started digging into the topic, I realized there are a lot of discussions around <code>#temp</code> tables and <code>@table</code> variables, especially around when one might be a better choice over the other.</p>



<p class="wp-block-paragraph">One of the common arguments is that <code>#temp </code>tables go to disk while <code>@table</code> variables stay in memory. Another argument is that this is a myth. That got me curious about what the actual differences are and when they really matter. Discussions like this almost always starts with where the data is stored (disk vs memory).</p>



<p class="wp-block-paragraph">Both <code>#temp </code>tables and <code>@table</code> variables <a href="https://learn.microsoft.com/en-us/sql/t-sql/data-types/table-transact-sql?view=sql-server-ver17#:~:text=Table%20variables%20are%20created%20in,in%20memory%20(data%20cache)" target="_blank" rel="noopener nofollow" title="">use <code><strong>tempdb</strong></code></a>. Both allocate pages that are managed by the <code>buffer pool</code>. Depending on the size of the data, memory availability, and other factors, those pages may or may not ever be written to disk. If that&#8217;s the case, then maybe &#8220;memory versus disk&#8221; isn&#8217;t really the question we should be asking.</p>



<p class="wp-block-paragraph">Let&#8217;s take a look at a couple of scenarios.</p>



<p class="wp-block-paragraph"><strong>Test SQL:</strong> SQL Server 2022 CU25 (16.0.4255.1) container (Orbstack on Macbook pro)<br /><strong>CPU:</strong> 8 (schedulers)<br /><strong>MAXDOP:</strong> 0 <br /><strong>Cost Threshold for Parallelism:</strong> Default 5 (for parallelism test scenario)</p>



<h2 class="wp-block-heading">The difference is statistics</h2>



<p class="wp-block-paragraph">One of the biggest differences between <code>#temp</code> tables and <code>@table</code> variables is statistics. <code>#temp</code> tables can have statistics created and maintained by SQL Server, which gives the optimizer more information when generating an execution plan. <code>@table</code> variables do not have column statistics, although newer versions of SQL Server have improved their cardinality estimates through <strong>deferred compilation</strong>.</p>



<p class="wp-block-paragraph"><code>#temp</code> tables can have statistics. Those statistics include histograms that help the optimizer understand how the data is distributed and make better estimates.</p>



<p class="wp-block-paragraph">A table variable does not get column statistics. Even with newer versions of SQL Server and improvements like deferred compilation, the optimizer still does not have the same level of information that it has with a <code>#temp</code> table.</p>



<p class="wp-block-paragraph">This is where many of the differences start to show up. Things like indexes, constraints, recompiles, and parallelism are often related to the fact that a <code>#temp</code> table is a temporary table with metadata that the optimizer can use, while a table variable has different behavior.</p>



<p class="wp-block-paragraph">The rule I am starting to use from this research is simple:</p>



<p class="wp-block-paragraph">Use a <code>#temp</code> table when the optimizer needs more information about the data. Use a table variable when the amount of data is small, the usage is simple, or you specifically need the behavior that a table variable provides.</p>



<p class="wp-block-paragraph">Let&#8217;s take a look at a simple test.</p>



<p class="wp-block-paragraph">I wanted to keep this as fair as possible. Both objects have the same structure, the same data, and the same nonclustered index. All the test scripts can be found in the Github repo (see link at the bottom of this post).</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:16px 0 0 16px;width:100%;text-align:left;background-color:#292d3e"><span style="background:#aaafcf;padding:0.3rem 0.5rem 0.2rem;border-radius:1rem;font-size:0.8em;line-height:1;height:1.25rem;text-align:center;display:inline-flex;align-items:center;justify-content:center;color:#292d3e">SQL</span></span><span role="button" tabindex="0" style="color:#babed8;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>CREATE TABLE #temptable 
(
    id int NOT NULL, 
    grp int NOT NULL, 
    INDEX ix_grp NONCLUSTERED (grp)
);

DECLARE @tablevariable TABLE 
(
    id int NOT NULL, 
    grp int NOT NULL, 
    INDEX ix_grp NONCLUSTERED (grp)
);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki material-theme-palenight" style="background-color: #292D3E" tabindex="0"><code><span class="line"><span style="color: #F78C6C">CREATE</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">TABLE</span><span style="color: #BABED8"> #temptable </span></span>
<span class="line"><span style="color: #BABED8">(</span></span>
<span class="line"><span style="color: #BABED8">    id </span><span style="color: #C792EA">int</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">NOT NULL</span><span style="color: #BABED8">, </span></span>
<span class="line"><span style="color: #BABED8">    grp </span><span style="color: #C792EA">int</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">NOT NULL</span><span style="color: #BABED8">, </span></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">INDEX</span><span style="color: #BABED8"> ix_grp </span><span style="color: #F78C6C">NONCLUSTERED</span><span style="color: #BABED8"> (grp)</span></span>
<span class="line"><span style="color: #BABED8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F78C6C">DECLARE</span><span style="color: #BABED8"> @tablevariable </span><span style="color: #F78C6C">TABLE</span><span style="color: #BABED8"> </span></span>
<span class="line"><span style="color: #BABED8">(</span></span>
<span class="line"><span style="color: #BABED8">    id </span><span style="color: #C792EA">int</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">NOT NULL</span><span style="color: #BABED8">, </span></span>
<span class="line"><span style="color: #BABED8">    grp </span><span style="color: #C792EA">int</span><span style="color: #BABED8"> </span><span style="color: #F78C6C">NOT NULL</span><span style="color: #BABED8">, </span></span>
<span class="line"><span style="color: #BABED8">    </span><span style="color: #F78C6C">INDEX</span><span style="color: #BABED8"> ix_grp </span><span style="color: #F78C6C">NONCLUSTERED</span><span style="color: #BABED8"> (grp)</span></span>
<span class="line"><span style="color: #BABED8">);</span></span></code></pre></div>



<p class="wp-block-paragraph">The data is intentionally skewed. <code>grp = 1</code> has 9,000 rows, while <code>grp = 2</code> has only 10 rows.</p>



<p class="wp-block-paragraph">When I checked the estimated row counts, the difference was obvious.</p>



<p class="wp-block-paragraph">The <code>#temp</code> table had statistics, so the optimizer had information about the data distribution. The estimates were close to the actual row counts.</p>



<p class="wp-block-paragraph">The table variable was different. Even though it had the same index, the optimizer did not have the same information available. The index gave it something to seek on, but it did not tell the optimizer how the data was distributed.</p>



<p class="wp-block-paragraph">That distinction matters because estimates influence the rest of the execution plan. Row estimates affect join choices, memory grants, and other optimizer decisions. If SQL Server estimates 100 rows but the query actually returns 9,000 rows, the optimizer may choose a plan that isn&#8217;t optimal for the actual workload. That can also lead to memory spills to tempdb if the memory grant is too small. </p>



<p class="wp-block-paragraph">One thing I noticed while testing this is that the estimate for a table variable is not always the same. For example, removing the index can change the estimate. The important part is that the optimizer still has limited information about the data inside a table variable.</p>



<p class="wp-block-paragraph">So what about SQL Server 2019 and <strong>table variable deferred compilation?</strong></p>



<p class="wp-block-paragraph">Deferred compilation helps because SQL Server can see the table variable row count before compiling the statement. This improves cardinality estimates in many scenarios.</p>



<p class="wp-block-paragraph">However, it does not create statistics. Knowing that a table variable has 10,000 rows is different from knowing how those 10,000 rows are distributed. A row count can help, but it does not replace a histogram.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>source</th><th>predicate</th><th>estimated</th><th>true</th></tr></thead><tbody><tr><td><code>#temp</code></td><td><code>grp = 1</code></td><td><strong>9,000</strong></td><td>9,000</td></tr><tr><td><code>#temp</code></td><td><code>grp = 2</code></td><td><strong>10</strong></td><td>10</td></tr><tr><td><code>@tablevar</code></td><td><code>grp = 1</code></td><td><strong>100</strong></td><td>9,000</td></tr><tr><td><code>@tablevar</code></td><td><code>grp = 2</code></td><td><strong>100</strong></td><td>10</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">Table variables migh have a problem with parallelism</h2>



<p class="wp-block-paragraph"><em><strong>Note</strong>: It is quite complicated to test paralellism on my test environment. Some tests results returned &#8220;inconclusive&#8221;. Your mileage varies (depending on your config basically).</em></p>



<p class="wp-block-paragraph">I also looked at how <code>#temp</code> tables and <code>@table</code> variables behave when it comes to parallelism.</p>



<p class="wp-block-paragraph">A common statement is: &#8220;<code>@table</code> variables are always serial.&#8221; That may not be completely accurate. But again, I have not tested this extensively. FYI.</p>



<p class="wp-block-paragraph">The restriction is related to modifying the table variable, not reading from it. For example, a regular <code>SELECT</code> or a query that reads from a table variable can still use parallelism. The limitation shows up when SQL Server is inserting into or modifying the table variable. I need to test this with bigger workload when I get the chance. </p>



<p class="wp-block-paragraph">Again, this is not conclusive.</p>



<p class="wp-block-paragraph"></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>statement</th><th>DOP</th></tr></thead><tbody><tr><td>control: plain&nbsp;<code>SELECT ... GROUP BY</code></td><td>8</td></tr><tr><td><code>INSERT INTO #temp ... SELECT</code></td><td>8</td></tr><tr><td><code>INSERT INTO @tablevar ... SELECT</code></td><td><strong>1</strong></td></tr><tr><td><code>SELECT</code>&nbsp;joining&nbsp;<code>@tablevar</code></td><td><strong>8</strong></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Download the test scripts to see how this looks in your environment. Link to my Github repo is at the bottom of this post.</p>



<h2 class="wp-block-heading">The impact really depends on how much data you are working with.</h2>



<p class="wp-block-paragraph">If you are inserting a small number of rows into a table variable and reading from it later, the lack of parallelism during the insert probably does not matter. The difference between a serial and parallel insert for a small dataset is not something you will likely notice.</p>



<p class="wp-block-paragraph">Where this becomes important is when you start loading a large amount of data. A table variable that is used as a staging area for thousands or millions of rows can limit the insert operation to a serial plan when a <code>#temp</code> table could take advantage of parallelism.</p>



<p class="wp-block-paragraph">For small amounts of data, use whichever option makes the code easier to understand. For larger data loads, a <code>#temp</code> table is usually worth considering.</p>



<p class="wp-block-paragraph">The test scenario used in this post covers only a couple of the differences between <code>#temp</code> tables and <code>@table</code> variables. If you want to experiment with the scenarios yourself, you can grab the test scripts from my GitHub repository and run them in your own environment.<br /></p>



<p class="wp-block-paragraph"><a href="https://github.com/MarlonRibunal/sqlserver-demos/tree/main/tempdb">https://github.com/MarlonRibunal/sqlserver-demos/tree/main/tempdb</a></p>



<div class="wp-block-comments"><h2 id="comments" class="wp-block-comments-title">One response to &#8220;When Do You Reach For A #temp Table?&#8221;</h2>

<ol class="wp-block-comment-template"><li id="comment-13397" class="pingback even thread-even depth-1">

<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-8f761849 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:40px"><div class="wp-block-avatar"></div></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow"><div class="wp-block-comment-author-name has-small-font-size"><a rel="external nofollow ugc" href="https://curatedsql.com/2026/08/13/comparing-temp-tables-to-table-variables/" target="_self" >Comparing Temp Tables to Table Variables &#8211; Curated SQL</a></div>


<div class="wp-block-group is-layout-flex wp-block-group-is-layout-flex" style="margin-top:0px;margin-bottom:0px"><div class="wp-block-comment-date has-small-font-size"><time datetime="2026-08-13T05:05:07-07:00"><a href="https://marlonribunal.com/when-do-you-reach-for-a-temp-table/comment-page-1/#comment-13397">08/13/2026</a></time></div>

</div>


<div class="wp-block-comment-content"><p>[&#8230;] Marlon Ribunal compares two techniques: [&#8230;]</p>
</div>

<div class="wp-block-comment-reply-link has-small-font-size"><a rel="nofollow" class="comment-reply-link" href="#comment-13397" data-commentid="13397" data-postid="2869" data-belowelement="comment-13397" data-respondelement="respond" data-replyto="Reply to Comparing Temp Tables to Table Variables &#8211; Curated SQL" aria-label="Reply to Comparing Temp Tables to Table Variables &#8211; Curated SQL">Reply</a></div></div>
</div>

</li></ol>

</div><p>The post <a href="https://marlonribunal.com/when-do-you-reach-for-a-temp-table/">When Do You Reach For A #temp Table?</a> first appeared on <a href="https://marlonribunal.com">SQL, Code, Coffee, Etc.</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://marlonribunal.com/when-do-you-reach-for-a-temp-table/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server Security Hardening Guide Using the DoD STIG Checklist</title>
		<link>https://marlonribunal.com/sql-server-security-hardening-guide-using-the-dod-stig-checklist/</link>
					<comments>https://marlonribunal.com/sql-server-security-hardening-guide-using-the-dod-stig-checklist/#respond</comments>
		
		<dc:creator><![CDATA[Marlon Ribunal]]></dc:creator>
		<pubDate>Thu, 06 Aug 2026 11:10:00 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://marlonribunal.com/?p=2751</guid>

					<description><![CDATA[<p>Security on your SQL Server is important. That doesn&#8217;t need any explaining. But where do you start when evaluating the security of your SQL Server? If you are like me, and probably for many DBAs, that&#8217;s the hardest part. You &#8230; <a href="https://marlonribunal.com/sql-server-security-hardening-guide-using-the-dod-stig-checklist/">Continue reading <span class="meta-nav">&#8594;</span></a></p>
<p>The post <a href="https://marlonribunal.com/sql-server-security-hardening-guide-using-the-dod-stig-checklist/">SQL Server Security Hardening Guide Using the DoD STIG Checklist</a> first appeared on <a href="https://marlonribunal.com">SQL, Code, Coffee, Etc.</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Security on your SQL Server is important. That doesn&#8217;t need any explaining. But where do you start when evaluating the security of your SQL Server? If you are like me, and probably for many DBAs, that&#8217;s the hardest part. You know security matters, but without a structured baseline, it&#8217;s easy to overlook configuration issues that could expose your environment to unnecessary risk. Starting with a proven checklist gives you a clear way to identify gaps before they become problems.</p>



<p class="wp-block-paragraph">And how do you even implement the principle of least privilege on the instance and database level?</p>



<p class="wp-block-paragraph">Well, we&#8217;re lucky that the <strong>U.S. Department of Defense</strong> has provided a <strong>Security Technical Implementation Guide (STIG)</strong> for SQL Server, along with many other technologies. It gives you a well-established security baseline that you can use to evaluate your SQL Server environment, whether you&#8217;re working with an on-prem deployment, a virtual machine, or even a lab environment. Even if you&#8217;re not in a government-regulated organization, the STIG is still a practical reference for identifying security gaps and strengthening your SQL Server configuration.</p>



<p class="wp-block-paragraph">You need two things: the<strong> SQL Server 2022 STIG</strong> and the <strong>STIG viewer</strong>.</p>



<h2 class="wp-block-heading">Download the STIG file</h2>



<p class="wp-block-paragraph">Download the <strong><a href="https://www.cyber.mil/stigs/downloads" target="_blank" rel="noopener nofollow" title="">guide here</a></strong>. Search for <em><strong>SQL Server</strong></em> and select <code>Microsoft SQL Server 2022 STIG</code>. Click the <code>Download</code> button to, well, download the zipped STIG file. </p>



<p class="wp-block-paragraph"></p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="710" src="https://marlonribunal.com/wp-content/uploads/2026/08/image-1024x710.png" alt="" class="wp-image-2883" srcset="https://marlonribunal.com/wp-content/uploads/2026/08/image-1024x710.png 1024w, https://marlonribunal.com/wp-content/uploads/2026/08/image-300x208.png 300w, https://marlonribunal.com/wp-content/uploads/2026/08/image-768x532.png 768w, https://marlonribunal.com/wp-content/uploads/2026/08/image-1536x1065.png 1536w, https://marlonribunal.com/wp-content/uploads/2026/08/image-2048x1420.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">You don&#8217;t have to extract the zip file. We&#8217;ll load the whole zip file on the viewer in the next step.</p>



<h2 class="wp-block-heading">Install the STIG viewer</h2>



<p class="wp-block-paragraph">Download the STIG viewer here: https://www.cyber.mil/stigs/srg-stig-tools/. Download the msi installer called <code>STIG Viewer 3.7.0-Win64 msi</code>. Extract the installer package. Unfortunately, macOS is not supported. This is a Windows-only app.</p>



<p class="wp-block-paragraph">Click <strong>Yes</strong> to allow the app to make changes to your device. </p>



<p class="wp-block-paragraph">There is no wizard steps to follow. Upon clicking Yes, the STIG Viewer should be installed right away.</p>



<p class="wp-block-paragraph"></p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="752" src="https://marlonribunal.com/wp-content/uploads/2026/08/image-1-1024x752.png" alt="" class="wp-image-2884" srcset="https://marlonribunal.com/wp-content/uploads/2026/08/image-1-1024x752.png 1024w, https://marlonribunal.com/wp-content/uploads/2026/08/image-1-300x220.png 300w, https://marlonribunal.com/wp-content/uploads/2026/08/image-1-768x564.png 768w, https://marlonribunal.com/wp-content/uploads/2026/08/image-1-1536x1128.png 1536w, https://marlonribunal.com/wp-content/uploads/2026/08/image-1-2048x1504.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">Load the STIG file</h2>



<p class="wp-block-paragraph">To load the STIG zip file (the first zip file we downloaded above) in the viewer, click the <code>Open</code> button in the STIG Viewer section (top panel). That will load both the STIG for SQL Server <strong>Instance</strong> and <strong>Database</strong> in the viewer.</p>



<p class="wp-block-paragraph"></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="749" src="https://marlonribunal.com/wp-content/uploads/2026/08/image-3-1024x749.png" alt="" class="wp-image-2887" srcset="https://marlonribunal.com/wp-content/uploads/2026/08/image-3-1024x749.png 1024w, https://marlonribunal.com/wp-content/uploads/2026/08/image-3-300x220.png 300w, https://marlonribunal.com/wp-content/uploads/2026/08/image-3-768x562.png 768w, https://marlonribunal.com/wp-content/uploads/2026/08/image-3-1536x1124.png 1536w, https://marlonribunal.com/wp-content/uploads/2026/08/image-3-2048x1499.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">You may want to <strong>add STIG to the Library</strong> so you don&#8217;t have to reload the STIG documentation each time. You can simply add the whole zip file.</p>



<p class="wp-block-paragraph">If the STIG docs will not automatically appear after they are added in the library, close the app and open it again. </p>



<p class="wp-block-paragraph">You can always go to the Dashboard by clicking the <strong><code>Home</code></strong> buttom at the upper right-hand navigation menu. Click the <strong><code>STIG Viewer</code></strong> button to open the STIG docs.</p>



<p class="wp-block-paragraph"></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="747" src="https://marlonribunal.com/wp-content/uploads/2026/08/image-5-1024x747.png" alt="" class="wp-image-2889" srcset="https://marlonribunal.com/wp-content/uploads/2026/08/image-5-1024x747.png 1024w, https://marlonribunal.com/wp-content/uploads/2026/08/image-5-300x219.png 300w, https://marlonribunal.com/wp-content/uploads/2026/08/image-5-768x560.png 768w, https://marlonribunal.com/wp-content/uploads/2026/08/image-5-1536x1120.png 1536w, https://marlonribunal.com/wp-content/uploads/2026/08/image-5-2048x1494.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">You can now then view the rules contained in the STIG docs. Select the <strong><code>Microsoft SQL Server 2022 Intance</code></strong> to view the rules for the instance (and Database for the database-level checks).</p>



<p class="wp-block-paragraph"></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="749" src="https://marlonribunal.com/wp-content/uploads/2026/08/image-4-1024x749.png" alt="" class="wp-image-2888" srcset="https://marlonribunal.com/wp-content/uploads/2026/08/image-4-1024x749.png 1024w, https://marlonribunal.com/wp-content/uploads/2026/08/image-4-300x220.png 300w, https://marlonribunal.com/wp-content/uploads/2026/08/image-4-768x562.png 768w, https://marlonribunal.com/wp-content/uploads/2026/08/image-4-1536x1124.png 1536w, https://marlonribunal.com/wp-content/uploads/2026/08/image-4-2048x1499.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">We will create our custom <strong>Checklist</strong> in our follow up post. We&#8217;ll also customize it so you can have a version of this STIG for your organization.</p>



<p class="wp-block-paragraph"></p><p>The post <a href="https://marlonribunal.com/sql-server-security-hardening-guide-using-the-dod-stig-checklist/">SQL Server Security Hardening Guide Using the DoD STIG Checklist</a> first appeared on <a href="https://marlonribunal.com">SQL, Code, Coffee, Etc.</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://marlonribunal.com/sql-server-security-hardening-guide-using-the-dod-stig-checklist/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How I Troubleshoot a Slow SQL Server Live</title>
		<link>https://marlonribunal.com/how-i-troubleshoot-a-slow-sql-server-live/</link>
					<comments>https://marlonribunal.com/how-i-troubleshoot-a-slow-sql-server-live/#respond</comments>
		
		<dc:creator><![CDATA[Marlon Ribunal]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 11:10:00 +0000</pubDate>
				<category><![CDATA[How-To]]></category>
		<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://marlonribunal.com/?p=2782</guid>

					<description><![CDATA[<p>No two SQL Server environments are alike. A fix that resolves a performance issue in one environment may not work in another because every SQL Server instance has different workloads, hardware, data distribution, application behavior, and configuration. Even if you reproduce an issue in a test environment, there's no guarantee the same solution will produce identical results in production.</p>
<p>So, probably instead of establishing a rigid step-by-step process, putting together a repeatable methodology can lead you to a resolution. This will eliminate blind guesses and at least bring you to an educated guess if the issue is too complicated. <a href="https://marlonribunal.com/how-i-troubleshoot-a-slow-sql-server-live/">Continue reading <span class="meta-nav">&#8594;</span></a></p>
<p>The post <a href="https://marlonribunal.com/how-i-troubleshoot-a-slow-sql-server-live/">How I Troubleshoot a Slow SQL Server Live</a> first appeared on <a href="https://marlonribunal.com">SQL, Code, Coffee, Etc.</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">No two SQL Server environments are alike. A fix that resolves a performance issue in one environment may not work in another because every SQL Server instance has different workloads, hardware, data distribution, application behavior, and configuration. Even if you reproduce an issue in a test environment, there&#8217;s no guarantee the same solution will produce identical results in production.</p>



<p class="wp-block-paragraph">So, probably instead of establishing a rigid step-by-step process, putting together a repeatable methodology can lead you to a resolution. This will eliminate blind guesses and at least bring you to an educated guess if the issue is too complicated.</p>



<p class="wp-block-paragraph">A common workflow for troubleshooting performance issues on SQL Server may involve the following. But again, these are not necessarily phases or stages that you have to follow in a specific order. Real-world troubleshooting rarely happens in a perfect sequence. You may jump between these areas, revisit previous findings, or investigate multiple things simultaneously depending on the complexity of the issue.</p>



<ol class="wp-block-list">
<li>Define the scope</li>



<li>Determine what changed</li>



<li>Measure server health</li>



<li>Identify bottlenecks</li>



<li>Investigate the root cause</li>



<li>Validate the fix / Monitor after implementation</li>



<li>Document the incident</li>
</ol>



<h2 class="wp-block-heading">Scope it first</h2>



<p class="wp-block-paragraph">Every time I get a call about slow performance, the first thing I want to do is qualify the question itself. What does <em>slow</em> mean? Is it a widget that used to run for a few seconds and is now running for five minutes? By the way, this is one of the most common &#8220;slow&#8221; incidents I&#8217;ve seen in my last job at an ISV.</p>



<p class="wp-block-paragraph">Scoping and discovery are probably the most critical parts of any troubleshooting task. This is the part where you can grasp the scope and nature of the issue at hand. Here, you will probably be able to establish your footing on how to approach the issue. Of course, a batch job running in SSIS or SQL Server Agent will require a different approach than, say, a process triggered by a user in the application&#8217;s frontend.</p>



<p class="wp-block-paragraph">And, again, as I&#8217;ve said in my other <strong><a href="https://marlonribunal.com/do-this-before-you-contact-your-sql-server-consultant/" target="_blank" rel="noopener" title="post">post</a></strong>, I think it&#8217;s important to know when this issue started occurring. That alone might reveal a few important details about the very nature of the issue. Knowing whether you&#8217;re dealing with a single bad query or a server-wide issue will prevent you from investigating the wrong layer of the stack. For all you know, it&#8217;s not a performance issue at all, but a slowdown caused by the user&#8217;s ISP while working from home. I&#8217;ve seen this many times before.</p>



<p class="wp-block-paragraph">My typical goal at the onset is to reproduce or replicate the issue in a test environment. That can give you the most bang for your buck because it allows you to investigate the issue and test potential solutions in a controlled environment. </p>



<p class="wp-block-paragraph">The one thing we&#8217;re trying to prevent in the scoping is getting down the proverbial rabbit hole or chasing that red herring. Yes, I still love using those idioms in real life.</p>



<h2 class="wp-block-heading">What changed?</h2>



<p class="wp-block-paragraph">Actually, this should be part of scoping, but I think it merits a whole section of its own. Knowing what changed could hold critical information as to the reason behind the issue. Maybe somebody just tweaked some lever on the application side, and that kind of caused a domino effect of some sort.<br /><br />The main goal here is to find all the possible root causes and not point fingers at whoever might have been involved. There are a ton of reasons why a SQL Server might suddenly run slow. Configuration changes, new procedures, introduction of new indexes, unanticipated workload, network I/O, and even user workflows can cause slowdowns given the right circumstances where SQL Server can really slow down.</p>



<p class="wp-block-paragraph">I&#8217;d start with the most recent change. And, of course, the typical answer to &#8220;What changed?&#8221; is almost always &#8220;nothing.&#8221; So, it&#8217;s up to you to determine how you surface what actually changed, if anything.</p>



<h2 class="wp-block-heading">Know your baseline</h2>



<p class="wp-block-paragraph">If nothing has changed at all, then the next step is to determine any other activities that could be affecting the particular issue that you are investigating. It&#8217;s possible that the slowdown is caused by another task that is not necessarily related to the slowdown issue but is somehow affecting, for example, server resources.</p>



<p class="wp-block-paragraph">I&#8217;ve seen cases where a third party started to send an amount of data that was not typical for an intra-day workload, which caused a domino effect on normal business operations. Not knowing what a &#8220;typical intra-day workload&#8221; looks like can complicate your investigation because you wouldn&#8217;t know if things are a factor in the issue or not.</p>



<p class="wp-block-paragraph">For example, high CPU isn&#8217;t automatically a problem if the server normally runs at 90% CPU during month-end processing. While a query that normally consumes an infinitesimal amount of CPU and is suddenly taking 30% of the total CPU is almost certainly an issue. Always compare current metrics against a known baseline before concluding that something is abnormal.</p>



<h2 class="wp-block-heading">Paul can tell you where it hurts</h2>



<p class="wp-block-paragraph">If you haven&#8217;t identified a possible root cause or figured out a narrative that may possibly connect the dots at this point, then it&#8217;s time to dig deeper. Paul Randal&#8217;s wait stats <a href="https://www.sqlskills.com/blogs/paul/wait-statistics-or-please-tell-me-where-it-hurts/" target="_blank" rel="noopener nofollow" title=""><strong>query</strong></a> can surface some clues about the ongoing issue. If you are troubleshooting a live issue, you probably want to combine that with Paul&#8217;s waiting task <strong><a href="https://www.sqlskills.com/blogs/paul/updated-sys-dm_os_waiting_tasks-script-to-correctly-parse-nodeid/" target="_blank" rel="noopener nofollow" title="">query</a></strong>. Glenn Berry&#8217;s Diagnostic <a href="https://glennsqlperformance.com/" target="_blank" rel="noopener nofollow" title=""><strong>queries</strong></a> are a must-have tool for any SQL Server DBA. If you are like me, when I encounter slow issues that are happening live, my instinct is to run Adam Machanic&#8217;s sp_whoisactive <strong><a href="http://whoisactive.com/downloads/" target="_blank" rel="noopener nofollow" title="">procedure</a></strong>. For many others running Brent O&#8217;s <code>sp_BlitzFirst</code> <strong><a href="https://www.brentozar.com/sql/wait-stats/" target="_blank" rel="noopener nofollow" title="">procedure</a></strong> is their go-to. </p>



<p class="wp-block-paragraph">Wait stats and waiting tasks can be a superpower when it comes to troubleshooting a performance issue. Here is an <strong><a href="https://marlonribunal.com/waiting-tasks-reveal-pain-points/" target="_blank" rel="noopener" title="">example of how I used these two</a></strong> in the past to troubleshoot a live performance issue.</p>



<p class="wp-block-paragraph">The wait type typically tells you where to go next. For example, blocking and lock waits send you to the head blocker (<code>sp_WhoIsActive</code> does a great job at identifying this). <code>PAGEIOLATCH</code> sends you toward storage I/O issues or queries performing large reads, possibly due to missing indexes. <code>RESOURCE_SEMAPHORE</code> points you toward memory grant pressure, while <code>SOS_SCHEDULER_YIELD</code> points toward CPU pressure. <code>CXCONSUMER</code> can be a benign wait type, but you should still investigate whether it is contributing to a bottleneck in a highly concurrent system.</p>



<p class="wp-block-paragraph">Wait stats can help you identify the type of bottleneck SQL Server is experiencing, whether it&#8217;s related to I/O, CPU pressure, memory pressure, locking, or other resource contention.</p>



<h2 class="wp-block-heading">Resource utilization</h2>



<p class="wp-block-paragraph">Ok, just like with wait stats, if you need to dig deeper, you should look into system resources as well. There is only so much you can achieve by optimizing indexes and queries. At some point, you need to ask whether the server has enough resources to handle the workload it is being asked to process. A perfectly tuned query can still struggle if the underlying infrastructure is already under pressure.</p>



<p class="wp-block-paragraph">Here&#8217;s what I had to say about Disk IO on a LinkedIn <strong><a href="https://www.linkedin.com/posts/marlonribunal_sqlserver-activity-7481394723207925760-CctB?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABwvznYB3NldBkI_v2xrEy5IdizQkSdAo3Q" target="_blank" rel="noopener nofollow" title="">post</a></strong>:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="509" height="400" src="https://marlonribunal.com/wp-content/uploads/2026/07/image-32.png" alt="IOPS and Throughput (MB/s) in SQL Server" class="wp-image-2853" srcset="https://marlonribunal.com/wp-content/uploads/2026/07/image-32.png 509w, https://marlonribunal.com/wp-content/uploads/2026/07/image-32-300x236.png 300w" sizes="auto, (max-width: 509px) 100vw, 509px" /></figure>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">The key here is understanding whether you are dealing with a query problem, a workload problem, or a capacity problem. You can only go so far with index tuning and query optimization if the real issue is resource pressure underneath. </p>



<p class="wp-block-paragraph">At some point, you have to understand what SQL Server is waiting on and whether the current infrastructure can support the workload. <strong>This is where troubleshooting becomes less about applying fixes and more about understanding the behavior of the system as a whole.</strong></p>



<h2 class="wp-block-heading">Bottleneck at the query level</h2>



<p class="wp-block-paragraph">We&#8217;re at the point where we are ruling out server-wide issues and closing in on specific queries that may be contributing to the problem. This is where tools like Query Store become extremely valuable because they allow you to look back at query behavior over time instead of relying only on what is happening at the exact moment of the incident.<br /><br />One thing that I have been trying to learn lately is Query Store. At the time of writing, I am taking the SQLskills course on Query Store, <strong><a href="https://www.sqlskills.com/sql-server-training/ieqs/" target="_blank" rel="noopener nofollow" title="">IEQS</a></strong>. The more I learn about it, the more I realize how useful it can be when troubleshooting query performance issues, especially when dealing with plan regressions or queries that suddenly start behaving differently.</p>



<p class="wp-block-paragraph">Of course, actual execution plans can give you a lot of information about where the bottleneck is occurring at the query level. Is it a cardinality estimation issue, memory grant pressure, spills, missing indexes, outdated statistics, or even parameter sniffing? The information you can glean from execution plans or Query Store can help you understand why a query is behaving the way it is and guide you toward the right tuning approach.</p>



<h2 class="wp-block-heading">Is it blocking</h2>



<p class="wp-block-paragraph">If it&#8217;s a blocking chain, the first thing you want to do is find the head blocker and understand what it is doing before you start killing sessions. A blocked session is usually just a victim waiting for something else to finish. The real question is: what is holding the lock, and why is it taking so long?</p>



<p class="wp-block-paragraph">This is where experience and judgment come into play. Just because you found a blocking session does not mean the immediate answer is to kill it. You need to understand what the session is doing, how long it has been running, and what impact terminating it will have. One interesting case I encountered in the past is a <strong><a href="https://marlonribunal.com/killing-service-broker-spid/" target="_blank" rel="noopener nofollow" title="">Service Broker queue that got stuck in limbo</a></strong>.</p>



<h2 class="wp-block-heading">Communicate to stakeholders</h2>



<p class="wp-block-paragraph">During an incident, people understand that troubleshooting takes time. What they don&#8217;t want is uncertainty and silence while the issue is impacting the business. Keeping stakeholders informed shows that you are engaged, even when the root cause has not been identified yet.</p>



<p class="wp-block-paragraph">In my experience, you don&#8217;t always have to provide a breakthrough every time you communicate. Sometimes a simple update like &#8220;we are seeing high I/O waits, we are checking storage latency, and we are looking at the queries involved&#8221; is enough to keep everyone aligned. The technical investigation may still be in progress, but at least everyone understands where things stand.</p>



<h2 class="wp-block-heading">Validate the fix</h2>



<p class="wp-block-paragraph">Once the fix has been implemented, the work is not done yet. This is where you go back and validate if the change actually fixed the problem. Did the waits decrease? Did CPU return to its normal behavior? Did response times improve? Did the blocking clear? And probably the most important question: did the users notice the improvement?</p>



<p class="wp-block-paragraph">One thing I always try to avoid is assuming that the issue is fixed just because the immediate symptom disappeared. A SQL Server environment is a complex system, and sometimes fixing one problem can expose another one. Keep monitoring after the change and make sure that things continue to behave as expected.</p>



<p class="wp-block-paragraph">I&#8217;ve seen cases where a change looked successful during the initial troubleshooting session, only to find out later that the original problem came back or another issue surfaced. This is why validation and monitoring after the change are just as important as finding the fix in the first place.</p>



<h2 class="wp-block-heading">Document the incident</h2>



<p class="wp-block-paragraph">Document what happened, what you checked, and what fixed the issue. Include the root cause, symptoms, timeline, diagnostics, fix, and any lessons learned. The next time a similar incident happens, this documentation can save you from starting the investigation from scratch.</p>



<p class="wp-block-paragraph">Effective SQL Server troubleshooting is less about having the perfect fix ready and more about knowing how to narrow down the possibilities. You start with what you know, gather evidence, validate your assumptions, and slowly eliminate what doesn&#8217;t fit.</p>



<p class="wp-block-paragraph">In my experience, the DBAs who troubleshoot well are not the ones who have memorized every possible solution. They are the ones who know how to ask the right questions, understand what the system is telling them, and make changes based on evidence. Having a repeatable methodology will take you much further than a collection of random tuning techniques.</p>



<p class="wp-block-paragraph">Well, take it with a grain of salt. It depends. It always is.</p>



<p class="wp-block-paragraph"></p>



<div class="wp-block-comments">



</div><p>The post <a href="https://marlonribunal.com/how-i-troubleshoot-a-slow-sql-server-live/">How I Troubleshoot a Slow SQL Server Live</a> first appeared on <a href="https://marlonribunal.com">SQL, Code, Coffee, Etc.</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://marlonribunal.com/how-i-troubleshoot-a-slow-sql-server-live/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to Provision an Azure SQL Database</title>
		<link>https://marlonribunal.com/how-to-provision-an-azure-sql-database/</link>
					<comments>https://marlonribunal.com/how-to-provision-an-azure-sql-database/#respond</comments>
		
		<dc:creator><![CDATA[Marlon Ribunal]]></dc:creator>
		<pubDate>Thu, 30 Jul 2026 11:15:00 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://marlonribunal.com/?p=2717</guid>

					<description><![CDATA[<p>Aside from spinning up a SQL Server instance container, the free Azure SQL Database is another great tool for learning SQL. You can even use it for low-traffic or lightweight app. See the documentations for the limits. I will not &#8230; <a href="https://marlonribunal.com/how-to-provision-an-azure-sql-database/">Continue reading <span class="meta-nav">&#8594;</span></a></p>
<p>The post <a href="https://marlonribunal.com/how-to-provision-an-azure-sql-database/">How to Provision an Azure SQL Database</a> first appeared on <a href="https://marlonribunal.com">SQL, Code, Coffee, Etc.</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Aside from spinning up a SQL Server instance container, the free Azure SQL Database is another great tool for learning SQL. You can even use it for low-traffic or lightweight app. See the documentations for the limits. I will not be responsible for your usage. <br /><br />That said, let&#8217;s provision the database.</p>



<h2 class="wp-block-heading">Provision an Azure SQL Database</h2>



<p class="wp-block-paragraph">Go to you Azure Portal and search for Azure SQL Database</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="539" src="https://marlonribunal.com/wp-content/uploads/2026/07/image-19-1024x539.png" alt="" class="wp-image-2727" srcset="https://marlonribunal.com/wp-content/uploads/2026/07/image-19-1024x539.png 1024w, https://marlonribunal.com/wp-content/uploads/2026/07/image-19-300x158.png 300w, https://marlonribunal.com/wp-content/uploads/2026/07/image-19-768x404.png 768w, https://marlonribunal.com/wp-content/uploads/2026/07/image-19-1536x809.png 1536w, https://marlonribunal.com/wp-content/uploads/2026/07/image-19-scaled.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">On the upper left-hand of the UI, click on <strong>Create</strong> and select <strong>SQL database (Free offer)</strong>.<br /><br />Configuring an Azure SQL Database is pretty much intutive. For the Server option, use an existing SQL Database Server or create a new one. <br /><br />Click <strong>Review + create</strong> to finish the setup.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="914" src="https://marlonribunal.com/wp-content/uploads/2026/07/image-21-1024x914.png" alt="" class="wp-image-2729" srcset="https://marlonribunal.com/wp-content/uploads/2026/07/image-21-1024x914.png 1024w, https://marlonribunal.com/wp-content/uploads/2026/07/image-21-300x268.png 300w, https://marlonribunal.com/wp-content/uploads/2026/07/image-21-768x686.png 768w, https://marlonribunal.com/wp-content/uploads/2026/07/image-21-1536x1371.png 1536w, https://marlonribunal.com/wp-content/uploads/2026/07/image-21-scaled.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Connect from VS Code on a MacBook</h2>



<p class="wp-block-paragraph">Go to your Resource Group and find your Azure SQL Database. Or, you can simply search for Azure SQL Database in the search bar again and that will take you to your databases.</p>



<p class="wp-block-paragraph">Copy the Server name.</p>



<p class="wp-block-paragraph">Now, open your VS Code (install the <code>mssql</code> extension if you haven&#8217;t already). Why VS Code? That&#8217;s because Mirosoft will never port SSMS to macOS. That&#8217;s why.<br /><br />Create new connection. Look for the plug icon with the &#8216;+&#8217; sign next to it.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="674" height="470" src="https://marlonribunal.com/wp-content/uploads/2026/07/image-23.png" alt="" class="wp-image-2735" srcset="https://marlonribunal.com/wp-content/uploads/2026/07/image-23.png 674w, https://marlonribunal.com/wp-content/uploads/2026/07/image-23-300x209.png 300w" sizes="auto, (max-width: 674px) 100vw, 674px" /></figure>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">For the Input type, <strong>Browse Azure</strong> wouldn&#8217;t work for me even if I already took care of the networking setting. Let me know in the comment if you made it work. Using <strong>Parameters </strong>worked for me. <br /><br />Paste the <strong>Server name</strong>. Don&#8217;t forget to tick the <strong>Trust server certificate</strong>. Use <strong>SQL login</strong> and input the sa user and password that you set when you provisioned your SQL Server.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="643" src="https://marlonribunal.com/wp-content/uploads/2026/07/image-25-1024x643.png" alt="" class="wp-image-2738" srcset="https://marlonribunal.com/wp-content/uploads/2026/07/image-25-1024x643.png 1024w, https://marlonribunal.com/wp-content/uploads/2026/07/image-25-300x188.png 300w, https://marlonribunal.com/wp-content/uploads/2026/07/image-25-768x483.png 768w, https://marlonribunal.com/wp-content/uploads/2026/07/image-25-1536x965.png 1536w, https://marlonribunal.com/wp-content/uploads/2026/07/image-25-scaled.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">That should be it. Your Azure SQL Database is now ready to use.<br /><br /></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="780" src="https://marlonribunal.com/wp-content/uploads/2026/07/image-27-1024x780.png" alt="" class="wp-image-2742" srcset="https://marlonribunal.com/wp-content/uploads/2026/07/image-27-1024x780.png 1024w, https://marlonribunal.com/wp-content/uploads/2026/07/image-27-300x228.png 300w, https://marlonribunal.com/wp-content/uploads/2026/07/image-27-768x585.png 768w, https://marlonribunal.com/wp-content/uploads/2026/07/image-27-1536x1170.png 1536w, https://marlonribunal.com/wp-content/uploads/2026/07/image-27.png 1610w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure><p>The post <a href="https://marlonribunal.com/how-to-provision-an-azure-sql-database/">How to Provision an Azure SQL Database</a> first appeared on <a href="https://marlonribunal.com">SQL, Code, Coffee, Etc.</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://marlonribunal.com/how-to-provision-an-azure-sql-database/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>