<?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 management - dbi Blog</title>
	<atom:link href="https://www.dbi-services.com/blog/category/database-management/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.dbi-services.com/blog/category/database-management/</link>
	<description></description>
	<lastBuildDate>Mon, 10 Aug 2026 11:39:13 +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 management - dbi Blog</title>
	<link>https://www.dbi-services.com/blog/category/database-management/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<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 fetchpriority="high" 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="(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 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="(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 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="(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>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>Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived</title>
		<link>https://www.dbi-services.com/blog/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived/</link>
					<comments>https://www.dbi-services.com/blog/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Fri, 26 Jun 2026 19:11:41 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[HAProxy]]></category>
		<category><![CDATA[keepalived]]></category>
		<category><![CDATA[Load]]></category>
		<category><![CDATA[load balancer]]></category>
		<category><![CDATA[load balancing]]></category>
		<category><![CDATA[postgresql]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=45342</guid>

					<description><![CDATA[<p>Patroni runs your PostgreSQL cluster and handles failover, promoting a replica the moment the primary dies and recording the change in its distributed store (etcd, Consul, or ZooKeeper). That part works on its own. Your applications still need one stable address to connect to, and they need writes to reach the primary while reads spread [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived/">Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived</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">Patroni runs your PostgreSQL cluster and handles failover, promoting a replica the moment the primary dies and recording the change in its distributed store (etcd, Consul, or ZooKeeper). That part works on its own.</p>



<p class="wp-block-paragraph">Your applications still need one stable address to connect to, and they need writes to reach the primary while reads spread across replicas. HAProxy handles that routing, with a floating IP from Keepalived in front of it.</p>



<h1 id="h-how-the-tools-work-together" class="wp-block-heading">How the tools work together</h1>



<p class="wp-block-paragraph">Three components stands between your application and the database.</p>



<p class="wp-block-paragraph"><strong>Patroni</strong> manages replication and failover, and it runs an agent on every PostgreSQL node. Each agent exposes a small REST API (port 8008 by default) that reports that node&#8217;s role.</p>



<p class="wp-block-paragraph"><strong>HAProxy</strong> accepts client connections and forwards them to the right node. It asks Patroni&#8217;s REST API which node is the primary and which are replicas, then sends each connection to a matching node.</p>



<p class="wp-block-paragraph"><strong>Keepalived</strong> publishes a virtual IP that floats between your HAProxy hosts using VRRP. Your application connects to the VIP, so one HAProxy host going down doesn&#8217;t take the whole entry point with it.</p>



<p class="wp-block-paragraph">Your application talks to the VIP. Keepalived points the VIP at a live HAProxy. HAProxy forwards the connection to whichever PostgreSQL node Patroni reports as healthy for that role.</p>



<h1 id="h-the-health-check-method" class="wp-block-heading">The health-check method</h1>



<p class="wp-block-paragraph">HAProxy checks one port and routes to another.</p>



<p class="wp-block-paragraph">Patroni&#8217;s REST API returns an HTTP status that depends on the node&#8217;s role:</p>



<ul class="wp-block-list">
<li><code>GET /</code> returns <code>200</code> only on the leader (the primary). A non-leader node returns <code>503</code>.</li>



<li><code>GET /primary</code> is the explicit name for the same leader check.</li>



<li><code>GET /replica</code> returns <code>200</code> only on a running replica.</li>



<li><code>GET /read-only</code> returns <code>200</code> on the primary or a replica, any node that can serve a read.</li>
</ul>



<p class="wp-block-paragraph">In our case, HAProxy runs its health check against the API port (8008) and reads that status code, then forwards the SQL connection to the database port (5432). A node receives traffic only when its API answers <code>200</code> for the role that listener cares about. Point a listener&#8217;s check at <code>/</code> and it follows the primary. Point it at <code>/replica</code> and it follows the replicas. Patroni promotes a new leader, the status codes change, and HAProxy moves traffic to match within a couple of health-check cycles.</p>



<h1 id="h-a-first-and-simple-working-configuration" class="wp-block-heading">A first and simple working configuration</h1>



<p class="wp-block-paragraph">A two-node setup with <code>10.5.5.147</code> and <code>10.5.5.148</code> looks like this. One listener handles writes, the other handles reads.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
listen PG1
    bind *:5000
    option httpchk
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008
    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008

listen PG1_ro
    bind *:5001
    option httpchk GET /replica
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008
    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008
</pre></div>


<p class="wp-block-paragraph">This example runs PostgreSQL on port 5432 and the Patroni API on 8008, so swap in whatever ports your deployment uses (the defaults are 5432 and 8008).</p>



<p class="wp-block-paragraph">Line by line:</p>



<ul class="wp-block-list">
<li><code>bind *:5000</code> and <code>bind *:5001</code> are the two addresses your applications connect to. Send writes to 5000 and reads to 5001.</li>



<li><code>option httpchk</code> (with no path) on the first listener checks Patroni&#8217;s root endpoint. Only the leader answers <code>200</code>, so HAProxy sends port 5000 traffic to the current primary.</li>



<li><code>option httpchk GET /replica</code> on the second listener checks the replica endpoint, so HAProxy sends port 5001 traffic to a replica.</li>



<li><code>http-check expect status 200</code> tells HAProxy that <code>200</code> means healthy and anything else means down.</li>



<li><code>inter 3s fall 3 rise 2</code> checks every 3 seconds, marks a server down after 3 failures, and brings it back after 2 successes.</li>



<li><code>on-marked-down shutdown-sessions</code> kills existing connections to a server the instant HAProxy marks it down, so clients reconnect and get rerouted instead of hanging on a dead node.</li>



<li><code>check port 8008</code> is the trick in action: health checks hit the Patroni API on 8008 while HAProxy forwards traffic to PostgreSQL on 5432.</li>



<li><code>maxconn 100</code> limit connections per server so you don&#8217;t exhaust PostgreSQL&#8217;s connection slots.</li>
</ul>



<p class="wp-block-paragraph">For a primary plus one or more replicas, this routes writes and reads to the right node and survives a failover.</p>



<h1 id="h-the-failure-mode-hiding-in-the-read-path" class="wp-block-heading">The failure mode hiding in the read path</h1>



<p class="wp-block-paragraph">Imagine a two-node cluster: one primary, one replica. The replica goes down. Maybe it crashed, maybe Patroni is mid-switchover and no standby exists for a few seconds.</p>



<p class="wp-block-paragraph">Your read traffic hits port 5001. That listener marks a server up only when <code>GET /replica</code> returns <code>200</code>, and right now no node is a replica. HAProxy has zero usable servers in the pool, so it refuses the connection. Read queries start failing.</p>



<p class="wp-block-paragraph">The primary is up the entire time, and it can serve those reads. Your config won&#8217;t send them there, because you told the read listener to look for replicas and nothing else. You&#8217;ve turned a degraded cluster that could still serve reads into a read outage. You feel this most on small clusters, and each failover passes through a window where the old primary becomes a replica and no standby is available yet. In the worst case, your replica is down, and one of your application is connecting to port 5001, resulting in errors.</p>



<h1 id="h-the-fix-fall-back-to-the-primary" class="wp-block-heading">The fix: fall back to the primary</h1>



<p class="wp-block-paragraph">Send reads to the primary when the read listener runs out of replicas, instead of dropping them.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
listen PG1
    bind *:5000
    option httpchk
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008
    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008

listen PG1_ro
    bind *:5001
    option httpchk GET /replica
    http-check expect status 200
    use_backend PG1_ro_leader if { nbsrv(PG1_ro) eq 0 }
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008
    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008

backend PG1_ro_leader
    option httpchk GET /primary
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008
    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008
</pre></div>


<p class="wp-block-paragraph">This new line carries the whole fix:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
use_backend PG1_ro_leader if { nbsrv(PG1_ro) eq 0 }
</pre></div>


<p class="wp-block-paragraph"><code>nbsrv(PG1_ro)</code> counts the usable servers in the <code>PG1_ro</code> pool, which here means the number of available replicas, since those servers pass the check only when <code>GET /replica</code> returns <code>200</code>. While at least one replica is up, the count stays above zero, the condition is false, and reads stay on the replicas. The moment the last replica drops, <code>nbsrv(PG1_ro)</code> hits zero, the condition fires, and HAProxy diverts reads to the <code>PG1_ro_leader</code> backend.</p>



<p class="wp-block-paragraph">That backend health-checks with <code>GET /primary</code>, so the only server it counts as up is the current primary. HAProxy sends reads to the primary until a replica returns, then shifts them back to the replica pool once a replica passes its check again.</p>



<p class="wp-block-paragraph">Three names have to agree for this to work. The backend you define (<code>backend PG1_ro_leader</code>), the backend you route to (<code>use_backend PG1_ro_leader</code>), and the pool you count (<code>nbsrv(PG1_ro)</code>) all reference the real section names. Drop in a stale name from an earlier version and HAProxy either refuses to start or counts the wrong pool.</p>



<h2 class="wp-block-heading">The /read-only shortcut and what it costs</h2>



<p class="wp-block-paragraph">Patroni offers <code>GET /read-only</code>, which returns <code>200</code> on the primary and the replicas alike. Point the read listener there and both the primary and the replicas serve reads, no fallback backend needed.</p>



<p class="wp-block-paragraph">The cost is read load on the primary even when your replicas are healthy and idle. The fallback approach keeps reads off the primary until the replicas are gone, then leans on it as a safety net. You protect the primary&#8217;s write capacity during normal operation and still keep reads alive during a replica outage.</p>



<p class="wp-block-paragraph">To keep lagging replicas out of the read pool, Patroni accepts a threshold on the replica check, for example <code>GET /replica?lag=10MB</code>, which fails any replica more than 10 MB behind. Pair that with the fallback and HAProxy drops the lagging replicas from rotation while reads still have somewhere to go.</p>



<h1 class="wp-block-heading">Keepalived: removing HAProxy as a single point of failure</h1>



<p class="wp-block-paragraph">One HAProxy host fronting the cluster moves the single point of failure up a layer. Run HAProxy on two hosts and let Keepalived float a virtual IP between them with VRRP. Your application connects to the VIP, and whichever HAProxy holds it answers.</p>



<p class="wp-block-paragraph">A minimal <code>keepalived.conf</code> on the primary HAProxy host:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
vrrp_script chk_haproxy {
    script &quot;killall -0 haproxy&quot;   # succeeds while the haproxy process is alive
    interval 2
    weight 2
}

vrrp_instance VI_1 {
    interface eth0
    state MASTER
    virtual_router_id 51
    priority 101
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass changeme
    }
    virtual_ipaddress {
        10.0.0.1
    }
    track_script {
        chk_haproxy
    }
}
</pre></div>


<p class="wp-block-paragraph">The second HAProxy host runs the same file with <code>state BACKUP</code> and a lower <code>priority</code> (100). Both advertise over VRRP, and the higher priority holds the VIP. <code>chk_haproxy</code> runs every two seconds. If HAProxy dies on the active host, its priority drops and the backup takes the VIP, so an HAProxy crash on one host no longer takes the entry point down with it.</p>



<p class="wp-block-paragraph">Point your applications at <code>10.0.0.1:5000</code> for writes and <code>10.5.5.100:5001</code> for reads. Your applications never see which physical HAProxy does the work.</p>



<h1 class="wp-block-heading">Summary</h1>



<p class="wp-block-paragraph">Patroni keeps the cluster healthy and picks the leader. HAProxy turns Patroni&#8217;s REST API into routing, sending writes to the primary and reads to the replicas by health-checking the API port while forwarding to the database port. The naive read-only listener drops reads when the last replica goes down, even though the primary could serve them. Adding <code>use_backend ... if { nbsrv(...) eq 0 }</code> with a primary-checking backend closes that gap, and a lag threshold on the replica check keeps stale standbys out of rotation. Keepalived puts a floating VIP in front of two HAProxy instances so the proxy layer survives a host failure too.</p>



<p class="wp-block-paragraph">Writes reach the primary and reads spread across the replicas. Reads stay up as long as one node in the cluster is alive.</p>



<p class="wp-block-paragraph">Let me know if you find any improvements to this configuration <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f600.png" alt="😀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> </p>
<p>L’article <a href="https://www.dbi-services.com/blog/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived/">Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived</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/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server Always On Availability Groups and Database Master Keys: A Hidden Failover Pitfall</title>
		<link>https://www.dbi-services.com/blog/sql-server-always-on-availability-groups-and-database-master-keys-a-hidden-failover-pitfall/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-always-on-availability-groups-and-database-master-keys-a-hidden-failover-pitfall/#respond</comments>
		
		<dc:creator><![CDATA[Hocine Mechara]]></dc:creator>
		<pubDate>Tue, 09 Jun 2026 15:01:28 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[data]]></category>
		<category><![CDATA[Data Security]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[disaster recovery]]></category>
		<category><![CDATA[encryption]]></category>
		<category><![CDATA[High availability]]></category>
		<category><![CDATA[SQL-Server]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44986</guid>

					<description><![CDATA[<p>I recently came across an interesting case involving a client whose application used SQL Server symmetric keys to encrypt sensitive data. The database was hosted in an Always On Availability Group environment for high-availability and disaster-recovery. The interesting and challenging aspect of this setup is ensuring that the encryption and decryption remains intact after a [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-always-on-availability-groups-and-database-master-keys-a-hidden-failover-pitfall/">SQL Server Always On Availability Groups and Database Master Keys: A Hidden Failover Pitfall</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">I recently came across an interesting case involving a client whose application used SQL Server symmetric keys to encrypt sensitive data. The database was hosted in an Always On Availability Group environment for high-availability and disaster-recovery.</p>



<p class="wp-block-paragraph">The interesting and challenging aspect of this setup is ensuring that the encryption and decryption remains intact after a failover. </p>



<p class="wp-block-paragraph">This challenge originates from SQL Server&#8217;s encryption hierarchy. In a typical setup, the Service Master Key (SMK) on server level protects the Database Master Key (DMK) on database level. The Database Master Key, in turn, protects certificates and asymmetric keys, while certificates are commonly used to protect symmetric keys. These symmetric keys may then be used by the application to encrypt and decrypt sensitive data.</p>



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



<p class="wp-block-paragraph">When a failover to a secondary replica occurs, the Service Master Key (SMK) is no longer the same as on the previous primary replica. Since the Database Master Key (DMK) is typically encrypted by the local Service Master Key, SQL Server may no longer be able to open the DMK transparently after the failover. As a result, any encryption objects that depend on the DMK, such as certificates and symmetric keys, may become inaccessible, potentially causing application failures when encrypted data needs to be read or written.</p>



<p class="wp-block-paragraph">Let&#8217;s now explore the built-in approaches in SQL Server to ensure that the encryption hierarchy remains functional across replicas and that applications continue to operate seamlessly after a failover.</p>



<p class="wp-block-paragraph">First, let&#8217;s create a test database and a Database Master Key:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
-- Create Demo DB
CREATE DATABASE TestDMK;
GO
--Backup DB
BACKUP DATABASE TestDMK
TO DISK = &#039;C:\SQLServer_mnt\BACKUP\TestDMK.bak&#039;;
go
-- Create Database Master Key
CREATE MASTER KEY
ENCRYPTION BY PASSWORD = &#039;MyVeryStrongPassword_123!&#039;;
GO
-- Check Master Key
SELECT DB_NAME() as DB, *
FROM sys.symmetric_keys
WHERE name = &#039;##MS_DatabaseMasterKey##&#039;;
GO

</pre></div>


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



<p class="wp-block-paragraph">It is worth noting that you can verify whether a Database Master Key is also encrypted by the Service Master Key by querying the <strong>sys.databases</strong> catalog view. When a Database Master Key is created, SQL Server typically adds encryption by the Service Master Key automatically. This behaviour can be changed by explicitly removing the Service Master Key encryption.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
select is_master_key_encrypted_by_server, * from sys.databases where name = &#039;TestDMK&#039;
</pre></div>


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



<p class="wp-block-paragraph">With the Database Master Key in place, let&#8217;s create a certificate and a symmetric key that will be used throughout the remainder of this demonstration.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
CREATE CERTIFICATE &#x5B;mainDBCert] --&gt; This cert is going to be encrypted by the dmk
WITH SUBJECT = &#039;test Cert&#039;;
go
CREATE SYMMETRIC KEY mainKey
WITH ALGORITHM = AES_256
ENCRYPTION BY CERTIFICATE mainDBCert;
go
SELECT * FROM sys.certificates;
SELECT * FROM sys.symmetric_keys WHERE name &lt;&gt; &#039;##MS_DatabaseMasterKey##&#039;;
SELECT * FROM sys.symmetric_keys;
go

</pre></div>


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



<p class="wp-block-paragraph">To demonstrate the encryption and decryption process, let&#8217;s create a simple table that will contain sensitive information and insert some data.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
--create table with sensitive data
CREATE TABLE CreditCards
(
    Id int,
    CreditCardNr varbinary(max),
    CVC varbinary(max),
    ExpirationDate varbinary(max)
);
go
--open symmetric key
OPEN SYMMETRIC KEY mainKey
DECRYPTION BY CERTIFICATE mainDBCert;
--insert sensitive data
INSERT INTO CredîtCards
VALUES
(
    1,
    EncryptByKey(Key_GUID(&#039;mainKey&#039;), &#039;1234-5678-9012-3456&#039;),
    EncryptByKey(Key_GUID(&#039;mainKey&#039;), &#039;123&#039;),
    EncryptByKey(Key_GUID(&#039;mainKey&#039;), &#039;1233&#039;)
);
CLOSE SYMMETRIC KEY mainKey;
GO
Select * from CreditCards

</pre></div>


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



<p class="wp-block-paragraph">If we query the table without opening the symmetric key and decrypting the values, we can see that we won’t see the data.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
--select data without decryption
SELECT
    Id,
    CONVERT(varchar(50), CreditCardNr) AS CreditCardNr,
    CONVERT(varchar(10), CVC) AS CVC,
    CONVERT(varchar(10), ExpirationDate) AS ExpirationDate
FROM CreditCards;

</pre></div>


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



<p class="wp-block-paragraph">However, if we open the symmetric key first and use it to decrypt the data, we are able to retrieve the original sensitive information:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
OPEN SYMMETRIC KEY mainKey
DECRYPTION BY CERTIFICATE mainDBCert;
GO
SELECT
    Id,
    CONVERT(varchar(50), DecryptByKey(CreditCardNr)) AS CreditCardNr,
    CONVERT(varchar(10), DecryptByKey(CVC)) AS CVC,
    CONVERT(varchar(10), DecryptByKey(ExpirationDate)) AS ExpirationDate
FROM CredîtCards;
GO
CLOSE SYMMETRIC KEY mainKey;
GO
</pre></div>


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



<p class="wp-block-paragraph">Now let&#8217;s add our database to an Always On Availability Group to provide high availability and disaster recovery. If you follow the Availability Group wizard, you may notice that the wizard asks you to enter a password. What the wizards is asking for here is the password of the Database Master Key (DMK). The DMK protects the certificate in our encryption hierarchy, and the certificate, in turn, protects the symmetric key used to encrypt and decrypt the sensitive data.</p>



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



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



<p class="wp-block-paragraph">On the right-hand side of the wizard, there is a small and easily overlooked text box where you can enter the Database Master Key password.</p>



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



<p class="wp-block-paragraph">And now comes the tricky &#8211; and at the same time somewhat surprising &#8211; part. If you use automatic seeding, which is my preferred option whenever the database size allows it, the process will skip the part where the Master Key password is verified and applied on the secondary replicas.</p>



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



<p class="wp-block-paragraph">As a result, when a failover occurs, SQL Server would no longer be able to open the Database Master Key (DMK) transparently on the new primary replica. Consequently, the application would no longer be able to encrypt or decrypt data unless the Database Master Key is explicitly opened first with the password.</p>



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



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



<p class="wp-block-paragraph">In such a scenario, the issue can be resolved by storing the Database Master Key password as a credential on each replica using the <strong>sp_control_dbmasterkey_password </strong>stored procedure. SQL Server can then use this credential to automatically open the Database Master Key after a failover, allowing the encryption hierarchy to remain intact and ensuring that certificates, symmetric keys, and encrypted data remain accessible transparently.</p>



<p class="wp-block-paragraph"><strong>!!Note that you must do that on each replica!!</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
sp_control_dbmasterkey_password @db_name = N&#039;TestDMK&#039;
    , @password = N&#039;MyVeryStrongPassword_123!&#039;
    , @action = N&#039;add&#039;;
select * from sys.master_key_passwords;
select * from sys.credentials;

</pre></div>


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



<p class="wp-block-paragraph">Once the credential has been created, SQL Server is again able to open the Database Master Key transparently. As a result, the application can continue to encrypt and decrypt data without having to explicitly open the Master Key with the password.</p>



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



<p class="wp-block-paragraph">There is also an option by which SQL Server automatically creates the Database Master Key credential across all replicas when a database is joined to an Availability Group. This happens when <strong>&#8220;Full Database Backup and Log Backup&#8221;</strong> is selected as the initial data synchronization option in the Availability Group wizard.</p>



<p class="wp-block-paragraph">To demonstrate this, let&#8217;s remove the previously created credential using the same stored procedure.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
sp_control_dbmasterkey_password @db_name = N&#039;TestDMK&#039;
    , @password = N&#039;MyVeryStrongPassword_123!&#039;
    , @action = N&#039;drop&#039;;
select * from sys.master_key_passwords;
select * from sys.credentials;

</pre></div>


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



<p class="wp-block-paragraph">Now, let&#8217;s select <strong>&#8220;Full Database and Log Backup&#8221;</strong> as the initial data synchronization option in the Availability Group wizard.</p>



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



<p class="wp-block-paragraph">As you can see, the process is no longer skipping the Database Master Key password validation step.</p>



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



<p class="wp-block-paragraph">In the summary, you can see that the process automatically created the Database Master Key password credential on each replica.</p>



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



<p class="wp-block-paragraph">If you check the credentials on each replica afterwards, you can verify that they were created automatically.</p>



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



<p class="wp-block-paragraph">As the Database Master Key password credential has been added to all replicas, SQL Server can transparently open the DMK after a failover. As a result, the application can continue to encrypt and decrypt data without having to explicitly open the Database Master Key.</p>



<p class="wp-block-paragraph"><strong>But one question still puzzles me:</strong> why the same operation isn&#8217;t performed automatically when automatic seeding is used?</p>



<p class="wp-block-paragraph">Honestly, I have no clue yet. If you know more let me know it in the comments section. I&#8217;d love to learn more about it.</p>



<p class="wp-block-paragraph">Thanks for reading – Hocine 😉</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-always-on-availability-groups-and-database-master-keys-a-hidden-failover-pitfall/">SQL Server Always On Availability Groups and Database Master Keys: A Hidden Failover Pitfall</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-always-on-availability-groups-and-database-master-keys-a-hidden-failover-pitfall/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Data Point Prague 2026</title>
		<link>https://www.dbi-services.com/blog/data-point-prague-2026/</link>
					<comments>https://www.dbi-services.com/blog/data-point-prague-2026/#respond</comments>
		
		<dc:creator><![CDATA[Stéphane Savorgnano]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 10:41:37 +0000</pubDate>
				<category><![CDATA[Business Intelligence]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[MS Teams]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[Data Point Prague]]></category>
		<category><![CDATA[Fabric]]></category>
		<category><![CDATA[PowerBI]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44870</guid>

					<description><![CDATA[<p>For the first time, I joined some days ago the Data Point Prague . The biggest International Data Conference in Prague. A two-day event dedicated to advancing knowledge in data technologies. Thanks to dbi-services to let me the possibility to attend this event. Thursday, May 28, 2026 The first day was dedicated to pre-conference Workshops. [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/data-point-prague-2026/">Data Point Prague 2026</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="410" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/WelcomeDataPointPrague-1024x410.jpeg" alt="" class="wp-image-44871" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/WelcomeDataPointPrague-1024x410.jpeg 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/WelcomeDataPointPrague-300x120.jpeg 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/WelcomeDataPointPrague-768x307.jpeg 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/WelcomeDataPointPrague-1536x614.jpeg 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/WelcomeDataPointPrague-2048x819.jpeg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">For the first time, I joined some days ago the <a href="https://datapointprague.cz/">Data Point Prague</a> . The biggest International Data Conference in Prague. A two-day event dedicated to advancing knowledge in data technologies. <br>Thanks to <a href="https://www.dbi-services.com/">dbi-services</a> to let me the possibility to attend this event.</p>



<h2 class="wp-block-heading" id="h-thursday-may-28-2026">Thursday, May 28, 2026</h2>



<p class="wp-block-paragraph">The first day was dedicated to pre-conference Workshops. I choose the workshop &#8220;Performance optimization by identifying and correcting bad SQL code&#8221; with <a href="https://mvp.microsoft.com/en-us/mvp/profile/fc630ff3-3c9a-e411-93f2-9cb65495d3c4">Uwe Ricken</a> as a speaker.<br>During the full day, Uwe shown us through different realistic scenarios how to identify, analyze and optimize inefficient SQL Codes with usage of tools like Query Store, Windows Admin Center or even Perfmon.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="768" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/UweDPP-1024x768.jpeg" alt="" class="wp-image-44873" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/UweDPP-1024x768.jpeg 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/UweDPP-300x225.jpeg 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/UweDPP-768x576.jpeg 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/UweDPP-1536x1152.jpeg 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/UweDPP-2048x1536.jpeg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading" id="h-friday-may-29-2026">Friday, May 29, 2026</h2>



<p class="wp-block-paragraph" id="h-friday-may-29-2026the-second-day-started-witht-the">The second day started with the Keynote session by Jorge Docampo Carro, Senior Program Manager at Microsoft.<br>He shown us that AI is no longer just used to create peace of codes, it is becoming an active collaborator in how Spark jobs, lakehouses, and data pipelines are designed, implemented, and operated.<br>Moreover Visual Studio Code is appearing as a preferred workspace for Fabric data engineering — bringing notebooks, Spark job definitions, environments, and lakehouse artifacts directly into the developer experience.<br></p>



<p class="wp-block-paragraph" id="h-friday-may-29-2026the-second-day-started-witht-the">I then followed a very interesting but quite intensive session of Torsten Strauss named &#8220;A Deep Dive into Optimized Locking in SQL Server 2025&#8221;.<br>During this session Torsten talked about the introduction with SQL Server 2025 of Optimized Locking and Lock After Qualification (LAQ) to reduce contention and improve concurrency.<br>He explained that LAQ defers lock acquisition until rows are fully evaluated, meaning only qualifying rows are locked instead of locking during the scan phase. This significantly lowers the number and duration of locks, reducing blocking, deadlocks, and lock escalation in high-concurrency workloads. <br>He shows us this new Locking mechanism through different concrete examples. The improvements are especially impactful for large updates, selective queries, and OLTP systems with heavy parallel activity. Combined with proper indexing, transaction design, and features like RCSI, these enhancements enable higher throughput and more stable performance.</p>



<p class="wp-block-paragraph">In the Afternoon I saw an interesting session from Uwe again about the Security Techniques for cross database access. It was a good refresh on how to grant access but keep the security at a high level to protect data. Discussions and examples turned around Synonyms, signed Stored Procedures and Trustworthy database options.</p>



<p class="wp-block-paragraph">I joined also some sessions more related to PowerBi, Fabric or AI which were also really interesting even if I&#8217;m not an expert in those domains.</p>



<p class="wp-block-paragraph">It was my first time at Data Point Prague #DataPointPrague but certainly not the last one as this conference is really well organized. The first full day workshop was really great and the second day with more than 30 sessions let you choose the ones you are interesting on.<br>Thank you very much for this event and see you next time <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>L’article <a href="https://www.dbi-services.com/blog/data-point-prague-2026/">Data Point Prague 2026</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/data-point-prague-2026/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server Snapshot Backup and Restore with Proxmox ZFS &#8211; REST API with SQL Server 2025 (3/4)</title>
		<link>https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-rest-api-with-sql-server-2025-3-3/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-rest-api-with-sql-server-2025-3-3/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Thu, 14 May 2026 21:39:18 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[proxmox]]></category>
		<category><![CDATA[ZFS]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44525</guid>

					<description><![CDATA[<p>The proposed architecture consists in adding a small internal REST API on the Proxmox server in order to expose a controlled ZFS snapshot operation. SQL Server 2025 can then call this API through sp_invoke_external_rest_endpoint, instead of running SSH commands directly or relying on an external tool. The role of the API is deliberately limited: it [&#8230;]</p>
<p>L’article <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/">SQL Server Snapshot Backup and Restore with Proxmox ZFS &#8211; REST API with SQL Server 2025 (3/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">The proposed architecture consists in adding a small internal REST API on the Proxmox server in order to expose a controlled ZFS snapshot operation. SQL Server 2025 can then call this API through sp_invoke_external_rest_endpoint, instead of running SSH commands directly or relying on an external tool.</p>



<p class="wp-block-paragraph">The role of the API is deliberately limited: it receives a snapshot request, checks that the requested zvol is authorized, and then runs the zfs snapshot command on the Proxmox side. An allowlist is used to restrict the ZFS volumes that can be accessed. This prevents a REST call from being able to manipulate any dataset on the server.</p>



<p class="wp-block-paragraph">With this approach, we can reproduce a behavior close to what an enterprise storage array provides, but using Proxmox and ZFS. It is important to note that Proxmox does not natively provide the same level of integration as Pure Storage for SQL Server snapshots. Pure Storage provides dedicated mechanisms and integrations. In our case, we need to build a specific orchestration layer. The REST API therefore acts as an adapter between SQL Server, which drives the snapshot backup workflow, and ZFS, which actually performs the storage-level snapshot.</p>



<h2 class="wp-block-heading" id="h-architecture">Architecture</h2>



<p class="wp-block-paragraph">Here is a global overview of the architecture:</p>



<ul class="wp-block-list">
<li>SQL Server freezes the database I/Os</li>



<li>SQL Server 2025 calls the internal REST API</li>



<li>The REST API validates the request and checks the zvol allowlist</li>



<li>The API triggers the ZFS snapshot on Proxmox</li>



<li>The API returns the snapshot information to SQL Server</li>



<li>SQL Server creates the metadata-only backup</li>



<li>The database I/Os are released</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="998" height="1024" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-65-998x1024.png" alt="" class="wp-image-44526" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-65-998x1024.png 998w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-65-292x300.png 292w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-65-768x788.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-65-1496x1536.png 1496w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-65-1995x2048.png 1995w" sizes="auto, (max-width: 998px) 100vw, 998px" /></figure>



<h2 class="wp-block-heading">REST API implementation</h2>



<p class="wp-block-paragraph">Under Proxmox, we install the required packages:</p>



<pre class="wp-block-code"><code>apt update
apt install -y python3-venv sudo openssl</code></pre>



<p class="wp-block-paragraph">We create a dedicated user:</p>



<pre class="wp-block-code"><code>useradd --system \
&nbsp; --home /opt/sql-zfs-api \
&nbsp; --shell /usr/sbin/nologin \
&nbsp; sqlsnap</code></pre>



<p class="wp-block-paragraph">We create the following folders:</p>



<pre class="wp-block-code"><code>mkdir -p /opt/sql-zfs-api
mkdir -p /etc/sql-zfs-api</code></pre>



<p class="wp-block-paragraph">We declare the authorized zvol :</p>



<pre class="wp-block-code"><code>cat &gt;/etc/sql-zfs-api/allowed-zvols &lt;&lt;'EOF'
sqlpool/pve/vm-302-disk-0
EOF</code></pre>



<p class="wp-block-paragraph">We create a root-only allowlist:</p>



<pre class="wp-block-code"><code>chown root:root /etc/sql-zfs-api/allowed-zvols
chmod 600 /etc/sql-zfs-api/allowed-zvols</code></pre>



<p class="wp-block-paragraph">Then we create the secured ZFS helper. This script is executed as root through sudo, but it rejects any dataset that is not defined in the allowlist.</p>



<pre class="wp-block-code"><code>cat &gt;/usr/local/sbin/sql-zfs-helper &lt;&lt;'EOF'
#!/usr/bin/env bash
set -euo pipefail

ALLOW_FILE="/etc/sql-zfs-api/allowed-zvols"
LOCK_FILE="/run/sql-zfs-helper.lock"

die() {
  echo "$*" &gt;&amp;2
  exit 1
}

exec 9&gt;"$LOCK_FILE"
flock -n 9 || die "another snapshot operation is already running"

&#091;&#091; -r "$ALLOW_FILE" ]] || die "allowlist not readable: $ALLOW_FILE"

mapfile -t ALLOWED_DATASETS &lt; &lt;(grep -Ev '^\s*(#|$)' "$ALLOW_FILE")

is_allowed() {
  local ds="$1"
  local allowed
  for allowed in "${ALLOWED_DATASETS&#091;@]}"; do
    &#091;&#091; "$ds" == "$allowed" ]] &amp;&amp; return 0
  done
  return 1
}

valid_snapname() {
  &#091;&#091; "$1" =~ ^&#091;A-Za-z0-9_.:-]{1,120}$ ]]
}

ACTION="${1:-}"
shift || true

case "$ACTION" in
  snapshot)
    SNAPNAME="${1:-}"
    shift || true

    valid_snapname "$SNAPNAME" || die "invalid snapshot name: $SNAPNAME"
    &#091;&#091; "$#" -ge 1 ]] || die "no zvol specified"
    &#091;&#091; "$#" -le 8 ]] || die "too many zvols"

    SNAPSHOTS=()

    for DS in "$@"; do
      is_allowed "$DS" || die "dataset not allowed: $DS"
      /sbin/zfs list -H -t volume -o name "$DS" &gt;/dev/null 2&gt;&amp;1 || die "zvol not found: $DS"

      FULLSNAP="${DS}@${SNAPNAME}"

      if /sbin/zfs list -H -t snapshot -o name "$FULLSNAP" &gt;/dev/null 2&gt;&amp;1; then
        die "snapshot already exists: $FULLSNAP"
      fi

      SNAPSHOTS+=("$FULLSNAP")
    done

    /sbin/zfs snapshot "${SNAPSHOTS&#091;@]}"
    /sbin/zfs hold sqlsnap "${SNAPSHOTS&#091;@]}"

    printf '{"status":"ok","snapshots":&#091;'
    SEP=""
    for S in "${SNAPSHOTS&#091;@]}"; do
      printf '%s"%s"' "$SEP" "$S"
      SEP=","
    done
    printf ']}\n'
    ;;

  list)
    /sbin/zfs list -H -t snapshot -o name -r sqlpool | grep '@sql_' || true
    ;;

  *)
    die "usage: sql-zfs-helper snapshot SNAPNAME ZVOL &#091;ZVOL...]"
    ;;
esac
EOF

chown root:root /usr/local/sbin/sql-zfs-helper
chmod 750 /usr/local/sbin/sql-zfs-helper
</code></pre>



<p class="wp-block-paragraph">We only allow the helper through sudo:</p>



<pre class="wp-block-code"><code>cat &gt;/etc/sudoers.d/sql-zfs-helper &lt;&lt;'EOF'
sqlsnap ALL=(root) NOPASSWD: /usr/local/sbin/sql-zfs-helper *
EOF

chmod 440 /etc/sudoers.d/sql-zfs-helper
visudo -cf /etc/sudoers.d/sql-zfs-helper</code></pre>



<p class="wp-block-paragraph">We install the FastAPI API:</p>



<pre class="wp-block-code"><code>python3 -m venv /opt/sql-zfs-api/venv
/opt/sql-zfs-api/venv/bin/pip install fastapi "uvicorn&#091;standard]"</code></pre>



<p class="wp-block-paragraph">We create the application file:</p>



<pre class="wp-block-code"><code>cat &gt;/opt/sql-zfs-api/app.py &lt;&lt;'EOF'
import os
import re
import json
import socket
import secrets
import subprocess
from datetime import datetime, timezone
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel, Field

API_KEY = os.environ.get("SQL_ZFS_API_KEY", "")
ALLOW_FILE = "/etc/sql-zfs-api/allowed-zvols"
SNAP_RE = re.compile(r"^&#091;A-Za-z0-9_.:-]{1,120}$")

app = FastAPI(title="SQL ZFS Snapshot API", version="1.0.0")


class SnapshotRequest(BaseModel):
    database: str = Field(..., min_length=1, max_length=128)
    vmid: int = 302
    snapname: str = Field(..., min_length=1, max_length=120)
    zvols: list&#091;str] = Field(..., min_length=1, max_length=8)


def load_allowed_zvols() -&gt; set&#091;str]:
    with open(ALLOW_FILE, "r", encoding="utf-8") as f:
        return {
            line.strip()
            for line in f
            if line.strip() and not line.strip().startswith("#")
        }


def check_api_key(x_sqlsnap_key: str | None) -&gt; None:
    if not API_KEY:
        raise HTTPException(status_code=500, detail="API key not configured")

    if not x_sqlsnap_key:
        raise HTTPException(status_code=401, detail="missing API key")

    if not secrets.compare_digest(x_sqlsnap_key, API_KEY):
        raise HTTPException(status_code=403, detail="invalid API key")


@app.get("/health")
def health():
    return {
        "status": "ok",
        "host": socket.gethostname(),
        "utc": datetime.now(timezone.utc).isoformat(),
    }


@app.post("/v1/sql-zfs/snapshot")
def create_snapshot(
    req: SnapshotRequest,
    x_sqlsnap_key: str | None = Header(default=None, alias="x-sqlsnap-key"),
):
    check_api_key(x_sqlsnap_key)

    if not SNAP_RE.fullmatch(req.snapname):
        raise HTTPException(status_code=400, detail="invalid snapname")

    allowed = load_allowed_zvols()

    for zvol in req.zvols:
        if zvol not in allowed:
            raise HTTPException(status_code=403, detail=f"zvol not allowed: {zvol}")

    cmd = &#091;
        "sudo",
        "/usr/local/sbin/sql-zfs-helper",
        "snapshot",
        req.snapname,
        *req.zvols,
    ]

    try:
        completed = subprocess.run(
            cmd,
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=30,
            check=False,
        )
    except subprocess.TimeoutExpired:
        raise HTTPException(status_code=504, detail="zfs snapshot timeout")

    if completed.returncode != 0:
        raise HTTPException(
            status_code=500,
            detail={
                "error": completed.stderr.strip(),
                "stdout": completed.stdout.strip(),
            },
        )

    snapshots = &#091;f"{zvol}@{req.snapname}" for zvol in req.zvols]

    return {
        "status": "ok",
        "database": req.database,
        "vmid": req.vmid,
        "snapname": req.snapname,
        "snapshots": snapshots,
        "media_description": "zfs|" + socket.gethostname() + "|" + ";".join(snapshots),
    }
EOF

chown -R root:root /opt/sql-zfs-api
chmod 755 /opt/sql-zfs-api
chmod 644 /opt/sql-zfs-api/app.py
</code></pre>



<p class="wp-block-paragraph">We configure and generate the key:</p>



<pre class="wp-block-code"><code>APIKEY="$(openssl rand -hex 32)"
echo "$APIKEY"</code></pre>



<p class="wp-block-paragraph">We create the environment file:</p>



<pre class="wp-block-code"><code>cat &gt;/etc/sql-zfs-api/sql-zfs-api.env &lt;&lt;EOF
SQL_ZFS_API_KEY=$APIKEY
EOF

chown root:root /etc/sql-zfs-api/sql-zfs-api.env
chmod 600 /etc/sql-zfs-api/sql-zfs-api.env</code></pre>



<p class="wp-block-paragraph">We need to save the generated key.</p>



<p class="wp-block-paragraph">Next, we enable HTTPS. SQL Server sp_invoke_external_rest_endpoint calls HTTPS endpoints, and the documentation specifies that only HTTPS endpoints with TLS are supported.</p>



<pre class="wp-block-code"><code>openssl req -x509 -newkey rsa:4096 -sha256 -days 360 -nodes \
  -keyout /etc/sql-zfs-api/tls.key \
  -out /etc/sql-zfs-api/tls.crt \
  -subj "/CN=promox1" \
  -addext "subjectAltName=DNS:promox1,IP:192.168.1.110"

chown root:sqlsnap /etc/sql-zfs-api/tls.key /etc/sql-zfs-api/tls.crt
chmod 640 /etc/sql-zfs-api/tls.key
chmod 644 /etc/sql-zfs-api/tls.crt</code></pre>



<p class="wp-block-paragraph">The /etc/sql-zfs-api/tls.crt certificate must be imported into the Windows trusted root certification authorities on the SQL Server side. Otherwise, the HTTPS call may fail.</p>



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



<pre class="wp-block-code"><code>cat &gt;/etc/systemd/system/sql-zfs-api.service &lt;&lt;'EOF'
&#091;Unit]
Description=SQL Server to ZFS Snapshot API
After=network-online.target
Wants=network-online.target

&#091;Service]
User=sqlsnap
Group=sqlsnap
WorkingDirectory=/opt/sql-zfs-api
EnvironmentFile=/etc/sql-zfs-api/sql-zfs-api.env
ExecStart=/opt/sql-zfs-api/venv/bin/uvicorn app:app --host 0.0.0.0 --port 8443 --ssl-keyfile /etc/sql-zfs-api/tls.key --ssl-certfile /etc/sql-zfs-api/tls.crt
Restart=on-failure
RestartSec=3

&#091;Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now sql-zfs-api
systemctl status sql-zfs-api
</code></pre>



<p class="wp-block-paragraph">We check the status of our API:</p>



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



<p class="wp-block-paragraph">It is possible to call the API in PowerShell using Invoke-RestMethod with PowerShell 7:</p>



<pre class="wp-block-code"><code>$headers = @{
"Content-Type"  = "application/json"
"x-sqlsnap-key" = "MyKey"
}

$body = @{
database = "StackOverflow"
vmid     = 302
snapname = "StackOverflow_test010"
zvols    = @("sqlpool/pve/vm-302-disk-0")
} | ConvertTo-Json -Depth 5

Invoke-RestMethod `
-Uri "https://192.168.1.110:8443/v1/sql-zfs/snapshot" `
-Method Post `
-Headers $headers `
-Body $body `
-ContentType "application/json" `
-SkipCertificateCheck
</code></pre>



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



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



<h2 class="wp-block-heading" id="h-test-from-sql-server">Test from SQL Server</h2>



<p class="wp-block-paragraph">A certificate was generated on Proxmox and it needs to be imported on the SQL Server host. In my case, it was located here:</p>



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



<p class="wp-block-paragraph">I then imported it on Windows Server:</p>



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



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="118" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-71-1024x118.png" alt="" class="wp-image-44532" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-71-1024x118.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-71-300x34.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-71-768x88.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-71.png 1384w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">For testing purposes, I created something simple. On the SQL Server side, we can create a database that will be used to store our future stored procedure. This procedure will allow us to interact with the API. In my case, I created a database called dbi_tools:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="244" height="131" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-72.png" alt="" class="wp-image-44533" /></figure>



<p class="wp-block-paragraph">This database will contain a credential. In our case, the DATABASE SCOPED CREDENTIAL is used to securely store the authentication information required to call the REST API from SQL Server. This allows us, for example, to protect the API key:</p>



<pre class="wp-block-code"><code>USE &#091;dbi_tools]
GO

IF NOT EXISTS (
    SELECT 1
    FROM sys.symmetric_keys
    WHERE name = '##MS_DatabaseMasterKey##'
)
BEGIN
    CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'MyStrongPassword_%99';
END
GO

CREATE DATABASE SCOPED CREDENTIAL &#091;https://192.168.1.110:8443/v1/sql-zfs/snapshot]
WITH
    IDENTITY = 'HTTPEndpointHeaders',
    SECRET = '{"x-sqlsnap-key":"MyAPIKey"}';
GO</code></pre>



<p class="wp-block-paragraph">We then create a stored procedure to encapsulate the code used to call the API:</p>



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

CREATE OR ALTER PROCEDURE dbo.usp_BackupDatabase_WithZfsSnapshot
    @DatabaseName sysname,
    @BackupDirectory nvarchar(4000) = N'D:\Backups\'
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @Url nvarchar(4000) =
        N'https://192.168.1.110:8443/v1/sql-zfs/snapshot';

    DECLARE @Vmid int = 302;

    DECLARE @ZvolsJson nvarchar(max) =
        N'&#091;"sqlpool/pve/vm-302-disk-0"]';

    DECLARE @Stamp varchar(20) =
        REPLACE(REPLACE(CONVERT(varchar(19), SYSUTCDATETIME(), 126), '-', ''), ':', '') + 'Z';

    DECLARE @SafeDbName nvarchar(128) =
        REPLACE(REPLACE(REPLACE(@DatabaseName, N' ', N'_'), N'&#091;', N''), N']', N'');

    DECLARE @SnapName nvarchar(128) =
        CONCAT(N'sql_', @SafeDbName, N'_', @Stamp);

    DECLARE @BackupFile nvarchar(4000) =
        CONCAT(@BackupDirectory, N'\', @SafeDbName, N'_', @Stamp, N'.bkm');

    DECLARE @Payload nvarchar(max) =
    (
        SELECT
            @DatabaseName AS &#091;database],
            @Vmid AS &#091;vmid],
            @SnapName AS &#091;snapname],
            JSON_QUERY(@ZvolsJson) AS &#091;zvols]
        FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
    );

    DECLARE @ReturnCode int;
    DECLARE @Response nvarchar(max);
    DECLARE @SnapshotList nvarchar(max);

    SELECT @SnapshotList =
        STRING_AGG(CONCAT(&#091;value], N'@', @SnapName), N';')
    FROM OPENJSON(@ZvolsJson);

    DECLARE @MediaDescription nvarchar(max) =
        CONCAT(N'zfs|promox1|', @SnapshotList);

    DECLARE @Sql nvarchar(max);

    BEGIN TRY
        SET @Sql =
            N'ALTER DATABASE ' + QUOTENAME(@DatabaseName) +
            N' SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON;';

        EXEC sys.sp_executesql @Sql;

        EXEC @ReturnCode = sys.sp_invoke_external_rest_endpoint
            @url = @Url,
            @method = N'POST',
            @headers = N'{"Content-Type":"application/json","Accept":"application/json"}',
            @payload = @Payload,
            @credential = &#091;https://192.168.1.110:8443/v1/sql-zfs/snapshot],
            @timeout = 30,
            @response = @Response OUTPUT;

        IF @ReturnCode &lt;&gt; 0
        BEGIN
            DECLARE @Err nvarchar(max) =
                CONCAT(N'ZFS snapshot API failed. ReturnCode=', @ReturnCode, N' Response=', @Response);
            THROW 51001, @Err, 1;
        END;

        SET @Sql =
            N'BACKUP DATABASE ' + QUOTENAME(@DatabaseName) + N'
              TO DISK = @BackupFile
              WITH METADATA_ONLY,
                   FORMAT,
                   MEDIANAME = @MediaName,
                   MEDIADESCRIPTION = @MediaDescription,
                   NAME = @BackupName;';

        EXEC sys.sp_executesql
            @Sql,
            N'@BackupFile nvarchar(4000),
              @MediaName nvarchar(128),
              @MediaDescription nvarchar(max),
              @BackupName nvarchar(128)',
            @BackupFile = @BackupFile,
            @MediaName = @SnapName,
            @MediaDescription = @MediaDescription,
            @BackupName = @SnapName;

        SELECT
            @DatabaseName AS database_name,
            @SnapName AS zfs_snapshot_name,
            @SnapshotList AS zfs_snapshots,
            @BackupFile AS metadata_backup_file,
            @MediaDescription AS media_description,
            @Response AS api_response;
    END TRY
    BEGIN CATCH
        IF DATABASEPROPERTYEX(@DatabaseName, 'IsDatabaseSuspendedForSnapshotBackup') = 1
        BEGIN
            SET @Sql =
                N'ALTER DATABASE ' + QUOTENAME(@DatabaseName) +
                N' SET SUSPEND_FOR_SNAPSHOT_BACKUP = OFF;';

            EXEC sys.sp_executesql @Sql;
        END;

        THROW;
    END CATCH
END;
GO
</code></pre>



<p class="wp-block-paragraph">We then call the stored procedure:</p>



<pre class="wp-block-code"><code>EXEC dbi_tools.dbo.usp_BackupDatabase_WithZfsSnapshot
    @DatabaseName = N'StackOverflow',
    @BackupDirectory = N'D:\Backups\';</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="137" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-73-1024x137.png" alt="" class="wp-image-44534" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-73-1024x137.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-73-300x40.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-73-768x102.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-73.png 1432w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">The backup was generated :</p>



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



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



<h2 class="wp-block-heading" id="h-references">References</h2>



<p class="wp-block-paragraph"><a href="https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-invoke-external-rest-endpoint-transact-sql?view=sql-server-ver17&amp;tabs=request-headers">sp_invoke_external_rest_endpoint</a></p>



<p class="wp-block-paragraph">Thank you. <a href="https://www.linkedin.com/in/amine-haloui-76968056/">Amine Haloui</a></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-rest-api-with-sql-server-2025-3-3/">SQL Server Snapshot Backup and Restore with Proxmox ZFS &#8211; REST API with SQL Server 2025 (3/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/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-rest-api-with-sql-server-2025-3-3/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server Snapshot Backup and Restore with Proxmox ZFS &#8211; Powershell implementation (2/4)</title>
		<link>https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Thu, 14 May 2026 21:35:41 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[proxmox]]></category>
		<category><![CDATA[ZFS]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44497</guid>

					<description><![CDATA[<p>In the previous section, we discussed the drawbacks of running the commands manually. Indeed, the manual process was taking too much time and could directly impact the database state while the freeze was occurring. To address this issue, it is possible to automate the solution with PowerShell. The idea is to automate the different operations [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/">SQL Server Snapshot Backup and Restore with Proxmox ZFS &#8211; Powershell implementation (2/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 previous section, we discussed the drawbacks of running the commands manually. Indeed, the manual process was taking too much time and could directly impact the database state while the freeze was occurring.</p>



<p class="wp-block-paragraph">To address this issue, it is possible to automate the solution with PowerShell. The idea is to automate the different operations involved in the snapshot backup and restore process.</p>



<p class="wp-block-paragraph">We will use two scripts:</p>



<ul class="wp-block-list">
<li>One script to perform the backups and create the snapshots.</li>



<li>One script to perform the restores.</li>
</ul>



<h2 class="wp-block-heading" id="h-backup-process">Backup process</h2>



<p class="wp-block-paragraph">Here is how the backup process works:</p>



<ul class="wp-block-list">
<li>We connect to the corresponding SQL Server instance.</li>



<li>We change the state of the database using ALTER DATABASE &#8230; SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON. At this point, the I/Os are frozen.</li>



<li>We connect to the hypervisor through SSH.</li>



<li>We create the snapshot.</li>



<li>We back up the database using BACKUP DATABASE &#8230; WITH METADATA_ONLY.</li>



<li>We change the state of the database using ALTER DATABASE &#8230; SET SUSPEND_FOR_SNAPSHOT_BACKUP = OFF. At this point, the I/Os are unfrozen.</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="627" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-1024x627.png" alt="" class="wp-image-44499" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-1024x627.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-300x184.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-768x470.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-1536x941.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-2048x1254.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Powershell implementation (backup)</h2>



<p class="wp-block-paragraph">Here is the code used to perform the backup:</p>



<pre class="wp-block-code"><code>param(
    &#091;string]$SqlInstance = "VM-WS25-SQL2",
    &#091;string]$Database    = "StackOverflow",
    &#091;string]$BackupDir   = "D:\Backups",
    &#091;string]$PveHost     = "192.168.1.110",
    &#091;string]$PveUser     = "MyUser",
    &#091;string&#091;]]$Zvols     = @("sqlpool/pve/vm-302-disk-0")
)

$Timestamp = Get-Date -Format "yyyyMMddTHHmmss"
$SnapName  = "sql_${Database}_${Timestamp}"

$DbSafe = $Database.Replace("]", "]]")
$BackupFile = Join-Path $BackupDir "${Database}_${Timestamp}.bkm"

$ZfsSnapshots = $Zvols | ForEach-Object { "$_@$SnapName" }
$ZfsSnapshotArgs = $ZfsSnapshots -join " "

$MediaDescription = "zfs|$PveHost|$ZfsSnapshotArgs"

$BackupFileSql = $BackupFile.Replace("'", "''")
$MediaSql = $MediaDescription.Replace("'", "''")

$connString = "Server=$SqlInstance;Database=master;Integrated Security=True;TrustServerCertificate=True;Application Name=ZFS-TSQL-Snapshot;"
$conn = New-Object System.Data.SqlClient.SqlConnection $connString

function Invoke-SqlNonQuery {
    param(&#091;string]$Sql)

    $cmd = $conn.CreateCommand()
    $cmd.CommandTimeout = 0
    $cmd.CommandText = $Sql
    &#091;void]$cmd.ExecuteNonQuery()
}

try {
    $conn.Open()

    Write-Host "Freezing SQL database writes..."
    Invoke-SqlNonQuery "ALTER DATABASE &#091;$DbSafe] SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON;"

    Write-Host "Taking ZFS snapshot on Proxmox..."
    ssh "$PveUser@$PveHost" "zfs snapshot $ZfsSnapshotArgs &amp;&amp; zfs hold sqlsnap $ZfsSnapshotArgs"

    if ($LASTEXITCODE -ne 0) {
        throw "ZFS snapshot failed on $PveHost"
    }

    Write-Host "Writing SQL metadata backup..."

    Invoke-SqlNonQuery @"
BACKUP DATABASE &#091;$DbSafe]
TO DISK = N'$BackupFileSql'
WITH METADATA_ONLY,
     MEDIADESCRIPTION = N'$MediaSql',
     NAME = N'$SnapName';
"@

    Write-Host "Snapshot backup completed:"
    Write-Host "  Snapshot: $ZfsSnapshotArgs"
    Write-Host "  Metadata: $BackupFile"
}
catch {
    Write-Warning $_

    try {
        Write-Warning "Attempting to unfreeze SQL database..."
        Invoke-SqlNonQuery "ALTER DATABASE &#091;$DbSafe] SET SUSPEND_FOR_SNAPSHOT_BACKUP = OFF;"
    }
    catch {
        Write-Warning "Could not unfreeze cleanly. Check SQL Server error log."
    }

    throw
}
finally {
    $conn.Close()
}</code></pre>



<h2 class="wp-block-heading">Restore process</h2>



<p class="wp-block-paragraph">Here is how the restore process works:</p>



<ul class="wp-block-list">
<li>We connect to the corresponding SQL Server instance.</li>



<li>We take the database offline.</li>



<li>The volume dedicated to the StackOverflow database is taken offline.</li>



<li>We connect to the hypervisor through SSH.</li>



<li>We roll back the corresponding snapshot.</li>



<li>We restore the database using the corresponding backup, which was created at the same time as the snapshot.</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="627" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-1024x627.png" alt="" class="wp-image-44501" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-1024x627.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-300x184.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-768x470.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-1536x941.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-2048x1254.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Powershell implementation (restore)</h2>



<p class="wp-block-paragraph">Here is the code used to perform the restore:</p>



<pre class="wp-block-code"><code>param(
    &#091;string]$SqlInstance = "VM-WS25-SQL2",
    &#091;string]$Database    = "StackOverflow",
    &#091;string]$BackupFile  = "D:\Backups\StackOverflow_20260514T122642.bkm",
    &#091;string]$SnapName    = "sql_StackOverflow_20260514T122642",
    &#091;string]$PveHost     = "192.168.1.110",
    &#091;string]$PveUser     = "MyUser",
    &#091;string&#091;]]$Zvols     = @("sqlpool/pve/vm-302-disk-0"),
    &#091;string&#091;]]$DatabaseDriveLetters = @("T"),
    &#091;switch]$NoRecovery
)

$ErrorActionPreference = "Stop"

function Assert-SafeName {
    param(
        &#091;string]$Value,
        &#091;string]$Name,
        &#091;string]$Pattern
    )

    if ($Value -notmatch $Pattern) {
        throw "$Name contained not allowed characters : $Value"
    }
}

function Normalize-DriveLetter {
    param(&#091;string]$DriveLetter)

    $letter = $DriveLetter.Trim().TrimEnd(":").ToUpperInvariant()

    if ($letter -notmatch '^&#091;A-Z]$') {
        throw "Drive letter invalid : $DriveLetter"
    }

    return $letter
}

function Get-DiskForDriveLetter {
    param(&#091;string]$DriveLetter)

    $letter = Normalize-DriveLetter $DriveLetter

    $partition = Get-Partition -DriveLetter $letter -ErrorAction Stop
    $disk = $partition | Get-Disk -ErrorAction Stop

    return &#091;pscustomobject]@{
        DriveLetter = $letter
        DiskNumber  = &#091;int]$disk.Number
        IsOffline   = &#091;bool]$disk.IsOffline
        FriendlyName = $disk.FriendlyName
        Size        = $disk.Size
    }
}

function Invoke-SshChecked {
    param(&#091;string]$Command)

    Write-Host "SSH $PveUser@$PveHost :: $Command"

    &amp; ssh "$PveUser@$PveHost" "$Command"

    if ($LASTEXITCODE -ne 0) {
        throw "SSH command failed with code $LASTEXITCODE : $Command"
    }
}

function New-SqlConnection {
    $connString = "Server=$SqlInstance;Database=master;Integrated Security=True;TrustServerCertificate=True;Application Name=ZFS-TSQL-Restore-NoVmRestart;"
    return New-Object System.Data.SqlClient.SqlConnection $connString
}

function Invoke-SqlNonQuery {
    param(&#091;string]$Sql)

    $conn = New-SqlConnection

    try {
        $conn.Open()
        $cmd = $conn.CreateCommand()
        $cmd.CommandTimeout = 0
        $cmd.CommandText = $Sql
        &#091;void]$cmd.ExecuteNonQuery()
    }
    finally {
        $conn.Close()
    }
}

function Invoke-SqlScalar {
    param(&#091;string]$Sql)

    $conn = New-SqlConnection

    try {
        $conn.Open()
        $cmd = $conn.CreateCommand()
        $cmd.CommandTimeout = 0
        $cmd.CommandText = $Sql
        return $cmd.ExecuteScalar()
    }
    finally {
        $conn.Close()
    }
}

function Set-DatabaseDisksOffline {
    param(&#091;object&#091;]]$DiskInfos)

    $offlinedByScript = @()

    foreach ($diskInfo in ($DiskInfos | Sort-Object DiskNumber -Unique)) {
        if ($diskInfo.IsOffline) {
            Write-Host "Disque $($diskInfo.DiskNumber) déjà offline. Lecteur $($diskInfo.DriveLetter):"
            continue
        }

        Write-Host "Taking the Windows disk offline $($diskInfo.DiskNumber), drive $($diskInfo.DriveLetter):"
        Set-Disk -Number $diskInfo.DiskNumber -IsOffline $true

        $offlinedByScript += $diskInfo
    }

    return $offlinedByScript
}

function Set-DatabaseDisksOnline {
    param(&#091;object&#091;]]$DiskInfos)

    foreach ($diskInfo in ($DiskInfos | Sort-Object DiskNumber -Unique)) {
        Write-Host "Bringing the Windows disk back online. $($diskInfo.DiskNumber), drive $($diskInfo.DriveLetter):"
        Set-Disk -Number $diskInfo.DiskNumber -IsOffline $false
    }

    Write-Host "Update-HostStorageCache..."
    Update-HostStorageCache
}

Assert-SafeName -Value $SnapName -Name "SnapName" -Pattern '^&#091;A-Za-z0-9_.:-]{1,160}$'

foreach ($zvol in $Zvols) {
    Assert-SafeName -Value $zvol -Name "Zvol" -Pattern '^&#091;A-Za-z0-9_.:/-]{1,240}$'
}

$DbQuoted = "&#091;" + $Database.Replace("]", "]]") + "]"
$DbLiteral = $Database.Replace("'", "''")
$BackupFileSql = $BackupFile.Replace("'", "''")

$ZfsSnapshots = $Zvols | ForEach-Object { "$_@$SnapName" }
$ZfsSnapshotArgs = ($ZfsSnapshots | ForEach-Object { "'$_'" }) -join " "

$RecoveryOption = if ($NoRecovery) { "NORECOVERY" } else { "RECOVERY" }

$DatabaseDiskInfos = @()
$DisksOfflinedByScript = @()

Write-Host ""
Write-Host "Restore SQL Server from a ZFS snapshot, without restarting the VM"
Write-Host "SQL Instance : $SqlInstance"
Write-Host "Database     : $Database"
Write-Host "BackupFile   : $BackupFile"
Write-Host "DB volumes   : $($DatabaseDriveLetters -join ', ')"
Write-Host "Snapshots    :"
$ZfsSnapshots | ForEach-Object { Write-Host "  $_" }
Write-Host ""

try {
    Write-Host "Checking ZFS snapshots..."
    Invoke-SshChecked "zfs list -H -t snapshot -o name $ZfsSnapshotArgs &gt;/dev/null"

    Write-Host "Identifying Windows disks containing SQL Server files..."
    foreach ($driveLetter in $DatabaseDriveLetters) {
        $diskInfo = Get-DiskForDriveLetter $driveLetter
        $DatabaseDiskInfos += $diskInfo

        Write-Host "Drive $($diskInfo.DriveLetter): -&gt; Windows disk $($diskInfo.DiskNumber) &#091;$($diskInfo.FriendlyName)]"
    }

    $backupDrive = $null
    if ($BackupFile -match '^(&#091;A-Za-z]):\\') {
        $backupDrive = Normalize-DriveLetter $Matches&#091;1]

        try {
            $backupDiskInfo = Get-DiskForDriveLetter $backupDrive
            $targetDiskNumbers = @($DatabaseDiskInfos | ForEach-Object { $_.DiskNumber } | Select-Object -Unique)

            if ($targetDiskNumbers -contains $backupDiskInfo.DiskNumber) {
                throw @"
The backup file $BackupFile is located on drive $backupDrive, which is on the same Windows disk as the SQL Server data volume.
Taking the data disk offline would make the .bkm file inaccessible, and a rollback could also make the .bkm file disappear.
Move the .bkm file to C:, a network share, or another disk that is not rolled back.
"@
            }
        }
        catch {
            throw
        }
    }

    Write-Host "Checking whether the SQL Server database exists..."
    $DbExists = Invoke-SqlScalar "SELECT CASE WHEN DB_ID(N'$DbLiteral') IS NULL THEN 0 ELSE 1 END;"

    if ($DbExists -eq 1) {
        Write-Host "Taking database $Database OFFLINE..."
        Invoke-SqlNonQuery @"
ALTER DATABASE $DbQuoted SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
ALTER DATABASE $DbQuoted SET OFFLINE WITH ROLLBACK IMMEDIATE;
"@
    }
    else {
        Write-Host "Database $Database does not exist in SQL Server. Continuing with disk offline and ZFS rollback."
    }

    Write-Host "Taking Windows disks containing MDF/LDF files offline..."
    $DisksOfflinedByScript = Set-DatabaseDisksOffline -DiskInfos $DatabaseDiskInfos

    Write-Host "Rolling back ZFS snapshot..."
    $RollbackCommands = ($ZfsSnapshots | ForEach-Object { "zfs rollback -r '$_'" }) -join "; "
    Invoke-SshChecked "set -e; $RollbackCommands"

    Write-Host "Bringing Windows disks back online..."
    Set-DatabaseDisksOnline -DiskInfos $DisksOfflinedByScript
    $DisksOfflinedByScript = @()

    Write-Host "Short pause to let Windows and SQL Server detect the restored disk state..."
    Start-Sleep -Seconds 5

    Write-Host "Restoring SQL Server metadata-only backup..."

    $RestoreSql = @"
RESTORE DATABASE $DbQuoted
FROM DISK = N'$BackupFileSql'
WITH METADATA_ONLY,
     REPLACE,
     $RecoveryOption;
"@

    Invoke-SqlNonQuery $RestoreSql

    if (-not $NoRecovery) {
        Write-Host "Setting database back to MULTI_USER..."
        Invoke-SqlNonQuery @"
ALTER DATABASE $DbQuoted SET MULTI_USER;
"@
    }

    Write-Host ""
    Write-Host "Restore completed."
    Write-Host "Database : $Database"
    Write-Host "Snapshot : $SnapName"
    Write-Host "Backup   : $BackupFile"
}
catch {
    Write-Warning "Restore failed: $_"

    if ($DisksOfflinedByScript.Count -gt 0) {
        try {
            Write-Warning "Attempting to bring disks offlined by the script back online..."
            Set-DatabaseDisksOnline -DiskInfos $DisksOfflinedByScript
            $DisksOfflinedByScript = @()
        }
        catch {
            Write-Warning "Unable to automatically bring the disks back online. Check with Get-Disk."
        }
    }

    try {
        $DbExistsAfterError = Invoke-SqlScalar "SELECT CASE WHEN DB_ID(N'$DbLiteral') IS NULL THEN 0 ELSE 1 END;"

        if ($DbExistsAfterError -eq 1 -and -not $NoRecovery) {
            Write-Warning "Attempting to set the database back ONLINE/MULTI_USER..."
            Invoke-SqlNonQuery @"
ALTER DATABASE $DbQuoted SET ONLINE;
ALTER DATABASE $DbQuoted SET MULTI_USER;
"@
        }
    }
    catch {
        Write-Warning "Unable to automatically set the database back ONLINE/MULTI_USER."
    }

    throw
}</code></pre>



<h2 class="wp-block-heading">What does it look like?</h2>



<p class="wp-block-paragraph">We start the backup process:</p>



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



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



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



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



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



<p class="wp-block-paragraph">We drop the StackOverflow database:</p>



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



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



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



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



<p class="wp-block-paragraph">The database is available again. The restore took only a few seconds for a database of approximately 200 GB.</p>



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



<p class="wp-block-paragraph">In my case, the solution is executed from the SQL Server itself. Ideally, it should rather be hosted on another server or client machine. We could also imagine running these scripts from a scheduler such as RedDeck, for example.</p>



<p class="wp-block-paragraph">During the database restore, the database is switched to SINGLE_USER mode. This could be an issue if the applications using the database reconnect very frequently. A better approach would probably be to explicitly terminate the active sessions using the KILL command.</p>



<p class="wp-block-paragraph">We have also not yet covered the use of a REST API.</p>



<p class="wp-block-paragraph">Thank you. <a href="https://www.linkedin.com/in/amine-haloui-76968056/">Amine Haloui</a></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/">SQL Server Snapshot Backup and Restore with Proxmox ZFS &#8211; Powershell implementation (2/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/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/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-23 09:19:48 by W3 Total Cache
-->