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

<channel>
	<title>Archives des SQL Server - dbi Blog</title>
	<atom:link href="https://www.dbi-services.com/blog/category/sql-server/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.dbi-services.com/blog/category/sql-server/</link>
	<description></description>
	<lastBuildDate>Wed, 26 Aug 2026 08:54:54 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/05/cropped-favicon_512x512px-min-32x32.png</url>
	<title>Archives des SQL Server - dbi Blog</title>
	<link>https://www.dbi-services.com/blog/category/sql-server/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Fixing ORA-00904 on Oracle hidden columns during SSMA data migration</title>
		<link>https://www.dbi-services.com/blog/fixing-ora-00904-on-oracle-hidden-columns-during-ssma-data-migration/</link>
					<comments>https://www.dbi-services.com/blog/fixing-ora-00904-on-oracle-hidden-columns-during-ssma-data-migration/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Wed, 26 Aug 2026 08:54:51 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[Oracle]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[ssma]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46636</guid>

					<description><![CDATA[<p>SSMA fails with ORA-00904 on Oracle SET UNUSED columns. Fix it with a custom select aliasing NULL, no source DDL required.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/fixing-ora-00904-on-oracle-hidden-columns-during-ssma-data-migration/">Fixing ORA-00904 on Oracle hidden columns during SSMA data migration</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><a href="https://learn.microsoft.com/en-us/sql/ssma/sql-server-migration-assistant?view=sql-server-ver17">SSMA </a>(SQL Server Migration Assistant) handles the whole Oracle-to-SQL Server move: it reads the source data dictionary, converts the schema, then generates a SELECT per table to pull the rows across. That last step is where this story goes wrong.</p>



<h2 id="h-the-problem" class="wp-block-heading">The problem</h2>



<p class="wp-block-paragraph">Migrating ~5,600 Oracle tables to SQL Server with SSMA. Most load fine; ~100 tables fail <strong>Migrate Data</strong> with the same error:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
ERROR &#x5B;42S22] &#x5B;Oracle]&#x5B;ODBC]&#x5B;Ora]ORA-00904:
&quot;SYS_C00004_21081414:28:22$&quot;: invalid identifier
</pre></div>


<p class="wp-block-paragraph">The named column exists in no DDL anyone wrote. The <code>[Ora]</code> prefix says Oracle itself is rejecting the query: SSMA built an extraction SELECT naming a column Oracle refuses to resolve. Tellingly, <code>SELECT *</code> and <code>COUNT(*)</code> run fine against the same table, whatever this column is, Oracle is happy to ignore it, but not to be asked for it by name.</p>



<h2 id="h-where-the-column-actually-comes-from" class="wp-block-heading">Where the column actually comes from</h2>



<p class="wp-block-paragraph">The name is the giveaway: <code>SYS_C00004_21081414:28:22$</code> is what Oracle calls a column after <code>ALTER TABLE … SET UNUSED COLUMN</code>.</p>



<p class="wp-block-paragraph">Oracle offers two ways to get rid of a column: a logical delete and a physical one. The physical delete (<code>ALTER TABLE … DROP COLUMN</code>) is the honest one, but on a large table it is very time- and resource-consuming. That&#8217;s why people reach for the logical delete instead:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
ALTER TABLE table_name SET UNUSED (column_name);
</pre></div>


<p class="wp-block-paragraph">That statement is metadata-only and instant. The column immediately stops being visible to users, and the physical removal is deferred to whenever there is time for it (see <a href="https://oracle-base.com/articles/8i/dropping-columns" data-type="link" data-id="https://oracle-base.com/articles/8i/dropping-columns">Oracle Documentation</a>):</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
ALTER TABLE table_name DROP UNUSED COLUMNS;
-- on large tables, cap undo growth by checkpointing every N rows:
ALTER TABLE table_name DROP UNUSED COLUMNS CHECKPOINT 250;
</pre></div>


<p class="wp-block-paragraph">To free the original name for reuse, Oracle renames the column to <code>SYS_C&lt;internal column number&gt;_&lt;YYMMDDHH24:MI:SS&gt;$</code>, sets <code>USER_GENERATED</code> to NO, <code>HIDDEN_COLUMN</code> to YES and releases its <code>COLUMN_ID</code>. So the timestamp is not when the column was added, it is the second someone ran <code>SET UNUSED</code>. Ours says 14 August 2021, 14:28:22.</p>



<p class="wp-block-paragraph">That also explains the error pattern. The operation is one-way and the column is unreadable by design, so naming it gets you ORA-00904 «invalid identifier». <code>SELECT *</code> and <code>COUNT(*)</code> keep working because the column no longer has a <code>COLUMN_ID</code> and is simply excluded from the star. SSMA, however, lists it and builds an explicit column list Oracle then refuses.</p>



<p class="wp-block-paragraph"><strong>Not to be confused with <code>SYS_NC…$</code>.</strong> Those are a different animal: virtual columns backing a function-based index or extended statistics. </p>



<p class="wp-block-paragraph"><strong>Bottom line:</strong> returning NULL costs you nothing. This is a column its owner already decided to delete, holding data Oracle itself will no longer hand out. There is no information left to lose.</p>



<h2 id="h-what-doesn-t-work-for-the-migration" class="wp-block-heading">What doesn&#8217;t work for the migration</h2>



<ul class="wp-block-list">
<li><strong>SSMA setting </strong><code>Ignore hidden system columns = Yes</code> was not making any effect on this use case</li>



<li><strong>Dropping the column on SQL Server</strong> resolves nothing because the error is on the source SELECT, unaffected.</li>



<li><strong>Dropping it on Oracle</strong> could not be done in our scenario because the source is frozen; DDL not allowed.</li>



<li><strong>Custom select, column removed or bare <code>NULL</code></strong>: SSMA still expects the name in its mapping and fails with &#8220;key not present&#8221; or &#8220;does not match up&#8221; before the query ever reaches Oracle.</li>
</ul>



<h2 id="h-find-them-all-first" class="wp-block-heading">Find them all first</h2>



<p class="wp-block-paragraph">Before editing anything, get the full list. Discovering the affected tables one failed migration at a time is a waste of an afternoon because the data dictionary already knows.</p>



<p class="wp-block-paragraph">The reason the columns are findable at all is an asymmetry between two views: an unused column is gone from <code>ALL_TAB_COLUMNS</code>, but still listed in <code>ALL_TAB_COLS</code> with <code>HIDDEN_COLUMN = 'YES'</code>. That second view is what you query:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SELECT owner,
table_name,
column_name,
data_type,
internal_column_id,
TO_DATE(REGEXP_SUBSTR(column_name, &#039;\d{8}:\d{2}:\d{2}&#039;),
&#039;YYMMDDHH24:MI:SS&#039;) AS set_unused_at
FROM dba_tab_cols
WHERE hidden_column = &#039;YES&#039;
AND user_generated = &#039;NO&#039;
AND REGEXP_LIKE(column_name, &#039;^SYS_C\d+_\d{8}:\d{2}:\d{2}\$$&#039;)
-- AND owner = &#039;&lt;SCHEMA_NAME&gt;&#039;
ORDER BY owner, table_name, internal_column_id;
</pre></div>


<p class="wp-block-paragraph">The regex is deliberately strict: it matches only the <code>SET UNUSED</code> naming pattern, so virtual columns and other system-generated names stay out of the result. </p>



<p class="wp-block-paragraph">One more view is worth a look, as a cross-check:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SELECT owner, table_name, count AS unused_columns
FROM   dba_unused_col_tabs
--WHERE owner = &#039;&lt;SCHEMA_NAME&gt;&#039;
ORDER  BY count DESC, table_name;
</pre></div>


<p class="wp-block-paragraph"><code>DBA_UNUSED_COL_TABS</code> gives the number of unused columns per table. Sorting by that number puts the dangerous tables first: those with two or three hidden columns are the ones where you&#8217;ll forget a line in the custom select and be back at square one.</p>



<h2 id="h-the-fix" class="wp-block-heading">The fix</h2>



<p class="wp-block-paragraph">Keep the hidden column&#8217;s name as an <strong>alias</strong>, but return a literal NULL instead of reading it. SSMA&#8217;s mapping finds the name (no &#8220;key not present&#8221;); Oracle never resolves the real column (no ORA-00904).</p>



<ul class="wp-block-list">
<li><strong>Tools → Project Settings → General → Migration</strong> → enable <strong>Extended data migration options</strong>.</li>
</ul>



<ul class="wp-block-list">
<li><strong>Data Migration Settings</strong> tab → tick <strong>Use custom select</strong> → replace each hidden-column line with:</li>
</ul>



<ol class="wp-block-list"></ol>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
 SELECT ...   
    TO_CHAR(&quot;&lt;COLUMN_NAME&gt;&quot;, &#039;TM&#039;, &#039;NLS_NUMERIC_CHARACTERS = &#039;&#039;.,&#039;&#039;&#039;) as &quot;&lt;COLUMN_NAME&gt;&quot;,
    NULL as &quot;SYS_C00004_21081414:28:22$&quot;
from &lt;OWNER&gt;.&lt;TABLE_NAME&gt; t
</pre></div>


<ul class="wp-block-list">
<li><strong>Migrate Data</strong> → 100%. Drop the NULL-filled column(s) on SQL Server in post-migration cleanup.</li>
</ul>



<ol class="wp-block-list"></ol>



<h2 id="h-takeaway" class="wp-block-heading">Takeaway</h2>



<p class="wp-block-paragraph"><code>SYS_C…$</code> is not an exotic Oracle feature, it&#8217;s an ordinary column someone deleted years ago, logically. Oracle keeps the name on file; SSMA finds it, insists on naming it, and Oracle refuses to hand it over. Aliasing a NULL satisfies both, then you drop the column on the target. No source DDL, no external tooling, everything inside SSMA, behind a project setting that&#8217;s hidden by default.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/fixing-ora-00904-on-oracle-hidden-columns-during-ssma-data-migration/">Fixing ORA-00904 on Oracle hidden columns during SSMA data migration</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/fixing-ora-00904-on-oracle-hidden-columns-during-ssma-data-migration/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>When tempdb write latency points below SQL Server</title>
		<link>https://www.dbi-services.com/blog/when-tempdb-write-latency-points-below-sql-server/</link>
					<comments>https://www.dbi-services.com/blog/when-tempdb-write-latency-points-below-sql-server/#comments</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Sat, 15 Aug 2026 18:55:35 +0000</pubDate>
				<category><![CDATA[Hardware & Storage]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46474</guid>

					<description><![CDATA[<p>During a recent performance review on a SQL Server 2019 instance (AlwaysOn Failover Cluster Instance, bare-metal), one number stood out. This post follows the investigation: from a latency figure in a DMV, down the I/O path to a RAID controller setting nobody had ever chosen. The starting point: one number from a health check The [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/when-tempdb-write-latency-points-below-sql-server/">When tempdb write latency points below SQL Server</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="has-text-align-left wp-block-paragraph">During a recent performance review on a SQL Server 2019 instance (AlwaysOn Failover Cluster Instance, bare-metal), one number stood out. This post follows the investigation: from a latency figure in a DMV, down the I/O path to a RAID controller setting nobody had ever chosen.</p>



<h2 id="h-the-starting-point-one-number-from-a-health-check" class="wp-block-heading">The starting point: one number from a health check</h2>



<p class="wp-block-paragraph">The I/O statistics of the instance (sys.dm_io_virtual_file_stats) reported the following for tempdb hosted on a local volume D:</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-center" data-align="center"><strong>Files</strong></th><th class="has-text-align-center" data-align="center"><strong>Type</strong></th><th class="has-text-align-center" data-align="center"><strong>Avg write latency</strong></th><th class="has-text-align-center" data-align="center"><strong>Writes</strong></th><th class="has-text-align-center" data-align="center"><strong>Acceptable </strong>t<strong>hreshold</strong></th></tr></thead><tbody><tr><td class="has-text-align-center" data-align="center">8 data files</td><td class="has-text-align-center" data-align="center">ROWS</td><td class="has-text-align-center" data-align="center">about 380 ms</td><td class="has-text-align-center" data-align="center">About 15.8 M each</td><td class="has-text-align-center" data-align="center">20 ms</td></tr><tr><td class="has-text-align-center" data-align="center">Log file</td><td class="has-text-align-center" data-align="center">LOG</td><td class="has-text-align-center" data-align="center">90.6 ms</td><td class="has-text-align-center" data-align="center">2.2 M</td><td class="has-text-align-center" data-align="center">20 ms</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">And the global view of the volume:</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-center" data-align="center"><strong>Metric</strong></th><th class="has-text-align-center" data-align="center"><strong>Value</strong></th></tr></thead><tbody><tr><td class="has-text-align-center" data-align="center">Avg read latency</td><td class="has-text-align-center" data-align="center">1.76 ms</td></tr><tr><td class="has-text-align-center" data-align="center">Avg write latency</td><td class="has-text-align-center" data-align="center">375.91 ms</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Two details frame the whole investigation:</strong></p>



<ul class="wp-block-list">
<li>Reads are excellent. Writes are 19 times over the threshold. The read path is healthy, the write path is not.</li>



<li>io_stall_write_ms measures the time between I/O submission and completion, queue time included. A high average does not tell us whether each write is slow or whether writes are waiting behind each other.</li>
</ul>



<p class="wp-block-paragraph">When SQL Server performs a write operation, the request goes through the following (simplified) path:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img fetchpriority="high" decoding="async" width="220" height="240" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-77.png" alt="" class="wp-image-46488" style="width:317px;height:auto" /></figure>
</div>


<h2 class="wp-block-heading">What it is not</h2>



<p class="wp-block-paragraph">The 8 data files show nearly identical latencies and write counts (about 15.8 M each). The tempdb round-robin allocation works perfectly. This is not a hotspot, not a single bad file. The whole volume is affected.</p>



<p class="wp-block-paragraph">The volume D is local to each cluster node. It is not a shared disk (not on the storage array).</p>



<h2 class="wp-block-heading">Slow media or queuing?</h2>



<p class="wp-block-paragraph">The DMV cannot separate service time from queue time. So we measured the service time directly at idle with WinSAT:</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-center" data-align="center">Measure</th><th class="has-text-align-center" data-align="center">I/O profile</th><th class="has-text-align-center" data-align="center">Result</th><th class="has-text-align-center" data-align="center">IOPS</th></tr></thead><tbody><tr><td class="has-text-align-center" data-align="center">Random read</td><td class="has-text-align-center" data-align="center">16 KB</td><td class="has-text-align-center" data-align="center">451 MB/s</td><td class="has-text-align-center" data-align="center">About 28 900</td></tr><tr><td class="has-text-align-center" data-align="center">Sequential read</td><td class="has-text-align-center" data-align="center">64 KB</td><td class="has-text-align-center" data-align="center">1 965 MB/s</td><td class="has-text-align-center" data-align="center"></td></tr><tr><td class="has-text-align-center" data-align="center">Sequential write</td><td class="has-text-align-center" data-align="center">64 KB</td><td class="has-text-align-center" data-align="center">964 MB/s</td><td class="has-text-align-center" data-align="center"></td></tr><tr><td class="has-text-align-center" data-align="center">Random write</td><td class="has-text-align-center" data-align="center">8 KB (SQL Server page profile)</td><td class="has-text-align-center" data-align="center">394 MB/s</td><td class="has-text-align-center" data-align="center">About 50 500</td></tr><tr><td class="has-text-align-center" data-align="center">Read latency, maximum</td><td class="has-text-align-center" data-align="center"></td><td class="has-text-align-center" data-align="center">3.1 ms</td><td class="has-text-align-center" data-align="center"></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The media is excellent on all four access profiles. The verdict is simple: the 380 ms are queue time, not service time.</p>



<p class="wp-block-paragraph">A quick calculation confirms it. The DMV counted about 128 M writes over 104 hours of uptime: about 340 writes per second on average. The volume can absorb 50,000. Average utilization: 0.7%. A volume used at 0.7% that shows 376 ms of average latency means one thing: the load is not smooth. It arrives in bursts. During a burst, thousands of I/Os pile up in the queue, each one waits behind the others and since most of the write volume is concentrated in those bursts they dominate the average.</p>



<h2 class="wp-block-heading">Where the bursts come from</h2>



<p class="wp-block-paragraph">On this instance, the bursts are produced by sort and hash operations that do not fit in their memory grant and spill to tempdb, mainly during data loads and some heavy analytical queries. The workload side of this story (memory grants, parallelism, NUMA topology) is covered in this blog : <a href="https://www.dbi-services.com/blog/wait-stats-and-sub-numa-clustering/">https://www.dbi-services.com/blog/wait-stats-and-sub-numa-clustering/</a></p>



<p class="wp-block-paragraph">In this post, we follow the storage path only: whatever the workload does, a burst of writes should not cost 380 ms per I/O on a volume this fast.</p>



<h2 class="wp-block-heading">The invisible layer</h2>



<p class="wp-block-paragraph">When SQL Server writes a page to tempdb, the write goes through this chain:</p>



<p class="wp-block-paragraph">SQL Server &gt; Windows/NTFS &gt; driver (SmartPqi.sys) &gt; Smart Array controller &gt; physical SSDs</p>



<p class="wp-block-paragraph">Windows never talks to the SSDs. It talks to the RAID controller (an HPE Smart Array P408i-a) which assembles two SAS SSDs into a RAID 1 mirror and presents the result as volume D.</p>



<figure class="wp-block-image size-full is-resized"><img decoding="async" width="258" height="105" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-72.png" alt="" class="wp-image-46477" style="width:286px;height:auto" /></figure>



<figure class="wp-block-image size-full"><img decoding="async" width="286" height="109" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-73.png" alt="" class="wp-image-46478" /></figure>



<p class="wp-block-paragraph">Reference: <a href="https://support.hpe.com/connect/s/softwaredetails?language=en_US&amp;collectionId=MTX-3d51e7d6b8674f16&amp;tab=releaseNotes">https://support.hpe.com/connect/s/softwaredetails?language=en_US&amp;collectionId=MTX-3d51e7d6b8674f16&amp;tab=releaseNotes</a></p>



<p class="wp-block-paragraph">Here is the key point: every instrument used so far measures through that controller without seeing it. The DMVs measure above it. WinSAT measures above it. Only one question remains open: how is that card configured? And only one tool answers it: the Smart Storage Administrator CLI (ssacli).</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="394" height="109" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-74.png" alt="" class="wp-image-46479" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-74.png 394w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-74-300x83.png 300w" sizes="auto, (max-width: 394px) 100vw, 394px" /></figure>



<h2 id="h-factory-settings" class="wp-block-heading">Factory settings</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
ctrl slot=0 show detail
   
   Cache Board Present: True
   Total Cache Size: 2.0
   Cache Status: Not Configured
   Battery/Capacitor Status: OK
   No-Battery Write Cache: Disabled

ctrl slot=0 ld all show detail
   
   Logical Drive: 2          (volume D)
      Fault Tolerance: 1     (RAID 1)
      Caching: Disabled
      LD Acceleration Method: Smart Path
</pre></div>


<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-center" data-align="center">Output</th><th class="has-text-align-center" data-align="center">Value</th><th class="has-text-align-center" data-align="center">Details</th></tr></thead><tbody><tr><td class="has-text-align-center" data-align="center">Cache Board Present / Total Cache Size</td><td class="has-text-align-center" data-align="center">True / 2 GB</td><td class="has-text-align-center" data-align="center">The controller has a write cache module (1.8 GB usable)</td></tr><tr><td class="has-text-align-center" data-align="center">Battery/Capacitor Status</td><td class="has-text-align-center" data-align="center">OK</td><td class="has-text-align-center" data-align="center">Its power-loss protection is healthy</td></tr><tr><td class="has-text-align-center" data-align="center">Cache Status</td><td class="has-text-align-center" data-align="center">Not Configured</td><td class="has-text-align-center" data-align="center">The cache serves no volume: it is idle</td></tr><tr><td class="has-text-align-center" data-align="center">LD Acceleration Method (on D:)</td><td class="has-text-align-center" data-align="center">Smart Path</td><td class="has-text-align-center" data-align="center">The volume uses an I/O path that bypasses the cache</td></tr><tr><td class="has-text-align-center" data-align="center">Caching (on D:)</td><td class="has-text-align-center" data-align="center">Disabled</td><td class="has-text-align-center" data-align="center">Confirmation at the logical drive level</td></tr></tbody></table></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="780" height="552" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-75.png" alt="" class="wp-image-46481" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-75.png 780w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-75-300x212.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-75-768x544.png 768w" sizes="auto, (max-width: 780px) 100vw, 780px" /></figure>



<p class="wp-block-paragraph">HPE SSD Smart Path is a direct I/O path: requests skip the RAID firmware stack and go straight to the SSDs. It saves a few dozen microseconds per I/O which benefits reads. But it is a per-volume switch and it is mutually exclusive with the controller cache. Smart Path is enabled by default on every SSD array (factory default). It&#8217;s reasonable for a read-oriented volume but it was never revisited for a volume hosting tempdb (one of the most write-intensive profiles there is).</p>



<p class="wp-block-paragraph">The consequence: every write must be applied to both SSDs of the mirror and confirmed before it is acknowledged. There is no absorber anywhere in the chain. When a burst arrives the queue explodes.</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-center" data-align="center"></th><th class="has-text-align-center" data-align="center">Today (Smart Path)</th><th class="has-text-align-center" data-align="center">After (cache enabled)</th></tr></thead><tbody><tr><td class="has-text-align-center" data-align="center">Reads</td><td class="has-text-align-center" data-align="center">Direct path to the SSDs</td><td class="has-text-align-center" data-align="center">Classic path (+ a few dozen microseconds) + read cache</td></tr><tr><td class="has-text-align-center" data-align="center">Writes</td><td class="has-text-align-center" data-align="center">Wait for both SSDs to confirm</td><td class="has-text-align-center" data-align="center">Posted to DRAM: acknowledged in microseconds, mirror written in the background</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Is enabling the write cache safe?</h2>



<p class="wp-block-paragraph">The old advice &#8220;do not enable write caching&#8221; targets a different cache: the volatile DRAM inside the disks themselves which loses acknowledged writes on power failure. That one stays disabled (Drive Write Cache Policy: Disable).</p>



<p class="wp-block-paragraph">The controller cache is a different story. Microsoft&#8217;s requirement is stable media: an acknowledged write must survive a power failure. This controller qualifies through the flash-backed write cache mechanism:</p>



<ul class="wp-block-list">
<li>On power loss, the battery does not store any data. It powers the cache module for a few seconds just long enough for the controller to copy the DRAM content to the flash NAND chip on the module itself. Flash is non-volatile: the data survives without any power (indefinitely).</li>



<li>At reboot the controller restores that data and writes it to the SSDs of the volume before accepting any new I/O.</li>



<li>If the battery ever fails the controller detects it and automatically falls back to write-through.</li>
</ul>



<p class="wp-block-paragraph">There is a second safety belt specific to this volume: tempdb is recreated at every instance startup. Even in the worst theoretical scenario, there is no data anyone would come back for.</p>



<h2 id="h-the-possible-fix" class="wp-block-heading">The possible fix</h2>



<p class="wp-block-paragraph">Three online reversible commands:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
ssacli ctrl slot=0 array B modify ssdsmartpath=disable
ssacli ctrl slot=0 ld 2 modify caching=enable
ssacli ctrl slot=0 modify cacheratio=10/90
</pre></div>


<p class="wp-block-paragraph"><strong>Why cacheratio=10/90?</strong></p>



<p class="wp-block-paragraph">This setting splits the controller cache: 10% for reads, 90% for writes. It is not an exotic choice, it is the HPE factory default for a configured cache documented as the best ratio for most workloads.</p>



<p class="wp-block-paragraph">Reference: <a href="https://support.hpe.com/hpesc/public/docDisplay?docId=a00019059en_us&amp;page=GUID-EE28F5A4-ADF5-4E27-81AA-8377A267FFA7.html">https://support.hpe.com/hpesc/public/docDisplay?docId=a00019059en_us&amp;page=GUID-EE28F5A4-ADF5-4E27-81AA-8377A267FFA7.html</a></p>



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



<p class="wp-block-paragraph">Expected result: LD Acceleration Method: Controller Cache on the logical drive, Cache Status: OK on the controller.</p>



<p class="wp-block-paragraph">To validate we should not rely on the cumulative DMV averages they will stay polluted by history. We should measure deltas:</p>



<ul class="wp-block-list">
<li>WinSAT after the change: service time should stay excellent (nothing was broken).</li>



<li>sys.dm_io_virtual_file_stats deltas over a defined window covering the load phases.</li>



<li>PerfMon during the load window: Avg. Disk sec/Write should stay in single digits at the peak of a burst and the queue should drain between bursts.</li>
</ul>



<p class="wp-block-paragraph">One expectation to set correctly: the cache absorbs bursts but it does not add throughput. All the bytes still land on the same two SSDs.</p>



<p class="wp-block-paragraph"><strong>Note:</strong> these commands have not been implemented. They are proposals only. The change must be reviewed, validated and scheduled by the customer before any implementation.</p>



<h2 class="wp-block-heading">Local tempdb volumes on FCI nodes: a good idea</h2>



<p class="wp-block-paragraph">Placing tempdb on a local volume in a Failover Cluster Instance is supported since SQL Server 2012 and it is a good design: tempdb is recreated at startup so there is nothing to fail over. It offloads the shared storage and local SSDs deliver excellent performance for one of the hottest write profiles of the instance (our measurements above prove it).</p>



<p class="wp-block-paragraph">But this choice turns storage health into a per-node responsibility:</p>



<ul class="wp-block-list">
<li>Check the RAID controller configuration on every node. The passive node most likely carries the same factory default. After a failover the problem would silently come back.</li>



<li>Monitor Battery/Capacitor Status. A dead battery silently disables the write cache and brings the symptom back.</li>
</ul>



<p class="wp-block-paragraph">The architecture is right. It just makes your RAID controller part of your database health check.</p>



<p class="wp-block-paragraph">Thank you. <a href="https://www.linkedin.com/in/amine-haloui-76968056/">Amine Haloui</a></p>
<p>L’article <a href="https://www.dbi-services.com/blog/when-tempdb-write-latency-points-below-sql-server/">When tempdb write latency points below SQL Server</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/when-tempdb-write-latency-points-below-sql-server/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server vs MongoDB: When the cloud is your adversary (Always Encrypted vs Queryable Encryption)</title>
		<link>https://www.dbi-services.com/blog/sql-server-vs-mongodb-when-the-cloud-is-your-adversary-always-encrypted-vs-queryable-encryption/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-vs-mongodb-when-the-cloud-is-your-adversary-always-encrypted-vs-queryable-encryption/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Mon, 10 Aug 2026 13:47:55 +0000</pubDate>
				<category><![CDATA[Cloud]]></category>
		<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[MongoDB]]></category>
		<category><![CDATA[NoSQL]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[NoSQL MongoDB]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46282</guid>

					<description><![CDATA[<p>When the cloud admin is the threat: how SQL Server and MongoDB let you query encrypted data, and what each approach costs.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-vs-mongodb-when-the-cloud-is-your-adversary-always-encrypted-vs-queryable-encryption/">SQL Server vs MongoDB: When the cloud is your adversary (Always Encrypted vs Queryable Encryption)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 id="h-the-real-barrier-to-the-cloud" class="wp-block-heading">The real barrier to the cloud</h2>



<p class="wp-block-paragraph">When a company still refuses to put its sensitive data in the cloud, the reason usually isn&#8217;t cost or performance: it&#8217;s <strong>data sovereignty</strong>. In the cloud, someone else is the administrator of the machine; therefore the provider can, in theory, read the data files, see the data being used in memory, or read the backups. The threat model is no longer the external attacker but the privileged insider hosting the database.</p>



<p class="wp-block-paragraph">As covered previously on my blog <em><a href="https://www.dbi-services.com/blog/tde-tls-data-security-governance-gap-in-lower-environments/">Beyond TDE and TLS: Bridging the Data Security Governance Gap in Lower Environments</a></em>, various encryption methods can protect you. For example, TLS protects data <em>in transit</em> and TDE protects it <em>at rest</em>, but as soon as the engine runs a query, it handles plaintext in memory. So there are three states to protect: <em>at-rest</em>, <em>in-transit</em>, <em>in-use</em>. The gap this article focuses on is the last one.</p>



<p class="wp-block-paragraph">That is exactly what <a href="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-database-engine?view=sql-server-ver17">Always Encrypted</a> (SQL Server) and <a href="https://www.mongodb.com/docs/manual/core/queryable-encryption/?msockid=17bb9dfe67e76329082e8b4866e962fe">Queryable Encryption</a> (MongoDB) target. The common principle: encryption and decryption happen client-side, in the driver; the keys never reach the engine. The data stays encrypted at rest, in transit, <strong>and</strong> during processing. The DBA, the cloud operator, the hypervisor admin: all of them see only cyphertext.</p>



<p class="wp-block-paragraph">That leaves one question: if the engine sees only cyphertext, how does it answer a <code>WHERE</code> condition? Both database engines do answer it, but through different technical means.</p>



<h2 id="h-sql-server-queryability-lives-in-the-cyphertext" class="wp-block-heading">SQL Server: queryability lives in the cyphertext</h2>



<p class="wp-block-paragraph">To demonstrate all this, let&#8217;s start by creating a table with two columns, Salary and Department, encrypted deterministically on one side and randomized on the other:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
DROP TABLE IF EXISTS dbo.Employees;
CREATE TABLE dbo.Employees (
  Id         INT IDENTITY(1,1) PRIMARY KEY,
  LastName   NVARCHAR(50) COLLATE Latin1_General_BIN2 NOT NULL,
  FirstName  NVARCHAR(50) COLLATE Latin1_General_BIN2 NOT NULL,
  DeptDet    NVARCHAR(30) COLLATE Latin1_General_BIN2 NOT NULL, -- will be DETERMINISTIC
  DeptRand   NVARCHAR(30) COLLATE Latin1_General_BIN2 NOT NULL, -- will be RANDOMIZED
  SalaryDet  INT NOT NULL, -- will be DETERMINISTIC
  SalaryRand INT NOT NULL  -- will be RANDOMIZED
);
GO

INSERT INTO dbo.Employees (LastName, FirstName, DeptDet, DeptRand, SalaryDet, SalaryRand) VALUES
(&#039;Martin&#039;,  &#039;Alice&#039;, &#039;Sales&#039;, &#039;Sales&#039;, 55000, 55000),
(&#039;Dubois&#039;,  &#039;Bob&#039;,   &#039;Sales&#039;, &#039;Sales&#039;, 48000, 48000),
(&#039;Bernard&#039;, &#039;Chloe&#039;, &#039;Sales&#039;, &#039;Sales&#039;, 52000, 52000),
(&#039;Petit&#039;,   &#039;David&#039;, &#039;IT&#039;,    &#039;IT&#039;,    72000, 72000),
(&#039;Durand&#039;,  &#039;Emma&#039;,  &#039;IT&#039;,    &#039;IT&#039;,    68000, 68000);

</pre></div>


<p class="wp-block-paragraph">Five rows: three <code>Sales</code>, two <code>IT</code>. The <code>BIN2</code> collation is required by Always Encrypted (<a href="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-database-engine?view=sql-server-ver17">link to documentation</a>), and the master key lives outside the database (Key Vault, certificate store, or HSM).</p>



<p class="wp-block-paragraph">Column encryption isn&#8217;t done in T-SQL, because the engine doesn&#8217;t have the keys. It&#8217;s driven from the client (here in PowerShell), declaring for each value a deterministic column and its randomized twin:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
Import-Module SqlServer -MinimumVersion 22.0.59
$sqlConnectionString = &quot;Data Source=.\LAB2025;Initial Catalog=AEDEMO;Integrated Security=True;Encrypt=False;Trust Server Certificate=False&quot;
$smoDatabase = Get-SqlDatabase -ConnectionString $sqlConnectionString

$encryptionChanges  = @()
$encryptionChanges += New-SqlColumnEncryptionSettings -ColumnName dbo.Employees.DeptDet    -EncryptionType Deterministic -EncryptionKey &quot;CEK1&quot;
$encryptionChanges += New-SqlColumnEncryptionSettings -ColumnName dbo.Employees.DeptRand   -EncryptionType Randomized    -EncryptionKey &quot;CEK1&quot;
$encryptionChanges += New-SqlColumnEncryptionSettings -ColumnName dbo.Employees.SalaryDet  -EncryptionType Deterministic -EncryptionKey &quot;CEK1&quot;
$encryptionChanges += New-SqlColumnEncryptionSettings -ColumnName dbo.Employees.SalaryRand -EncryptionType Randomized    -EncryptionKey &quot;CEK1&quot;

Set-SqlColumnEncryption -ColumnEncryptionSettings $encryptionChanges -InputObject $smoDatabase

</pre></div>


<p class="wp-block-paragraph">In this example, I&#8217;m working on my 2025 SQL Server instance, on the AEDEMO database, using the column encryption key <code>CEK1</code> I created beforehand (itself protected by a column master key stored outside the database).</p>



<p class="wp-block-paragraph">We check that the engine sees the right type per column:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SELECT c.name, c.encryption_type_desc
FROM sys.columns c
WHERE c.object_id = OBJECT_ID(&#039;dbo.Employees&#039;);

</pre></div>


<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="350" height="251" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-32.png" alt="" class="wp-image-46293" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-32.png 350w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-32-300x215.png 300w" sizes="auto, (max-width: 350px) 100vw, 350px" /></figure>



<p class="wp-block-paragraph">The deterministic mechanism works like this: <strong>same plaintext, same cyphertext</strong> (an injective function). On an Always Encrypted-enabled connection with <em>Parameterization for Always Encrypted</em> and parameters for the predicates (never literals), equality works:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
DECLARE @d NVARCHAR(30) = &#039;Sales&#039;;
SELECT DeptDet AS Enc, COUNT(*) FROM dbo.Employees WHERE DeptDet = @d
GROUP BY DeptDet;

</pre></div>


<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="274" height="88" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-33.png" alt="" class="wp-image-46295" /></figure>



<p class="wp-block-paragraph">The driver encrypts <code>@d</code> with the same key, the server finds the encrypted values that are identical to the parameter, and the result is correct.</p>



<p class="wp-block-paragraph">You pay for it in three ways&#8230;</p>



<p class="wp-block-paragraph"><strong>First weakness: deterministic encryption can cause a data leak.</strong> You don&#8217;t need the keys to see it. Just connect <em>without</em> Always Encrypted and read the table: the encrypted columns come out as raw binary.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
SELECT FirstName, DeptDet, DeptRand FROM dbo.Employees;

</pre></div>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="171" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-35-1024x171.png" alt="" class="wp-image-46298" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-35-1024x171.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-35-300x50.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-35-768x128.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-35.png 1164w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Look at the <code>DeptDet</code> column: Alice, Bob, and Chloe share <strong>exactly the same blob</strong>, and David and Emma share another. Two distinct values across five rows. The adversary doesn&#8217;t know what <code>0x012536…</code> means, but reads the structure: two departments, one with three people, the other with two, and who goes with whom. The <code>DeptRand</code> column, on the other hand, shows five all-different blobs: nothing to read.</p>



<p class="wp-block-paragraph">It&#8217;s harmless on five rows; it isn&#8217;t on a real table. On a low-cardinality column (region, sex, status), the distribution of blobs can be compared to a known distribution, and frequency analysis often reconstructs the plaintext. <a href="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-database-engine?view=sql-server-ver17" data-type="link" data-id="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-database-engine?view=sql-server-ver17">Microsoft&#8217;s documentation</a> puts it bluntly: an unauthorized user can guess information by examining patterns, especially when the set of possible values is small.</p>



<p class="wp-block-paragraph"><strong>Second weakness: randomized encryption blocks every query.</strong> With randomized encryption, the driver adds a fresh random value to each cell before encrypting, so the same input produces a different cyphertext every time. Therefore, the leak is gone <span style="text-decoration: underline">but so is the query</span>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
DECLARE @d NVARCHAR(30) = &#039;Sales&#039;;
SELECT DeptRand AS Enc, COUNT(*) FROM dbo.Employees WHERE DeptRand = @d
GROUP BY DeptRand;  

</pre></div>


<p class="has-vivid-red-color has-text-color has-link-color wp-elements-1 wp-block-paragraph"><em>Msg 33277, Level 16, State 2, Line 6<br>Encryption scheme mismatch for columns/variables &#8216;DeptRand&#8217;, &#8216;@d&#8217;. The<br>encryption scheme for the columns/variables is (encryption_type =<br>&#8216;RANDOMIZED&#8217;, …) and the expression near line &#8216;6&#8217; expects it to be<br>DETERMINISTIC, or RANDOMIZED, a BIN2 collation for string data types,<br>and an enclave-enabled column encryption key, or PLAINTEXT.</em></p>



<p class="wp-block-paragraph">The server can no longer compare, since the same plaintext produces a different cyphertext on every row. We gained confidentiality and lost the query. It&#8217;s all or nothing.</p>



<p class="wp-block-paragraph"><strong>Third weakness: no range, even with deterministic.</strong> Byte equality says nothing about order:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
DECLARE @s INT = 55000;
SELECT FirstName FROM dbo.Employees WHERE SalaryDet &amp;lt; @s;

</pre></div>


<p class="wp-block-paragraph"><code>WHERE SalaryDet &lt; @s</code> fails even though the column is deterministic. Sorting, <code>BETWEEN</code>, <code>LIKE</code>: out of reach for Always Encrypted alone.</p>



<p class="wp-block-paragraph">The verdict is clear: bare Always Encrypted means equality, or nothing (=, IN, GROUP BY, and DISTINCT supported).</p>



<h2 id="h-mongodb-queryability-lives-in-a-protocol" class="wp-block-heading">MongoDB: queryability lives in a protocol</h2>



<p class="wp-block-paragraph">MongoDB&#8217;s Queryable Encryption makes optimal use of randomized encryption. On the server side, everything is Randomized-encrypted, as <a href="https://www.mongodb.com/docs/manual/core/queryable-encryption/?msockid=17bb9dfe67e76329082e8b4866e962fe">the documentation explains</a>: <em>the server has no knowledge of the data it processes</em>. The <strong>encrypted view</strong> (a client connected without the keys) confirms it: even the three <code>Sales</code> employees come out with all-different <code>BinData</code>.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="598" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-55-1024x598.png" alt="" class="wp-image-46327" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-55-1024x598.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-55-300x175.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-55-768x448.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-55.png 1374w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">No frequency leak, unlike SQL Server&#8217;s deterministic encryption. With the keys, the same <code>find</code> returns the plaintext:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="970" height="842" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-54.png" alt="" class="wp-image-46325" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-54.png 970w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-54-300x260.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-54-768x667.png 768w" sizes="auto, (max-width: 970px) 100vw, 970px" /></figure>



<p class="wp-block-paragraph">The technical mechanism behind this lies in the collection&#8217;s declaration. Each encrypted field carries a <code>queryType</code>, its parameters, and a distinct key (<code>keyId</code>), one Data Encryption Key per field, which is mandatory:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="979" height="762" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-56.png" alt="" class="wp-image-46328" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-56.png 979w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-56-300x234.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-56-768x598.png 768w" sizes="auto, (max-width: 979px) 100vw, 979px" /></figure>



<p class="wp-block-paragraph">And queries with equality tests, range, and even equality on a field declared as range all work:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="880" height="362" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-59.png" alt="" class="wp-image-46331" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-59.png 880w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-59-300x123.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-59-768x316.png 768w" sizes="auto, (max-width: 880px) 100vw, 880px" /></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="874" height="259" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-66.png" alt="" class="wp-image-46341" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-66.png 874w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-66-300x89.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-66-768x228.png 768w" sizes="auto, (max-width: 874px) 100vw, 874px" /></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="778" height="145" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-67.png" alt="" class="wp-image-46342" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-67.png 778w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-67-300x56.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-67-768x143.png 768w" sizes="auto, (max-width: 778px) 100vw, 778px" /></figure>



<p class="wp-block-paragraph">On the server side, MongoDB then maintains encrypted index structures, and for each query the driver generates cryptographic <em>tokens</em> that the server checks against those structures without ever seeing the plaintext. This is a <em><a href="https://www.mongodb.com/docs/manual/core/queryable-encryption/?msockid=17bb9dfe67e76329082e8b4866e962fe">searchable encryption</a></em> scheme. Range is available in <strong>GA</strong>, with no special hardware.</p>



<h2 id="h-the-real-difference-comes-down-to-one-thing-the-driver" class="wp-block-heading">The real difference comes down to one thing: the driver</h2>



<p class="wp-block-paragraph">On the SQL Server side, the driver&#8217;s work stays thin: it encrypts the parameters, rewrites the query, decrypts the results. Queryability itself is already carried by the cyphertext; the driver doesn&#8217;t have to handle it. On the MongoDB side, the driver carries the whole protocol: it generates the tokens checked against the encrypted indexes.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-68-1024x572.png" alt="" class="wp-image-46348" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-68-1024x572.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-68-300x168.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-68-768x429.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-68.png 1392w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Once the collection is in place, <code>find({ department: "Sales" })</code> is written like a normal query and the driver handles the encryption on its own. Each encrypted field needs its own key (Data Encryption Key). The master key must be pinned, otherwise orphaned keys return an <code>HMAC validation failure</code> error. And the encryption API is only available from a client created as encrypted, so not from a standard Compass connection, for example.</p>



<h2 id="h-summary" class="wp-block-heading">Summary</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th></th><th>Always Encrypted</th><th>Queryable Encryption</th></tr></thead><tbody><tr><td>Server-side encryption</td><td>deterministic <em>or</em> randomized</td><td>always randomized</td></tr><tr><td>Equality</td><td>yes (deterministic)</td><td>yes</td></tr><tr><td>Range / sort</td><td>no</td><td>yes (GA)</td></tr><tr><td>Frequency analysis attack</td><td>yes, with deterministic</td><td>no</td></tr><tr><td>Where the search happens</td><td>in the cyphertext</td><td>in the protocol</td></tr><tr><td>Driver weight</td><td>light</td><td>heavy</td></tr></tbody></table></figure>



<h2 id="h-what-if-you-wanted-range-while-staying-on-sql-server" class="wp-block-heading">What if you wanted range while staying on SQL Server?</h2>



<p class="wp-block-paragraph">This is where <strong><a href="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-enclaves?view=sql-server-ver17" data-type="link" data-id="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-enclaves?view=sql-server-ver17">secure enclaves</a></strong> come in. The engine delegates the computation to an enclave: a protected memory region where the data is decrypted and processed in the clear, out of reach, including from the machine&#8217;s administrator. This is what unlocks range, <code>LIKE</code>, sorting, and in-place encryption: inside the enclave the server no longer compares encrypted bytes, it works on the plaintext. This gain has a price. It requires compatible hardware or secure virtualization, an attestation service to deploy and maintain, and keys configured for the enclave, which noticeably increases architectural complexity compared with classic Always Encrypted.</p>



<p class="wp-block-paragraph">But it also changes the <strong>nature of the trust</strong>. Queryable Encryption rests on a cryptographic guarantee: the server <em>cannot</em> read, it&#8217;s a mathematical property. Enclaves rest on a hardware guarantee: you trust the CPU, and its attestation, to isolate the protected region.</p>



<p class="wp-block-paragraph">There remains a third path, often mentioned: homomorphic encryption (FHE), which computes directly on the cyphertext without ever decrypting it. Elegant on paper, but out of the game for database search: the computational cost is massive and response time collapses as the volume grows. So the practical choice really does play out between the two worlds described here.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-vs-mongodb-when-the-cloud-is-your-adversary-always-encrypted-vs-queryable-encryption/">SQL Server vs MongoDB: When the cloud is your adversary (Always Encrypted vs Queryable Encryption)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/sql-server-vs-mongodb-when-the-cloud-is-your-adversary-always-encrypted-vs-queryable-encryption/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Wait stats and Sub-NUMA clustering </title>
		<link>https://www.dbi-services.com/blog/wait-stats-and-sub-numa-clustering/</link>
					<comments>https://www.dbi-services.com/blog/wait-stats-and-sub-numa-clustering/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Mon, 10 Aug 2026 11:07:30 +0000</pubDate>
				<category><![CDATA[Hardware & Storage]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[NUMA]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46300</guid>

					<description><![CDATA[<p>During a healthcheck at a client&#8217;s site, we collected performance data from a SQL Server 2019 Enterprise instance hosting a BI / data warehouse workload. Two lines in the wait statistics report immediately caught the eye: hundreds of hours of parallelism waits accumulated in only 11 days of uptime.&#160; Here are the questions that arise&#160; [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/wait-stats-and-sub-numa-clustering/">Wait stats and Sub-NUMA clustering </a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">During a healthcheck at a client&#8217;s site, we collected performance data from a SQL Server 2019 Enterprise instance hosting a BI / data warehouse workload. Two lines in the wait statistics report immediately caught the eye: hundreds of hours of parallelism waits accumulated in only 11 days of uptime.&nbsp;</p>



<p class="wp-block-paragraph"><strong>Here are the questions that arise</strong>&nbsp;</p>



<ul class="wp-block-list">
<li>Can a wait type really account for more than 100% of server uptime?&nbsp;</li>



<li>Are these values a problem in themselves?&nbsp;</li>



<li>What do they tell us about the configuration of the instance?&nbsp;</li>
</ul>



<p class="wp-block-paragraph">The waits themselves were not the problem. But they pointed us to a BIOS option (Sub-NUMA Clustering) that was silently reshaping the entire NUMA topology of the server and putting it in conflict with the MAXDOP configuration.&nbsp;</p>



<p class="wp-block-paragraph">This post is the story of that investigation: from the wait statistics to the BIOS, step by step.&nbsp;</p>



<h2 id="h-unusual-wait-statistics-nbsp" class="wp-block-heading">Unusual wait statistics&nbsp;</h2>



<p class="wp-block-paragraph"><strong>The collected data</strong>&nbsp;</p>



<p class="wp-block-paragraph">The instance had been up for about 11 days (roughly 273 hours). The two top wait types were:&nbsp;</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Wait type</strong>&nbsp;</td><td><strong>% of uptime</strong>&nbsp;</td><td><strong>Total hours</strong>&nbsp;</td><td><strong>Average per wait</strong>&nbsp;</td></tr><tr><td>CXCONSUMER</td><td>278.95%&nbsp;</td><td>761.91 h&nbsp;</td><td>0 ms&nbsp;</td></tr><tr><td>CXPACKET</td><td>240.83%&nbsp;</td><td>657.78 h&nbsp;</td><td>1 ms</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">761 hours is almost 32 days. How can a server accumulate 32 days of waits in 11 days of wall-clock time?</p>



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



<p class="wp-block-paragraph" id="h-"><strong>Reading percentages above 100%</strong>&nbsp;</p>



<p class="wp-block-paragraph">Wait statistics are cumulated across all threads that are waiting at the same time. A waiting thread is in the SUSPENDED state: it does not occupy a scheduler, so the number of simultaneous waiters is not limited by the number of CPUs. It is only limited by the worker thread pool (704 workers on this instance).&nbsp;</p>



<p class="wp-block-paragraph">A simple example: 16 threads, each waiting for one hour during one hour of wall-clock time, produce 16 hours of wait time for 1 hour of uptime. That is 1600%.&nbsp;</p>



<p class="wp-block-paragraph">This gives us the only robust way to read these percentages: divide by 100 to get the average number of threads simultaneously waiting on that wait type.&nbsp;</p>



<ul class="wp-block-list">
<li>CXCONSUMER: 761.91 h / 273 h ≈ 2.79 → about 2.8 threads permanently waiting&nbsp;</li>



<li>CXPACKET: 657.78 h / 273 h ≈ 2.41 → about 2.4 threads permanently waiting&nbsp;</li>
</ul>



<p class="wp-block-paragraph">On a server that can host 704 workers, 2.5 permanent waiters is not an alarming number.</p>
</div></div>



<p class="wp-block-paragraph"><strong>CXPACKET and CXCONSUMER</strong>&nbsp;</p>



<p class="wp-block-paragraph">Both wait types belong to parallel query execution. A parallel plan splits its work into branches and the branches synchronize at exchange operators.&nbsp;</p>



<ul class="wp-block-list">
<li><strong>CXCONSUMER</strong>: the consumer side of an exchange is simply waiting for the producers to deliver rows. This is the structural, unavoidable wait of any parallel plan. It is considered benign.&nbsp;</li>
</ul>



<ul class="wp-block-list">
<li><strong>CXPACKET</strong>: a thread has finished its share of the work and waits for slower branches at the synchronization point. This is the potentially actionable signal: it reveals an imbalance between the branches of the plan.&nbsp;</li>
</ul>



<p class="wp-block-paragraph">An analogy: in a factory, CXCONSUMER is a workstation waiting for parts to arrive (normal), CXPACKET is a workstation that has finished its batch and waits for a slower colleague (an imbalance worth examining).&nbsp;</p>



<p class="wp-block-paragraph"><strong>Are these values abnormal, then?</strong>&nbsp;</p>



<p class="wp-block-paragraph">Look at the averages again: 0.00 ms and 1.00 ms per wait. 657.78 hours at 1 ms average means roughly 2.4 billion individual waits. These counters do not describe long blockings, they describe the normal tick-tock of exchange operators in a heavily parallel workload. A server suffering from severe skew would show averages of tens of milliseconds.&nbsp;</p>



<p class="wp-block-paragraph">The healthcheck tool itself says it in its own message: &#8220;Usually a cost threshold for parallelism / MAXDOP tuning issue rather than a problem in itself.&#8221; The line is an inventory entry (any wait above 10% of uptime gets reported), not an alarm.&nbsp;</p>



<p class="wp-block-paragraph">So why did these two lines matter? Not because of their height because of their rank. CXCONSUMER and CXPACKET were number 1 and number 2 of the entire wait inventory, ahead of everything else. Their rank designated parallelism as the dominant workload of this instance and therefore its configuration as the first thing to confront with the topology.&nbsp;</p>



<p class="wp-block-paragraph"><strong>The instance configuration</strong>&nbsp;</p>



<pre class="wp-block-code"><code>SELECT &#091;name], value_in_use 
FROM sys.configurations 
WHERE &#091;name] IN (N'max degree of parallelism', N'cost threshold for parallelism'); </code></pre>



<ul class="wp-block-list">
<li><strong>Cost threshold for parallelism: 50</strong>, correctly raised from the default of 5. This setting decides&nbsp;<em>which</em>&nbsp;queries are allowed to go parallel (those whose estimated cost exceeds the threshold).&nbsp;</li>



<li><strong>MAXDOP: 8</strong>, this setting decides how wide: the maximum number of worker threads per parallel branch.&nbsp;</li>
</ul>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="272" height="80" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-36.png" alt="" class="wp-image-46302" /></figure>



<p class="wp-block-paragraph">MAXDOP 8 is a perfectly reasonable value in isolation. The problem only appears when we look at the topology.&nbsp;</p>



<p class="wp-block-paragraph"><strong>The topology</strong>&nbsp;</p>



<pre class="wp-block-code"><code>SELECT cpu_count, hyperthread_ratio, socket_count, cores_per_socket, 
numa_node_count, softnuma_configuration_desc 
FROM sys.dm_os_sys_info; </code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="595" height="37" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-39.png" alt="" class="wp-image-46303" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-39.png 595w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-39-300x19.png 300w" sizes="auto, (max-width: 595px) 100vw, 595px" /></figure>



<p class="wp-block-paragraph"><strong>Two sockets. Four NUMA nodes. That is the anomaly this whole post is about.</strong>&nbsp;</p>



<h2 id="h-from-the-anomaly-to-the-root-cause-nbsp" class="wp-block-heading">From the anomaly to the root cause&nbsp;</h2>



<p class="wp-block-paragraph">On modern processors, there is no central memory controller shared by all cores. Each processor package has its own memory controllers with its own RAM modules attached to them. Such a group (cores + memory controllers + local RAM) is a NUMA node.&nbsp;</p>



<p class="wp-block-paragraph">When a core reads an address that lives in its own node&#8217;s RAM: direct path, fast. When it reads an address that lives in another node&#8217;s RAM: the request crosses the interconnect, with roughly 1.5 to 2 times the latency. That is the meaning of the name: Non-Uniform Memory Access. The cost of a memory access depends on the physical distance between the core that asks and the RAM module that answers.&nbsp;</p>



<p class="wp-block-paragraph">The natural NUMA boundary is therefore the socket: one socket = its memory controllers = its local RAM = one NUMA node.&nbsp;</p>



<p class="wp-block-paragraph">Here we have 4 nodes for 2 sockets: 2 nodes per socket. Only three mechanisms can produce that:&nbsp;</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Suspect</strong></td><td><strong>Signature</strong></td><td><strong>Conclusion</strong></td></tr><tr><td>Soft-NUMA (SQL Server subdivides by itself)&nbsp;</td><td>softnuma_configuration_desc &lt;&gt; OFF and soft-NUMA only splits schedulers, the memory nodes stay at the hardware count&nbsp;</td><td>OFF</td></tr><tr><td>Multi-die silicon (e.g. AMD EPYC Naples: 4 dies per socket)&nbsp;</td><td>CPU identity&nbsp;</td><td>Intel Xeon Gold 6244 = monolithic die&nbsp;</td></tr><tr><td>A BIOS option that subdivides the socket&nbsp;</td><td>On this Intel generation: Sub-NUMA Clustering&nbsp;</td><td>The only suspect left&nbsp;</td></tr></tbody></table></figure>
</div></div>



<p class="wp-block-paragraph"><strong>Cross-checking with the hardware</strong>&nbsp;</p>



<p class="wp-block-paragraph">The server is a physical HPE ProLiant DL380 Gen10 with 2 × Intel Xeon Gold 6244 (8 cores / 16 threads each, 3.60 GHz base) and 384 GB of RAM (12 × 32 GB, balanced across the memory channels).&nbsp;</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="675" height="455" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-70.png" alt="" class="wp-image-46359" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-70.png 675w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-70-300x202.png 300w" sizes="auto, (max-width: 675px) 100vw, 675px" /></figure>



<p class="wp-block-paragraph">Windows server shows &#8220;8 Core(s), 8 Logical Processor(s)&#8221; per socket, and Task Manager shows Cores: 16 = Logical processors: 16. Hyper-threading is disabled: 16 physical cores, one logical processor per core. This matters for the rest of the post, because Microsoft&#8217;s MAXDOP recommendations are expressed in logical processors per node.&nbsp;</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="698" height="677" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-43.png" alt="" class="wp-image-46312" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-43.png 698w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-43-300x291.png 300w" sizes="auto, (max-width: 698px) 100vw, 698px" /></figure>



<p class="wp-block-paragraph">A corroborating detail: Task Manager reports L3 cache = 99.0 MB. The Gold 6244 has 24.75 MB of L3 per socket, so the server has 49.5 MB but 99.0 = 4 × 24.75. Windows counts the socket&#8217;s L3 once per NUMA node it is presented with. The only wrong line in the cache arithmetic is exactly the one that depends on NUMA counting.&nbsp;</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="698" height="163" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-44.png" alt="" class="wp-image-46314" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-44.png 698w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-44-300x70.png 300w" sizes="auto, (max-width: 698px) 100vw, 698px" /></figure>



<h2 id="h-what-sub-numa-clustering-is-nbsp" class="wp-block-heading"><strong>What Sub-NUMA Clustering is</strong>&nbsp;</h2>



<p class="wp-block-paragraph">Our socket contains 8 cores, 6 memory channels and a shared L3 cache connected by an internal mesh. Sub-NUMA Clustering (SNC) is a BIOS option that cuts this socket into two domains, each with 4 cores, 3 memory channels and its half of the L3 affinity and presents each half as a full NUMA node to the operating system.&nbsp;</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="414" height="318" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-46.png" alt="" class="wp-image-46315" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-46.png 414w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-46-300x230.png 300w" sizes="auto, (max-width: 414px) 100vw, 414px" /></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="921" height="506" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-48.png" alt="" class="wp-image-46318" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-48.png 921w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-48-300x165.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-48-768x422.png 768w" sizes="auto, (max-width: 921px) 100vw, 921px" /></figure>



<p class="wp-block-paragraph">Two sockets × SNC = 4 NUMA nodes of 4 logical processors and about 96 GB of RAM each. Exactly what our DMVs show.&nbsp;</p>



<p class="wp-block-paragraph">Why does this option exist? Because for some workloads it helps. A public SPEC CPU2017 result published by Dell on the equivalent platform (PowerEdge R640, same 2 × Xeon Gold 6244, same 384 GB in 12 × 32 GB) is instructive on this point the BIOS notes literally list &#8220;Sub NUMA Cluster enabled&#8221; and the published numactl output shows the resulting topology:&nbsp;</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="638" height="407" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-51.png" alt="" class="wp-image-46319" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-51.png 638w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-51-300x191.png 300w" sizes="auto, (max-width: 638px) 100vw, 638px" /></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="622" height="102" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-52.png" alt="" class="wp-image-46322" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-52.png 622w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-52-300x49.png 300w" sizes="auto, (max-width: 622px) 100vw, 622px" /></figure>



<p class="wp-block-paragraph">Reference :&nbsp;<a href="https://www.spec.org/cpu2017/results/res2019q2/cpu2017-20190429-12798.html" target="_blank" rel="noreferrer noopener">https://www.spec.org/cpu2017/results/res2019q2/cpu2017-20190429-12798.html</a></p>



<p class="wp-block-paragraph"><strong>What SNC makes SQLOS build</strong>&nbsp;</p>



<p class="wp-block-paragraph">At startup, SQLOS mirrors the presented topology: one memory node and one group of schedulers per NUMA node. On this server: 4 groups of 4 schedulers and the decisive point the buffer pool is partitioned across the 4 nodes. With max server memory at 210 GB, each node manages roughly 52 GB, and a cached data page physically lives in the RAM of one node.&nbsp;</p>



<h2 id="h-the-mechanics-then-the-corrections-nbsp" class="wp-block-heading">The mechanics then the corrections&nbsp;</h2>



<p class="wp-block-paragraph">Now we can close the loop with Part 1:&nbsp;</p>



<ul class="wp-block-list">
<li>SNC (BIOS) cuts 2 sockets into 4 NUMA nodes of 4 logical processors.&nbsp;</li>
</ul>



<ul class="wp-block-list">
<li>MAXDOP is 8 and a node only offers 4 schedulers: every parallel query spans two nodes by construction at every execution.&nbsp;</li>
</ul>



<ul class="wp-block-list">
<li>The workers on the remote node access non-local memory. This cost is mostly invisible in the wait statistics a thread reading remote memory is RUNNING, not waiting. The direct cost hides in queries that simply run slower.&nbsp;</li>
</ul>



<ul class="wp-block-list">
<li>Only the indirect effect surfaces: asymmetric memory distances desynchronize the branches, the fast workers finish their packets and wait for the slow ones at the exchanges and that spills into CXPACKET.&nbsp;</li>
</ul>



<ul class="wp-block-list">
<li>The placement follows the load of the moment, so the same query can have different costs from one execution to the next. SNC does not only add cost it adds variance.&nbsp;</li>
</ul>



<p class="wp-block-paragraph"><strong>Why MAXDOP 4 alone is not the fix</strong>&nbsp;</p>



<p class="wp-block-paragraph">Capping MAXDOP at 4 confines each query&#8217;s workers to a single node. That offers two things: the working memory (memory grants, hash tables, sort runs, exchange buffers) becomes local and all branches advance at the same speed and nobody waits for a remote colleague anymore.&nbsp;</p>



<p class="wp-block-paragraph">But it does not buy data locality. A buffer pool page is allocated on the node of the worker that read it from disk&nbsp; potentially days ago for another query on another node and it never migrates afterwards. With 4 nodes, a query confined to node 2 finds on average only about 25% of the already-cached pages locally. And MAXDOP 4 also halves the width of every query on a data warehouse that lives on parallelism. MAXDOP 4 is the bandage. The correction is the topology itself.&nbsp;</p>



<h2 id="h-microsoft-s-recommendations-nbsp" class="wp-block-heading"><strong>Microsoft&#8217;s recommendations</strong>&nbsp;</h2>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Configuration</strong>&nbsp;</td><td><strong>MAXDOP recommendation</strong>&nbsp;</td></tr><tr><td>Single NUMA node, ≤ 8 logical processors&nbsp;</td><td>≤ number of logical processors&nbsp;</td></tr><tr><td>Single NUMA node, &gt; 8 logical processors&nbsp;</td><td>8&nbsp;</td></tr><tr><td>Multiple NUMA nodes, ≤ 16 logical processors per node&nbsp;</td><td>≤ number of logical processors per node&nbsp;</td></tr><tr><td>Multiple NUMA nodes, &gt; 16 logical processors per node&nbsp;</td><td>Half the logical processors per node, max 16&nbsp;</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Our server sits on the third line in every scenario:&nbsp;</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Scenario</strong>&nbsp;</td><td><strong>Topology</strong>&nbsp;</td><td><strong>MAXDOP recommendation</strong>&nbsp;</td></tr><tr><td>Today (SNC enabled)&nbsp;</td><td>4 nodes × 4 logical processors&nbsp;</td><td>≤ 4 (currently 8: non-compliant)&nbsp;</td></tr><tr><td>SNC disabled&nbsp;</td><td>2 nodes × 8 logical processors&nbsp;</td><td>≤ 8 → the current setting becomes compliant&nbsp;</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Current (SNC enabled) :&nbsp;</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="235" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-58-1024x235.png" alt="" class="wp-image-46329" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-58-1024x235.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-58-300x69.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-58-768x177.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-58.png 1153w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">After (with SNC disabled) :&nbsp;</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="277" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-61-1024x277.png" alt="" class="wp-image-46333" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-61-1024x277.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-61-300x81.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-61-768x207.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-61.png 1192w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><strong>The possible two corrections:</strong>&nbsp;</p>



<p class="wp-block-paragraph">Short term (online, reversible, no restart): MAXDOP = 4. It confines each query to one node as the server is presented today. It is a transitional measure not the target state.&nbsp;</p>



<pre class="wp-block-code"><code>EXEC sp_configure 'max degree of parallelism', 4;&nbsp;
RECONFIGURE;</code></pre>



<p class="wp-block-paragraph">Root-cause correction (through a maintenance window): disable SNC in the BIOS. On an HPE Gen10, the option lives in:&nbsp;</p>



<p class="wp-block-paragraph">System Configuration &gt; BIOS/Platform Configuration (RBSU) &gt; Power and Performance Options &gt; Sub-NUMA Clustering &gt; Disabled&nbsp;</p>



<p class="wp-block-paragraph">It looks like that:&nbsp;</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="484" height="453" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-65.png" alt="" class="wp-image-46337" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-65.png 484w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-65-300x281.png 300w" sizes="auto, (max-width: 484px) 100vw, 484px" /></figure>



<p class="wp-block-paragraph">Reference : <a href="https://lenovopress.lenovo.com/lp1499.pdf">https://lenovopress.lenovo.com/lp1499.pdf</a></p>



<h2 id="h-conclusion" class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">The wait statistics never proved anything in this story and that is the point. Values above 100% of uptime are normal (they cumulate across all simultaneous waiters), the averages were small and CXCONSUMER is benign by nature. What the two lines provided was a characterization: normalized by uptime, they showed about 2.5 threads permanently synchronizing exchanges and this instance lives on parallelism. Their rank designated parallelism as the dominant workload and therefore its configuration as the first thing to confront with the topology.&nbsp;</p>



<p class="wp-block-paragraph">Thank you. <a href="https://www.linkedin.com/in/amine-haloui-76968056/">Amine Haloui</a></p>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/wait-stats-and-sub-numa-clustering/">Wait stats and Sub-NUMA clustering </a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/wait-stats-and-sub-numa-clustering/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Green SQL Server DBA Tips #3 – The hidden cost of forgotten databases</title>
		<link>https://www.dbi-services.com/blog/green-sql-server-dba-tips-3-the-hidden-cost-of-forgotten-databases/</link>
					<comments>https://www.dbi-services.com/blog/green-sql-server-dba-tips-3-the-hidden-cost-of-forgotten-databases/#respond</comments>
		
		<dc:creator><![CDATA[Stéphane Haby]]></dc:creator>
		<pubDate>Thu, 06 Aug 2026 13:10:24 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Microsoft]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46270</guid>

					<description><![CDATA[<p>How many times have I found, during a migration, that I also have to clean up the databases no more used?Why keep them for so long if we’re not going to use them? This is something we often see among our customers. If we assume that we carry out a migration project roughly every five [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/green-sql-server-dba-tips-3-the-hidden-cost-of-forgotten-databases/">Green SQL Server DBA Tips #3 – The hidden cost of forgotten databases</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">How many times have I found, during a migration, that I also have to clean up the databases no more used?<br>Why keep them for so long if we’re not going to use them?</p>



<p class="wp-block-paragraph">This is something we often see among our customers. If we assume that we carry out a migration project roughly every five years, this means we’re keeping databases that are taking up space for no reason.</p>



<p class="wp-block-paragraph">We, however, are Green SQL Server DBA; we’ll look to manage it correctly.</p>



<h2 id="h-the-audit" class="wp-block-heading">The AUDIT</h2>



<p class="wp-block-paragraph">First of all, what do we need to analyse?</p>



<p class="wp-block-paragraph">This does not apply to all databases, but only to those that are obsolete.</p>



<p class="wp-block-paragraph">They can be categorised into two types:</p>



<ul class="wp-block-list">
<li><strong>Databases Never Accessed</strong>
<ul class="wp-block-list">
<li>No application connections detected</li>



<li>No recent transactions or queries after a period (3 months for exemple)</li>
</ul>
</li>



<li><strong>Abandoned Databases</strong>
<ul class="wp-block-list">
<li>Linked to cancelled projects</li>



<li>No activity and nobody tell it</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">To see databases never accessed and abandoned databases I use the DMV sys.dm_db_index_usage_stats to see if I have an access to the database (read or write):</p>



<pre class="wp-block-code"><code>SELECT d.name AS DatabaseName, MAX(ius.last_user_seek) AS LastSeek,MAX(ius.last_user_scan) AS LastScan,
    MAX(ius.last_user_lookup) AS LastLookup,

    (
        SELECT MAX(v.ActivityDate)
        FROM (VALUES
                (MAX(ius.last_user_seek)),
                (MAX(ius.last_user_scan)),
                (MAX(ius.last_user_lookup))
             ) v(ActivityDate)
    ) AS LastReadActivity,

    MAX(ius.last_user_update) AS LastWriteActivity,

    DATEDIFF(DAY,
        (
            SELECT MAX(v.ActivityDate)
            FROM (VALUES
                    (MAX(ius.last_user_seek)),
                    (MAX(ius.last_user_scan)),
                    (MAX(ius.last_user_lookup))
                 ) v(ActivityDate)
        ),
        GETDATE()
    ) AS DaysSinceLastRead,

    DATEDIFF(DAY,
        MAX(ius.last_user_update),
        GETDATE()
    ) AS DaysSinceLastWrite

FROM sys.databases d
LEFT JOIN sys.dm_db_index_usage_stats ius
    ON d.database_id = ius.database_id
WHERE d.database_id &gt; 4  -- Exclude system databases
GROUP BY d.name
ORDER BY LastWriteActivity ASC;
</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="591" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/blog_green_dba_3_01-1024x591.png" alt="" class="wp-image-46272" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/blog_green_dba_3_01-1024x591.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/blog_green_dba_3_01-300x173.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/blog_green_dba_3_01-768x443.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/blog_green_dba_3_01.png 1201w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">We can see on the first databases in the list, that we have null everywhere&#8230; This means that the databases were not solicited since the last restart&#8230; Good candidate for unused databases!</p>



<p class="wp-block-paragraph">This brings us to the next point.</p>



<h2 id="h-the-analisys" class="wp-block-heading">The ANALISYS</h2>



<p class="wp-block-paragraph">If we can see that there is little activity on the database, we can, of course, carry out an initial analysis using criteria such as these:</p>



<p class="wp-block-paragraph">&lt; 30 days &#8211;&gt; the database is still active</p>



<p class="wp-block-paragraph">30–180 days &#8211;&gt; the database is on hold pending to be dropped</p>



<p class="wp-block-paragraph">&gt; 180 days &#8211;&gt; the database is a strong candidate to be dropped</p>



<p class="wp-block-paragraph">Of course, it also depends on your environment – whether it’s dev, test or prod – and the criteria will vary accordingly.</p>



<h2 id="h-the-impact" class="wp-block-heading">The IMPACT</h2>



<p class="wp-block-paragraph">Like in my precedent post, we will have same impacts but not for queries of courses!</p>



<p class="wp-block-paragraph"><strong>The first impact is the storage and the backup</strong></p>



<p class="wp-block-paragraph">If the database represents ~100 GB on the disk , it’s also 100 GB backed up unnecessarily.</p>



<p class="wp-block-paragraph">If you have Dev, Test, PreProd &amp; Prod and all in HA… I let you do the calculation but it’s more 1 TB!</p>



<p class="wp-block-paragraph"><strong>The third impact is on the maintenance plan</strong></p>



<p class="wp-block-paragraph">The following operations must also handle the unused database:</p>



<p class="wp-block-paragraph">– CheckDB<br>– Backups<br><br>As the database is not used, the index rebuild/reorg &amp; update Stats will be fast!</p>



<p class="wp-block-paragraph">Each unused database extends the maintenance windows for the checkdb and backups…</p>



<h2 id="h-the-advise" class="wp-block-heading">The ADVISE</h2>



<p class="wp-block-paragraph">The first advice would be to plan well in advance.</p>



<p class="wp-block-paragraph">Indeed, when creating or restoring a database, it is important to know its lifecycle.</p>



<p class="wp-block-paragraph">Don’t hesitate to challenge the application owners on a deadline and make sure to add it to your calendar +1day.</p>



<p class="wp-block-paragraph">The second advice is to set up a monthly monitoring process prior to Windows or SQL Server patch cycles so that you can identify these unnecessary databases.</p>



<p class="wp-block-paragraph">And before dropping a database, I advice you to put the database offline and see if after some days somebody complaints&#8230;</p>



<p class="wp-block-paragraph">In the majority of cases, you will not have reactions, believe me! <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<h2 id="h-the-green-sql-server-dba-score" class="wp-block-heading">The Green SQL Server DBA Score</h2>



<p class="wp-block-paragraph">Like for my precedent post, I setup a ‘SQL Server DBA Score’ to use for my tips on the subject:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td><strong><strong>Database unused per instance per 3 months</strong></strong></td><td><strong>Score</strong></td></tr><tr><td>0 to 2</td><td>10</td></tr><tr><td>3 to 5</td><td>8</td></tr><tr><td>6 to 10</td><td>5</td></tr><tr><td>11 to 20</td><td>2</td></tr><tr><td>&gt; 20</td><td>0</td></tr></tbody></table></figure>



<h2 id="h-conclusion" class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">A traditional SQL Server DBA or system administrator will let the unused databases until the next migration.</p>



<p class="wp-block-paragraph">A Green SQL Server DBA will check it periodically and manage it.</p>



<p class="wp-block-paragraph">An unused database is like a car that never leaves the garage.<br> It still costs money, requires maintenance, and consumes resources.<br>The Green SQL Server DBA notices it before it becomes waste&#8230;</p>



<p class="wp-block-paragraph"><br>Think about it and begin now to see to monitor your unused databases!</p>



<p class="wp-block-paragraph">See you soon for the next one!&nbsp;</p>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/green-sql-server-dba-tips-3-the-hidden-cost-of-forgotten-databases/">Green SQL Server DBA Tips #3 – The hidden cost of forgotten databases</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/green-sql-server-dba-tips-3-the-hidden-cost-of-forgotten-databases/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server:  msdb is in Suspect State</title>
		<link>https://www.dbi-services.com/blog/sql-server-msdb-is-in-suspect-state/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-msdb-is-in-suspect-state/#respond</comments>
		
		<dc:creator><![CDATA[Stéphane Haby]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 10:00:17 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Microsoft]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46190</guid>

					<description><![CDATA[<p>This Monday morning, we begin our SLA Support with a customer call.He cannot do a select on a table in his database&#8230; After creating the Ticket, I connect to the instance and see that the msdb system database is in a Suspect State: After taking a deep breath because I don’t see that every day, [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-msdb-is-in-suspect-state/">SQL Server:  msdb is in Suspect State</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">This Monday morning, we begin our <a href="https://www.dbi-services.com/services/sla/">SLA Support</a> with a customer call.<br>He cannot do a select on a table in his database&#8230;</p>



<p class="wp-block-paragraph">After creating the Ticket, I connect to the instance and see that the msdb system database is in a Suspect State:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="415" height="389" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect01.png" alt="" class="wp-image-46191" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect01.png 415w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect01-300x281.png 300w" sizes="auto, (max-width: 415px) 100vw, 415px" /></figure>



<p class="wp-block-paragraph">After taking a deep breath because I don’t see that every day, I begin my investigation by asking the state with the system view sys.databases first:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="623" height="531" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect02.png" alt="" class="wp-image-46192" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect02.png 623w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect02-300x256.png 300w" sizes="auto, (max-width: 623px) 100vw, 623px" /></figure>



<p class="wp-block-paragraph">msdb is really in a suspect mode&#8230;</p>



<p class="wp-block-paragraph">Now, to begin the analysis, I go to read the error log.:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="646" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect03-1024x646.png" alt="" class="wp-image-46193" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect03-1024x646.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect03-300x189.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect03-768x484.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect03.png 1169w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">The Error Log give me the error that I comments in SSMS query to read it better:</p>



<p class="wp-block-paragraph">Error: 3314, Severity: 21, State: 1.<br>During undoing of a logged operation in database &#8216;msdb&#8217;, an error occurred at log record ID (821931:128397:5).<br>Typically, the specific failure is logged previously as an error in the Windows Event Log service. Restore the database or file from a backup, or repair the database.</p>



<p class="wp-block-paragraph">If you have this error message, the best is to restore the msdb;<br>As the information of the backups is in the msdb, I cannot do a right-click to restore the database&#8230;  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2639.png" alt="☹" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">I looked for the latest backup in the error log and find it:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="453" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect04-1024x453.png" alt="" class="wp-image-46194" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect04-1024x453.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect04-300x133.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect04-768x340.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect04.png 1223w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">But I also sse that the checkdb before each backup give 12 errors&#8230;</p>



<p class="wp-block-paragraph">My question is at this point: Wasn’t there an alert saying that the database was corrupted?<br><br>As the users are blocked, I need to react quickly.<br>This is why, I do my restore and a CHECKDB with  REPAIR_ALLOW_DATA_LOSS:</p>



<pre class="wp-block-code"><code>RESTORE DATABASE msdb FROM DISK = 'xxx\msdb\msdb_fullbackup_xxxx20260801xxxx.BAK' WITH REPLACE;
GO
 ALTER DATABASE msdb SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
GO
DBCC CHECKDB ('msdb', REPAIR_ALLOW_DATA_LOSS) WITH NO_INFOMSGS, ALL_ERRORMSGS;
GO
ALTER DATABASE msdb SET MULTI_USER;
</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="562" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect05-1024x562.png" alt="" class="wp-image-46195" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect05-1024x562.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect05-300x165.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect05-768x422.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect05.png 1067w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">The commands completed successfully! YES!</p>



<p class="wp-block-paragraph">The database msdb is online and working fine. SUPER!</p>



<p class="wp-block-paragraph">To be sure, I go in the errorlog to see the restore and the checkdb with REPAIR_ALLOW_DATA_LOSS:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="570" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect06-1024x570.png" alt="" class="wp-image-46196" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect06-1024x570.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect06-300x167.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect06-768x428.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/Blog_msdb_suspect06.png 1508w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">The restore was successful and in the checkDB with can see 12 errors and repaired 12 errors.</p>



<p class="wp-block-paragraph">If I will investigate deeper, I will go the dump file and analyse it.</p>



<p class="wp-block-paragraph">I finally managed to sort out and resolve the critical alert on monday morning.<br>Users can connect and read their tables again&#8230;<br>The customer knows why he comes to us for an SLA&#8230; <br></p>



<p class="wp-block-paragraph">A good victory for a monday morning in our <a href="https://www.dbi-services.com/services/sla/">support SLA dbi-services</a>!<br>See you soon for sharing again customer cases! <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-msdb-is-in-suspect-state/">SQL Server:  msdb is in Suspect State</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/sql-server-msdb-is-in-suspect-state/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Green SQL Server DBA Tips #2 – When logs and history records cause your database to growth and crash</title>
		<link>https://www.dbi-services.com/blog/green-sql-server-dba-tips-2-when-logs-and-history-records-cause-your-database-to-growth-and-crash/</link>
					<comments>https://www.dbi-services.com/blog/green-sql-server-dba-tips-2-when-logs-and-history-records-cause-your-database-to-growth-and-crash/#respond</comments>
		
		<dc:creator><![CDATA[Stéphane Haby]]></dc:creator>
		<pubDate>Thu, 30 Jul 2026 06:30:38 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[Development & Performance]]></category>
		<category><![CDATA[Enterprise content management]]></category>
		<category><![CDATA[Non classifié(e)]]></category>
		<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46040</guid>

					<description><![CDATA[<p>How often do I come across this sort of problem when visiting clients? Even more with our customers’ service desk (support) SLAs. We turn up on a Monday morning and there are alerts from the weekend saying [MSSQL] &#8211; Instance Dark_Vador-Data(F:): Disk space in GB on server Death_Star is critical Info: NOT OK &#8211; Free [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/green-sql-server-dba-tips-2-when-logs-and-history-records-cause-your-database-to-growth-and-crash/">Green SQL Server DBA Tips #2 – When logs and history records cause your database to growth and crash</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">How often do I come across this sort of problem when visiting clients? Even more with our customers’ service desk (support) SLAs.</p>



<p class="wp-block-paragraph">We turn up on a Monday morning and there are alerts from the weekend saying</p>



<p class="wp-block-paragraph"><em>[MSSQL] &#8211; Instance Dark_Vador-Data(F:): Disk space in GB on server Death_Star is critical</em></p>



<p class="wp-block-paragraph"><em>Info: NOT OK &#8211; Free space: 20 MB out of 200 GB</em></p>



<p class="wp-block-paragraph">The storage is almost full and:<br> &#8211; no projects were deployed over the weekend<br> &#8211; no exceptional data loads were scheduled<br> &#8211; nobody works at the weekend</p>



<p class="wp-block-paragraph">A strange phenomenon to come across on a Monday morning&#8230;.</p>



<p class="wp-block-paragraph">On closer inspection, the findings are surprising; we can see that a database within the instance has been undergoing regular autogrowth for weeks&#8230;</p>



<p class="wp-block-paragraph">Every day, there are several regular autogrowth on the data file for this database until the saturation of the disk.</p>



<p class="wp-block-paragraph">Who is the culprit?</p>



<p class="wp-block-paragraph">Not the business data, but one or more tables named xxxxLog or Historyxxx, or the combination of both HistoryLog!</p>



<p class="wp-block-paragraph">PS: It’s an example but in the reality before the weekend, we will have Warnings and in the majority of the cases we react before the critical alert of course!</p>



<p class="wp-block-paragraph">Data created to diagnose a problem, track an activity or maintain a history, which is then never cleaned up or has a default retention period that is too long and unsuitable for the context, is not always easy to detect.</p>



<p class="wp-block-paragraph">On the other hand, it’s always easier to expand disk capacity without too much effort, and everything runs smoothly in an ideal world…</p>



<p class="wp-block-paragraph">We, however, are Green SQL Server DBA; we’ll look deeper.<br>Our curiosity will lead us to challenge the business on these points.</p>



<h2 id="h-the-audit" class="wp-block-heading">The AUDIT</h2>



<p class="wp-block-paragraph">First, you need to know the autogrowth settings for these databases and how the data file growth. <br>Here is a simple script that shows the settings:</p>



<pre class="wp-block-code"><code>SELECT
    DB_NAME(database_id) AS DatabaseName,
    name AS LogicalFileName,
    type_desc AS FileType,
    physical_name AS PhysicalFileName,
    size / 128.0 AS CurrentSizeMB,
    CASE
        WHEN is_percent_growth = 1
            THEN CAST(growth AS VARCHAR(10)) + ' %'
        ELSE CAST(growth / 128 AS VARCHAR(10)) + ' MB'
    END AS AutoGrowth,
    max_size,
    CASE
        WHEN max_size = -1 THEN 'Unlimited'
        ELSE CAST(max_size / 128 AS VARCHAR(20)) + ' MB'
    END AS MaxSize
FROM sys.master_files where type_desc='ROWS' AND database_id &gt; 4
ORDER BY
    DatabaseName,
    FileType,
    LogicalFileName;
</code></pre>



<p class="wp-block-paragraph">Like for my precedent blog “<strong><a href="https://www.dbi-services.com/blog/green-sql-server-dba-tips-1-are-unnecessary-indexes-cluttering-up-your-database/">Green SQL Server DBA Tips  #1 – Are unnecessary indexes cluttering up your database?</a></strong>”, I use the Dynamics database as example:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="892" height="556" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/autogrowth01.png" alt="" class="wp-image-46041" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/autogrowth01.png 892w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/autogrowth01-300x187.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/autogrowth01-768x479.png 768w" sizes="auto, (max-width: 892px) 100vw, 892px" /></figure>



<p class="wp-block-paragraph">In my script, I exclude all system databases and the Log File to be concentrated to the Data File.</p>



<p class="wp-block-paragraph">Now, I need to have the data file growth. This is through the default trace file and the event 92:</p>



<pre class="wp-block-code"><code>DECLARE @TraceFile NVARCHAR(500);

SELECT @TraceFile = path
FROM sys.traces
WHERE is_default = 1;

IF @TraceFile IS NULL
BEGIN
    RAISERROR('Default Trace is disabled.',16,1);
    RETURN;
END

;WITH GrowthEvents AS
(
    SELECT
        DB_NAME(DatabaseID) AS DatabaseName, FileName, CAST(StartTime AS DATE) AS GrowthDate,
        DATEPART(YEAR, StartTime) AS YearNum, DATEPART(WEEK, StartTime) AS WeekNum,
        DATEPART(MONTH, StartTime) AS MonthNum, IntegerData * 8.0 / 1024 AS GrowthMB
    FROM fn_trace_gettable(@TraceFile, DEFAULT)
    WHERE EventClass IN (92) -- 92 is data growth
)

SELECT DatabaseName,FileName,GrowthDate,COUNT(*) AS GrowthEvents,SUM(GrowthMB) AS TotalGrowthMB
FROM GrowthEvents GROUP BY DatabaseName,FileName,GrowthDate ORDER BY DatabaseName, GrowthDate
</code></pre>



<p class="wp-block-paragraph">As I don’t have any growth on the dynamicsBC database, I switch to the M-Files database for my example:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="723" height="685" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/autogrowth02.png" alt="" class="wp-image-46042" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/autogrowth02.png 723w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/autogrowth02-300x284.png 300w" sizes="auto, (max-width: 723px) 100vw, 723px" /></figure>



<p class="wp-block-paragraph">We have here some daily growth&#8230;</p>



<p class="wp-block-paragraph">Go to see if we have historical or log tables in M-Files with this simple script who give us  the table and the row count and MB:</p>



<pre class="wp-block-code"><code>SELECT s.name AS Schema_Name,t.name AS Table_Name,
    SUM(p.rows) AS Row_Count, CAST(SUM(a.total_pages) * 8.0 / 1024 AS DECIMAL(18,2)) AS SizeMB
FROM sys.tables t INNER JOIN sys.schemas s     ON t.schema_id = s.schema_id
INNER JOIN sys.indexes i ON t.object_id = i.object_id
INNER JOIN sys.partitions p ON i.object_id = p.object_id  AND i.index_id = p.index_id
INNER JOIN sys.allocation_units a ON p.partition_id = a.container_id
WHERE t.name LIKE 'Histor%' OR t.name LIKE '%Log'
GROUP BY s.name, t.name ORDER BY SizeMB DESC;
</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="714" height="291" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/autogrowth03.png" alt="" class="wp-image-46043" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/autogrowth03.png 714w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/autogrowth03-300x122.png 300w" sizes="auto, (max-width: 714px) 100vw, 714px" /></figure>



<h2 id="h-the-analisys" class="wp-block-heading">The ANALISYS</h2>



<p class="wp-block-paragraph">In my case, we can see that we have 383550 lignes (19.73Mb) on the table named OBJECTTYPECHANGELOG for a M-Files database.</p>



<p class="wp-block-paragraph">It’s only ~20MB here but in few months perhaps 20 GB or more&#8230;<br></p>



<p class="wp-block-paragraph">As a Green SQL Server DBA, I will contact the owner of M-Files project otherwise don&#8217;t forget that dbi services has also the expertise in ECM tools like M-Files.</p>



<h2 id="h-the-impact" class="wp-block-heading">The IMPACT</h2>



<p class="wp-block-paragraph">Like in my precedent post, we will have the same impacts</p>



<p class="wp-block-paragraph"><strong>The first impact is on DML INSERT operations.</strong></p>



<p class="wp-block-paragraph">With every insertion, the log or historical information is written to the table and update perhaps also index and at the end is never used by a query.</p>



<p class="wp-block-paragraph"><strong>The second impact is the storage and the backup</strong></p>



<p class="wp-block-paragraph">If the log table represents ~20 GB of the database.</p>



<p class="wp-block-paragraph">It’s 20 GB backed up unnecessarily, 20 GB restored unnecessarily and, above all, 20 GB stored unnecessarily and just for one environment.</p>



<p class="wp-block-paragraph">If you have Dev, Test, PreProd &amp; Prod and all in HA… I let you do the calculation but it’s more 100 GB!</p>



<p class="wp-block-paragraph"><strong>The third impact is on the maintenance plan</strong></p>



<p class="wp-block-paragraph">The following operations must also handle the log/historical tables:<br>– Rebuild Index<br>– Reorganise Index<br>– CheckDB<br>– Backups<br>– Restores</p>



<p class="wp-block-paragraph">Each growth of log/historical tables extends the maintenance windows…</p>



<h2 id="h-the-advise" class="wp-block-heading">The ADVISE</h2>



<p class="wp-block-paragraph">You really need to be careful before deleting anything in these table.</p>



<p class="wp-block-paragraph">Don’t Truncate the table because you see it!</p>



<p class="wp-block-paragraph">First see with the vendor if a purge or a retention parameter exists through the application.&nbsp;</p>



<p class="wp-block-paragraph">In my case with M-Files and the table OBJECTTYPECHANGELOG, in the administration interface, you can configure the retention. Go to challenge your M-Files team on it! <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">Finally, for every large log or historical table, you need to ask yourself the following questions:<br>– Who uses it?<br>– How often?<br>– Is there a legal obligation?<br>– What retention period is required?<br>– Can it be archived?<br>– Can it be purged?</p>



<h2 id="h-the-green-sql-server-dba-score" class="wp-block-heading">The Green SQL Server DBA Score</h2>



<p class="wp-block-paragraph">Like for my precedent post, I setup a ‘SQL Server DBA Score’ to use for my tips on the subject:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td><strong>Data growth per month</strong></td><td><strong>Score</strong></td></tr><tr><td>0 to 2</td><td>10</td></tr><tr><td>3 to 5</td><td>8</td></tr><tr><td>6 to 10</td><td>5</td></tr><tr><td>11 to 20</td><td>2</td></tr><tr><td>&gt; 20</td><td>0</td></tr></tbody></table></figure>



<h2 id="h-conclusion" class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">A traditional SQL Server DBA or system administrator will see a data disk full and says:</p>



<p class="wp-block-paragraph"><strong>&#8220;How much GB need we to extend the disk?&#8221;</strong></p>



<p class="wp-block-paragraph">A Green SQL Server DBA sees a data disk full and says:</p>



<p class="wp-block-paragraph"><strong>&#8220;Why is my disk full and what can I do?&#8221;</strong></p>



<p class="wp-block-paragraph">Just like adding a larger trunk doesn&#8217;t make a car more efficient, adding more GB doesn&#8217;t make a database healthier.</p>



<p class="wp-block-paragraph">The greenest SQL Server DBA is not the one with the largest disks but it&#8217;s the one that keeps only data that continues to deliver value. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">Think about it and begin now to see if you have cases!</p>



<p class="wp-block-paragraph">See you soon for the next one!&nbsp;</p>
<p>L’article <a href="https://www.dbi-services.com/blog/green-sql-server-dba-tips-2-when-logs-and-history-records-cause-your-database-to-growth-and-crash/">Green SQL Server DBA Tips #2 – When logs and history records cause your database to growth and crash</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/green-sql-server-dba-tips-2-when-logs-and-history-records-cause-your-database-to-growth-and-crash/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server: Automatic index compaction in Azure (preview) – Part 2</title>
		<link>https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-in-azure-preview-part-2/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-in-azure-preview-part-2/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Tue, 28 Jul 2026 23:48:17 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=45983</guid>

					<description><![CDATA[<p>Why does index fragmentation matter less than before ? Logical fragmentation was a problem on spinning disks: an ordered scan on a fragmented index broke the read-ahead into smaller I/Os and each jump cost a disk head movement. On SSD, NVMe and cloud storage, the penalty of non-sequential reads is marginal. And the &#8220;physical order&#8221; [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-in-azure-preview-part-2/">SQL Server: Automatic index compaction in Azure (preview) – Part 2</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><strong>Why does index fragmentation matter less than before ?</strong></p>



<p class="wp-block-paragraph">Logical fragmentation was a problem on spinning disks: an ordered scan on a fragmented index broke the read-ahead into smaller I/Os and each jump cost a disk head movement. On SSD, NVMe and cloud storage, the penalty of non-sequential reads is marginal. And the &#8220;physical order&#8221; we used to restore so carefully is only the order of the pages inside the data file: below it, the file system, the SSD controller and the storage layer place the blocks wherever they want anyway.</p>



<p class="wp-block-paragraph"><strong>Here is the mapping to remember</strong></p>



<ul class="wp-block-list">
<li>Internal fragmentation = page density = fixed by automatic index compaction</li>



<li>External fragmentation = page order = ignored by automatic index compaction (it can even increase and we will see it in the demo)</li>
</ul>



<h2 id="h-how-does-it-work" class="wp-block-heading">How does it work ?</h2>



<p class="wp-block-paragraph">Automatic index compaction is not a hidden maintenance job. It is an additional task performed by a background process that already exists: the PVS cleaner is a component of Accelerated Database Recovery (ADR).</p>



<p class="wp-block-paragraph"><strong>Here is the principle</strong></p>



<ul class="wp-block-list">
<li>The PVS cleaner periodically visits the pages that were recently modified (insert, update, delete) to remove obsolete row versions</li>



<li>When automatic index compaction is enabled the cleaner also checks if the visited page has free space excluding the space reserved by the fill factor</li>



<li>If so, it moves rows from the next page into the current page as long as they fit and repeats the operation on a few consecutive page pairs</li>



<li>A page that becomes empty is deallocated</li>
</ul>



<p class="wp-block-paragraph">The result: the number of pages decreases, the page density increases, and the storage space, I/O, CPU and buffer pool consumption decrease.</p>



<p class="wp-block-paragraph">The overhead is minimal because the process only considers recently modified pages, unlike a rebuild or a reorganize which process all the pages. Like a reorganize, the compaction acquires short-term exclusive page locks to move the rows. If a lock cannot be acquired immediately, the page is simply skipped and will be considered again later.</p>



<p class="wp-block-paragraph">One command per database, no restart, no exclusive access. The compaction starts within minutes:</p>



<pre class="wp-block-code"><code>-- Enable the feature
ALTER DATABASE &#091;SQL-DB-1] SET AUTOMATIC_INDEX_COMPACTION = ON;

-- Check if it's enabled
SELECT name, is_automatic_index_compaction_on FROM sys.databases;</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="619" height="189" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-27.png" alt="" class="wp-image-45984" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-27.png 619w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-27-300x92.png 300w" sizes="auto, (max-width: 619px) 100vw, 619px" /></figure>



<p class="wp-block-paragraph"><strong>One important point before you enable it</strong></p>



<p class="wp-block-paragraph">The compaction process only considers the pages modified after you enable the feature. If the page density of your indexes is already low, run a one-time reorganize or rebuild to fix the existing situation. From that point on, the automatic compaction keeps the indexes compact without any action on your side.</p>



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



<ul class="wp-block-list">
<li>We create a table with a clustered primary key and we insert 5 million rows</li>



<li>We measure the baseline: page count, page density, fragmentation</li>



<li>We delete 2 rows out of 3, scattered over the whole table, to simulate index bloat</li>



<li>We measure again and we let the engine work</li>
</ul>



<p class="wp-block-paragraph">We create the table and we insert 50K rows:</p>



<pre class="wp-block-code"><code>CREATE TABLE dbo.Demo (
    Id      int IDENTITY CONSTRAINT PK_Demo PRIMARY KEY CLUSTERED,
    Payload char(400) NOT NULL DEFAULT REPLICATE('X', 400)
);

INSERT INTO dbo.Demo (Payload)
SELECT TOP (50000) REPLICATE('X', 400)
FROM sys.all_columns a CROSS JOIN sys.all_columns b;</code></pre>



<p class="wp-block-paragraph">We measure the baseline:</p>



<pre class="wp-block-code"><code>SELECT index_level, page_count, avg_page_space_used_in_percent,
       avg_fragmentation_in_percent, record_count
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('dbo.Demo'),1, NULL, 'SAMPLED')
WHERE alloc_unit_type_desc = 'IN_ROW_DATA';</code></pre>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="561" height="187" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-28.png" alt="" class="wp-image-45985" style="width:561px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-28.png 561w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-28-300x100.png 300w" sizes="auto, (max-width: 561px) 100vw, 561px" /></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="644" height="301" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-29.png" alt="" class="wp-image-45986" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-29.png 644w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-29-300x140.png 300w" sizes="auto, (max-width: 644px) 100vw, 644px" /></figure>



<p class="wp-block-paragraph">We measure the baseline:</p>



<pre class="wp-block-code"><code>SELECT index_level, page_count, avg_page_space_used_in_percent,
       avg_fragmentation_in_percent, record_count
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('dbo.Demo'),1, NULL, 'SAMPLED')
WHERE alloc_unit_type_desc = 'IN_ROW_DATA';</code></pre>



<p class="wp-block-paragraph">The internal fragmentation is low.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="600" height="48" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-30.png" alt="" class="wp-image-45987" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-30.png 600w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-30-300x24.png 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /></figure>



<p class="wp-block-paragraph">Here is what we have:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="609" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-33-1024x609.png" alt="" class="wp-image-45992" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-33-1024x609.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-33-300x178.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-33-768x457.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-33.png 1500w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">However to observe the automatic index compaction in action we need to create the following situation:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="337" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-34-1024x337.png" alt="" class="wp-image-45991" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-34-1024x337.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-34-300x99.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-34-768x253.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-34.png 1500w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Here is the baseline:</p>



<figure class="wp-block-table alignwide is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>page_count</strong></td><td><strong>avg_page_space_used_in_percent</strong></td><td><strong>avg_fragmentation_in_percent</strong></td><td><strong>record_count</strong></td></tr><tr><td>2778</td><td>94.9269335310106</td><td>0.0359971202303816</td><td>50000</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>We create the bloat</strong></p>



<p class="wp-block-paragraph">We delete 2 rows out of 3 scattered uniformly over the table. No page becomes empty. Every page keeps around one third of its rows. We change page density:</p>



<pre class="wp-block-code"><code>DELETE FROM dbo.Demo WHERE Id % 3 &lt;&gt; 0;</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="393" height="49" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-36.png" alt="" class="wp-image-45996" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-36.png 393w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-36-300x37.png 300w" sizes="auto, (max-width: 393px) 100vw, 393px" /></figure>



<p class="wp-block-paragraph">Without automatic index compaction, the expectation is simple: the page count stays at 277778 (deletes alone do not deallocate non-empty pages) and the density drops to around 31% (one third of 94.93%).</p>



<p class="wp-block-paragraph"><strong>We measure again</strong></p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>page_count</strong></td><td><strong>avg_page_space_used_in_percent</strong></td><td><strong>avg_fragmentation_in_percent</strong></td><td><strong>record_count</strong></td></tr><tr><td>2778</td><td>31.6245737583395</td><td>0.0359971202303816</td><td>16666</td></tr></tbody></table></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="768" height="160" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-37.png" alt="" class="wp-image-45997" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-37.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-37-300x63.png 300w" sizes="auto, (max-width: 768px) 100vw, 768px" /></figure>



<p class="wp-block-paragraph"><strong>We let the engine finish its job and we check again</strong></p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>page_count</strong></td><td><strong>avg_page_space_used_in_percent</strong></td><td><strong>avg_fragmentation_in_percent</strong></td><td>re<strong>cord_count</strong></td></tr><tr><td>929</td><td>94.6165184086978</td><td>99.8923573735199</td><td>16666</td></tr></tbody></table></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="574" height="43" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-40.png" alt="" class="wp-image-46000" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-40.png 574w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-40-300x22.png 300w" sizes="auto, (max-width: 574px) 100vw, 574px" /></figure>



<p class="wp-block-paragraph">The engine did the work in the background while the database stayed fully available.</p>



<p class="wp-block-paragraph">This feature replaces the compaction part of your maintenance strategy not the whole strategy.</p>



<p class="wp-block-paragraph"><strong>A summary</strong>:</p>



<ul class="wp-block-list">
<li>It does not update the statistics. A rebuild updates them the automatic compaction does not. If you rely on your rebuild jobs to refresh the statistics, keep a statistics update job.</li>



<li>It does not reduce the logical fragmentation and it can even increase as we saw in the demo. For most workloads this has no measurable impact</li>



<li>It does not recreate the free space reserved by the fill factor. Only a rebuild does. Workloads that need a fill factor below 100 to reduce page splits can still benefit from an occasional rebuild</li>



<li>It does not shrink the data files. The used space decreases, the allocated size does not change</li>



<li>It only applies to the leaf level of B-tree indexes in IN_ROW_DATA allocation units. Heaps, LOB data, row-overflow data, compressed columnstore rowgroups and memory-optimized tables are not concerned</li>



<li>Indexes with page locks disabled (ALLOW_PAGE_LOCKS = OFF) are not eligible</li>
</ul>



<h2 id="h-reference" class="wp-block-heading">Reference</h2>



<p class="wp-block-paragraph"><a href="https://learn.microsoft.com/en-us/sql/relational-databases/indexes/automatic-index-compaction?view=fabric-sqldb">Automatic Index Compaction &#8211; SQL Server | Microsoft Learn</a></p>



<p class="wp-block-paragraph">Thank you. <a href="https://www.linkedin.com/in/amine-haloui-76968056/" data-type="link" data-id="https://www.linkedin.com/in/amine-haloui-76968056/">Amine Haloui</a></p>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-in-azure-preview-part-2/">SQL Server: Automatic index compaction in Azure (preview) – Part 2</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-in-azure-preview-part-2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server: Automatic index compaction in Azure (preview) – Part 1</title>
		<link>https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-preview-part-1/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-preview-part-1/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Tue, 28 Jul 2026 23:46:39 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=45961</guid>

					<description><![CDATA[<p>For years, we have all scheduled the same thing on our SQL Server environments: a nightly or weekly index maintenance job. We check the fragmentation level, we run a REORGANIZE between 5% and 30%, a REBUILD above 30% and we hope the job finishes before the business day starts. Microsoft recently released a new feature [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-preview-part-1/">SQL Server: Automatic index compaction in Azure (preview) – Part 1</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">For years, we have all scheduled the same thing on our SQL Server environments: a nightly or weekly index maintenance job. We check the fragmentation level, we run a REORGANIZE between 5% and 30%, a REBUILD above 30% and we hope the job finishes before the business day starts.</p>



<p class="wp-block-paragraph">Microsoft recently released a new feature in public preview: automatic index compaction. The database engine takes care of compacting the indexes itself, in the background, while the data changes. No job, no maintenance window, no script to maintain.</p>



<p class="wp-block-paragraph">Here is what we will cover in this blog post</p>



<ul class="wp-block-list">
<li>Which problems this feature addresses</li>



<li>The requirements</li>



<li>The difference between page density and index fragmentation (and why it matters)</li>



<li>What the feature does not do</li>
</ul>



<p class="wp-block-paragraph">This is the first part of a series of two blog posts.</p>



<h2 id="h-which-problems-does-it-address" class="wp-block-heading">Which problems does it address?</h2>



<p class="wp-block-paragraph">Automatic index compaction is Microsoft&#8217;s answer to a routine operational task: index maintenance jobs.</p>



<p class="wp-block-paragraph">Here are the problems with these jobs</p>



<ul class="wp-block-list">
<li>They are expensive in CPU and I/O. A rebuild reads and rewrites the entire index</li>



<li>They take time to run, and maintenance windows keep shrinking</li>



<li>They generate a lot of transaction log, which impacts log backups and Always On replication</li>



<li>They can block applications. An offline rebuild locks the table and even an online rebuild needs a short exclusive lock at the beginning and at the end</li>



<li>Someone has to set them up, monitor them and fix them when they fail</li>
</ul>



<p class="wp-block-paragraph">There is also a less obvious problem: for years, these jobs have been optimizing the wrong metric. We will come back to this point in a moment.</p>



<h2 id="h-requirements" class="wp-block-heading">Requirements</h2>



<p class="wp-block-paragraph">The feature is currently in public preview and available on</p>



<ul class="wp-block-list">
<li>Azure SQL Database</li>



<li>Azure SQL Managed Instance, with the Always-up-to-date update policy</li>



<li>SQL database in Microsoft Fabric</li>
</ul>



<p class="wp-block-paragraph">It is not available on SQL Server on-premises, including SQL Server 2025.</p>



<p class="wp-block-paragraph">There is nothing to install. The feature relies on a background process that already runs on these platforms (the ADR cleaner, more on this below), and Accelerated Database Recovery is always enabled on Azure SQL Database, Azure SQL Managed Instance and SQL database in Fabric.</p>



<h2 id="h-internal-and-external-fragmentation" class="wp-block-heading">Internal and external fragmentation</h2>



<p class="wp-block-paragraph">When we talk about index fragmentation, we are actually talking about two different phenomena. They are measured by two different columns of sys.dm_db_index_physical_stats, they have different causes, and above all they have very different costs.</p>



<p class="wp-block-paragraph">Before we start: how is a data file organized ?</p>



<p class="wp-block-paragraph">To understand fragmentation, we need one prerequisite: how SQL Server sees its data files.</p>



<ul class="wp-block-list">
<li>For any program, a file is an array of bytes provided by the operating system. Byte 0, byte 1, byte 2, and so on. Reading means asking for a range: an offset and a length</li>
</ul>



<ul class="wp-block-list">
<li>SQL Server slices this array into pages of 8 KB. The page N is, by definition, the bytes N × 8192 to (N+1) × 8192 − 1. The page ID is not a label, it is an address in the file</li>
</ul>



<ul class="wp-block-list">
<li>Everything below the file (NTFS clusters, SSD, SAN volumes) is translated and hidden by the storage stack.</li>
</ul>



<p class="wp-block-paragraph">Every time we say &#8220;physical order&#8221; in the rest of this post, we mean the order of the pages inside the data file not their placement on the hardware.</p>



<p class="wp-block-paragraph"><strong>What does it look like?</strong></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="960" height="740" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-26.png" alt="" class="wp-image-45980" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-26.png 960w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-26-300x231.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-26-768x592.png 768w" sizes="auto, (max-width: 960px) 100vw, 960px" /></figure>



<h2 id="h-internal-fragmentation-the-pages-are-not-full" class="wp-block-heading">Internal fragmentation: the pages are not full</h2>



<p class="wp-block-paragraph">Internal fragmentation means that the pages of an index contain free space. It is measured by avg_page_space_used_in_percent, also called page density.</p>



<p class="wp-block-paragraph"><strong>Where does it come from?</strong></p>



<ul class="wp-block-list">
<li>Deletes, especially scattered deletes: the rows disappear, the pages stay</li>



<li>Page splits: when a page is full, an insert in the middle of the key range or an update that makes a row grow moves half of the rows to a newly allocated page. Both pages end up around 50% full</li>



<li>Updates that shrink variable length columns</li>



<li>Large rows that do not pack well: a 5000 bytes row means one row per page, and around 38% of every page is lost mechanically</li>
</ul>



<p class="wp-block-paragraph">Note that some free space is normal, and even intentional: the space reserved by the fill factor is internal fragmentation on purpose to absorb future inserts without page splits.</p>



<p class="wp-block-paragraph"><strong>What does it look like?</strong></p>



<pre class="wp-block-code"><code>CREATE DATABASE DemoFrag
GO

ALTER DATABASE DemoFrag SET RECOVERY SIMPLE
GO

USE DemoFrag
GO

CREATE TABLE dbo.SplitDemo (
    Id      int NOT NULL CONSTRAINT PK_SplitDemo PRIMARY KEY CLUSTERED,
    Payload varchar(8000) NOT NULL
);

-- 2 lines about 4000 bytes per page
INSERT INTO dbo.SplitDemo VALUES
(1, REPLICATE('A', 4000)), (2, REPLICATE('B', 4000));</code></pre>



<p class="wp-block-paragraph">My page is almost full:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="865" height="43" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-25.png" alt="" class="wp-image-45976" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-25.png 865w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-25-300x15.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-25-768x38.png 768w" sizes="auto, (max-width: 865px) 100vw, 865px" /></figure>



<p class="wp-block-paragraph"><strong>Why does it cost ?</strong></p>



<p class="wp-block-paragraph">Because the same rows are spread over more pages than necessary and everything in SQL Server is done at the page level:</p>



<ul class="wp-block-list">
<li>The buffer pool caches pages, not rows. A page at 40% density wastes 60% of the memory it occupies</li>



<li>Every read, logical or physical, handles more pages for the same data</li>



<li>Backups are bigger, integrity checks are longer</li>
</ul>



<p class="wp-block-paragraph">This is the key point: internal fragmentation follows the data everywhere, including in RAM. It is a permanent tax on memory, I/O and CPU. However it&#8217;s not the case with external fragmentation.</p>



<h2 id="h-external-fragmentation-the-offsets-do-not-follow-the-logical-order" class="wp-block-heading">External fragmentation: the offsets do not follow the logical order</h2>



<p class="wp-block-paragraph">At the leaf level of a B-tree, the pages are chained together in the order of the index keys with two pointers stored in every page header (m_nextPage, m_prevPage). This chain is the logical order and it cannot be wrong: it is the order of the keys by definition.</p>



<p class="wp-block-paragraph">External fragmentation (also called logical fragmentation) means that following this chain no longer corresponds to reading the file sequentially. The pages are logically ordered, but their offsets in the file are not. It takes two forms:</p>



<ul class="wp-block-list">
<li><strong>Holes</strong>: the pages of the index are not at consecutive offsets. Pages of other objects, or free pages, sit in between</li>



<li><strong>Disorder</strong>: the pages are at consecutive offsets, but chained in a different order</li>
</ul>



<p class="wp-block-paragraph">avg_fragmentation_in_percent counts both forms without distinction. The columns fragment_count and avg_fragment_size_in_pages are closer to the real cost: they measure the length of the contiguous runs.</p>



<p class="wp-block-paragraph"><strong>Where does it come from?</strong></p>



<ul class="wp-block-list">
<li>Several objects growing at the same time: the engine allocates the extents in the order the requests arrive, so the objects interleave in the file. This is the normal life of a database</li>



<li>Page splits: the new page is allocated where free space is found, rarely next to the original page</li>



<li>Page deallocations: massive deletes, and the automatic index compaction itself</li>
</ul>



<p class="wp-block-paragraph"><strong>What does it look like?</strong></p>



<pre class="wp-block-code"><code>USE DemoFrag
GO

CREATE TABLE dbo.SplitDemo (
&nbsp;&nbsp;&nbsp; Id&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int NOT NULL CONSTRAINT PK_SplitDemo PRIMARY KEY CLUSTERED,
&nbsp;&nbsp;&nbsp; Payload varchar(8000) NOT NULL
);

-- 2 lines about 4000 bytes per page : lines 1 and 2 on 1 page, 3-4 on next page

INSERT INTO dbo.SplitDemo VALUES
(1, REPLICATE('A', 4000)), (2, REPLICATE('B', 4000)),
(3, REPLICATE('C', 4000)), (4, REPLICATE('D', 4000))

-- Lines location (file:page:slot)?
SELECT Id, sys.fn_PhysLocFormatter(%%physloc%%) AS physical_location
FROM dbo.SplitDemo;</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="163" height="122" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-21.png" alt="" class="wp-image-45969" /></figure>



<p class="wp-block-paragraph">My first 2 rows are located on page 376 and the last 2 rows on page 378.</p>



<p class="wp-block-paragraph">What does page 377 contain? I thought the pages were contiguous?</p>



<p class="wp-block-paragraph">Since my table has a clustered index on Id it is stored as a B-tree. Can we see it?</p>



<pre class="wp-block-code"><code>SELECT allocated_page_page_id, page_type_desc, page_level, previous_page_page_id, next_page_page_id
FROM sys.dm_db_database_page_allocations(DB_ID(), OBJECT_ID('dbo.SplitDemo'),1, NULL, 'DETAILED');</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="540" height="196" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-16.png" alt="" class="wp-image-45966" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-16.png 540w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-16-300x109.png 300w" sizes="auto, (max-width: 540px) 100vw, 540px" /></figure>



<p class="wp-block-paragraph"><strong>What does it look like internally?</strong></p>



<pre class="wp-block-code"><code>SELECT * FROM sys.dm_db_page_info (10, 1, 376, DEFAULT);</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="931" height="44" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-19.png" alt="" class="wp-image-45971" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-19.png 931w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-19-300x14.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-19-768x36.png 768w" sizes="auto, (max-width: 931px) 100vw, 931px" /></figure>



<pre class="wp-block-code"><code>SELECT * FROM sys.dm_db_page_info (10, 1, 378, DEFAULT);</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="45" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-18-1024x45.png" alt="" class="wp-image-45968" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-18-1024x45.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-18-300x13.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-18-768x34.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-18.png 1051w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><strong>Page split to generate external fragmentation:</strong></p>



<pre class="wp-block-code"><code>-- Split happens
UPDATE dbo.SplitDemo SET Payload = REPLICATE('B', 4100) WHERE Id = 2;

-- Check page density
SELECT index_level, page_count, avg_page_space_used_in_percent,
avg_fragmentation_in_percent, record_count,
ghost_record_count, version_ghost_record_count
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('dbo.SplitDemo'),1, NULL, 'SAMPLED')
WHERE alloc_unit_type_desc = 'IN_ROW_DATA';</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="569" height="46" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-17.png" alt="" class="wp-image-45967" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-17.png 569w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-17-300x24.png 300w" sizes="auto, (max-width: 569px) 100vw, 569px" /></figure>



<p class="wp-block-paragraph">After the page split:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="118" height="104" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-22.png" alt="" class="wp-image-45972" /></figure>



<p class="wp-block-paragraph">Line 1 is located at page 376</p>



<p class="wp-block-paragraph">Line 2 is located at page 379</p>



<p class="wp-block-paragraph">Line 3 is located at page 378</p>



<p class="wp-block-paragraph">Line 4 is located at page 378</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="824" height="360" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-20.png" alt="" class="wp-image-45970" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-20.png 824w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-20-300x131.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-20-768x336.png 768w" sizes="auto, (max-width: 824px) 100vw, 824px" /></figure>



<p class="wp-block-paragraph"><strong>Why does it cost so little today?</strong></p>



<p class="wp-block-paragraph">There is exactly one mechanism through which external fragmentation ever costed something: the read-ahead. During a scan, the engine prefetches pages with large I/Os and an I/O is a contiguous range of the file: one offset, one length, up to 512 KB. When the index is contiguous one I/O brings back 64 pages. When it is not the same pages arrive in several smaller I/Os.</p>



<p class="wp-block-paragraph">When do these smaller I/Os hurt ? Three conditions must be met at the same time:</p>



<ul class="wp-block-list">
<li>The data is cold: a page already in the buffer pool generates no physical I/O at all</li>



<li>The operation is a large scan: a seek navigates the B-tree by page ID and does not care about the order</li>



<li>Each additional I/O has a price</li>
</ul>



<p class="wp-block-paragraph">On SSD, NVMe and cloud storage, an additional I/O costs a few dozen microseconds and the physical placement below the file is arbitrary anyway (SSD controllers and SANs put the blocks wherever they want).</p>



<p class="wp-block-paragraph">External fragmentation survived its own usefulness for another reason: it was a good symptom. It increases because of page splits, and page splits destroy the page density and inflate the transaction log.</p>



<h2 id="h-reference" class="wp-block-heading">Reference</h2>



<p class="wp-block-paragraph"><a href="https://learn.microsoft.com/en-us/sql/relational-databases/indexes/automatic-index-compaction?view=fabric-sqldb">Automatic Index Compaction &#8211; SQL Server | Microsoft Learn</a></p>



<p class="wp-block-paragraph">Thank you. <a href="https://www.linkedin.com/in/amine-haloui-76968056/" data-type="link" data-id="https://www.linkedin.com/in/amine-haloui-76968056/">Amine Haloui</a></p>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-preview-part-1/">SQL Server: Automatic index compaction in Azure (preview) – Part 1</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-preview-part-1/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Split, Sort, Collapse: how SQL Server survives an overlapping unique update</title>
		<link>https://www.dbi-services.com/blog/split-sort-collapse-how-sql-server-survives-an-overlapping-unique-update/</link>
					<comments>https://www.dbi-services.com/blog/split-sort-collapse-how-sql-server-survives-an-overlapping-unique-update/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Mon, 27 Jul 2026 12:07:11 +0000</pubDate>
				<category><![CDATA[Development & Performance]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Constraint]]></category>
		<category><![CDATA[dml]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=45810</guid>

					<description><![CDATA[<p>How SQL Server shifts a whole unique column by +1 without ever creating a duplicate using Split/Sort/Collapse.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/split-sort-collapse-how-sql-server-survives-an-overlapping-unique-update/">Split, Sort, Collapse: how SQL Server survives an overlapping unique update</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">One table, one single column, 10,000 contiguous values from 1 to 10,000. We shift everyone up by 1:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
UPDATE u SET val = val + 1;
</pre></div>


<p class="wp-block-paragraph">Each new value lands on a value that already exists: <code>1</code> wants to become <code>2</code>, but a <code>2</code> is already there; <code>2</code> wants to become <code>3</code>, but a <code>3</code> is already there… Every target overlaps a neighboring value. Yet a unique index allows <strong>no duplicate, at any instant</strong>.<br>How can this update succeed?</p>



<h2 id="h-two-tables-one-single-difference" class="wp-block-heading">Two tables, one single difference</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
-- Unique column
CREATE TABLE u (val INT NOT NULL);
CREATE UNIQUE INDEX ux_u ON u(val);
INSERT INTO u (val)
  SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
  FROM sys.all_objects a CROSS JOIN sys.all_objects b;

-- Same column, without uniqueness
CREATE TABLE n (val INT NOT NULL);
INSERT INTO n (val) SELECT val FROM u;
</pre></div>


<p class="wp-block-paragraph">The same <code>UPDATE ... SET val = val + 1</code> on each. Yet the two execution plans differ completely: on <code>n</code>, a direct update, a single modification operator. On <code>u</code>, three unexpected operators appear: <strong>Split</strong>, <strong>Sort</strong>, <strong>Collapse</strong>. That is the entire gap between &#8220;any column&#8221; and &#8220;a unique column&#8221;.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="283" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-1-1-1024x283.png" alt="" class="wp-image-45812" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-1-1-1024x283.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-1-1-300x83.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-1-1-768x212.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-1-1.png 1473w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 id="h-why-the-naive-method-doesn-t-work" class="wp-block-heading">Why the naive method doesn&#8217;t work</h2>



<p class="wp-block-paragraph">Let&#8217;s process <code>u</code> row by row, in ascending order. First row: <code>1 → 2</code>. But <code>2</code> still exists. Two rows carry the value <code>2</code>: uniqueness is violated immediately. Processing in descending order would get by (<code>10000 → 10001</code> first, then <code>9999 → 10000</code>…), but SQL Server can&#8217;t bet on the order in which the rows to process are acquired. It needs a method that works regardless of order, and that never lets the same value coexist twice.</p>



<h2 id="h-split-sort-collapse" class="wp-block-heading">Split, sort, collapse</h2>



<p class="wp-block-paragraph">This is the engine&#8217;s answer, in three steps. Let&#8217;s take five rows with the values <code>3,4,5,6,7</code> and follow each step closely.</p>



<h3 id="h-1-split-the-update-becomes-delete-insert" class="wp-block-heading">1. Split (the update becomes delete + insert)</h3>



<p class="wp-block-paragraph">Modifying the key of a unique index directly is risky: the target value may overlap a value that is present. Split works around the problem by decomposing each update into a <strong>deletion</strong> of the old value followed by an <strong>insertion</strong> of the new one. Our 5 rows become 10 operations:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
DELETE 3     (old value of row #0)
INSERT 4     (new value of row #0)
DELETE 4     (row #1)
INSERT 5
DELETE 5     (row #2)
INSERT 6
DELETE 6     (row #3)
INSERT 7
DELETE 7     (row #4)
INSERT 8
</pre></div>


<p class="wp-block-paragraph">A delete can never fail on uniqueness; an insert fails only if the final value is truly a duplicate. We&#8217;ve separated the two risks.</p>



<h3 id="h-2-sort-sort-to-free-a-value-before-reoccupying-it" class="wp-block-heading">2. Sort (sort to free a value before reoccupying it)</h3>



<p class="wp-block-paragraph">The 10 operations are sorted by the index key. Decisive rule: for equal values, the <strong>delete comes before the insert</strong>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
DELETE 3
DELETE 4     -- 4 is freed...
INSERT 4     -- ...before being reoccupied
DELETE 5
INSERT 5
DELETE 6
INSERT 6
DELETE 7
INSERT 7
INSERT 8
</pre></div>


<p class="wp-block-paragraph">This is where the overlap is defused. For each value, the old occupant leaves before the new one arrives. At no point do two rows share the same key.</p>



<h3 id="h-3-collapse-reduce-the-intermediate-operations" class="wp-block-heading">3. Collapse (reduce the intermediate operations)</h3>



<p class="wp-block-paragraph">After sorting, each <code>DELETE k / INSERT k</code> pair for the same value sits adjacent. Collapse fuses each pair into a single in-place update of the existing index entry, rather than deleting and re-inserting it. The two unpaired ends survive as a real delete and a real insert.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
DELETE 3          (low end, no partner)
MODIFY 4
MODIFY 5
MODIFY 6
MODIFY 7
INSERT 8          (high end, no partner)
</pre></div>


<p class="wp-block-paragraph">Does the engine really execute it this way? The transaction log settles it.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SELECT Operation, Context, COUNT(*) AS n
FROM sys.fn_dblog(NULL, NULL)
WHERE AllocUnitName LIKE &#039;%ux_u%&#039;
GROUP BY Operation, Context
ORDER BY n DESC;
</pre></div>


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


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Operation            Context               n
LOP_MODIFY_ROW       LCX_INDEX_LEAF        4
LOP_DELETE_ROWS      LCX_MARK_AS_GHOST     1
LOP_SET_BITS         LCX_PFS               1
LOP_INSERT_ROWS      LCX_INDEX_LEAF        1
</pre></div>


<p class="wp-block-paragraph">This result is particularly revealing.</p>



<p class="wp-block-paragraph">If the five updates had truly been executed as five <code>DELETE</code>s followed by five <code>INSERT</code>s, we would have expected five deletions and five insertions in the log.</p>



<p class="wp-block-paragraph">Yet what we ultimately observe is:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
4x MODIFY_ROW
1x DELETE_ROWS
1x INSERT_ROWS
</pre></div>


<p class="wp-block-paragraph">Collapse is therefore not only a theoretical concept visible in the execution plan. The observations from <code>fn_dblog()</code> show that it translates concretely into a massive reduction of physical operations. The intermediate values are merged into simple index row modifications, while only the two ends of the shift survive as a genuine deletion (<code>3</code>) and a genuine insertion (<code>8</code>).</p>



<h3 id="h-4-visualization-of-the-complete-split-sort-collapse-process" class="wp-block-heading">4. Visualization of the complete SPLIT-SORT-COLLAPSE process</h3>



<p class="wp-block-paragraph">The preceding observations now make it possible to connect the theory to what is actually executed. The following visualization traces the complete sequence of the Split, Sort and Collapse phases, along with the progressive reduction of operations down to the final result observed in the transaction log:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="720" height="600" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/split-sort-collapse-animated.gif" alt="" class="wp-image-45848" /></figure>



<h2 id="h-what-it-costs" class="wp-block-heading">What it costs</h2>



<p class="wp-block-paragraph">The <code>Split / Sort / Collapse</code> mechanism lets SQL Server guarantee uniqueness throughout the entire UPDATE, but this safety comes at a significant cost. To measure this impact, we ran exactly the same update on the two 10,000-row tables created earlier, with <code>STATISTICS IO ON</code> enabled. The results are unambiguous. On table u, SQL Server performed <strong>40,036</strong> logical reads, consumed 266 ms of CPU and 360 ms of elapsed time. On table n, the same operation required only <strong>34</strong> logical reads, 15 ms of CPU and 71 ms of elapsed time.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="272" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-12-1024x272.png" alt="" class="wp-image-45818" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-12-1024x272.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-12-300x80.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-12-768x204.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-12-1536x408.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/image-12.png 1613w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">It&#8217;s worth noting that this extra cost corresponds mainly to the logical processing imposed by the uniqueness guarantee. Indeed, our analysis with fn_dblog() shows that the engine does not naively delete and then reinsert each row. After Collapse, the intermediate operations are largely merged into simple MODIFY_ROW operations, which limits the physical work actually performed on the index pages. The price to pay is therefore not so much a full rebuild of the index as a substantial amount of preparatory organization that lets SQL Server guarantee that at no instant do two rows simultaneously hold the same unique key.</p>



<h2 id="h-a-side-note-about-the-halloween-problem" class="wp-block-heading">A side note about the Halloween Problem</h2>



<p class="wp-block-paragraph">This need to read everything before writing anything has a name. In the mid-1970s, Don Chamberlin, Pat Selinger and Morton Astrahan wrote a query meant to give a 10% raise to every employee earning under $25,000. It ran without error but left everyone at exactly $25,000: the engine scanned the salary index upward, and each raised row crossed the threshold, moved back ahead of the scan, and got raised again. They found it on Halloween, and the name stuck after the day, not the nature of the bug (<a href="https://en.wikipedia.org/wiki/Halloween_Problem" data-type="link" data-id="https://en.wikipedia.org/wiki/Halloween_Problem">link</a>).</p>



<p class="wp-block-paragraph">Their fix was to make sure the optimizer never reads an update through an index built on the very column being updated. <code>Split/Sort/Collapse</code> is a descendant of that idea: when an <code>UPDATE</code> reads and writes the same index, reading and writing must be separated. That&#8217;s the Sort&#8217;s job. It is a <a href="https://www.oreilly.com/library/view/learn-t-sql-querying/9781789348811/a531ee25-124c-492d-8641-b7fc0e3ab39e.xhtml" data-type="link" data-id="https://www.oreilly.com/library/view/learn-t-sql-querying/9781789348811/a531ee25-124c-492d-8641-b7fc0e3ab39e.xhtml"><strong>blocking</strong> operator</a>: it must consume its entire input before emitting a single row and that property forms the barrier. In doing so it <strong>materializes</strong> the whole set of rows to modify (in memory via a <em>memory grant</em> or spilling to tempdb if the volume exceeds it). Once that snapshot is frozen, each source row is read exactly once, and no write can ever catch up to it.</p>



<h2 id="h-conclusion" class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">Updating a unique key that overlaps itself seems, at first glance, impossible without temporarily violating uniqueness. Yet, thanks to the Split / Sort / Collapse mechanism, SQL Server manages to turn a potentially conflicting operation into a sequence of actions that guarantees no duplicate ever appears at any instant. The execution plan analysis explains the logic the engine uses, while the study of the transaction log shows how this logic materializes concretely. The observations made with fn_dblog() reveal that the intermediate operations are largely reduced to MODIFY_ROW operations. In the end, the real complexity does not lie in the physical modifications themselves, but in the preparatory work needed to preserve uniqueness. That is precisely what explains the overhead observed compared to a classic update: SQL Server pays the price of safety and consistency, but then heavily optimizes the actual execution thanks to Collapse.</p>



<p class="wp-block-paragraph">This post was inspired by <a href="https://www.linkedin.com/in/franckpachot/" data-type="link" data-id="https://www.linkedin.com/in/franckpachot/">Franck Pachot</a>&#8216;s look at <a href="https://dev.to/franckpachot/following-rowids-through-an-oracle-unique-index-update-2lc" data-type="link" data-id="https://dev.to/franckpachot/following-rowids-through-an-oracle-unique-index-update-2lc">the same problem on Oracle</a>, where he follows the ROWIDs through the index at the physical level. A good companion to this post, to read by the fireplace (or the air-con) !</p>
<p>L’article <a href="https://www.dbi-services.com/blog/split-sort-collapse-how-sql-server-survives-an-overlapping-unique-update/">Split, Sort, Collapse: how SQL Server survives an overlapping unique update</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/split-sort-collapse-how-sql-server-survives-an-overlapping-unique-update/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 
Lazy Loading (feed)

Served from: www.dbi-services.com @ 2026-09-04 03:38:34 by W3 Total Cache
-->