<?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>Database Administration &amp; Monitoring - dbi Blog</title>
	<atom:link href="https://www.dbi-services.com/blog/category/database-administration-monitoring/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.dbi-services.com/blog/category/database-administration-monitoring/</link>
	<description></description>
	<lastBuildDate>Thu, 15 Oct 2026 12:11:31 +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>Database Administration &amp; Monitoring - dbi Blog</title>
	<link>https://www.dbi-services.com/blog/category/database-administration-monitoring/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>SQL Server: Disabling FileTable/FileStream on an AlwaysOn high-availability instance</title>
		<link>https://www.dbi-services.com/blog/sql-server-disabling-filetable-filestream-on-an-alwayson-high-availability-instance/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-disabling-filetable-filestream-on-an-alwayson-high-availability-instance/#respond</comments>
		
		<dc:creator><![CDATA[Stéphane Haby]]></dc:creator>
		<pubDate>Thu, 15 Oct 2026 12:11:29 +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=47891</guid>

					<description><![CDATA[<p>Contexte A customer recently asked me to disable FileTable/FileStream on their instance because the databases no longer use it, and to ensure that this is indeed the case.The aim is to migrate to a new architecture without FileTable/FileStream in a second time. Link to FileTable and FileStream if you need to know a bit more [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-disabling-filetable-filestream-on-an-alwayson-high-availability-instance/">SQL Server: Disabling FileTable/FileStream on an AlwaysOn high-availability instance</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-contexte" class="wp-block-heading">Contexte</h2>



<p class="wp-block-paragraph">A customer recently asked me to <strong>disable FileTable/FileStream</strong> on their instance because the databases no longer use it, and to ensure that this is indeed the case.<br>The aim is to migrate to a new architecture without FileTable/FileStream in a second time.</p>



<p class="wp-block-paragraph">Link to<a href="https://learn.microsoft.com/en-us/sql/relational-databases/blob/filetables-sql-server?view=sql-server-ver17"> FileTable</a> and <a href="https://learn.microsoft.com/en-us/sql/relational-databases/blob/filestream-sql-server?view=sql-server-ver17">FileStream </a>if you need to know a bit more about these technologies</p>



<p class="wp-block-paragraph">The environment is in a high-availability (HA) configuration using AlwaysOn.</p>



<p class="wp-block-paragraph">As this isn’t something we do every day, here is a 9-step procedure to achieve this:</p>



<p class="wp-block-paragraph">•&nbsp;&nbsp;&nbsp; Check that there are no more FileTables</p>



<p class="wp-block-paragraph">•&nbsp;&nbsp;&nbsp; Disable non-transactional access</p>



<p class="wp-block-paragraph">•&nbsp;&nbsp;&nbsp; Check that non-transactional access is disabled</p>



<p class="wp-block-paragraph">•&nbsp;&nbsp;&nbsp; Check the columns in the tables</p>



<p class="wp-block-paragraph">•&nbsp;&nbsp;&nbsp; Identify FILESTREAM filegroups &amp; check FILESTREAM files</p>



<p class="wp-block-paragraph">•&nbsp;&nbsp;&nbsp; Delete FILESTREAM containers &amp; the FILESTREAM filegroup</p>



<p class="wp-block-paragraph">•&nbsp;&nbsp;&nbsp; Disable FILESTREAM on the SQL Server instance</p>



<p class="wp-block-paragraph">•&nbsp;&nbsp;&nbsp; Disable FILESTREAM on the server</p>



<p class="wp-block-paragraph">•&nbsp;&nbsp;&nbsp; Disable FILESTREAM on the other servers</p>



<p class="wp-block-paragraph">Let’s get started!</p>



<h2 id="h-step-1-checking-the-filetables" class="wp-block-heading">Step 1: Checking the FileTables</h2>



<p class="wp-block-paragraph">Let’s start with the first, fairly simple step, which is to check that there are no more FileTables </p>



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



<pre class="wp-block-code"><code>SELECT name,object_id,is_filetable FROM &lt;database&gt;.sys.tables WHERE is_filetable = 1;</code></pre>



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



<figure class="wp-block-image size-full"><img fetchpriority="high" decoding="async" width="940" height="545" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-52.png" alt="" class="wp-image-47892" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-52.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-52-768x445.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-52-300x174.png 300w" sizes="(max-width: 940px) 100vw, 940px" /></figure>



<p class="wp-block-paragraph">In this example, it is clear that there is no longer a FileTable in the tables of the two databases checked here.</p>



<h2 id="h-step-2-disabling-non-transactional-access" class="wp-block-heading">Step 2: Disabling non-transactional access</h2>



<p class="wp-block-paragraph">To ensure that no further data is sent to FileTable/FileStream, we disable non-transactional access:</p>



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



<pre class="wp-block-code"><code>ALTER DATABASE &lt;database&gt; SET FILESTREAM ( NON_TRANSACTED_ACCESS = OFF );</code></pre>



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



<figure class="wp-block-image size-full"><img decoding="async" width="940" height="488" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-53.png" alt="" class="wp-image-47893" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-53.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-53-300x156.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-53-767x398.png 767w" sizes="(max-width: 940px) 100vw, 940px" /></figure>



<p class="wp-block-paragraph">The next step is, of course, to check carefully that it is disabled.</p>



<h2 id="h-step-3-check-that-non-transactional-access-is-disabled" class="wp-block-heading">Step 3: Check that non-transactional access is disabled</h2>



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



<pre class="wp-block-code"><code>SELECT db_name(database_id),* FROM sys.database_filestream_options;</code></pre>



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



<figure class="wp-block-image size-full"><img decoding="async" width="939" height="580" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-54.png" alt="" class="wp-image-47894" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-54.png 939w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-54-300x185.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-54-767x474.png 767w" sizes="(max-width: 939px) 100vw, 939px" /></figure>



<p class="wp-block-paragraph">As can be seen in the example, all databases are set to OFF<br></p>



<h2 id="h-step-4-checking-the-columns-in-the-tables" class="wp-block-heading">Step 4: Checking the columns in the tables</h2>



<p class="wp-block-paragraph">This is a very important step, as it is the one I missed in my example by the customer&#8230;</p>



<p class="wp-block-paragraph">In fact, in step 6, if there is still a column in a table that has a FileStream defined, it is not possible to delete the associated files and filegroups.</p>



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



<pre class="wp-block-code"><code>SELECT * FROM &lt;database&gt;.sys.columns WHERE is_filestream = 1;
select * from sys.objects where object_id= xxxx
</code></pre>



<p class="wp-block-paragraph">The first query returns the columns that contain FileStream data, and the second provides information about the table.</p>



<p class="wp-block-paragraph">You will then find a structure of the following type:<br>CREATE TABLE [schema].[table](<br> [ID] [uniqueidentifier] ROWGUIDCOL  NOT NULL,<br>&#8230;..<br>[FS] [varbinary](max) <strong>FILESTREAM  </strong>NULL<br>) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] <strong>FILESTREAM_ON</strong> [FileStreamFileGroup]</p>



<p class="wp-block-paragraph">We can therefore see that there is indeed a FileStream that needs to be removed at column level, but also at table level with the FILESTREAM_ON [FileStreamFileGroup].</p>



<p class="wp-block-paragraph">Initially, creating a new column with [FS2] [varbinary](max) NULL, copying the data and renaming it, seemed like a good idea but was not sufficient, as the table still had FILESTREAM_ON [FileStreamFileGroup].<br>To resolve this, you need to recreate a table, copy the data and add the keys, indexes and constraints.</p>



<p class="wp-block-paragraph">Then check everything again before proceeding to the next steps.</p>



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



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="368" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-55.png" alt="" class="wp-image-47895" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-55.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-55-766x300.png 766w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-55-300x117.png 300w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>



<h2 id="h-step-5-identify-filestream-filegroups-amp-check-filestream-files" class="wp-block-heading">Step 5:  Identify FILESTREAM filegroups &amp; Check FILESTREAM files</h2>



<p class="wp-block-paragraph">In this step, the aim is to identify the FILESTREAM filegroups defined in the databases so that they can be deleted.<br><br></p>



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



<pre class="wp-block-code"><code>SELECT name, type_desc FROM &lt;database&gt;.sys.filegroups WHERE type = 'FD';
SELECT name,physical_name,type_desc FROM &lt;database&gt;.sys.database_files WHERE type_desc = 'FILESTREAM';
</code></pre>



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



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="503" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-56.png" alt="" class="wp-image-47896" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-56.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-56-300x161.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-56-766x410.png 766w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>



<p class="wp-block-paragraph">As there are two databases in my example that have a FileStream, there are four lines.</p>



<h2 id="h-step-6-delete-the-filestream-containers-and-the-filestream-filegroup" class="wp-block-heading">Step 6: Delete the FILESTREAM containers and the FILESTREAM FILEGROUP</h2>



<p class="wp-block-paragraph">Once identified, delete them using the following command.<br>Query used:</p>



<pre class="wp-block-code"><code>ALTER DATABASE &#091;Database]  REMOVE FILE &#091;FileStreamFile]</code></pre>



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



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="368" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-57.png" alt="" class="wp-image-47897" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-57.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-57-300x117.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-57-766x300.png 766w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>



<h2 id="h-step-7-disable-filestream-on-the-sql-server-instance" class="wp-block-heading">Step 7: Disable FILESTREAM on the SQL Server instance</h2>



<p class="wp-block-paragraph">At this point, we disable FILESTREAM at instance level.<br><br>Query used:</p>



<pre class="wp-block-code"><code>EXEC sp_configure 'filestream access level';
GO
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'filestream access level',0;
GO
RECONFIGURE;
GO
EXEC sp_configure 'show advanced options', 0;
RECONFIGURE;
GO
</code></pre>



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



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="498" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-58.png" alt="" class="wp-image-47898" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-58.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-58-300x159.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-58-766x406.png 766w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>



<p class="wp-block-paragraph">As can be seen from the example, the `config_value` is indeed 0, but the `run_value` has changed to 1.</p>



<p class="wp-block-paragraph">In such cases, as always, we’ll check what the documentation says:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td><strong>Valeur</strong><strong></strong></td><td><strong>Signification</strong><strong></strong></td></tr><tr><td><strong>0</strong><strong></strong></td><td>FILESTREAM is disabled. No FILESTREAM access is permitted. FILESTREAM columns cannot be used.</td></tr><tr><td><strong>1</strong><strong></strong></td><td>T-SQL access only. SQL Server allows access to FILESTREAM data via Transact-SQL statements (INSERT, UPDATE, SELECT). Access via the Windows file system is disabled.</td></tr><tr><td><strong>2</strong><strong></strong></td><td>T-SQL access + Win32 streaming. FILESTREAM data can be accessed both via T-SQL and via the Windows APIs (direct file access using FILESTREAM sharing). This is generally the setting used to take advantage of FILESTREAM’s performance.</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">We are in T-SQL access only, it&#8217;s not really desactivated&#8230; Need a restart!</p>



<p class="wp-block-paragraph">At this stage, we could also carry this out on the secondary node(s) of our AlwaysOn high availability cluster, but it is preferable to do it first on a single node and verify that it works.</p>



<p class="wp-block-paragraph">Now it is time to disable Filestream on the SQL Server Engine service.<br><br></p>



<h2 id="h-step-8-disabling-filestream-on-the-server" class="wp-block-heading">Step 8: Disabling FileStream on the server</h2>



<p class="wp-block-paragraph">For this step, we have already performed a failover of the AAGs to a synchronous secondary node.</p>



<p class="wp-block-paragraph">We launch SQL Server Configuration Manager to disable FileStream in the Engine properties by unticking the box, as shown in the example above:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="536" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-60.png" alt="" class="wp-image-47901" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-60.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-60-766x437.png 766w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-60-300x171.png 300w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>



<p class="wp-block-paragraph">Then restart SQL Server and check that FileStream is disabled on the instance:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="536" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-59.png" alt="" class="wp-image-47900" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-59.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-59-300x171.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-59-766x437.png 766w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>



<h2 id="h-step-9-disabling-filestream-on-the-other-servers" class="wp-block-heading">Step 9: Disabling FileStream on the other servers</h2>



<p class="wp-block-paragraph">At this point, you will just need to repeat steps 7 and 8 on the other nodes in your AlwaysOn high-availability cluster.</p>



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



<p class="wp-block-paragraph">The steps are not complex in themselves, but where you can lose time is in the step 4 if you have to recreate a table without the FileStream and copy all data from this table. I was happy to have only one with not so much rows&#8230;</p>



<p class="wp-block-paragraph">All the steps up to the restart of the SQL Server Engine service in step 8 are carried out online without any service interruption or failover.</p>



<p class="wp-block-paragraph">I hope these steps will help anyone facing a similar issue! <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-disabling-filetable-filestream-on-an-alwayson-high-availability-instance/">SQL Server: Disabling FileTable/FileStream on an AlwaysOn high-availability instance</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-disabling-filetable-filestream-on-an-alwayson-high-availability-instance/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server 2025 CU5: max lock manager cache memory (%): what I could reproduce and what I could not</title>
		<link>https://www.dbi-services.com/blog/sql-server-2025-cu5-max-lock-manager-cache-memory-what-i-could-reproduce-and-what-i-could-not/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-2025-cu5-max-lock-manager-cache-memory-what-i-could-reproduce-and-what-i-could-not/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Mon, 05 Oct 2026 22:07:01 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=47676</guid>

					<description><![CDATA[<p>Cumulative Update 5 for SQL Server 2025 was released on May 20, 2026 (build 17.0.4045.5). Seven entries in the KB, mostly fixes. One of them is not a fix: it is a new server configuration option, max lock manager cache memory (%). Reference: https://support.microsoft.com/en-us/servicing/sql/sql-server-2025/cumulative-update/kb5084896-cu5 I built a lab on CU5 and on later builds up [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-2025-cu5-max-lock-manager-cache-memory-what-i-could-reproduce-and-what-i-could-not/">SQL Server 2025 CU5: max lock manager cache memory (%): what I could reproduce and what I could not</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">Cumulative Update 5 for SQL Server 2025 was released on May 20, 2026 (build 17.0.4045.5). Seven entries in the KB, mostly fixes. One of them is not a fix: it is a new server configuration option, max lock manager cache memory (%).</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="136" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-31-1024x136.png" alt="" class="wp-image-47678" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-31-1024x136.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-31-766x102.png 766w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-31-300x40.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-31.png 1224w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Reference: <a href="https://support.microsoft.com/en-us/servicing/sql/sql-server-2025/cumulative-update/kb5084896-cu5">https://support.microsoft.com/en-us/servicing/sql/sql-server-2025/cumulative-update/kb5084896-cu5</a></p>



<p class="wp-block-paragraph">I built a lab on CU5 and on later builds up to CU9 and I spent some time measuring. Most of the documentation is confirmed. The sentence that says what the option actually changes, is not: on my lab, under the conditions described below I could not reproduce it. This post is what I measured with the protocol so that you can run it yourself and tell me if you get something different.</p>



<h2 id="h-why-would-released-locks-still-cost-memory" class="wp-block-heading">Why would released locks still cost memory?</h2>



<p class="wp-block-paragraph">A lock is not one structure in memory, it is two. The lock block describes the locked resource: this row, this page, this table. The lock owner block describes who holds it and in which mode. Three sessions reading the same row give one lock block and three lock owner blocks. sys.dm_tran_locks returns one row per owner block: the resource_* columns describe the lock block, the request_* columns the owner block and lock_owner_address is literally its address in memory.</p>



<p class="wp-block-paragraph">The documentation gives 96 bytes per lock: 64 bytes for the lock block, plus 32 bytes per owner block. On my lab, a row lock costs about 195 bytes all included, the clerk’s pages divided by the number of locks held: twice the documented figure. Part of the gap is known. The 64 bytes are the 32-bit size of the lock block: on 64-bit, Kalen Delaney gives 128 bytes. That leaves about 67 bytes for the owner block, the share of the intent lock on the page and the overhead of the object store. Keep 200 bytes in mind. Ten million row locks are 2 GB.</p>



<p class="wp-block-paragraph">These structures are not database pages. They are stolen memory taken from a pool owned by the lock manager. This pool is visible as the OBJECTSTORE_LOCK_MANAGER memory clerk in sys.dm_os_memory_clerks (one clerk per NUMA node, named Lock Manager : Node 0…) and as the Lock Memory (KB) performance counter. The locks documentation says 2500 lock structures. On my lab, Lock Blocks Allocated reports 3050 right after a restart, on SQL Server 2022 as on 2025.</p>



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



<p class="wp-block-paragraph">Reference : <a href="https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-locks-server-configuration-option?view=sql-server-ver17">https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-locks-server-configuration-option?view=sql-server-ver17</a></p>



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



<pre class="wp-block-code"><code>SELECT
    cntr_value AS lock_blocks_allocated
FROM sys.dm_os_performance_counters
WHERE object_name LIKE N'%:Memory Manager%'
  AND counter_name = N'Lock Blocks Allocated';</code></pre>



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



<p class="wp-block-paragraph">Now the important part. When a transaction commits or rolls back, its locks are released. The lock structures are not returned to SQLOS. They stay in the pool, ready to be reused by the next lock request. This is a cache and the reason is simple: allocating memory for every lock acquisition would be expensive and lock acquisition is on the hot path of every query.</p>



<h2 id="h-the-numbers-to-remember" class="wp-block-heading">The numbers to remember</h2>



<p class="wp-block-paragraph">Four numbers. The documentation expresses the percentages against the &#8220;SQLOS committed memory&#8221;. On my lab, they behave as percentages of committed_target_kb in sys.dm_os_sys_info, the Target Server Memory (KB) counter: what SQLOS is allowed to use, not what it has used so far. Test 1 below shows why I say so.</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Threshold</strong></td><td><strong>Value</strong></td><td><strong>What happens</strong></td></tr><tr><td>Lock escalation per statement</td><td>5000 locks on one table or partition</td><td>The row and page locks of the statement are replaced by one table (or partition) lock</td></tr><tr><td>Lock escalation, memory</td><td>24 % of the engine memory (40 % of the lock memory limit)</td><td>The engine picks active statements and escalates their locks, every 1,250 new locks as long as lock memory stays above the threshold</td></tr><tr><td>Lock memory limit</td><td>60 % of the target</td><td>No more lock structures can be allocated: error 1204, the statement fails and the transaction is rolled back</td></tr><tr><td>Lock manager cache limit (new in CU5)</td><td>20 % of the target by default, 20 to 60 allowed</td><td><strong>The cached structures should fit under this size. Whether and when that is enforced is the subject of this post</strong></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Two remarks. Lock escalation exists precisely to keep lock memory small: 5000 row locks cost about 1 MB, one table lock costs 200 bytes. And the 60 % limit is not a cache limit: it is the maximum memory the lock manager may use, cache included.</p>



<h2 id="h-what-was-the-problem-before-cu5" class="wp-block-heading">What was the problem before CU5?</h2>



<p class="wp-block-paragraph">The cache could grow up to the 60 % limit. It was given back slowly, by a background task and that task could be slow enough for the cache to look permanent.</p>



<p class="wp-block-paragraph">The typical trigger is a workload that acquires a very large number of locks at once. The documentation mentions large concurrent query workloads with lock escalation disabled at the instance level (trace flag 1211). But you do not need a trace flag for that. ALTER TABLE &#8230; SET (LOCK_ESCALATION = DISABLE) on a big table, a reporting query in REPEATABLE READ or SERIALIZABLE scanning millions of rows, a purge that could not escalate because another session held an incompatible lock on the table: any of these can push lock memory to several GB.</p>



<p class="wp-block-paragraph">After the transaction ends, the lock structures stay in the cache. The buffer pool, the plan cache and every other cache have to live with what is left. Page life expectancy drops, physical reads increase, and nothing in the current workload explains it, because the workload that caused it is over.</p>



<p class="wp-block-paragraph">The symptom is easy to recognize once you know it. An idle instance, sys.dm_tran_locks almost empty and OBJECTSTORE_LOCK_MANAGER reporting GB. On an instance with a 128 GB target, the cache can hold up to about 77 GB.</p>



<h2 id="h-what-the-documentation-says-cu5-changes" class="wp-block-heading">What the documentation says CU5 changes</h2>



<p class="wp-block-paragraph">The lock manager cache now has a ceiling and you can configure it:</p>



<pre class="wp-block-code"><code>EXECUTE sp_configure 'show advanced options', 1;
RECONFIGURE;

EXECUTE sp_configure 'max lock manager cache memory (%)', 20;
RECONFIGURE;</code></pre>



<p class="wp-block-paragraph">The server must be restarted before the setting can take effect. The Server configuration options page lists it as A, RR (advanced, restart required): <a href="https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/server-configuration-options-sql-server?view=sql-server-ver17">https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/server-configuration-options-sql-server?view=sql-server-ver17</a></p>



<p class="wp-block-paragraph">What the option limits is the cache, not the locks.</p>



<p class="wp-block-paragraph">As stated in the documentation:</p>



<p class="wp-block-paragraph">Lock manager memory can still grow up to 60 percent of the SQLOS committed memory if required by the workload. However, when locks are released, memory is freed rather than cached if the lock manager cache already reached its configured limit.</p>



<p class="wp-block-paragraph">Reference : <a href="https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/max-lock-manager-cache-memory-configuration-option?view=sql-server-ver17">https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/max-lock-manager-cache-memory-configuration-option?view=sql-server-ver17</a></p>



<p class="wp-block-paragraph">It says two things. A transaction may still take up to 60 % of the memory in locks: true (test 1). And when locks are released while the cache is already at its limit, the memory is freed instead of cached: this is the sentence I could not reproduce (tests 2 and 3).</p>



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



<pre class="wp-block-code"><code>;WITH lm AS
(
    SELECT SUM(pages_kb) AS lock_manager_kb
    FROM sys.dm_os_memory_clerks
    WHERE type = N'OBJECTSTORE_LOCK_MANAGER'
),
si AS
(
    SELECT committed_kb, committed_target_kb
    FROM sys.dm_os_sys_info
),
pc AS
(
    SELECT
        MAX(CASE WHEN counter_name = N'Lock Blocks'                THEN cntr_value END) AS lock_blocks_in_use,
        MAX(CASE WHEN counter_name = N'Lock Blocks Allocated'      THEN cntr_value END) AS lock_blocks_allocated,
        MAX(CASE WHEN counter_name = N'Lock Memory (KB)'           THEN cntr_value END) AS lock_memory_kb,
        MAX(CASE WHEN counter_name = N'Database Cache Memory (KB)' THEN cntr_value END) AS database_cache_kb,
        MAX(CASE WHEN counter_name = N'Free Memory (KB)'           THEN cntr_value END) AS free_memory_kb
    FROM sys.dm_os_performance_counters
    WHERE object_name LIKE N'%Memory Manager%'
)
SELECT
    SYSDATETIME()  AS measured_at,
    lm.lock_manager_kb / 1024  AS lock_manager_mb,
    si.committed_kb / 1024  AS committed_mb,
    si.committed_target_kb / 1024 AS target_mb,
    CAST(100.0 * lm.lock_manager_kb / si.committed_target_kb AS DECIMAL(5, 1)) AS lock_manager_pct,
    pc.lock_blocks_in_use,
    pc.lock_blocks_allocated,
    pc.lock_blocks_allocated - pc.lock_blocks_in_use AS lock_blocks_cached,
    pc.database_cache_kb / 1024 AS buffer_pool_mb,
    pc.free_memory_kb / 1024 AS free_mb
FROM lm
CROSS JOIN si
CROSS JOIN pc;</code></pre>



<p class="wp-block-paragraph"><strong>How to read it:</strong></p>



<ul class="wp-block-list">
<li>lock_manager_mb is the whole lock manager memory: locks in use plus cached structures. On an idle instance, it is the size of the cache. This is the measure that matters.</li>



<li>committed_mb is what SQLOS has taken from the operating system so far. target_mb is what it is allowed to take. On an instance that has been running for a while, the two are equal. On a freshly restarted lab, they are far apart and nothing in the percentages makes sense until they meet.</li>



<li><strong>lock_manager_pct</strong> is the clerk against the target. The 20 % and the 60 % are read on this column.</li>



<li>buffer_pool_mb and free_mb show who pays for the cache, and who gets the memory back.</li>



<li>lock_blocks_in_use, lock_blocks_allocated and their difference lock_blocks_cached are the performance counters on lock blocks. They are coherent on a freshly started instance (3050 allocated, 0 in use) and stop refreshing after a lock storm on every 2025 build I tested: 6 624 047 allocated blocks reported while the clerk held 1 MB. They are here so that you can see it. Do not use them as evidence, sys.dm_tran_locks is the truth for locks held, the clerk for memory.</li>
</ul>



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



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



<p class="wp-block-paragraph">SQL Server 2025 Developer Edition, CU5 (17.0.4045.5) first, then CU9 (17.0.5005.3), on virtual machines of a Proxmox lab and on a Nutanix environment. One SQL Server 2022 instance for comparison, in test 5. The SQLOS target is 4096 MB unless stated otherwise (2048 MB in test 4), with min server memory and max server memory pinned to the same value.</p>



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



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



<p class="wp-block-paragraph">A database with one narrow table. Lock escalation is disabled on the table: we want millions of row locks, not one table lock. This is the scenario the documentation describes at table level instead of trace flag 1211:</p>



<pre class="wp-block-code"><code>CREATE DATABASE LockCacheDemo
GO
ALTER DATABASE LockCacheDemo SET RECOVERY SIMPLE
GO
USE LockCacheDemo
GO
CREATE TABLE dbo.T_Locks
(
    id     INT      NOT NULL,
    filler CHAR(20) NOT NULL CONSTRAINT DF_T_Locks_filler DEFAULT ('x'),
    CONSTRAINT PK_T_Locks PRIMARY KEY CLUSTERED (id)
)
GO
INSERT INTO dbo.T_Locks WITH (TABLOCK) (id)
SELECT value
FROM GENERATE_SERIES(1, 30000000)
GO
ALTER TABLE dbo.T_Locks SET (LOCK_ESCALATION = DISABLE)
GO</code></pre>



<p class="wp-block-paragraph">Thirty million rows, about 1 GB. Nothing in this block touches the lock manager: the TABLOCK on the insert takes one table lock to load fast. This block only provides lockable rows and removes the mechanism that would stop us from locking them one by one.</p>



<p class="wp-block-paragraph">The workload (session A):</p>



<pre class="wp-block-code"><code>USE LockCacheDemo
GO
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
GO
DECLARE @rows INT = 1000000;

BEGIN TRANSACTION

SELECT COUNT(*) AS rows_locked
FROM (
    SELECT TOP (@rows) id
    FROM dbo.T_Locks WITH (ROWLOCK)
    ORDER BY id
) AS x

-- Leave the transaction open. Measure from session B. Then:
-- ROLLBACK TRANSACTION</code></pre>



<p class="wp-block-paragraph"><strong>Three different settings:</strong></p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Setting</strong></td><td><strong>Why</strong></td></tr><tr><td>LOCK_ESCALATION = DISABLE on the table</td><td>Keeps SQL Server from swapping the row locks for one table lock at 5000 locks.</td></tr><tr><td>WITH (ROWLOCK)</td><td>One lock per row. Otherwise SQL Server may lock pages: one lock for 245 rows.</td></tr><tr><td>REPEATABLE READ</td><td>Every shared lock is kept until the end of the transaction. In READ COMMITTED it is released as soon as the row is read.</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">From a second session (session B): did we get row locks and did escalation stay out?</p>



<pre class="wp-block-code"><code>SELECT resource_type, request_mode, COUNT(*) AS lock_count
FROM sys.dm_tran_locks
WHERE request_session_id = 65   -- The session holding the locks
GROUP BY resource_type, request_mode
ORDER BY lock_count DESC;
</code></pre>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>resource_type</strong></td><td><strong>request_mode</strong></td><td><strong>lock_count</strong></td></tr><tr><td>KEY</td><td>S</td><td>1 000 000</td></tr><tr><td>PAGE</td><td>IS</td><td>4 082</td></tr><tr><td>OBJECT</td><td>IS</td><td>1</td></tr><tr><td>DATABASE</td><td>S</td><td>1</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">One shared lock per row (KEY, the row of a clustered index). One intent lock per page, about 245 rows per page. One intent lock on the table and no OBJECT S: no escalation.</p>



<p class="wp-block-paragraph">Second check, the cost of a lock. Measured on a restarted instance each time so that nothing was cached before the run:</p>



<pre class="wp-block-code"><code>DECLARE @spid    INT    = 65;  -- session A
DECLARE @idle_kb BIGINT = 0;   -- clerk before the transaction

SELECT
    l.lock_count,
    l.key_locks,
    c.lock_manager_kb / 1024                                  AS lock_manager_mb,
    (c.lock_manager_kb - @idle_kb) * 1024.0 / l.lock_count    AS bytes_per_structure,
    (c.lock_manager_kb - @idle_kb) * 1024.0 / l.key_locks     AS bytes_per_row_lock
FROM (
    SELECT COUNT_BIG(*)                                            AS lock_count,
           SUM(CASE WHEN resource_type = N'KEY' THEN 1 ELSE 0 END) AS key_locks
    FROM sys.dm_tran_locks
    WHERE request_session_id = @spid
) AS l
CROSS JOIN (
    SELECT SUM(pages_kb) AS lock_manager_kb
    FROM sys.dm_os_memory_clerks
    WHERE type = N'OBJECTSTORE_LOCK_MANAGER'
) AS c;</code></pre>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Rows locked</strong></td><td><strong>lock_manager_mb</strong></td><td><strong>Bytes per row lock</strong></td></tr><tr><td>1 000 000</td><td>187</td><td>196</td></tr><tr><td>5 000 000</td><td>932</td><td>195</td></tr><tr><td>6 000 000</td><td>1 117</td><td>195</td></tr><tr><td>9 000 000</td><td>1 677</td><td>195</td></tr></tbody></table></figure>



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



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="476" height="45" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-38.png" alt="" class="wp-image-47685" style="aspect-ratio:10.577777777777778;width:476px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-38.png 476w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-38-300x28.png 300w" sizes="auto, (max-width: 476px) 100vw, 476px" /></figure>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="473" height="44" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-39.png" alt="" class="wp-image-47686" style="aspect-ratio:10.75;width:473px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-39.png 473w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-39-300x28.png 300w" sizes="auto, (max-width: 473px) 100vw, 473px" /></figure>



<p class="wp-block-paragraph">Four runs, the same figure: about 195 bytes per row lock, everything included. Twice the 96 bytes of the documentation (64 for the lock block, 32 per owner block).</p>



<h2 id="h-test-1-the-60-wall-has-not-moved" class="wp-block-heading">Test 1: The 60 % wall has not moved</h2>



<p class="wp-block-paragraph">The name of the option invites a misunderstanding: it is not a limit on locks. Option at its default of 20 and @rows = 70 000 000 far beyond what 60 % of the target can hold (Target at 4968 MB for this run). The lock manager climbs.</p>



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



<p class="wp-block-paragraph">The probe shows 20 %, then 24 % without any escalation (the table option prevents it), then 40 %, 50 % and the statement dies:</p>



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



<p class="wp-block-paragraph">Severity 19: the statement is terminated and the transaction is rolled back. The same message lands in the SQL Server error log and in the Windows application log.</p>



<p class="wp-block-paragraph">Option at its default of 20 and @rows = 70 000 000, more than the 30 million rows of the table and far beyond what 60 % of the target can hold (Target at 4968 MB for this run): the wall is at about 16 million row locks.</p>



<p class="wp-block-paragraph">This test also settled which memory figure the percentages refer to. On the last probe before the error, the clerk was at 2979 MB: 60 % of the 4968 MB target, and 70 % of the 4233 MB SQLOS had committed at that moment. A 60 % limit on the committed memory would have fired about 440 MB earlier, at 2540 MB. The 60 % limit, and with it the 20 % one, are percentages of the target, not of the memory committed so far. This is why the probe divides by committed_target_kb.</p>



<h2 id="h-test-2-releasing-the-locks-frees-nothing" class="wp-block-heading">Test 2: Releasing the locks frees nothing</h2>



<p class="wp-block-paragraph">This is the test I wrote this post for. Option at 20. Target at 4096 MB for this run, so the cache ceiling is 819 MB. Nine million locks:</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Step</strong></td><td><strong>lock_manager_mb</strong></td><td><strong>lock_manager_pct</strong></td><td><strong>committed_mb</strong></td><td><strong>buffer_pool_mb</strong></td></tr><tr><td>Idle, after restart</td><td>0</td><td>0.0</td><td>285</td><td>27</td></tr><tr><td>9 000 000 locks held</td><td>1 677</td><td>40.9</td><td>2 296</td><td>349</td></tr><tr><td>ROLLBACK, a few seconds later</td><td>1 677</td><td>40.9</td><td>2 296</td><td>349</td></tr><tr><td>ROLLBACK, minutes later</td><td>1 677</td><td>40.9</td><td>2 296</td><td>349</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">buffer_pool_mb is the Database Cache Memory (KB) counter: the data and index pages in memory. The 349 MB are the pages of the nine million rows the SELECT had to read to lock them, nothing else touched the buffer pool. sys.dm_tran_locks is empty, the session holds nothing and the clerk has not moved. 1677 MB of lock structures are sitting in the cache, twice the configured ceiling. The ROLLBACK did not free anything and neither did the minutes after it.</p>



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



<p class="wp-block-paragraph">I ran this on CU5 and on CU9, with 5, 6, 9 and 12 million locks with targets of 2 048 and 4 096 MB. Same result every time. The last run, 12 million locks on CU9 left 2193 MB in the cache, 54 % of the target and they were still there hours later.</p>



<h2 id="h-test-3-later-transactions-free-nothing-either" class="wp-block-heading">Test 3: Later transactions free nothing either</h2>



<p class="wp-block-paragraph">There is a second way to read the sentence of the documentation. Not the release of the storm but the releases that follow: once the cache is above its limit, every structure released by a later transaction should be freed instead of cached and the cache would erode with the lock activity of the workload, down to 20 %. &#8220;Large concurrent query workloads&#8221; as the documentation puts it.</p>



<p class="wp-block-paragraph">A first attempt with UPDATE TOP (1000000) &#8230; SET filler = &#8216;y&#8217; and a COMMIT, repeated, lowered the clerk by a few MB per pass. Not conclusive: without a ROWLOCK hint, the engine is free to take page locks for an update of this size and then only a few thousand structures travel through the cache at each pass.</p>



<p class="wp-block-paragraph">So the proper version, with row locks guaranteed and verified. Cache at 40.9 % of the target after a storm, option at 20, and short transactions in a loop:</p>



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

SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

DECLARE @i INT = 0, @n INT;

WHILE @i &lt; 100
BEGIN
    BEGIN TRANSACTION;
    SELECT @n = COUNT(*)
    FROM (SELECT TOP (200000) id
          FROM dbo.T_Locks WITH (ROWLOCK)
          WHERE id &gt; (@i % 50) * 200000
          ORDER BY id) AS x;
    COMMIT;            -- 200 000 structures released
    SET @i += 1;
END</code></pre>



<p class="wp-block-paragraph">One hundred transactions, 200,000 row locks each, verified in sys.dm_tran_locks while the loop ran: twenty million lock releases, more than four times the 4.6 million structures sitting above the ceiling (858 MB).</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Step</strong></td><td><strong>lock_manager_mb</strong></td><td><strong>lock_manager_pct</strong></td></tr><tr><td>Before the loop</td><td>1 677</td><td>40.9</td></tr><tr><td>After 20 000 000 releases</td><td>1 677</td><td>40.9</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The clerk did not move. Twenty million releases while the cache was more than twice its limit and the memory stayed. Whatever &#8220;when locks are released, memory is freed rather than cached&#8221; describes, it is not something I can observe with row locks, on CU5 or CU9, with the option at its default.</p>



<h2 id="h-test-4-what-does-shrink-the-cache" class="wp-block-heading">Test 4: What does shrink the cache</h2>



<p class="wp-block-paragraph">Two things did, during the week. None of them is the sentence of the documentation. Memory demand, once. Target at 2 048 MB, option at 20, six million locks, ROLLBACK: 1 117 MB in the cache, 54.6 %. Then a NOLOCK scan of a 17 GB table which takes one schema stability lock and nothing else but needs every page in the buffer pool:</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Step</strong></td><td><strong>lock_manager_mb</strong></td><td><strong>lock_manager_pct</strong></td><td><strong>committed_mb</strong></td><td><strong>target_mb</strong></td><td><strong>buffer_pool_mb</strong></td></tr><tr><td>6 000 000 locks held</td><td>1 117</td><td>54.6</td><td>1 549</td><td>2 048</td><td>203</td></tr><tr><td>After ROLLBACK</td><td>1 117</td><td>54.6</td><td>1 549</td><td>2 048</td><td>203</td></tr><tr><td>After the NOLOCK scan</td><td>332</td><td>16.2</td><td>2 048</td><td>2 048</td><td>1 408</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The scan pushed committed_mb up to the target at that moment the buffer pool still wanted pages, SQLOS had nothing left to take from the operating system and it took from the lock manager: 785 MB given back, the clerk under the ceiling. Not one lock released in between. That looked like the mechanism. It was not reproducible either: on CU9 with the cache at 2 193 MB and the instance already at its 4 096 MB target, the same 17 GB scan left the cache at 2 190 MB. sys.dm_os_ring_buffers tells why: the memory broker that governs the lock manager cache (MEMORYBROKER_FOR_CACHE, which is where the object store lives, not MEMORYBROKER_FOR_STEAL) kept sending it a target of 2 809 MB, 68 % of the target with SHRINK notifications of a few MB. There is no 20 % anywhere in the broker&#8217;s arithmetic.</p>



<p class="wp-block-paragraph">DBCC FREESYSTEMCACHE, instantly<strong>.</strong> The documentation only shows &#8216;ALL&#8217;, which releases the unused entries of every cache, plan cache included, at the price of recompiling every plan afterwards. It also says that most caches can be freed individually. The lock manager cache is one of them:</p>



<pre class="wp-block-code"><code>SELECT name, pages_kb / 1024 AS mb
FROM sys.dm_os_memory_clerks
WHERE name LIKE N'Lock Manager%'; 

DBCC FREESYSTEMCACHE('Lock Manager : Node 0');</code></pre>



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



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="187" height="58" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-43.png" alt="" class="wp-image-47690" /></figure>



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



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="175" height="60" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/image-44.png" alt="" class="wp-image-47691" /></figure>



<p class="wp-block-paragraph">Probe right after, on the 1677 MB cache left by test 3: 8 MB in the clerk, and the memory back in free_mb. The cache is releasable at any time in one instruction. What is missing on these builds is not the ability to release it, it is the trigger.</p>



<h2 id="h-test-5-sql-server-2022-does-the-same-without-the-option" class="wp-block-heading">Test 5: SQL Server 2022 does the same without the option</h2>



<p class="wp-block-paragraph">If the option changed something, the comparison with a version that does not have it should show it. Same table, same workload, @rows = 70000000 for the wall and a value under the wall for the clean ROLLBACK, on SQL Server 2022 and on SQL Server 2025 CU5, CU9 with the option at 20:</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><strong>Scenario</strong></td><td><strong>SQL Server 2022</strong></td><td><strong>SQL Server 2025 CU5, CU9, option at 20</strong></td></tr><tr><td>Locks acquired below the wall then ROLLBACK</td><td>Memory given back progressively, down to 17.6 %</td><td>Memory given back very progressively</td></tr><tr><td>60 % wall, error 1204</td><td>Memory given back very progressively</td><td>Descent very, very slow, then stagnation</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Two things in this table. The background release exists on 2022, without any option, and it brings the cache down to 17.6 %, which is where the 20 % ceiling of 2025 would put it. And 2025 is not faster than 2022 at giving the memory back.</p>



<h2 id="h-what-the-tests-say" class="wp-block-heading">What the tests say</h2>



<p class="wp-block-paragraph">Put together, on SQL Server 2025 CU5 and CU9 with one big transaction and then normal lock activity:</p>



<ul class="wp-block-list">
<li>Lock memory, cache included, can grow up to 60 % of the target. Beyond that, error 1204. Confirmed and unchanged by the option.</li>



<li>When the storm is released, nothing is freed. The structures go to the cache above the configured ceiling and stay there for hours.</li>



<li>The lock activity that follows does not free them either: twenty million row-lock releases above the ceiling, no measurable change.</li>



<li>The cache does come down, slowly, through the generic SQLOS mechanisms (background release, memory broker rebalancing when the instance is at its target), the same ones SQL Server 2022 has. On 2025, they are not faster, and the 20 % figure does not appear in the broker&#8217;s targets.</li>



<li>DBCC FREESYSTEMCACHE(&#8216;Lock Manager : Node N&#8217;) empties the cache immediately on any version I tried.</li>
</ul>



<p class="wp-block-paragraph">So the documented behavior, &#8220;when locks are released, memory is freed rather than cached if the lock manager cache already reached its configured limit&#8221;, is not something I could reproduce. Not on CU5, not on CU9, not with the release of the big transaction, not with twenty million releases afterwards. The option is there, it accepts 20 to 60, value_in_use says 20 and the cache sits around 40% for as long as I care to watch.</p>



<p class="wp-block-paragraph">Two caveats, because this is a lab and not a support case. One access pattern: a single transaction holding millions of row locks then released. Virtual machines with 2 and 4 GB targets. Developer Edition. It is possible that the mechanism needs a condition I did not create and that &#8220;large concurrent query workloads&#8221; means something more specific than what I ran. If you reproduce the protocol above and see the cache come back under 20 % after a release, I want to hear about it. If you do not, you have the same question I have.</p>



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



<ul class="wp-block-list">
<li>Released locks are not freed, they are cached by the lock manager. A row lock costs about 195 bytes all included.</li>



<li>The thresholds (24 % escalation, 60 % hard limit, 20 % cache ceiling) are read against committed_target_kb, not committed_kb. On a lab, set min and max server memory before measuring anything.</li>



<li>CU5 adds max lock manager cache memory (%): 20 by default, 20 to 60 allowed, a restart to take effect (is_dynamic = 0).</li>



<li>The 60 % wall and error 1204 are unchanged by the option.</li>



<li>On CU5 and CU9, releasing the locks does not bring the cache back under the ceiling and neither do twenty million later releases. The documented behavior was not reproducible on my lab.</li>



<li>The cache does shrink through the generic SQLOS mechanisms slowly as on SQL Server 2022 and the memory broker targets it well above 20 %.</li>



<li>DBCC FREESYSTEMCACHE(&#8216;Lock Manager : Node N&#8217;) empties it instantly (on any version I tried). It is the remedy that works.</li>



<li>The lock block performance counters stop refreshing on these builds. Use the clerk and sys.dm_tran_locks.</li>
</ul>



<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/sql-server-2025-cu5-max-lock-manager-cache-memory-what-i-could-reproduce-and-what-i-could-not/">SQL Server 2025 CU5: max lock manager cache memory (%): what I could reproduce and what I could not</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-2025-cu5-max-lock-manager-cache-memory-what-i-could-reproduce-and-what-i-could-not/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Setup an LDAP server for Oracle naming on Enterprise Linux 9</title>
		<link>https://www.dbi-services.com/blog/setup-an-ldap-server-for-oracle-naming-on-enterprise-linux-9/</link>
					<comments>https://www.dbi-services.com/blog/setup-an-ldap-server-for-oracle-naming-on-enterprise-linux-9/#respond</comments>
		
		<dc:creator><![CDATA[Martin Bracher]]></dc:creator>
		<pubDate>Fri, 18 Sep 2026 12:27:08 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[Oracle]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46966</guid>

					<description><![CDATA[<p>Introduction To centralise Oracle naming resolution, you can use an LDAP server. You can use the commercial products from Oracle like Oracle Internet Directory (OID) or Oracle Universal Directory (OUD). Alternatively, you can also use OpenLDAP from your Linux distribution. I have used this successfully with customers for many years. I now wanted to build [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/setup-an-ldap-server-for-oracle-naming-on-enterprise-linux-9/">Setup an LDAP server for Oracle naming on Enterprise Linux 9</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-introduction" class="wp-block-heading">Introduction</h2>



<p class="wp-block-paragraph">To centralise Oracle naming resolution, you can use an LDAP server. You can use the commercial products from Oracle like Oracle Internet Directory (OID) or Oracle Universal Directory (OUD). Alternatively, you can also use OpenLDAP from your Linux distribution. I have used this successfully with customers for many years.</p>



<p class="wp-block-paragraph">I now wanted to build a new test environment for myself on an Enterprise Linux 9 system (RedHat, Oracle Linux). But then I noticed that the opendap server is no longer available. After some internet searching, I found the following blog post that describes how to install OpenLDAP on EL9: <a href="https://seanstuber.com/2019/12/29/setting-up-openldap-on-oracle-linux-8-for-tns-name-resolution/">https://seanstuber.com/2019/12/29/setting-up-openldap-on-oracle-linux-8-for-tns-name-resolution/</a></p>



<p class="wp-block-paragraph">But is there no way to do this with on-board tools? A further search then revealed that RedHat has switched to 389-ds-base (<a href="https://www.port389.org/">https://www.port389.org/</a>). So I want to try it with this native tool. And as I fear, almost everything is different here again&#8230; To spare you this try-and-error, I&#8217;ll give you the commands here on how to get an executable environment.</p>



<h2 id="h-installation-and-configuration" class="wp-block-heading">Installation and configuration</h2>



<p class="wp-block-paragraph">First of all, install the software:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; gutter: false; title: ; notranslate">
&#x5B;root@ol9b ~]# dnf install -y 389-ds-base openldap-clients
</pre></div>


<p class="wp-block-paragraph">Caution! I stongly recommend to do a <code>dnf upgrade</code> before. I had the problem that you couldn&#8217;t log in with ssh afterwards and the ssh daemon couldn&#8217;t be restarted.</p>



<pre class="wp-block-code"><code>OpenSSL version mismatch. Built against 30000070, you have 30500080</code></pre>



<p class="wp-block-paragraph">At least a <code>dnf upgrade "openssh*"</code> was required and restart of sshd.</p>



<p class="wp-block-paragraph">Afterwards, you can configure and start the LDAP server. We will use &#8220;orcladmin&#8221; as administrative user and &#8220;dbi&#8221; as instance name.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; gutter: false; title: ; notranslate">
&#x5B;root@ol9b ~]# dscreate interactive
Install Directory Server (interactive mode)
===========================================

Enter system&#039;s hostname &#x5B;ol9b]:

Enter the instance name &#x5B;ol9b]: dbi

Enter port number &#x5B;389]:

Create self-signed certificate database &#x5B;yes]:

Enter secure port number &#x5B;636]:

Enter Directory Manager DN &#x5B;cn=Directory Manager]: cn=orcladmin

Enter the Directory Manager password:
Confirm the Directory Manager Password:

Choose whether mdb or bdb is used. &#x5B;bdb]:

Enter the database suffix (or enter &quot;none&quot; to skip) &#x5B;dc=ol9b]: none

Do you want to start the instance after the installation? &#x5B;yes]:

Are you ready to install? &#x5B;no]: yes
Starting installation ...
Validate installation settings ...
Create file system structures ...
Create self-signed certificate database ...
Perform SELinux labeling ...
Perform post-installation tasks ...
Completed installation for instance: slapd-dbi

&#x5B;root@ol9b ~]#  systemctl status dirsrv@dbi
● dirsrv@dbi.service - 389 Directory Server dbi.
     Loaded: loaded (/usr/lib/systemd/system/dirsrv@.service; enabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/dirsrv@.service.d
             └─custom.conf
...
</pre></div>


<p class="wp-block-paragraph">After configuration, the daemon is already started and the service is also set to enabled. If you use a firewall on your server, open the ldap ports:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; gutter: false; title: ; notranslate">
&#x5B;root@ol9b ~]# firewall-cmd --permanent --add-service=ldap
&#x5B;root@ol9b ~]# firewall-cmd --permanent --add-service=ldaps
&#x5B;root@ol9b ~]# firewall-cmd --reload
</pre></div>


<p class="wp-block-paragraph">Now we have to extend the schema with the extensions for Oracle naming resolution. Since Oracle 26ai (23.26), a schema file is available in $ORACLE_HOME/network/ldap/schema/openldap/orclnetnaming.schema; This file has to be converted to an ldif file for the ldapmodify tool. From:</p>



<pre class="wp-block-code"><code>attribute ( 2.16.840.1.113894.7.1.1
  NAME 'orclVersion'
  EQUALITY octetStringMatch
  SYNTAX '1.3.6.1.4.1.1466.115.121.1.40' )

attribute ( 2.16.840.1.113894.7.1.2
  NAME 'orclOracleHome'
  EQUALITY octetStringMatch
  SYNTAX '1.3.6.1.4.1.1466.115.121.1.40' )
...</code></pre>



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



<pre class="wp-block-code"><code>dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.7.1.1
  NAME 'orclVersion'
  EQUALITY octetStringMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.7.1.2
  NAME 'orclOracleHome'
  EQUALITY octetStringMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
...</code></pre>



<p class="wp-block-paragraph">(the full version you will find at the end of this post)</p>



<p class="wp-block-paragraph">And now you can load this schema extensions</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; gutter: false; title: ; notranslate">
&#x5B;root@ol9b ~]# ldapmodify -x -H ldap://ol9b:389 -D &quot;cn=orcladmin&quot; -W -f orclnetnaming.ldif
Enter LDAP Password:
modifying entry &quot;cn=schema&quot;
</pre></div>


<p class="wp-block-paragraph">We will create the DN dbi-services,dn=com</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; gutter: false; title: ; notranslate">
&#x5B;root@ol9b ~]# dsconf dbi backend create --suffix=&quot;dc=com&quot; --be-name=&quot;com&quot; --create-suffix
The database was sucessfully created

&#x5B;root@ol9b ~]# ldapadd -x -H ldap://ol9b:389 -D &quot;cn=orcladmin&quot;  -W &lt;&lt;EOD
dn: dc=dbi-services,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
o: Oracle Internet Directory
dc: dbi-services

dn: cn=OracleContext,dc=dbi-services,dc=com
objectclass: orclContext
cn: OracleContext
EOD
Enter LDAP Password:
adding new entry &quot;dc=dbi-services,dc=com&quot;

adding new entry &quot;cn=OracleContext,dc=dbi-services,dc=com&quot;
</pre></div>


<p class="wp-block-paragraph">Oracle tools need the so-called &#8220;anonymous bind&#8221; so that they can query the data without logging in. We still have to activate this.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; gutter: false; title: ; notranslate">
&#x5B;root@ol9b ~]# ldapmodify -x -H ldap://ol9b:389 -D &quot;cn=orcladmin&quot;  -W &lt;&lt;EOD
 dn: dc=dbi-services,dc=com
changetype: modify
add: aci
aci: (targetattr = &quot;*&quot;)(version 3.0; acl &quot;Anonymous read access&quot;; allow (read, search, compare) userdn = &quot;ldap:///anyone&quot;;)
EOD
</pre></div>


<p class="wp-block-paragraph">The configuration is now complete, and we can insert the first sqlnet alias</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; gutter: false; title: ; notranslate">
&#x5B;root@ol9b ~]# ldapadd -x -H ldap://ol9b:389 -D &quot;cn=orcladmin&quot;  -W &lt;&lt;EOD
dn: cn=DB1970,cn=OracleContext,dc=dbi-services,dc=com
objectclass: top
objectclass: orclNetService
cn: DB1970
orclNetDescString: (DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=ltp14s-i)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=DB1970A.erfinder)))
EOD
Enter LDAP Password:
adding new entry &quot;cn=DB1970,cn=OracleContext,dc=dbi-services,dc=com&quot;
</pre></div>


<p class="wp-block-paragraph">Hint: this is the syntax of OpenLdap. As oracle user, use /usr/bin/ldapadd, or adapt the syntax for Oracle&#8217;s ldapadd.</p>



<h2 id="h-testing" class="wp-block-heading">Testing</h2>



<p class="wp-block-paragraph">Now it gets exciting: does the Oracle name resolution work?</p>



<p class="wp-block-paragraph">Probably we have to adapt our ldap.ora and sqlnet.ora files. For the test, I will create a temporary TNS_ADMIN directory.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; gutter: false; title: ; notranslate">
&#x5B;oracle@ltp14 ~]# mkdir /tmp/ldap
&#x5B;oracle@ltp14 ~]# export TNS_ADMIN=/tmp/ldap

&#x5B;oracle@ltp14 ~]# echo &#039;DIRECTORY_SERVERS= (ol9b:389:636)
DEFAULT_ADMIN_CONTEXT = &quot;dc=dbi-services,dc=com&quot;
DIRECTORY_SERVER_TYPE = OID&#039; &gt;$TNS_ADMIN/ldap.ora

&#x5B;oracle@ltp14 ~]# echo &#039;NAMES.DEFAULT_DOMAIN=dbi-services.com
NAMES.DIRECTORY_PATH=(LDAP)&#039; &gt;sqlnet.ora

&#x5B;oracle@ltp14 ~]# tnsping DB1970

TNS Ping Utility for Linux: Version 19.0.0.0.0 - Production on 18-SEP-2026 13:48:26

Copyright (c) 1997, 2019, Oracle.  All rights reserved.

Used parameter files:
/u00/app/oracle/network/admin/ldapdbi/sqlnet.ora

Used LDAP adapter to resolve the alias
Attempting to contact (DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=ltp14s-i)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=DB1970A.erfinder)))
OK (0 msec)
</pre></div>


<p class="wp-block-paragraph">And YES! it works 🙂</p>



<p class="wp-block-paragraph">Disclaimer: I&#8217;m not the expert of LDAP. I wanted to be able to quickly set up an LDAP server for Oracle name resolution. The setup certainly still has some optimization potential. Feel free to adapt it for your needs.</p>



<h2 id="h-annex" class="wp-block-heading">Annex</h2>



<p class="wp-block-paragraph">orclnetnaming.ldif (converted from 26ai $ORACLE_HOME/network/ldap/schema/openldap/orclnetnaming.schema)</p>



<pre class="wp-block-code"><code>dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.7.1.1 NAME 'orclVersion' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.7.1.2 NAME 'orclOracleHome' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.7.1.3 NAME 'orclSystemName' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.7.1.4 NAME 'orclServiceType' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.7.1.5 NAME 'orclSid' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.7.1.6 NAME 'orclProductVersion' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.1 NAME 'orclNetSourceRoute' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.2 NAME 'orclNetLoadBalance' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.3 NAME 'orclNetFailover' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.4 NAME 'orclNetSdu' EQUALITY numericStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.5 NAME 'orclNetServer' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.6 NAME 'orclNetServiceName' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.7 NAME 'orclNetInstanceName' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.8 NAME 'orclNetHandlerName' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.9 NAME 'orclNetParamList' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.10 NAME 'orclNetAuthenticationType' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.11 NAME 'orclNetAuthParams' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.12 NAME 'orclNetDescName' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.13 NAME 'orclNetDescString' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.14 NAME 'orclNetAddressString' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.15 NAME 'orclNetProtocol' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.16 NAME 'orclNetShared' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.17 NAME 'orclNetAddrList' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.18 NAME 'orclNetProtocolStack' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.19 NAME 'orclNetDescList' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.20 NAME 'orclNetConnParamList' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: attributeTypes
attributeTypes: ( 2.16.840.1.113894.3.1.21 NAME 'orclNetAuthenticationService' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
-
add: objectClasses
objectClasses: ( 2.16.840.1.113894.7.2.3 NAME 'orclContext' SUP top MAY ( cn $   orclNetDescName ) )
-
add: objectClasses
objectClasses: ( 2.16.840.1.113894.7.2.6 NAME 'orclSchemaVersion' SUP top MUST ( cn $   orclProductVersion ) )
-
add: objectClasses
objectClasses: ( 2.16.840.1.113894.3.2.1 NAME 'orclNetAddress' SUP top MUST (cn) MAY ( orclNetAddressString $ orclNetProtocol $ orclNetShared $ orclVersion $description) )
-
add: objectClasses
objectClasses: ( 2.16.840.1.113894.3.2.2 NAME 'orclNetAddressList' SUP top MUST (cn)
     MAY ( orclNetAddrList $ orclNetSourceRoute $ orclNetLoadBalance $ orclNetFailover $ orclNetShared $ orclVersion $ description ) )
-
add: objectClasses
objectClasses: ( 2.16.840.1.113894.3.2.3
     NAME 'orclNetDescription' SUP top
     MUST (cn)
     MAY ( orclVersion  $ orclNetSourceRoute  $  orclNetSdu $ orclNetServer $ orclNetServiceName $ orclNetInstanceName $ orclNetHandlerName $ orclNetParamList $ orclNetAuthenticationType $ orclNetAuthParams $ orclNetShared $ orclNetAddrList $  orclNetProtocolStack $ orclNetConnParamList $ orclNetAuthenticationService $ orclNetLoadBalance $   orclNetDescList $ orclNetFailover $ orclNetShared $ orclNetAddrList ) )
-
add: objectClasses
objectClasses: ( 2.16.840.1.113894.3.2.30
     NAME 'orclNetDescriptionAux1' SUP top
     AUXILIARY MUST (cn)
     MAY ( orclVersion  $ orclNetSourceRoute  $  orclNetSdu $ orclNetServer $ orclNetServiceName $ orclNetInstanceName $ orclNetHandlerName $ orclNetParamList $ orclNetAuthenticationType $ orclNetAuthParams $ orclNetShared $ orclNetAddrList $  orclNetProtocolStack $ orclNetConnParamList $ orclNetAuthenticationService $ orclNetLoadBalance $   orclNetDescList $ orclNetFailover $ orclNetShared $ orclNetAddrList ) )
-
add: objectClasses
objectClasses: ( 2.16.840.1.113894.3.2.4
     NAME 'orclNetDescriptionList' SUP top
     MUST (cn)
     MAY ( orclNetLoadBalance $   orclNetDescList $ orclNetFailover $ orclNetShared $ orclNetAddrList ) )
-
add: objectClasses
objectClasses: ( 2.16.840.1.113894.3.2.5
     NAME 'orclNetService' SUP top
     MUST (cn)
     MAY ( orclNetServiceName $ orclNetDescString $ orclNetDescName $ orclNetSourceRoute ) )</code></pre>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/setup-an-ldap-server-for-oracle-naming-on-enterprise-linux-9/">Setup an LDAP server for Oracle naming on Enterprise Linux 9</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/setup-an-ldap-server-for-oracle-naming-on-enterprise-linux-9/feed/</wfw:commentRss>
			<slash:comments>0</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>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/#comments</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>1</slash:comments>
		
		
			</item>
		<item>
		<title>PostgreSQL Snapshot Backup and Restore with Proxmox ZFS (4/4)</title>
		<link>https://www.dbi-services.com/blog/postgresql-snapshot-backup-and-restore-with-proxmox-zfs-4-4/</link>
					<comments>https://www.dbi-services.com/blog/postgresql-snapshot-backup-and-restore-with-proxmox-zfs-4-4/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 22:13:44 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[proxmox]]></category>
		<category><![CDATA[ZFS]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46203</guid>

					<description><![CDATA[<p>In the blog series I previously wrote, I did not answer all the customer&#8217;s questions. The last one was the following: Can this also be applied to PostgreSQL? In short, yes, it is possible. Let&#8217;s see how. Here is the list of the previous blog posts: Partitioning and filesystem We will reuse the sqlpool ZFS [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-snapshot-backup-and-restore-with-proxmox-zfs-4-4/">PostgreSQL Snapshot Backup and Restore with Proxmox ZFS (4/4)</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">In the blog series I previously wrote, I did not answer all the customer&#8217;s questions. The last one was the following:</p>



<p class="wp-block-paragraph"><strong>Can this also be applied to PostgreSQL? </strong></p>



<p class="wp-block-paragraph">In short, yes, it is possible. Let&#8217;s see how.</p>



<p class="wp-block-paragraph">Here is the list of the previous blog posts:</p>



<ul class="wp-block-list">
<li><a href="https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs/">https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs/</a></li>



<li><a href="https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/">https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/</a></li>



<li><a href="https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-rest-api-with-sql-server-2025-3-3/">https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-rest-api-with-sql-server-2025-3-3/</a></li>
</ul>



<h2 id="h-partitioning-and-filesystem" class="wp-block-heading">Partitioning and filesystem</h2>



<p class="wp-block-paragraph">We will reuse the sqlpool ZFS pool created in the first part of this series.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="134" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-1-1024x134.png" alt="" class="wp-image-46207" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-1-1024x134.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-1-300x39.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-1-768x100.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-1.png 1187w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">We identify the 300 GB disk attached to the VM. In our case, it is /dev/sdb, backed by the sqlpool/pve/vm-307-disk-0 zvol on the Proxmox side:</p>



<pre class="wp-block-code"><code>lsblk</code></pre>



<p class="wp-block-paragraph">We create a single partition of type Linux filesystem:</p>



<pre class="wp-block-code"><code>sudo sgdisk -n 1:0:0 -t 1:8300 /dev/sdb</code></pre>



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



<p class="wp-block-paragraph">We format the partition with XFS, which is the most commonly recommended filesystem for PostgreSQL data directories:</p>



<pre class="wp-block-code"><code>sudo mkfs.xfs -L pgdata /dev/sdb1 -f</code></pre>



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



<p class="wp-block-paragraph">We verify the result:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
ahi@pgl:~$ sudo blkid /dev/sdb1
/dev/sdb1: LABEL=&quot;pgdata&quot; UUID=&quot;028afa2f-7bb3-4a40-92aa-91c1a33f18ae9&quot; BLOCK_SIZE=&quot;512&quot; TYPE=&quot;xfs&quot; PARTUUID=&quot;bd54f285-092e-4b1b-ba5e-6877f054fa7&quot;
</pre></div>


<h3 id="h-mountpoint" class="wp-block-heading"><strong>Mountpoint:</strong></h3>



<p class="wp-block-paragraph">We create the mount point:</p>



<pre class="wp-block-code"><code>sudo mkdir -p /pgdata</code></pre>



<h3 id="h-" class="wp-block-heading"></h3>



<p class="wp-block-paragraph"><strong>Persistent mount via fstab</strong>:</p>



<p class="wp-block-paragraph">We add the mount entry to /etc/fstab using the filesystem label rather than the device name. The device name (/dev/sdb) may change if disks are added or removed while the label remains stable:</p>



<pre class="wp-block-code"><code>echo 'LABEL=pgdata /pgdata xfs noatime,nodiratime 0 2' | sudo tee -a /etc/fstab
sudo systemctl daemon-reload
sudo mount /pgdata</code></pre>



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



<p class="wp-block-paragraph">We verify that the volume is mounted:</p>



<pre class="wp-block-code"><code>df -h /pgdata</code></pre>



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



<h2 id="h-postgresql-installation" class="wp-block-heading">PostgreSQL installation</h2>



<p class="wp-block-paragraph">We install PostgreSQL 18 from the official PGDG repository, which provides the latest PostgreSQL versions for Ubuntu:</p>



<pre class="wp-block-code"><code>sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
sudo apt install -y postgresql-18</code></pre>



<p class="wp-block-paragraph"><strong>Cluster creation on /pgdata</strong>:</p>



<p class="wp-block-paragraph">The Ubuntu packages create a default cluster under /var/lib/postgresql. This is not what we want. The data files and the WAL must both reside on the ZFS-backed volume, so that a single ZFS snapshot captures a consistent state of the database. If they were on different volumes, the snapshot would not be atomic.</p>



<p class="wp-block-paragraph">We drop the default cluster and recreate it on /pgdata:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
ahi@pgl:~$ sudo pg_dropcluster --stop 18 main
sudo install -d -o postgres -g postgres -m 700 /pgdata/18
sudo pg_createcluster -d /pgdata/18/main 18 main
sudo systemctl enable --now postgresql@18-main
Creating new PostgreSQL cluster 18/main ...
/usr/lib/postgresql/18/bin/initdb -D /pgdata/18/main --auth-local peer --auth-host scram-sha-256 --no-instructions
The files belonging to this database system will be owned by user &quot;postgres&quot;.
This user must also own the server process.

The database cluster will be initialized with locale &quot;en_US.UTF-8&quot;.
The default database encoding has accordingly been set to &quot;UTF8&quot;.
The default text search configuration will be set to &quot;english&quot;.

Data page checksums are enabled.

fixing permissions on existing directory /pgdata/18/main ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default &quot;max_connections&quot; ... 100
selecting default &quot;shared_buffers&quot; ... 128MB
selecting default time zone ... Etc/UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
             Ver Cluster Port Status Owner     Data directory  Log file
             18  main    5432 down   postgres  /pgdata/18/main /var/log/postgresql/postgresql-18-main.log
Created symlink /etc/systemd/system/multi-user.target.wants/postgresql@18-main.service → /usr/lib/systemd/system/postgresql@.service.
</pre></div>


<p class="wp-block-paragraph">We verify that the cluster is online and located on the right volume:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
ahi@pgl:~$ pg_lsclusters
Ver Cluster Port Status Owner     Data directory  Log file
18  main    5432 online &amp;lt;unknown&amp;gt; /pgdata/18/main /var/log/postgresql/postgresql-18-main.log
ahi@pgl:~$ sudo -u postgres psql -c &quot;SHOW data_directory;&quot;
 data_directory
-----------------
 /pgdata/18/main
(1 row)

ahi@pgl:~$ sudo -u postgres psql -c &quot;SELECT version();&quot;
                                                             version
-------------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 18.4 (Ubuntu 18.4-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, 64-bit
(1 row)
</pre></div>


<p class="wp-block-paragraph">We can also confirm that the WAL directory lives inside the data directory, and therefore on the zvol:</p>



<pre class="wp-block-code"><code>ls -ld /pgdata/18/main/pg_wal</code></pre>



<h2 id="h-creating-a-large-database" class="wp-block-heading">Creating a large database</h2>



<p class="wp-block-paragraph">We need a database large enough to make traditional backup and restore operations time-consuming. In the SQL Server part of this series, we used the StackOverflow database (about 207 GB). For PostgreSQL, we use pgbench, the benchmarking tool shipped with PostgreSQL.</p>



<p class="wp-block-paragraph">We create the database and initialize it with a scale factor of 10000. This produces a database of approximately 146 GB, with 1 billion rows in the pgbench_accounts table:</p>



<pre class="wp-block-code"><code>sudo -u postgres createdb bench
sudo -u postgres pgbench -i -s 10000 --partitions=8 bench</code></pre>



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



<p class="wp-block-paragraph">A few minutes later:</p>



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



<p class="wp-block-paragraph">We can monitor the data growth during the initialization:</p>



<pre class="wp-block-code"><code>watch -n 30 'df -h /pgdata'</code></pre>



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



<p class="wp-block-paragraph">A few minutes later:</p>



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



<p class="wp-block-paragraph">On the Proxmox side:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="292" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-13-1024x292.png" alt="" class="wp-image-46219" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-13-1024x292.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-13-300x86.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-13-768x219.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-13.png 1103w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">After some time, the process completes:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
vacuuming...

creating primary keys...
done in 1559.55 s (drop tables 0.00 s, create tables 0.02 s, client-side generate 598.12 s, vacuum 675.53 s, primary keys 285.88 s).
ahi@pgl:~$
</pre></div>


<p class="wp-block-paragraph">We check the database size:</p>



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



<p class="wp-block-paragraph">We run a checkpoint before taking the snapshot. The recovery process starts replaying the WAL from the last checkpoint. By running it right before the snapshot, almost nothing needs to be replayed when the database starts after a restore:</p>



<pre class="wp-block-code"><code>sudo -u postgres psql -c "CHECKPOINT;"</code></pre>



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



<p class="wp-block-paragraph"><strong>Comparison with SQL Server</strong>:</p>



<p class="wp-block-paragraph">On the SQL Server side, we had to run SUSPEND_FOR_SNAPSHOT_BACKUP and BACKUP WITH METADATA_ONLY. On the PostgreSQL side, none of that is needed.</p>



<p class="wp-block-paragraph">The data files and the WAL are on the same zvol. An atomic ZFS snapshot therefore captures a state equivalent to a power loss, and PostgreSQL is designed to recover cleanly from that state through crash recovery: the WAL is replayed from the last checkpoint. This is documented and officially supported.</p>



<p class="wp-block-paragraph">The snapshot is the backup. There is no .bkm file, no metadata backup.</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td></td><td><strong>SQL Server</strong></td><td><strong>PostgreSQL</strong></td></tr><tr><td><strong>Before the snapshot</strong></td><td>ALTER DATABASE&#8230;SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON</td><td>CHECKPOINT (optional)</td></tr><tr><td><strong>Backup record</strong></td><td>BACKUP WITH METADATA_ONLY</td><td>None</td></tr><tr><td><strong>During the restore</strong></td><td>RESTORE WITH METADATA_ONLY</td><td>Automatic crash recovery (WAL replay)</td></tr><tr><td><strong>Evidence in the logs</strong></td><td>&#8220;I/O is frozen&#8221; in the ERRORLOG</td><td>&#8220;redo starts/redo done&#8221; in the PostgreSQL log</td></tr></tbody></table></figure>



<h2 id="h-snapshot-process-flow" class="wp-block-heading">Snapshot process flow</h2>



<p class="wp-block-paragraph">On the Proxmox side, we create the snapshot and protect it with a hold:</p>



<pre class="wp-block-code"><code>SNAP="sqlpool/pve/vm-307-disk-0@pg_bench_$(date +%Y%m%dT%H%M%S)" 
zfs snapshot "$SNAP" 
zfs hold sqlsnap "$SNAP" 
echo "$SNAP"</code></pre>



<p class="wp-block-paragraph">The hold protects the snapshot from an accidental destruction, as we did in part 2 of this series. We note the exact snapshot name, it will be needed for the restore.</p>



<p class="wp-block-paragraph">The database stays online during the whole operation. No I/O freeze is required.</p>



<p class="wp-block-paragraph">We list the snapshots:</p>



<pre class="wp-block-code"><code>zfs list -t snapshot -r sqlpool/pve/vm-307-disk-0</code></pre>



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



<p class="wp-block-paragraph">We drop the database then we restore the snapshot:</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/08/image-18-1024x272.png" alt="" class="wp-image-46224" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-18-1024x272.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-18-300x80.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-18-768x204.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/08/image-18.png 1048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">We run the snapshot restore procedure. On the VM, we stop the cluster and unmount the volume:</p>



<pre class="wp-block-code"><code>sudo systemctl stop postgresql@18-main
sudo umount /pgdata
</code></pre>



<p class="wp-block-paragraph">On the Proxmox side, we want to restore our snapshot. We can list the available snapshots:</p>



<pre class="wp-block-code"><code>zfs list -t snapshot -r sqlpool/pve/vm-307-disk-0</code></pre>



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



<p class="wp-block-paragraph">We roll back the snapshot:</p>



<pre class="wp-block-code"><code>zfs rollback -r sqlpool/pve/vm-307-disk-0@pg_bench_20260803T165409</code></pre>



<p class="wp-block-paragraph">On the VM, we mount the volume and start the service:</p>



<pre class="wp-block-code"><code>sudo mount /pgdata
sudo systemctl start postgresql@18-main</code></pre>



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



<p class="wp-block-paragraph">We check a few elements in the logs:</p>



<pre class="wp-block-code"><code>sudo tail -30 /var/log/postgresql/postgresql-18-main.log</code></pre>



<p class="wp-block-paragraph">The service shutdown, then the restart:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
UTC &#x5B;232108] LOG:  database system is shut down
UTC &#x5B;233968] LOG:  starting PostgreSQL 18.4 (Ubuntu 18.4-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, 64-bit
UTC &#x5B;233968] LOG:  listening on IPv4 address &quot;0.0.0.0&quot;, port 5432
UTC &#x5B;233968] LOG:  listening on IPv6 address &quot;::&quot;, port 5432
UTC &#x5B;233968] LOG:  listening on Unix socket &quot;/var/run/postgresql/.s.PGSQL.5432&quot;
</pre></div>


<p class="wp-block-paragraph">PostgreSQL detects that the database was not shut down properly and replays the WAL. This is the same crash recovery mechanism as in SQL Server. Finally, the database starts.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
UTC &#x5B;233974] LOG:  database system was not properly shut down; automatic recovery in progress
UTC &#x5B;233974] LOG:  redo starts at 20/2CB76278
UTC &#x5B;233974] LOG:  invalid record length at 20/2CB76380: expected at least 24, got 0
UTC &#x5B;233974] LOG:  redo done at 20/2CB76348 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
UTC &#x5B;233974] LOG:  checkpoint starting: end-of-recovery immediate wait
UTC &#x5B;233972] LOG:  checkpoint complete: wrote 0 buffers (0.0%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.002 s, sync=0.009 s, total=0.030 s; sync files=0, longest=0.000 s, average=0.005 s; distance=0 kB, estimate=0 kB; lsn=20/2CB76380, redo lsn=20/2CB76380
UTC &#x5B;233968] LOG:  database system is ready to accept connections
</pre></div>


<p class="wp-block-paragraph">We then verify that the database is available again:</p>



<pre class="wp-block-code"><code>sudo -u postgres psql -d bench -c "SELECT pg_size_pretty(pg_database_size('bench'));"</code></pre>



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



<h2 id="h-consistency-proof-under-load" class="wp-block-heading">Consistency proof under load</h2>



<p class="wp-block-paragraph">The previous test was done on a quiesced database: we ran a CHECKPOINT right before the snapshot, and nothing was writing. The real question is: what happens if the snapshot is taken while the database is being written to?</p>



<p class="wp-block-paragraph">This is where PostgreSQL differs the most from SQL Server. There is no SUSPEND_FOR_SNAPSHOT_BACKUP. We take the snapshot in the middle of the write activity and we let the WAL replay do the work.</p>



<p class="wp-block-paragraph">We start the load. The built-in pgbench script runs a TPC-B-like transaction: three UPDATE statements on the accounts, tellers and branches tables and one INSERT into the history table:</p>



<pre class="wp-block-code"><code>sudo -u postgres pgbench -c 8 -j 4 -T 300 bench &amp;</code></pre>



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



<p class="wp-block-paragraph">While the load is running, we take a snapshot on the Proxmox side:</p>



<pre class="wp-block-code"><code>zfs snapshot sqlpool/pve/vm-307-disk-0@pg_bench_$(date +%Y%m%dT%H%M%S)

zfs hold sqlsnap sqlpool/pve/vm-307-disk-0@pg_bench_20260803T224551</code></pre>



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



<p class="wp-block-paragraph">No CHECKPOINT this time, no freeze. The database is actively writing while the snapshot is taken.</p>



<p class="wp-block-paragraph">The state after some time under load:</p>



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



<p class="wp-block-paragraph">We stop the service:</p>



<pre class="wp-block-code"><code>sudo systemctl stop postgresql@18-main 
sudo umount /pgdata</code></pre>



<p class="wp-block-paragraph">We restore the snapshot:</p>



<pre class="wp-block-code"><code>zfs rollback -r sqlpool/pve/vm-307-disk-0@pg_bench_20260803T224551 </code></pre>



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



<p class="wp-block-paragraph">We mount the volume, start the service and check the logs:</p>



<pre class="wp-block-code"><code>sudo mount /pgdata 
sudo systemctl start postgresql@18-main</code></pre>



<p class="wp-block-paragraph">This time the log shows a real recovery:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
2026-08-03 20:49:03.244 UTC &#x5B;234405] LOG:  database system is shut down
2026-08-03 20:50:34.333 UTC &#x5B;234620] LOG:  starting PostgreSQL 18.4 (Ubuntu 18.4-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, 64-bit
2026-08-03 20:50:34.333 UTC &#x5B;234620] LOG:  listening on IPv4 address &quot;0.0.0.0&quot;, port 5432
2026-08-03 20:50:34.333 UTC &#x5B;234620] LOG:  listening on IPv6 address &quot;::&quot;, port 5432
2026-08-03 20:50:34.335 UTC &#x5B;234620] LOG:  listening on Unix socket &quot;/var/run/postgresql/.s.PGSQL.5432&quot;
2026-08-03 20:50:34.343 UTC &#x5B;234626] LOG:  database system was interrupted; last known up at 2026-08-03 20:44:36 UTC
2026-08-03 20:50:34.381 UTC &#x5B;234626] LOG:  database system was not properly shut down; automatic recovery in progress
2026-08-03 20:50:34.384 UTC &#x5B;234626] LOG:  redo starts at 20/7A3618B0
2026-08-03 20:50:38.024 UTC &#x5B;234626] LOG:  invalid record length at 20/8BC0A4F8: expected at least 24, got 0
2026-08-03 20:50:38.024 UTC &#x5B;234626] LOG:  redo done at 20/8BC0A4D0 system usage: CPU: user: 0.71 s, system: 0.63 s, elapsed: 3.63 s
2026-08-03 20:50:38.028 UTC &#x5B;234624] LOG:  checkpoint starting: end-of-recovery immediate wait
2026-08-03 20:50:53.936 UTC &#x5B;234624] LOG:  checkpoint complete: wrote 105355 buffers (53.6%), wrote 5 SLRU buffers; 0 WAL file(s) added, 17 removed, 0 recycled; write=3.648 s, sync=12.232 s, total=15.911 s; sync files=189, longest=12.223 s, average=0.065 s; distance=287395 kB, estimate=287395 kB; lsn=20/8BC0A4F8, redo lsn=20/8BC0A4F8
2026-08-03 20:50:53.948 UTC &#x5B;234620] LOG:  database system is ready to accept connections
</pre></div>


<p class="wp-block-paragraph">Three differences compared to the first test:</p>



<ul class="wp-block-list">
<li>The &#8220;last known up at&#8221; timestamp (20:44:36) does not match a checkpoint we ran manually. It matches the last automatic checkpoint triggered during the load.</li>
</ul>



<ul class="wp-block-list">
<li>The redo is not instantaneous anymore: 3.63 seconds, replaying about 280 MB of WAL (from LSN 20/7A3618B0 to 20/8BC0A4D0). All the write activity between the last checkpoint and the snapshot had to be replayed. The transactions committed before the snapshot are recovered, the ones that were in flight are rolled back.</li>
</ul>



<ul class="wp-block-list">
<li>The end-of-recovery checkpoint then writes everything the redo rebuilt in memory: 105355 buffers, 53.6% of the buffer pool. The database is ready to accept connections about 19 seconds after the service start.</li>
</ul>



<p class="wp-block-paragraph">The crash recovery completed correctly and the database has been restored. We verify the TPC-B invariant. Each pgbench transaction applies the same delta to the accounts, tellers and branches tables in a single transaction. On a consistent database, the three sums must be equal:</p>



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



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



<h2 id="h-major-drawbacks" class="wp-block-heading">Major drawbacks</h2>



<ul class="wp-block-list">
<li>The snapshot covers the whole zvol. All the databases of the cluster are captured and restored together. There is no per-database restore, unlike the METADATA_ONLY approach on SQL Server which targets a single database.</li>



<li>There is no backup history. SQL Server records the metadata backup in msdb. Here, the only trace is the snapshot itself on the ZFS side.</li>



<li>Point-in-time recovery is not covered. The snapshot alone brings the database back to the moment it was taken. For PITR, WAL archiving would still be required on top of it.</li>
</ul>



<h3 id="h-" class="wp-block-heading"></h3>



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



<ul class="wp-block-list">
<li>The snapshot backup and restore model of the SQL Server series applies to PostgreSQL (no I/O freeze, no metadata backup).</li>



<li>One important rule: data files and WAL must reside on the same zvol so the snapshot is atomic.</li>



<li>A 146 GB database was restored in a few seconds and in less than 20 seconds under active load, WAL replay included.</li>
</ul>



<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/postgresql-snapshot-backup-and-restore-with-proxmox-zfs-4-4/">PostgreSQL Snapshot Backup and Restore with Proxmox ZFS (4/4)</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/postgresql-snapshot-backup-and-restore-with-proxmox-zfs-4-4/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/#comments</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>1</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>
	</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-10-19 10:42:43 by W3 Total Cache
-->