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

<channel>
	<title>Archives des 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>Mon, 10 Aug 2026 13:47:58 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/05/cropped-favicon_512x512px-min-32x32.png</url>
	<title>Archives des 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 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 fetchpriority="high" 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="(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 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 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="(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-fb3f4cf17fe4032b7fdd63edf105c5d1 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/#respond</comments>
		
		<dc:creator><![CDATA[Stéphane Haby]]></dc:creator>
		<pubDate>Thu, 06 Aug 2026 13:10:24 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Microsoft]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46270</guid>

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



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



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



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



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



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



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



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



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



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



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



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



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

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

    MAX(ius.last_user_update) AS LastWriteActivity,

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

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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

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

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



<li>The requirements</li>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

ALTER DATABASE DemoFrag SET RECOVERY SIMPLE
GO

USE DemoFrag
GO

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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

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

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-preview-part-1/">SQL Server: Automatic index compaction in Azure (preview) – Part 1</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/sql-server-automatic-index-compaction-preview-part-1/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Green SQL Server DBA Tips  #1 – Are unnecessary indexes cluttering up your database?</title>
		<link>https://www.dbi-services.com/blog/green-sql-server-dba-tips-1-are-unnecessary-indexes-cluttering-up-your-database/</link>
					<comments>https://www.dbi-services.com/blog/green-sql-server-dba-tips-1-are-unnecessary-indexes-cluttering-up-your-database/#respond</comments>
		
		<dc:creator><![CDATA[Stéphane Haby]]></dc:creator>
		<pubDate>Tue, 21 Jul 2026 15:32:49 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[MS Teams]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Technology Survey]]></category>
		<category><![CDATA[Microsoft]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=45650</guid>

					<description><![CDATA[<p>This summer I start a series of blog posts on the ‘green SQL Server DBA’, as it’s a topic that continues to interest me and one that hasn’t been covered very much so far. I hope you’ll enjoy my tips, starting with this one on unused indexes. When discussing SQL Server performance, we often hear [&#8230;]</p>
<p>L’article <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> 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 summer I start a series of blog posts on the ‘green SQL Server DBA’, as it’s a topic that continues to interest me and one that hasn’t been covered very much so far. I hope you’ll enjoy my tips, starting with this one on unused indexes.</p>



<p class="wp-block-paragraph">When discussing SQL Server performance, we often hear our performance tool say, ‘<em>An index is missing</em>’, but never ‘<em>There are too many indexes</em>’.</p>



<p class="wp-block-paragraph">However, in many databases, certain indexes are never used by business queries and they are maintained with every INSERT, UPDATE or DELETE operation.</p>



<p class="wp-block-paragraph">The question is really simple:</p>



<ul class="wp-block-list">
<li>How much does an unused index actually cost?</li>



<li>Why is an unused index a problem?</li>
</ul>



<p class="wp-block-paragraph">Every time there is a change to the data, SQL Server must also update all the relevant indexes, and that is not free.</p>



<p class="wp-block-paragraph">As an example, I’m going to use a widely recognised database: the famous DynamicsBC.</p>



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



<p class="wp-block-paragraph">The first step in any study is always to carry out an inventory to gather all the data required for the analysis. I need the size of the database, size of each data file, size of the data and size of the indexes</p>



<p class="wp-block-paragraph">I will use a query using the DMV: <a href="https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-objects/sys-dm-db-partition-stats-transact-sql?view=sql-server-ver17" data-type="link" data-id="https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-objects/sys-dm-db-partition-stats-transact-sql?view=sql-server-ver17">sys.dm_db_partition_stats</a></p>



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

SELECT
DB_NAME() AS DatabaseName,
CAST(SUM(reserved_page_count) * 8.0 / 1024 AS DECIMAL(18,2)) AS TotalSizeMB,
CAST(SUM(used_page_count) * 8.0 / 1024 AS DECIMAL(18,2)) AS UsedSizeMB,
CAST((SUM(reserved_page_count) - SUM(used_page_count)) * 8.0 / 1024 AS DECIMAL(18,2)) AS FreeSizeMB,
CAST(SUM(
CASE
WHEN index_id &lt; 2 THEN in_row_data_page_count
+ lob_used_page_count
+ row_overflow_used_page_count
ELSE 0
END
) * 8.0 / 1024 AS DECIMAL(18,2)) AS DataSizeMB,
CAST(SUM(
CASE
WHEN index_id &gt;= 2 THEN used_page_count
ELSE 0
END
) * 8.0 / 1024 AS DECIMAL(18,2)) AS IndexSizeMB
FROM sys.dm_db_partition_stats;
GO
</code></pre>



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



<p class="wp-block-paragraph">If we look at the ratio between the indexes and the data, we get:</p>



<p class="wp-block-paragraph">64966.91/92898.87 × 100 = ~70 %</p>



<p class="wp-block-paragraph">A ratio exceeding 50–60 % often requires an analysis of unused indexes, index overlap and compression.</p>



<p class="wp-block-paragraph"><strong>YES, we are good with this database! <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;" /></strong></p>



<p class="wp-block-paragraph">Now, let see if we have unused Indexes I use a script that I created years ago for DWH audit:</p>



<pre class="wp-block-code"><code>/******************************************************************************************/
--Summary
/******************************************************************************************/
USE &#091;master];

DECLARE @sqlcmd NVARCHAR(max);
DECLARE @ExcludeFlag BIT = 0;
DECLARE @IncludeDB VARCHAR(1000);
DECLARE @ExcludeDB VARCHAR(1000);


SET  @IncludeDB = 'DynamicsBC'

SELECT @ExcludeFlag = 0;
--SELECT @ExcludeDB = 'distribution,master,model,msdb,tempdb';
 

DECLARE @InDBList TABLE (indb SYSNAME);
DECLARE @ExDBList TABLE (exdb SYSNAME);
DECLARE @dbList TABLE (dbID INT NOT NULL PRIMARY KEY, dbName SYSNAME NOT NULL);


IF (@ExcludeFlag = 0)
BEGIN
	INSERT INTO @InDBList SELECT * FROM string_split(@IncludeDB,',');
	INSERT INTO @dbList (dbID, dbName) 
		SELECT d.database_id, d.name 
		FROM sys.databases d 
		WHERE d.name IN (SELECT indb FROM @InDBList);
END
ELSE
BEGIN
--	INSERT INTO @ExDBList SELECT * FROM string_split(@ExcludeDB,',');
	INSERT INTO @dbList (dbID, dbName) 
		SELECT d.database_id, d.name 
		FROM sys.databases d 
--		WHERE d.name NOT IN (SELECT exdb FROM @ExDBList);
--		WHERE d.name NOT IN ('distribution','master','model','msdb','tempdb');

END

/*********************************************************************
Creation of the result table variable to store the information
--*********************************************************************/

IF (SELECT OBJECT_ID('tempdb.dbo.#dbUnUsedIndex')) IS NOT NULL  
	DROP TABLE dbo.#dbUnUsedIndex
CREATE TABLE #dbUnUsedIndex (
	dbID INT NOT NULL,
	dbName SYSNAME NOT NULL,
	objName SYSNAME NULL,
	idxName SYSNAME NULL,
	idxID INT NULL,
	UserSeek BIGINT NULL,
	UserScans BIGINT NULL,
	UserLookups BIGINT NULL,
	UserUpdates BIGINT NULL,
	nbRows BIGINT NULL,
	dropStatement NVARCHAR(1000) NULL)

DECLARE @dbID INT;
DECLARE @dbName SYSNAME;
DECLARE cur_dblst CURSOR 
  LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR 
SELECT dbID, dbName
FROM @dbList;

OPEN cur_dblst
FETCH NEXT FROM cur_dblst INTO @dbID, @dbName
WHILE @@FETCH_STATUS = 0
BEGIN 
    --Do something with Id here

	SELECT @sqlcmd = N'
		USE &#091;' + @dbName + ']'
		+ ' SELECT
			@dbID
			, @dbName			
			, o.name AS objName
			, i.name AS idxName
			, i.index_id AS idxID
			, dm_ius.user_seeks AS UserSeek
			, dm_ius.user_scans AS UserScans
			, dm_ius.user_lookups AS UserLookups
			, dm_ius.user_updates AS UserUpdates
			, p.TableRows AS nbRows
			, ''DROP INDEX '' + QUOTENAME(i.name)
			+ '' ON '' + QUOTENAME(s.name) + ''.''
			+ QUOTENAME(OBJECT_NAME(dm_ius.OBJECT_ID)) AS ''dropStatement''
		FROM sys.dm_db_index_usage_stats dm_ius
			INNER JOIN sys.indexes i ON i.index_id = dm_ius.index_id AND dm_ius.OBJECT_ID = i.OBJECT_ID
			INNER JOIN sys.objects o ON dm_ius.OBJECT_ID = o.OBJECT_ID
			INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
			INNER JOIN (SELECT SUM(p.rows) TableRows, p.index_id, p.OBJECT_ID
			FROM sys.partitions p GROUP BY p.index_id, p.OBJECT_ID) p
			ON p.index_id = dm_ius.index_id AND dm_ius.OBJECT_ID = p.OBJECT_ID
		WHERE OBJECTPROPERTY(dm_ius.OBJECT_ID,''IsUserTable'') = 1
			AND dm_ius.database_id = DB_ID()
			AND i.type_desc = ''nonclustered''
			AND i.is_primary_key = 0
			AND i.is_unique_constraint = 0
		ORDER BY (dm_ius.user_seeks + dm_ius.user_scans + dm_ius.user_lookups) ASC; '

	INSERT INTO #dbUnUsedIndex (
					dbID,
					dbName,
					objName,
					idxName,
					idxID,
					UserSeek,
					UserScans,
					UserLookups,
					UserUpdates,
					nbRows,
					dropStatement)
	EXEC sp_executesql  @sqlCmd, N'@dbID INT, @dbName SYSNAME', @dbID, @dbName

    FETCH NEXT FROM cur_dblst INTO @dbID, @dbName
END
CLOSE cur_dblst
DEALLOCATE cur_dblst

SELECT Count(*) FROM #dbUnUsedIndex

SELECT * FROM #dbUnUsedIndex
ORDER BY dbName, objName


 IndexInfo;
GO
</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="617" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/unused-index-04-1024x617.png" alt="" class="wp-image-45655" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/unused-index-04-1024x617.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/unused-index-04-300x181.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/unused-index-04-768x463.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/07/unused-index-04.png 1153w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">For the total number of Indexes, we use this simple query:</p>



<pre class="wp-block-code"><code>SELECT COUNT(*) AS TotalIndexes
FROM dynamicsBC.sys.indexes i
INNER JOIN sys.tables t
ON i.object_id = t.object_id
WHERE i.index_id &gt; 0;</code></pre>



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



<p class="wp-block-paragraph">We have 998 unused Index for a total of 19494 Index. This is 5% of the Indexes.<br>In this case, we will not win a lot. 5% is good but we can have a look because we never known&#8230;</p>



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



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



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



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



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



<p class="wp-block-paragraph">With every insertion, the data is written to the table and several index structures must also be updated, even when one of these indexes is never used by a query.</p>



<p class="wp-block-paragraph">So we have the following sequence:</p>



<p class="wp-block-paragraph">INSERT &#8211;&gt; write to the table &#8211;&gt; update index 1&nbsp; &#8211;&gt; update index 2&nbsp; &#8211;&gt; update index 3&nbsp; &#8211;&gt; update index 4 &#8230;.</p>



<p class="wp-block-paragraph">This is even more costly for this command because, for each operation, the sequence is as follows:</p>



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



<p class="wp-block-paragraph">UPDATE &#8211;&gt; Delete the old index entry + Create the new entry for each index</p>



<p class="wp-block-paragraph">The result of all of this is more I/O, more CPU usage and also more entries in the Transaction Log</p>



<p class="wp-block-paragraph">On heavily used transactional tables, a few unused indexes can account for several per cent of additional load.</p>



<p class="wp-block-paragraph"></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 unused index represents ~100B of the database.</p>



<p class="wp-block-paragraph">If 100 GB of indexes are never used, that’s 100 GB backed up unnecessarily, 100 GB restored unnecessarily and, above all, 100 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&#8230; I let you do the calculation but it’s near 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 unused indexes:<br>&#8211; Rebuild Index<br>&#8211; Reorganise Index<br>&#8211; CheckDB<br>&#8211; Backups<br>&#8211; Restores</p>



<p class="wp-block-paragraph">Each additional index extends the maintenance windows&#8230;</p>



<p class="wp-block-paragraph"></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, as an unused index is not necessarily useless.</p>



<p class="wp-block-paragraph">You should check several factors before you delete unused indexes:<br> &#8211; Full business cycle<br> &#8211; End/Begin of month<br> &#8211; Annual processing<br> &#8211; Reporting<br> &#8211; ETL<br> &#8211; SQL Agent jobs</p>



<p class="wp-block-paragraph">I generally recommend observing the index between 1 and 3 months before deleting it and without restart. A restart will reset the DMV used for the stats&#8230; And always keep the script for recreating the index to hand!</p>



<p class="wp-block-paragraph"></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">Just for fun, we’re going to set up 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>Unused index rates (unused index/total index)</td><td>Score</td></tr><tr><td>&lt; 5%</td><td>10/10</td></tr><tr><td>5 to 10%</td><td>8/10</td></tr><tr><td>10 to 20 %</td><td>5/10</td></tr><tr><td>&gt; 20 %</td><td>2/10</td></tr></tbody></table></figure>



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



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



<p class="wp-block-paragraph">An unused index is a bit like car that never gets driven:<br>it takes up space;it costs money;it requires maintenance;but it produces no value.</p>



<p class="wp-block-paragraph">The Green SQL Server DBA doesn’t just seek to add indexes.<br>They aim to retain only those that deliver measurable business value.</p>



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



<p class="wp-block-paragraph"></p>
<p>L’article <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> 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-1-are-unnecessary-indexes-cluttering-up-your-database/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>When an idle transaction starves the worker pool (THREADPOOL)</title>
		<link>https://www.dbi-services.com/blog/when-an-idle-transaction-starves-the-worker-pool-threadpool/</link>
					<comments>https://www.dbi-services.com/blog/when-an-idle-transaction-starves-the-worker-pool-threadpool/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Mon, 20 Jul 2026 09:58:44 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=45334</guid>

					<description><![CDATA[<p>How a forgotten transaction exhausted SQL Server's worker pool, triggered THREADPOOL waits, and only DAC could fix it.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/when-an-idle-transaction-starves-the-worker-pool-threadpool/">When an idle transaction starves the worker pool (THREADPOOL)</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-context" class="wp-block-heading">Context</h2>



<p class="wp-block-paragraph">A production instance, mid-afternoon, nothing unusual on any dashboard. An engineer opens a transaction to patch a single row while investigating a data issue:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
BEGIN TRANSACTION;
UPDATE dbo.Orders SET Status = &#039;Reviewed&#039; WHERE OrderId = 482193;
</pre></div>


<p class="wp-block-paragraph">No <code>COMMIT</code>. No <code>ROLLBACK</code>. The tab gets buried under three others, the investigation moves on, and the lock is still held an hour later.</p>



<p class="wp-block-paragraph">Every query, every batch, every login needs a worker thread to execute on. That pool is not infinite, it is sized by <code>max worker threads</code>, either left on its computed default or pinned to a fixed number.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SELECT name, value_in_use FROM sys.configurations WHERE name = &#039;max worker threads&#039;;

name                                value_in_use
----------------------------------- -------------------------------------------------------------------------------------------------------------------------
max worker threads                  128
</pre></div>


<h2 id="h-what-does-sleeping-really-mean" class="wp-block-heading">What does &#8220;Sleeping&#8221; really mean?</h2>



<p class="wp-block-paragraph">SQL Server schedules work cooperatively, not preemptively. Each worker is handed a quantum (4 milliseconds) to run before it is expected to voluntarily yield the scheduler to the next runnable task. This is the mechanism behind <code>SOS_SCHEDULER_YIELD</code>: a worker that still has work to do, but whose quantum has expired, stepping aside so someone else gets a turn. </p>



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



<p class="wp-block-paragraph">None of this applies to the open transaction from earlier. A session that has issued no command has no task and holds no worker. Its status in <code>sys.dm_exec_sessions</code> is <code><strong>sleeping</strong></code>, not <code>running</code>, not <code>suspended</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SELECT
    s.session_id,
    s.status AS session_status,
    ct.text
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON s.session_id = r.session_id
LEFT JOIN sys.dm_exec_connections c ON s.session_id = c.session_id
OUTER APPLY sys.dm_exec_sql_text(c.most_recent_sql_handle) ct
WHERE s.session_id = 54;

session_id session_status                 text
---------- ------------------------------ --------------------------------------------------------------------------------------------------------------------
54         sleeping                       UPDATE dbo.Orders SET Status = &#039;Reviewed&#039; WHERE OrderId = 482193;
</pre></div>


<p class="wp-block-paragraph"> It is not waiting for a quantum, because it is not competing for one. The lock it holds costs the engine nothing in scheduling terms; it is bookkeeping in the lock manager, entirely separate from the worker pool.</p>



<h2 id="h-two-hundred-sessions-walk-into-a-lock" class="wp-block-heading">Two hundred sessions walk into a lock</h2>



<p class="wp-block-paragraph">Let&#8217;s say that the application wants to confirm that the orders has been reviewed now it&#8217;s in the processed state. </p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
BEGIN TRANSACTION;
UPDATE dbo.Orders SET Status = &#039;Processed&#039; WHERE OrderId = 482193;
</pre></div>


<p class="wp-block-paragraph">Seeing that the query didn&#8217;t complete to update the item, it will keep sending this transaction again and again, sending it 200 times let&#8217;s say. </p>



<p class="wp-block-paragraph">Unlike the sleeping session above, each of these has issued a command. Each one is granted a worker to execute it, immediately hits the lock, and transitions to <code>suspended</code>, waiting on <code>LCK_M_X</code>. </p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
wait_type                   waiting_tasks_count       wait_time_ms
THREADPOOL                      521                      2881770
SOS_SCHEDULER_YIELD             710                      41
</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
 session_id     status        wait_type        wait_time   blocking_session_id 
    68         suspended       LCK_M_X          29873             54
   ...
   206         suspended       LCK_M_X          29478             68
   207         suspended       LCK_M_X          29478             68
</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
scheduler_id runnable_tasks_count work_queue_count active_workers_count
       0              0               19                 43
       1              0               5                  45
       2              0               10                 45
       3              0               2                  44
</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
active_workers    max_workers_count       
         205                    128
</pre></div>


<p class="wp-block-paragraph"><span style="text-decoration: underline">Note:</span> <code>max_workers_count</code> only counts the user-facing pool; internal system threads, including the DAC&#8217;s own reserved worker used to capture this very output, sit outside that ceiling.</p>



<p class="wp-block-paragraph">The worker is not released while the task waits. It stays attached to the suspended task for the entire duration of the block, doing nothing, simply reserved, waiting for the resource (the order line to update) to be available for updates.</p>



<p class="wp-block-paragraph">The remainder cannot even be granted a worker to start waiting. They queue behind everyone else, and eventually give up entirely:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
Login timeout expired
Login/Query timeout: 15/0 seconds
</pre></div>


<p class="wp-block-paragraph">By this point the server has simply stopped accepting new connections.</p>



<h2 id="h-when-the-fire-exit-is-also-on-fire" class="wp-block-heading">When the fire exit is also on fire</h2>



<p class="wp-block-paragraph">Releasing the original lock should be the easy part: switch back to the session from the very first transaction, issue a <code>ROLLBACK</code>, and watch everything clear. Except that session, which has been sitting <code>sleeping</code> and worker-free this whole time, now has to issue a command of its own. And issuing a command means asking the pool for a worker (the same exhausted pool every other session is already queued for). The session responsible for the deadlock has no priority for fixing it. It gets in line like everyone else, behind two hundred sessions it created the conditions for.</p>



<h2 id="h-when-dac-is-the-last-resort" class="wp-block-heading">When DAC is the last resort</h2>



<p class="wp-block-paragraph">This is where the Dedicated Admin Connection comes in the game. It runs on its own scheduler, with a worker reserved outside the regular pool, built specifically for an instance too exhausted to serve itself.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
sqlcmd -A -S&quot;.&quot; -E
</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SELECT blocking_session_id
FROM sys.dm_exec_requests
WHERE blocking_session_id &lt;&gt; 0;

KILL 54;
</pre></div>


<p class="wp-block-paragraph"><span style="text-decoration: underline">Note:</span> the &#8220;.&#8221; here resolves to the local default instance but unlike an ordinary local connection (which typically uses Shared Memory), the DAC always connects over its own dedicated TCP listener on the loopback adapter, regardless of protocol settings on the port 1434 or a dynamic one (full documentation <a href="https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/diagnostic-connection-for-database-administrators?view=sql-server-ver17" id="https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/diagnostic-connection-for-database-administrators?view=sql-server-ver17">here</a>).</p>



<p class="wp-block-paragraph">The <code>KILL</code> forces the rollback from outside the exhausted pool entirely. Workers free up in cascade, and the two hundred suspended sessions complete their updates and release their own.</p>



<h2 id="h-final-thoughts" class="wp-block-heading">Final thoughts</h2>



<p class="wp-block-paragraph">In this example, we set the parameter <code>max worker threads </code>to 128 to easily saturate the worker threads. However, the default value for <code>max worker threads</code> is 0, which lets SQL Server compute the number of worker threads automatically at startup based on the number of logical CPUs and the platform architecture. Microsoft best practice can be found <a href="https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-max-worker-threads-server-configuration-option?view=sql-server-ver17" id="https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-max-worker-threads-server-configuration-option?view=sql-server-ver17">here </a>and shows the following table:</p>



<figure class="wp-block-table aligncenter"><table class="has-fixed-layout"><thead><tr><th>Number of logical CPUs</th><th>64-bit computer</th></tr></thead><tbody><tr><td>&lt;= 4</td><td>512</td></tr><tr><td>&gt; 4 and &lt;= 64</td><td>512 + ((logical CPUs &#8211; 4) * 16)</td></tr><tr><td>&gt; 64</td><td>512 + ((logical CPUs &#8211; 4) * 32)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">And the key take-away from this experiment:</p>



<ul class="wp-block-list">
<li><strong>Sleeping costs nothing, suspended costs a worker.</strong> The distinction between an idle transaction and a blocked one is the entire mechanism behind this incident: both hold a lock, only one of them holds a thread.</li>



<li><strong>The scheduler&#8217;s quantum explains CPU pressure, not threadpool exhaustion.</strong> Yielding after 4ms is about sharing a CPU among runnable workers; it has nothing to do with how many workers exist in the first place.</li>



<li><strong>The session that caused the block is not exempt from the consequences of the block.</strong> It has to compete for a worker like anything else, the moment it tries to clean up after itself.</li>



<li><strong>Never let a statement end without a <code>COMMIT</code> or a <code>ROLLBACK</code></strong>.</li>
</ul>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/when-an-idle-transaction-starves-the-worker-pool-threadpool/">When an idle transaction starves the worker pool (THREADPOOL)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/when-an-idle-transaction-starves-the-worker-pool-threadpool/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Monitoring MSSQL Database Files in Zabbix</title>
		<link>https://www.dbi-services.com/blog/monitoring-mssql-database-files-in-zabbix/</link>
					<comments>https://www.dbi-services.com/blog/monitoring-mssql-database-files-in-zabbix/#respond</comments>
		
		<dc:creator><![CDATA[Aurélien Py]]></dc:creator>
		<pubDate>Mon, 29 Jun 2026 20:30:07 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Monitoring]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Zabbix]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44327</guid>

					<description><![CDATA[<p>Introduction Monitoring Microsoft SQL Server database files is an important part of infrastructure supervision.A database reaching its storage limit can rapidly lead to application outages, transaction failures, or even complete service interruptions. The Initial Problem In SQL Server, retrieving file information is relatively simple when working directly inside SQL Server Management Studio A common query [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/monitoring-mssql-database-files-in-zabbix/">Monitoring MSSQL Database Files in Zabbix</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h1 class="wp-block-heading" id="h-introduction">Introduction</h1>



<p class="wp-block-paragraph" id="h-monitoring-microsoft-sql-server-database-files-is-an-important-part-of-infrastructure-supervision-a-database-reaching-its-storage-limit-can-rapidly-lead-to-application-outages-transaction-failures-or-even-complete-service-interruptions">Monitoring Microsoft SQL Server database files is an important part of infrastructure supervision.<br>A database reaching its storage limit can rapidly lead to application outages, transaction failures, or even complete service interruptions.</p>



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



<p class="wp-block-paragraph">In SQL Server, retrieving file information is relatively simple when working directly inside SQL Server Management Studio</p>



<ul class="wp-block-list">
<li>database file size,</li>



<li>used space,</li>



<li>free space,</li>



<li>auto-growth configuration,</li>



<li>or maximum file size</li>
</ul>



<p class="wp-block-paragraph">A common query used by many DBAs is the following:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
USE &#x5B;TEST]

GO

SELECT
  DB_NAME(database_id) AS DBName,
  Name AS Logical_Name,
  Physical_Name AS &#x5B;PhysicalName],
  CAST(ROUND(((size)/128/1024), 2) AS float) AS &#x5B;SizeGB],
  CAST(ROUND((CAST(FILEPROPERTY(name, &#039;SpaceUsed&#039;) AS INT)/128/1024), 2) AS float) AS &#x5B;SpaceUsedGB],
  CAST(ROUND((size/128/1024 - CAST(FILEPROPERTY(name, &#039;SpaceUsed&#039;) AS INT)/128/1024), 2) AS float) AS &#x5B;FreeSpaceGB],
  ROUND(ISNULL(((CAST((fileproperty(name, &#039;SpaceUsed&#039;))/128/1024 as float)) / NULLIF((CAST(size/128/1024 as float)), 0)), 0)*100, 2) as &#x5B;SpaceUsed%],
  CASE WHEN &#x5B;max_size] = -1 THEN &#x5B;max_size] ELSE CAST(ROUND(((max_size)/128/1024), 2) AS float) END AS &#x5B;MaxSizeGB],
  CAST(ROUND((max_size/128/1024 - CAST(FILEPROPERTY(name, &#039;SpaceUsed&#039;) AS INT)/128/1024), 2) AS float) AS &#x5B;FreeSpaceGBtoMaxSize],
  ROUND(ISNULL(((CAST((fileproperty(name, &#039;SpaceUsed&#039;))/128/1024 as float)) / NULLIF((CAST(max_size/128/1024 as float)), 0)), 0)*100, 2) as &#x5B;SpaceUsed%toMaxSize]
FROM sys.master_files
WHERE DB_NAME(database_id) = &#039;TEST&#039;
ORDER BY Logical_Name
</pre></div>


<p class="wp-block-paragraph">The initial query works perfectly for manual analysis in SQL Server Management Studio.<br>However, integrating it into Zabbix becomes more complicated because <code>sys.database_files</code> only returns information from the current database context.</p>



<p class="wp-block-paragraph">This means the query must be executed individually for every database using:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
USE &#x5B;DatabaseName]
</pre></div>


<p class="wp-block-paragraph">In small environments this remains manageable, but in larger infrastructures containing dozens or even hundreds of databases, maintaining static queries quickly becomes impractical.</p>



<p class="wp-block-paragraph">To automate this process, we use:</p>


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


<p class="wp-block-paragraph">This SQL Server procedure dynamically executes the same query across every database on the instance.</p>



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


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
EXEC sp_MSforeachdb &#039;
USE &#x5B;?]

SELECT
    DB_NAME(),
    name,
    physical_name
FROM sys.database_files
&#039;
</pre></div>


<p class="wp-block-paragraph">The <code>?</code> placeholder is automatically replaced by the database name during execution, allowing SQL Server to iterate through all databases dynamically.</p>



<p class="wp-block-paragraph">At first glance, this seems to solve the problem entirely.<br>However, another limitation quickly appears: <code>sp_MSforeachdb</code> returns one independent result set per database.</p>



<p class="wp-block-paragraph">Instead of producing a single structured dataset, SQL Server generates multiple separate tables:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Database 1 result
-----------------
file1
file2

Database 2 result
-----------------
file1
file2
</pre></div>


<p class="wp-block-paragraph">While this output remains perfectly readable for a DBA inside SQL Server Management Studio, it becomes difficult to exploit inside Zabbix, especially for Low-Level Discovery and dependent items.</p>



<p class="wp-block-paragraph">To solve this limitation, we redesigned the query architecture to centralize all results into a single temporary table, allowing Zabbix to consume one normalized dataset fully compatible with automatic discovery and scalable monitoring.</p>



<h2 class="wp-block-heading" id="h-why-zabbix-cannot-easily-use-this-output">Why Zabbix Cannot Easily Use This Output</h2>



<p class="wp-block-paragraph">Zabbix works much better when:</p>



<ul class="wp-block-list">
<li>the result is normalized,</li>



<li>the structure is predictable,</li>



<li>and all rows belong to a single dataset.</li>
</ul>



<p class="wp-block-paragraph">With multiple independent result sets:</p>



<ul class="wp-block-list">
<li>Low-Level Discovery becomes difficult,</li>



<li>JSON conversion becomes complicated,</li>



<li>dependent items cannot parse values correctly,</li>



<li>preprocessing becomes unreliable.</li>
</ul>



<p class="wp-block-paragraph">The issue is therefore no longer the database context itself. The issue becomes the query output structure.</p>



<h2 class="wp-block-heading" id="h-building-a-zabbix-compatible-query">Building a Zabbix-Compatible Query</h2>



<p class="wp-block-paragraph">To make the output usable inside Zabbix, the query had to be redesigned completely.</p>



<p class="wp-block-paragraph">The objective was no longer simply to retrieve MSSQL file information, but to transform multiple independent database results into a single normalized dataset compatible with Low-Level Discovery.</p>



<p class="wp-block-paragraph">The new query therefore introduces three important concepts:</p>



<ul class="wp-block-list">
<li>dynamic iteration through all databases using <code>sp_MSforeachdb</code>,</li>



<li>centralized data collection using a global temporary table,</li>



<li>and a final unified output consumable by Zabbix dependent items.</li>
</ul>



<p class="wp-block-paragraph">The core logic becomes inside the <code>sp_MSforeachdb</code> execution.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
INSERT INTO ##Results
</pre></div>


<p class="wp-block-paragraph">Each database inserts its rows into the same centralized structure instead of returning independent result sets.</p>



<p class="wp-block-paragraph">At the end of the execution, the query simply returns:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
SELECT * FROM ##Results
</pre></div>


<p class="wp-block-paragraph">The final output is now normalized instead of multiple disconnected tables.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Database</th><th>File</th><th>Type</th><th>Size</th><th>Free</th></tr></thead><tbody><tr><td>TEST</td><td>TEST_Data</td><td>Data</td><td>&#8230;</td><td>&#8230;</td></tr><tr><td>TEST</td><td>TEST_Log</td><td>Log</td><td>&#8230;</td><td>&#8230;</td></tr><tr><td>PROD</td><td>PROD_Data</td><td>Data</td><td>&#8230;</td><td>&#8230;</td></tr></tbody></table></figure>



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


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SET NOCOUNT ON;

DECLARE @Granularity VARCHAR(10) = NULL
DECLARE @Database_Name sysname = NULL
 
DECLARE @SQL VARCHAR(5000)

IF EXISTS (SELECT NAME FROM tempdb..sysobjects WHERE NAME = &#039;##Results&#039;)
BEGIN
    DROP TABLE ##Results
END

CREATE TABLE ##Results (
    &#x5B;Database Name] sysname,
    &#x5B;File Name] sysname,
    &#x5B;Physical Name] NVARCHAR(260),
    &#x5B;File Type] VARCHAR(4),
    &#x5B;Total Size in Bytes] BIGINT,
    &#x5B;Available Space in Bytes] BIGINT,
    &#x5B;Growth Units] VARCHAR(20),
    &#x5B;Max File Size in Bytes] BIGINT
)

SELECT @SQL =
&#039;USE &#x5B;?] 
INSERT INTO ##Results(
    &#x5B;Database Name],
    &#x5B;File Name],
    &#x5B;Physical Name],
    &#x5B;File Type],
    &#x5B;Total Size in Bytes],
    &#x5B;Available Space in Bytes],
    &#x5B;Growth Units],
    &#x5B;Max File Size in Bytes]
)
SELECT 
    DB_NAME(),
    &#x5B;name],
    physical_name,
    CASE type
        WHEN 0 THEN &#039;&#039;Data&#039;&#039;
        WHEN 1 THEN &#039;&#039;Log&#039;&#039;
    END,

    -- TOTAL SIZE (bytes) SAFE
    CAST(size AS BIGINT) * 8 * 1024,

    -- FREE SPACE (bytes) SAFE
    (CAST(size AS BIGINT) - CAST(FILEPROPERTY(&#x5B;name], &#039;&#039;SpaceUsed&#039;&#039;) AS BIGINT)) * 8 * 1024,

    -- GROWTH
    CASE is_percent_growth
        WHEN 1 THEN CAST(growth AS varchar(20)) + &#039;&#039;%&#039;&#039;
        ELSE CAST(CAST(growth AS BIGINT) * 8 * 1024 AS varchar(20))
    END,

    -- MAX SIZE (bytes) SAFE
    CASE max_size
        WHEN -1 THEN NULL
        WHEN 268435456 THEN NULL
        ELSE CAST(max_size AS BIGINT) * 8 * 1024
    END

FROM sys.database_files
ORDER BY type, file_id
&#039;

EXEC sp_MSforeachdb @SQL

-- =========================
-- RESULTATS
-- =========================

IF @Database_Name IS NULL
BEGIN
    IF @Granularity = &#039;Database&#039;
    BEGIN
        SELECT
            T.&#x5B;Database Name],

            T.&#x5B;Total Size in Bytes] AS &#x5B;DB Size (Bytes)],
            T.&#x5B;Available Space in Bytes] AS &#x5B;DB Free (Bytes)],
            T.&#x5B;Consumed Space in Bytes] AS &#x5B;DB Used (Bytes)],

            D.&#x5B;Total Size in Bytes] AS &#x5B;Data Size (Bytes)],
            D.&#x5B;Available Space in Bytes] AS &#x5B;Data Free (Bytes)],
            D.&#x5B;Consumed Space in Bytes] AS &#x5B;Data Used (Bytes)],
            CEILING(CAST(D.&#x5B;Available Space in Bytes] AS decimal(20,2)) / NULLIF(D.&#x5B;Total Size in Bytes],0) * 100) AS &#x5B;Data Free %],

            L.&#x5B;Total Size in Bytes] AS &#x5B;Log Size (Bytes)],
            L.&#x5B;Available Space in Bytes] AS &#x5B;Log Free (Bytes)],
            L.&#x5B;Consumed Space in Bytes] AS &#x5B;Log Used (Bytes)],
            CEILING(CAST(L.&#x5B;Available Space in Bytes] AS decimal(20,2)) / NULLIF(L.&#x5B;Total Size in Bytes],0) * 100) AS &#x5B;Log Free %]

        FROM
        (
            SELECT &#x5B;Database Name],
                SUM(&#x5B;Total Size in Bytes]) AS &#x5B;Total Size in Bytes],
                SUM(&#x5B;Available Space in Bytes]) AS &#x5B;Available Space in Bytes],
                SUM(&#x5B;Total Size in Bytes] - &#x5B;Available Space in Bytes]) AS &#x5B;Consumed Space in Bytes]
            FROM ##Results
            GROUP BY &#x5B;Database Name]
        ) T

        INNER JOIN
        (
            SELECT &#x5B;Database Name],
                SUM(&#x5B;Total Size in Bytes]) AS &#x5B;Total Size in Bytes],
                SUM(&#x5B;Available Space in Bytes]) AS &#x5B;Available Space in Bytes],
                SUM(&#x5B;Total Size in Bytes] - &#x5B;Available Space in Bytes]) AS &#x5B;Consumed Space in Bytes]
            FROM ##Results
            WHERE &#x5B;File Type] = &#039;Data&#039;
            GROUP BY &#x5B;Database Name]
        ) D ON T.&#x5B;Database Name] = D.&#x5B;Database Name]

        INNER JOIN
        (
            SELECT &#x5B;Database Name],
                SUM(&#x5B;Total Size in Bytes]) AS &#x5B;Total Size in Bytes],
                SUM(&#x5B;Available Space in Bytes]) AS &#x5B;Available Space in Bytes],
                SUM(&#x5B;Total Size in Bytes] - &#x5B;Available Space in Bytes]) AS &#x5B;Consumed Space in Bytes]
            FROM ##Results
            WHERE &#x5B;File Type] = &#039;Log&#039;
            GROUP BY &#x5B;Database Name]
        ) L ON T.&#x5B;Database Name] = L.&#x5B;Database Name]

        ORDER BY T.&#x5B;Database Name]
    END
    ELSE
    BEGIN
        SELECT
            &#x5B;Database Name],
            &#x5B;File Name],
            &#x5B;Physical Name],
            &#x5B;File Type],
            &#x5B;Total Size in Bytes] AS &#x5B;DB Size (Bytes)],
            &#x5B;Available Space in Bytes] AS &#x5B;DB Free (Bytes)],
            CEILING(CAST(&#x5B;Available Space in Bytes] AS decimal(20,2)) / NULLIF(&#x5B;Total Size in Bytes],0) * 100) AS &#x5B;Free Space %],
            &#x5B;Growth Units],
            &#x5B;Max File Size in Bytes] AS &#x5B;Grow Max Size (Bytes)]
        FROM ##Results
    END
END
ELSE
BEGIN
    IF @Granularity = &#039;Database&#039;
    BEGIN
        SELECT
            T.&#x5B;Database Name],

            T.&#x5B;Total Size in Bytes] AS &#x5B;DB Size (Bytes)],
            T.&#x5B;Available Space in Bytes] AS &#x5B;DB Free (Bytes)],
            T.&#x5B;Consumed Space in Bytes] AS &#x5B;DB Used (Bytes)],

            D.&#x5B;Total Size in Bytes] AS &#x5B;Data Size (Bytes)],
            D.&#x5B;Available Space in Bytes] AS &#x5B;Data Free (Bytes)],
            D.&#x5B;Consumed Space in Bytes] AS &#x5B;Data Used (Bytes)],
            CEILING(CAST(D.&#x5B;Available Space in Bytes] AS decimal(20,2)) / NULLIF(D.&#x5B;Total Size in Bytes],0) * 100) AS &#x5B;Data Free %],

            L.&#x5B;Total Size in Bytes] AS &#x5B;Log Size (Bytes)],
            L.&#x5B;Available Space in Bytes] AS &#x5B;Log Free (Bytes)],
            L.&#x5B;Consumed Space in Bytes] AS &#x5B;Log Used (Bytes)],
            CEILING(CAST(L.&#x5B;Available Space in Bytes] AS decimal(20,2)) / NULLIF(L.&#x5B;Total Size in Bytes],0) * 100) AS &#x5B;Log Free %]

        FROM
        (
            SELECT &#x5B;Database Name],
                SUM(&#x5B;Total Size in Bytes]) AS &#x5B;Total Size in Bytes],
                SUM(&#x5B;Available Space in Bytes]) AS &#x5B;Available Space in Bytes],
                SUM(&#x5B;Total Size in Bytes] - &#x5B;Available Space in Bytes]) AS &#x5B;Consumed Space in Bytes]
            FROM ##Results
            WHERE &#x5B;Database Name] = @Database_Name
            GROUP BY &#x5B;Database Name]
        ) T

        INNER JOIN
        (
            SELECT &#x5B;Database Name],
                SUM(&#x5B;Total Size in Bytes]) AS &#x5B;Total Size in Bytes],
                SUM(&#x5B;Available Space in Bytes]) AS &#x5B;Available Space in Bytes],
                SUM(&#x5B;Total Size in Bytes] - &#x5B;Available Space in Bytes]) AS &#x5B;Consumed Space in Bytes]
            FROM ##Results
            WHERE &#x5B;File Type] = &#039;Data&#039;
              AND &#x5B;Database Name] = @Database_Name
            GROUP BY &#x5B;Database Name]
        ) D ON T.&#x5B;Database Name] = D.&#x5B;Database Name]

        INNER JOIN
        (
            SELECT &#x5B;Database Name],
                SUM(&#x5B;Total Size in Bytes]) AS &#x5B;Total Size in Bytes],
                SUM(&#x5B;Available Space in Bytes]) AS &#x5B;Available Space in Bytes],
                SUM(&#x5B;Total Size in Bytes] - &#x5B;Available Space in Bytes]) AS &#x5B;Consumed Space in Bytes]
            FROM ##Results
            WHERE &#x5B;File Type] = &#039;Log&#039;
              AND &#x5B;Database Name] = @Database_Name
            GROUP BY &#x5B;Database Name]
        ) L ON T.&#x5B;Database Name] = L.&#x5B;Database Name]

        ORDER BY T.&#x5B;Database Name]
    END
    ELSE
    BEGIN
        SELECT
            &#x5B;Database Name],
            &#x5B;File Name],
            &#x5B;Physical Name],
            &#x5B;File Type],
            &#x5B;Total Size in Bytes] AS &#x5B;DB Size (Bytes)],
            &#x5B;Available Space in Bytes] AS &#x5B;DB Free (Bytes)],
            CEILING(CAST(&#x5B;Available Space in Bytes] AS decimal(20,2)) / NULLIF(&#x5B;Total Size in Bytes],0) * 100) AS &#x5B;Free Space %],
            &#x5B;Growth Units],
            &#x5B;Max File Size in Bytes] AS &#x5B;Grow Max Size (Bytes)]
        FROM ##Results
        WHERE &#x5B;Database Name] = @Database_Name
    END
END

DROP TABLE ##Results
</pre></div>


<h2 class="wp-block-heading" id="h-discovery-in-zabbix">Discovery in Zabbix</h2>



<p class="wp-block-paragraph">Once the query has been implemented in Zabbix, the next step is to create a Low-Level Discovery (LLD) rule in order to automatically discover databases and files, then generate the associated dependent items and triggers dynamically.</p>



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



<p class="wp-block-paragraph">A dedicated blog explaining how to configure Low-Level Discovery in Zabbix is available here: Create DISCO Put the link here</p>



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



<p class="wp-block-paragraph">Monitoring MSSQL database files in Zabbix can become challenging when working with multiple databases and dynamic environments.<br>While the initial query is perfectly suitable for manual analysis, its structure and dependency on the current database context make it difficult to integrate directly into Zabbix.</p>



<p class="wp-block-paragraph">By using <code>sp_MSforeachdb</code> together with a centralized temporary table, we can transform multiple independent result sets into a single normalized dataset fully compatible with Zabbix Low-Level Discovery and dependent items.</p>



<p class="wp-block-paragraph">This approach provides a scalable and reusable solution capable of automatically monitoring database and log file growth across an entire SQL Server instance while significantly reducing manual configuration and maintenance efforts.</p>



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



<span id="more-44327"></span>
<p>L’article <a href="https://www.dbi-services.com/blog/monitoring-mssql-database-files-in-zabbix/">Monitoring MSSQL Database Files in Zabbix</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/monitoring-mssql-database-files-in-zabbix/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-08-24 20:20:07 by W3 Total Cache
-->