<?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, 01 Jun 2026 10:41:39 +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>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[Non classifié(e)]]></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 fetchpriority="high" 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="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>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>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 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="(max-width: 1024px) 100vw, 1024px" /></figure>



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



<p 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 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>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>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>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/3)</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/3)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![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.</p>



<p>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>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>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 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="(max-width: 998px) 100vw, 998px" /></figure>



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



<p>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>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>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>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>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>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>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>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>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>We configure and generate the key:</p>



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



<p>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>We need to save the generated key.</p>



<p>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>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>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>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>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>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>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>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>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>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>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>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>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><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>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/3)</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/3)</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/3)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![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.</p>



<p>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>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>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>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>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>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>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>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>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>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>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>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>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>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>We have also not yet covered the use of a REST API.</p>



<p>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/3)</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>
		<item>
		<title>SQL Server Snapshot Backup and Restore with Proxmox ZFS (1/3)</title>
		<link>https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Thu, 14 May 2026 21:26:03 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[Hardware & Storage]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[proxmox]]></category>
		<category><![CDATA[Storage]]></category>
		<category><![CDATA[ZFS]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44439</guid>

					<description><![CDATA[<p>We are currently working with clients on migrations to SQL Server 2022 and SQL Server 2025. During a discussion with one client, we reviewed some of the benefits introduced in the latest SQL Server 2022 and 2025 releases. Among the available features, starting with SQL Server 2022, we have: Starting with SQL Server 2025: The [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs/">SQL Server Snapshot Backup and Restore with Proxmox ZFS (1/3)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>We are currently working with clients on migrations to SQL Server 2022 and SQL Server 2025. During a discussion with one client, we reviewed some of the benefits introduced in the latest SQL Server 2022 and 2025 releases.</p>



<p>Among the available features, starting with SQL Server 2022, we have:</p>



<ul class="wp-block-list">
<li>T-SQL snapshot backup : <a href="https://learn.microsoft.com/en-us/sql/relational-databases/backup-restore/create-a-transact-sql-snapshot-backup?view=sql-server-ver17">https://learn.microsoft.com/en-us/sql/relational-databases/backup-restore/create-a-transact-sql-snapshot-backup?view=sql-server-ver17</a></li>
</ul>



<p>Starting with SQL Server 2025:</p>



<ul class="wp-block-list">
<li>REST API Call through sp_invoke_external_rest_endpoint : <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">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</a></li>
</ul>



<p>The customer’s environment consists of a very large number of instances, some of which host very large SQL Server databases. In this customer’s case, we are referring to a database of approximately 6–7 TB, configured for high availability using Always On Availability Groups. For this database, backups take around two hours, and restores take slightly longer.</p>



<p>In addition, the customer has a Pure Storage array.</p>



<p>We explained to the customer that it is possible to use certain SQL Server 2025 features together with their Pure Storage array to perform snapshots and restores very quickly.</p>



<p>In summary, the process consists of performing the following operations:</p>



<ul class="wp-block-list">
<li>Change the database state to suspend writes.</li>



<li>Create the snapshot using the storage system.</li>



<li>Perform a backup using the BACKUP DATABASE MyDB WITH METADATA_ONLY command to indicate that a snapshot has been taken.</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="231" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-36-1024x231.png" alt="" class="wp-image-44440" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-36-1024x231.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-36-300x68.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-36-768x173.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-36.png 1130w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>Reference: <a href="https://learn.microsoft.com/en-us/sql/relational-databases/backup-restore/create-a-transact-sql-snapshot-backup?view=sql-server-ver17">https://learn.microsoft.com/en-us/sql/relational-databases/backup-restore/create-a-transact-sql-snapshot-backup?view=sql-server-ver17</a></p>



<p>However, the customer raised several interesting questions, which, reading between the lines, can be summarized as follows:</p>



<ul class="wp-block-list">
<li>Can this also be applied to PostgreSQL?</li>



<li>Are we dependent on Pure Storage to achieve this?</li>
</ul>



<p>Several articles have been published about the implementation of this process between SQL Server and Pure Storage including the following one:</p>



<ul class="wp-block-list">
<li><a href="https://www.nocentino.com/posts/2025-05-19-t-sql-rest-api-integration-in-sql-server-2025-streamlining-t-sql-snapshot-backups">https://www.nocentino.com/posts/2025-05-19-t-sql-rest-api-integration-in-sql-server-2025-streamlining-t-sql-snapshot-backups</a></li>
</ul>



<p>In my opinion, it is possible to reproduce this operating model with other systems. In my case, we will use Proxmox and ZFS.</p>



<h2 class="wp-block-heading" id="h-context-and-environment">Context and environment</h2>



<p>ZFS pool provides fast, storage-level, copy-on-write snapshots with minimal space overhead. This makes it well suited for SQL Server snapshot backups, where the database writes are briefly suspended while the underlying virtual disk is captured. ZFS also allows precise rollback or cloning of a snapshot, which is useful for both restore testing and recovery scenarios.</p>



<p>On Proxmox, it integrates naturally with VM disks, making it a practical alternative to enterprise storage snapshot platforms.</p>



<p>The environment consists of a server and two disks: one disk used to store the VMs, and a 1 TB Samsung T7 disk that will be used to create our ZFS pool.</p>



<h2 class="wp-block-heading" id="h-proxmox-setup">Proxmox Setup</h2>



<p>We identity the path of the related volume (Samsung T7) :</p>



<pre class="wp-block-code"><code>for d in /dev/disk/by-id/*; do
&nbsp; &#091; "$(readlink -f "$d")" = "/dev/sda" ] &amp;&amp; echo "$d"
done</code></pre>



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



<p>We create the pool. Everything stored in the disk will be erased :</p>



<pre class="wp-block-code"><code>DISK="/dev/disk/by-id/usb-Samsung_PSSD_T7_S6TWNJ0T300328F-0:0"

wipefs -a "$DISK"
sgdisk --zap-all "$DISK"
zpool create \
&nbsp; -o ashift=12 \
&nbsp; -o autotrim=on \
&nbsp; -O compression=lz4 \
&nbsp; -O atime=off \
&nbsp; -O xattr=sa \
&nbsp; -O acltype=posixacl \
&nbsp; -m /mnt/sqlpool \
&nbsp; sqlpool "$DISK"</code></pre>



<p>Then we create a Proxmox dataset for the VM disks:</p>



<pre class="wp-block-code"><code>zfs create sqlpool/pve</code></pre>



<p>We add it to proxmox:</p>



<pre class="wp-block-code"><code>pvesm add zfspool sql-zfs \
  --pool sqlpool/pve \
  --content images,rootdir \
  --sparse 1</code></pre>



<p>We check the pool:</p>



<pre class="wp-block-code"><code>zpool status sqlpool

zfs list

pvesm status
pool: sqlpool
state: ONLINE

config:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAME&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATE&nbsp;&nbsp;&nbsp;&nbsp; READ WRITE CKSUM
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sqlpool&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ONLINE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp;&nbsp;&nbsp; 0
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;usb-Samsung_PSSD_T7_S6TWNJ0T300328F-0:0&nbsp;   ONLINE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp;&nbsp;&nbsp; 0&nbsp;&nbsp;&nbsp;&nbsp; 0

errors: No known data errors

NAME&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; USED&nbsp; AVAIL&nbsp; REFER&nbsp; MOUNTPOINT
sqlpool&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 636K&nbsp;&nbsp; 899G&nbsp;&nbsp;&nbsp; 96K&nbsp; /mnt/sqlpool
sqlpool/pve&nbsp;&nbsp;&nbsp; 96K&nbsp;&nbsp; 899G&nbsp;&nbsp;&nbsp; 96K&nbsp; /mnt/sqlpool/pve

Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type&nbsp;&nbsp;&nbsp;&nbsp; Status&nbsp;&nbsp;&nbsp;&nbsp; Total (KiB)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Used (KiB) Available (KiB)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %
local&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dir&nbsp;&nbsp;&nbsp;&nbsp; active&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 98497780&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 42429080&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 51019152&nbsp;&nbsp; 43.08%
local-lvm&nbsp;&nbsp;&nbsp;&nbsp; lvmthin&nbsp;&nbsp;&nbsp;&nbsp; active&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 3746553856&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 285112748&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 3461441107&nbsp;&nbsp;&nbsp; 7.61%
sql-zfs&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; zfspool&nbsp;&nbsp;&nbsp;&nbsp; active&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 942931428&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 96&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 942931332&nbsp;&nbsp;&nbsp; 0.00%</code></pre>



<p>My VM ID is 302 and we have to add the virtual disk into the ZFS pool:</p>



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



<pre class="wp-block-code"><code>VMID=302
qm set "$VMID" --agent enabled=1
qm set "$VMID" --scsihw virtio-scsi-single
qm set "$VMID" --scsi1 sql-zfs:700,cache=none,discard=on,iothread=1,ssd=1</code></pre>



<p>Be carefull to the scsi ID. You may overwrite a used volume.</p>



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



<p>Once the pool created we have something like this :</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="375" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-40-1024x375.png" alt="" class="wp-image-44450" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-40-1024x375.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-40-300x110.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-40-768x281.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-40-1536x562.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-40.png 1614w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>On the virtual machine side, I have 3 disks :</p>



<ul class="wp-block-list">
<li>1 for my virtual machine (for Windows Server)</li>



<li>1 for SQL Server</li>



<li>1 linked to the ZFS pool to store the user database (the StackOverflow database)</li>
</ul>



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



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



<p>The virtual machine used for the tests runs with:</p>



<ul class="wp-block-list">
<li>Windows Server 2025 Standard Edition</li>



<li>SQL Server 2025 Enterprise Developer Edition</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="346" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-47-1024x346.png" alt="" class="wp-image-44457" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-47-1024x346.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-47-300x101.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-47-768x259.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-47.png 1403w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>The mounted zvol is represented by the Databases (T:) volume. Most of the files related to the SQL Server installation are stored on the SQL (D:) volume while the StackOverflow database is located on the Databases (T:) volume.</p>



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



<h2 class="wp-block-heading" id="h-manual-process-flow-snapshot">Manual process flow (snapshot)</h2>



<p>Here is how we will proceed to create a snapshot and then restore the database:</p>



<ul class="wp-block-list">
<li>ALTER DATABASE [StackOverflow] SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON;</li>



<li>Create the snapshot using the zfs snapshot command.</li>



<li>Run BACKUP DATABASE [StackOverflow] &#8230; WITH METADATA_ONLY.</li>
</ul>



<p>To avoid confusion and to be able to link the snapshot to the backup, we will include the snapshot name in the MEDIADESCRIPTION clause.</p>



<p>Here are the corresponding commands to create the snapshot:</p>



<pre class="wp-block-code"><code>ALTER DATABASE &#091;StackOverflow] SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON;</code></pre>



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



<p>We perform the snapshot:</p>



<pre class="wp-block-code"><code>zfs snapshot sqlpool/pve/vm-302-disk-0@StackOverflow_11052026_235500</code></pre>



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



<p>In the same session as the ALTER DATABASE command, we perform a backup:</p>



<pre class="wp-block-code"><code>BACKUP DATABASE &#091;StackOverflow]
TO DISK = N'D:\Backups\StackOverflow_11052026_235500.bkm'
WITH METADATA_ONLY, MEDIADESCRIPTION = N'zfs|proxmox1|sqlpool/pve/vm-302-disk-0@StackOverflow_11052026_235500';</code></pre>



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



<p>The error log shows the following:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="57" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-31-1024x57.png" alt="" class="wp-image-44445" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-31-1024x57.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-31-300x17.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-31-768x43.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-31.png 1385w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>We verify that the snapshot has been successfully created:</p>



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



<p>And the SQL backup :</p>



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



<h2 class="wp-block-heading" id="h-manual-process-flow-restore">Manual process flow (restore)</h2>



<p>We now need to be able to restore the database. Before doing so, we can delete a few tables to verify that the database has been restored as expected. We deleted most of the tables, leaving only three:</p>



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



<p>To perform the restore, we will follow these steps:</p>



<ul class="wp-block-list">
<li>Take the database offline.</li>



<li>Rollback the snapshot using the zfs rollback command.</li>



<li>Restore the database using the SQL backup created earlier.</li>
</ul>



<p>This is done using the following commands:</p>



<pre class="wp-block-code"><code>ALTER DATABASE &#091;StackOverflow] SET OFFLINE WITH ROLLBACK IMMEDIATE;</code></pre>



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



<p>Snapshot restore:</p>



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



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



<p>Database restore:</p>



<pre class="wp-block-code"><code>RESTORE DATABASE &#091;StackOverflow]
FROM DISK = N'D:\Backups\StackOverflow_13052026_230000.bkm'
WITH METADATA_ONLY, REPLACE, NORECOVERY;

RESTORE DATABASE &#091;StackOverflow] WITH RECOVERY;</code></pre>



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



<p><strong>We were able to restore our database in less than one second, even though it is approximately 207 GB in size.</strong></p>



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



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



<p>The process is manual, and we need to switch between running commands in SQL Server and performing the snapshot/restore operations in Proxmox. This freezes the database for a certain amount of time. During that period, connected applications could generate errors or timeouts.</p>



<p>The solution to this problem would be to automate the process using PowerShell, for example.</p>



<h2 class="wp-block-heading" id="h-what-was-not-covered-in-this-section">What was not covered in this section</h2>



<p>While writing this blog post, I omitted two points:</p>



<ul class="wp-block-list">
<li>When the database is deleted, it is necessary to take the volume dedicated to the StackOverflow database, Databases (D:), offline. Indeed, When you run a DROP DATABASE, SQL Server deletes the files from disk, and the database no longer exists. Then, if you perform a zfs rollback while Windows still sees the disk as online, you are effectively changing the disk “under Windows feet” Windows may keep the previous NTFS state cached: an empty directory, MFT information, file handles, volume metadata, and so on. As a result, the ZFS rollback may have completed successfully, but Windows does not properly refresh its view of the disk.</li>



<li>We did not make any calls to a REST API. Indeed, this functionality does not exist in my case, but it is possible to implement it.</li>
</ul>



<p>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/">SQL Server Snapshot Backup and Restore with Proxmox ZFS (1/3)</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/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>When a Python driver configuration issue may cause blocking in SQL Server</title>
		<link>https://www.dbi-services.com/blog/when-a-python-driver-configuration-issue-may-cause-blocking-in-sql-server/</link>
					<comments>https://www.dbi-services.com/blog/when-a-python-driver-configuration-issue-may-cause-blocking-in-sql-server/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Thu, 14 May 2026 21:21:48 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[blocking]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44425</guid>

					<description><![CDATA[<p>One of our clients encountered blocking during their daily data load. The process loads several million rows and then performs an ALTER TABLE &#8230; SWITCH operation into a partitioned table. This operation usually takes some time, but in this case it became blocked. Context Initially, I did not have access to much information. The only [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/when-a-python-driver-configuration-issue-may-cause-blocking-in-sql-server/">When a Python driver configuration issue may cause blocking in SQL Server</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>One of our clients encountered blocking during their daily data load. The process loads several million rows and then performs an ALTER TABLE &#8230; SWITCH operation into a partitioned table. This operation usually takes some time, but in this case it became blocked.</p>



<h2 class="wp-block-heading" id="h-context">Context</h2>



<p>Initially, I did not have access to much information. The only element I received from the client was a extract of the output from the sp_WhoIsActive procedure.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="63" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-28-1024x63.png" alt="" class="wp-image-44428" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-28-1024x63.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-28-300x18.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-28-768x47.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-28-1536x94.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-28.png 1880w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading" id="h-initial-analysis">Initial analysis</h2>



<p>Based on this extract, we were able to perform a first-level analysis:</p>



<p>A Python session executed a query against MyTable without applying a date filter. On a table containing approximately 244 million rows, this prevented proper partition elimination and forced SQL Server to read a much broader data set than necessary. Queries against partitioned tables only benefit from partition elimination when the predicate references the partitioning column without such a predicate, SQL Server may have to search or scan all partitions.</p>



<p>The Python session eventually became sleeping but remained with open_tran_count = 1. This is a typical sign of an unclosed transaction on the client side: autocommit disabled, cursor not closed, result set not fully consumed, connection returned to the pool without a rollback…</p>



<p>Session 146 then attempted to perform the partition TRUNCATE/SWITCH operation. However, TRUNCATE TABLE requires a schema modification lock, Sch-M, and ALTER TABLE &#8230; SWITCH also requires a Sch-M lock on both the source and target tables.</p>



<p>This Sch-M lock could not be acquired while session 167 was still referencing the object. SQL Server documents Sch-M as the lock required to modify the schema and to ensure that no other session is referencing the object. Once the Sch-M request from session 146 was queued, new read queries were also blocked behind it. Even NOLOCK would not avoid this issue: queries still acquire Sch-S locks during compilation and execution, and Sch-S and Sch-M locks block each other.</p>



<h2 class="wp-block-heading" id="h-second-analysis">Second analysis</h2>



<p>After some time, we were able to access the client’s environment. Query Store was enabled on the affected database, and an Extended Events session was configured on the SQL Server instance to track blocking.</p>



<p>Querying the Extended Events session provided detailed information about the blocking events that occurred, and we were able to identify the specific blocking issue reported by the client.</p>



<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-27-1024x118.png" alt="" class="wp-image-44427" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-27-1024x118.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-27-300x35.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-27-768x89.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-27.png 1205w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>By looking more closely at this blocking issue, we found the following:</p>



<pre class="wp-block-code"><code>EXEC &#091;STAGING_DB].&#091;ETL].&#091;sp_ETL_Exec]
&nbsp;&nbsp;&nbsp; @ETL_StepIKs_List = '&#091;"Exec-&#091;TARGET_DB].dbo.&#091;SP_Load_TargetTable]"]',
&nbsp;&nbsp;&nbsp; @StartAsJob = 0

Which is blocked by:

WITH position AS
(
&nbsp;&nbsp;&nbsp; SELECT ...
&nbsp;&nbsp;&nbsp; FROM &#091;SOURCE_DB].&#091;SCHEMA_NAME].&#091;LARGE_PARTITIONED_TABLE]
&nbsp;&nbsp;&nbsp; ...
)

&lt;blocking-process&gt;
&nbsp;&nbsp;&nbsp; spid="167"
&nbsp;&nbsp;&nbsp; status="sleeping"
&nbsp;&nbsp;&nbsp; trancount="1"
&nbsp;&nbsp;&nbsp; clientapp="python&#091;version]"
&nbsp;&nbsp;&nbsp; hostname="client-host-..."
&nbsp;&nbsp;&nbsp; loginname="user_account"
&nbsp;&nbsp;&nbsp; inputbuf="... WITH position AS ..."
&lt;/blocking-process&gt;</code></pre>



<p>However, the blocking report highlights an important point: session 167 was no longer actively executing the query at the time the report was captured:</p>



<ul class="wp-block-list">
<li>status = sleeping</li>



<li>trancount = 1</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="85" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-26-1024x85.png" alt="" class="wp-image-44429" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-26-1024x85.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-26-300x25.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-26-768x64.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-26.png 1119w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>However, by correlating this information with Query Store data, we were able to obtain additional details. By retrieving the corresponding query, we could better understand what was happening.</p>



<p>The blocking report also showed that session 146 was requesting a Sch-M lock, meaning a Schema Modification Lock. This is a strong lock required for operations such as TRUNCATE, ALTER TABLE, and partition SWITCH.</p>



<p>According to the data, session 146 waited for more than two hours, approximately 7,770,160 ms.</p>



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



<p>However, by correlating this information with Query Store data, we were able to obtain additional details. Specifically, by retrieving the query:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="613" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-30-1024x613.png" alt="" class="wp-image-44430" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-30-1024x613.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-30-300x180.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-30-768x460.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-30.png 1086w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>It was executed 30 times during the following time interval: 05-05-2026 from 2:00 PM to 3:00 PM. The average execution time was 49.1 seconds, with a maximum execution time of approximately 57 seconds. This represents a total of around 24 minutes of cumulative execution time over a one-hour period.</p>



<p>Based on this data, the issue was therefore not caused by the performance of the query itself, but rather by the state of session 167. Indeed, the session left a transaction open, with an open_tran_count of 1, thereby locking the corresponding objects and preventing other sessions from accessing them.</p>



<h2 class="wp-block-heading" id="h-how-is-it-related-to-python-driver-configuration">How is it related to Python driver configuration?</h2>



<p>The observed blocking can likely be explained by a misconfiguration or misuse of the Python driver used to access SQL Server. The root session was a Python connection in a sleeping state, but with trancount = 1, which indicates that a transaction was still open even though the query was no longer actively running.</p>



<p>In this situation, SQL Server may continue to hold transaction-related locks even if the application appears to have completed its work.</p>



<p>If the Python driver was running with autocommit = 0, each SELECT statement could implicitly start a transaction that then had to be explicitly closed with a commit or rollback. If the cursor was not closed properly, the result set was not fully consumed, or a rollback was not issued before returning the connection to the pool, the session could remain open on the SQL Server side. This residual transaction likely prevented the related ETL process from acquiring the Sch-M lock required for the TRUNCATE or partition SWITCH operation.</p>



<p>As a result the ETL session was not the initial root cause. It was waiting for a lock held by an idle Python connection.</p>



<p>Next queries then accumulated behind the pending Sch-M lock request, creating the impression of a global outage.</p>



<p>Switching to autocommit = 1 significantly reduces this risk, because read operations are no longer tied to an open transaction by default. Finally, preventing parallel pipeline execution helps avoid amplifying the issue when a job is delayed.</p>



<p>Thank you. <a href="https://www.linkedin.com/in/amine-haloui-76968056/">Amine Haloui</a></p>
<p>L’article <a href="https://www.dbi-services.com/blog/when-a-python-driver-configuration-issue-may-cause-blocking-in-sql-server/">When a Python driver configuration issue may cause blocking in SQL Server</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/when-a-python-driver-configuration-issue-may-cause-blocking-in-sql-server/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>A Misleading SSAS Error in Power BI Report Server When Using DirectQuery Mode</title>
		<link>https://www.dbi-services.com/blog/a-misleading-ssas-error-in-power-bi-report-server-when-using-directquery-mode/</link>
					<comments>https://www.dbi-services.com/blog/a-misleading-ssas-error-in-power-bi-report-server-when-using-directquery-mode/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Thu, 14 May 2026 21:17:45 +0000</pubDate>
				<category><![CDATA[Business Intelligence]]></category>
		<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[Power BI Report Server]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44400</guid>

					<description><![CDATA[<p>Our client was experiencing issues after publishing a report that used Direct Query mode. Specifically, when the report was queried, the following error occurred: Error :&#160; We couldn&#8217;t connect to the Analysis Services server. Make sure you&#8217;ve entered the connection string correctly. However, this issue did not occur in Power BI Desktop. In Power BI, [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/a-misleading-ssas-error-in-power-bi-report-server-when-using-directquery-mode/">A Misleading SSAS Error in Power BI Report Server When Using DirectQuery Mode</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Our client was experiencing issues after publishing a report that used Direct Query mode. Specifically, when the report was queried, the following error occurred:</p>



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



<p>Error :&nbsp; We couldn&#8217;t connect to the Analysis Services server. Make sure you&#8217;ve entered the connection string correctly.</p>



<p>However, this issue did not occur in Power BI Desktop.</p>



<p>In Power BI, several data loading modes are available. Import mode loads data into the Power BI model, which usually provides faster performance and richer modeling capabilities. DirectQuery mode does not store the data in the model instead, each interaction sends queries to the source system in real time. Import is generally better for speed and flexibility, while DirectQuery is useful when data must stay in the source or remain near real-time. The trade-off is that DirectQuery depends more heavily on source performance, network latency, and source-system limitations.</p>



<h2 class="wp-block-heading" id="h-configuration">Configuration</h2>



<p>At first glance, one might think that the corresponding report is trying to connect to an SSAS service and that there is a connectivity issue between Power BI Report Server and a SQL Server Analysis Services instance.</p>



<p>However, after reviewing the data source, there was no connection to SSAS:</p>



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



<p>We did not have this type of configuration:</p>



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



<p><strong>The questions that arise</strong></p>



<p>Why are we getting an error message even though the report is not trying to connect to a SQL Server Analysis Services instance?</p>



<p>Why is our client seeing this error message and unable to query the report?</p>



<h2 class="wp-block-heading" id="h-troubleshooting">Troubleshooting</h2>



<p>By reviewing the Power BI Report Server logs, it was possible to see this type of message:</p>



<p>Failed to get CSDL. &#8212;&gt; MsolapWrapper.MsolapWrapperException: Failure encountered while getting schema.</p>



<p>CannotRetrieveModelException: An error occurred while loading the model&#8230; Verify that the connection information is correct and that you have permissions to access the data source.</p>



<p>It is also possible to retrieve some information from the ExecutionLog3 table:</p>



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



<p>Indeed,&nbsp; whenever a Power BI report is rendered or a scheduled refresh is executed, new entries are written to the ExecutionLog3 table. These entries can be queried through the ExecutionLog3 view in the Report Server catalog database. The ConceptualSchema event corresponds to a user viewing the report.</p>



<p>When querying the Event Viewer, it returned these errors at the time we tried to query the report:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="146" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-25-1024x146.png" alt="" class="wp-image-44404" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-25-1024x146.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-25-300x43.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-25-768x109.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-25.png 1348w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading" id="h-more-details-about-the-first-errors">More details about the first errors</h2>



<p>We have two error messages that seem to point in two different directions. In reality, the first error messages are not very useful and appear because although the error message refers to Analysis Services, the report was not connecting to an external SSAS instance. Power BI Report Server uses an internal Analysis Services engine to load and query Power BI report models. Therefore, the error was raised by the internal PBIRS Analysis Services engine, not by a standalone SQL Server Analysis Services instance.</p>



<p>Power BI Report Server may report an Analysis Services-related error even when the report does not connect to an external SSAS instance. This is because PBIRS uses an internal Analysis Services engine to host and execute the Power BI semantic model behind the report. In DirectQuery mode, the data remains in SQL Server, but the report model, metadata, relationships, measures, and DAX queries are still processed through this internal engine.</p>



<p>When a user opens the report, PBIRS asks this local Analysis Services process to load the model and generate the queries sent to SQL Server.</p>



<p>Therefore, if the internal engine fails while loading the model, validating metadata, or connecting to the SQL Server data source, the error may mention Analysis Services. This does not mean that the report is connected to a standalone SSAS instance.</p>



<h2 class="wp-block-heading" id="h-more-details-about-the-second-errors">More details about the second errors</h2>



<p>This was the second error that pointed us in the right direction to actually resolve the issue. After looking at it more closely, we started considering connection encryption and certificates. This problem is documented, and several solutions are available.</p>



<p>Indeed, the SQL Server instance queried to retrieve the data did not have a certificate issued by a trusted certificate authority. It was using a self-generated certificate.</p>



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



<p>This can lead to errors such as the ones mentioned above, or errors like the following:</p>



<p>Microsoft SQL: A connection was successfully established with the server, but then an error occurred during the login process. Provider: SSL Provider, error: 0 &#8211; The certificate chain was issued by an authority that is not trusted.</p>



<h2 class="wp-block-heading" id="h-solutions">Solutions</h2>



<p>We had at least three options to resolve this issue:</p>



<ul class="wp-block-list">
<li>Change the connection mode to Import</li>



<li>Install a certificate issued by a trusted certificate authority however this would represent a major change</li>



<li>Create a new environment variable on the Power BI Report Server</li>
</ul>



<p>The client chose the easiest solution to implement: creating the corresponding environment variable.</p>



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



<p>We then restarted the corresponding Power BI Report Server service and this resolved the issue.</p>



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



<p><a href="https://learn.microsoft.com/en-us/power-bi/report-server/scheduled-refresh-troubleshoot">https://learn.microsoft.com/en-us/power-bi/report-server/scheduled-refresh-troubleshoot</a></p>



<p><a href="https://learn.microsoft.com/en-us/power-query/connectors/sql-server#sql-server-certificate-isnt-trusted-on-the-client-power-bi-desktop-or-on-premises-data-gateway">https://learn.microsoft.com/en-us/power-query/connectors/sql-server#sql-server-certificate-isnt-trusted-on-the-client-power-bi-desktop-or-on-premises-data-gateway</a></p>



<p>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/a-misleading-ssas-error-in-power-bi-report-server-when-using-directquery-mode/">A Misleading SSAS Error in Power BI Report Server When Using DirectQuery Mode</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/a-misleading-ssas-error-in-power-bi-report-server-when-using-directquery-mode/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PostgreSQL 19: Dynamically adjust the I/O worker pool</title>
		<link>https://www.dbi-services.com/blog/postgresql-19-dynamically-adjust-the-i-o-worker-pool/</link>
					<comments>https://www.dbi-services.com/blog/postgresql-19-dynamically-adjust-the-i-o-worker-pool/#respond</comments>
		
		<dc:creator><![CDATA[Daniel Westermann]]></dc:creator>
		<pubDate>Wed, 13 May 2026 05:12:15 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44393</guid>

					<description><![CDATA[<p>When PostgreSQL 18 was released last year one of the major features was the introduction of the asynchronous I/O subsystem. The main configuration parameter for this was (and still is) io_method, which can be &#8220;worker&#8221; (the default), io_uring or sync (the old behavior). If you opted for &#8220;workers&#8221; the number of those workers is controlled [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-19-dynamically-adjust-the-i-o-worker-pool/">PostgreSQL 19: Dynamically adjust the I/O worker pool</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>When <a href="https://www.postgresql.org/docs/current/release-18.html#RELEASE-18-CHANGES" target="_blank" rel="noreferrer noopener">PostgreSQL 18 was released</a> last year one of the major features was the <a href="https://www.dbi-services.com/blog/postgresql-18-support-for-asynchronous-i-o/" target="_blank" rel="noreferrer noopener">introduction of the asynchronous I/O subsystem</a>. The main configuration parameter for this was (and still is) <a href="https://www.postgresql.org/docs/18/runtime-config-resource.html#GUC-IO-METHOD" target="_blank" rel="noreferrer noopener">io_method</a>, which can be &#8220;worker&#8221; (the default), <a href="https://en.wikipedia.org/wiki/Io_uring" target="_blank" rel="noreferrer noopener">io_uring</a> or sync (the old behavior). If you opted for &#8220;workers&#8221; the number of those workers is controlled by &#8220;<a href="https://www.postgresql.org/docs/18/runtime-config-resource.html#GUC-IO-WORKERS" target="_blank" rel="noreferrer noopener">io_workers</a>&#8221; and the default for this is 3. PostgreSQL 19 most probably will change the way how many of those workers are launched, not anymore using the static value of &#8220;io_workers&#8221; but making this dynamic by launching workers from a predefined pool.</p>



<p>The configuration parameter &#8220;io_workers&#8221; is gone and four additional parameters show up to control this:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1]; title: ; notranslate">
postgres=# \dconfig io_*work*
 List of configuration parameters
         Parameter         | Value 
---------------------------+-------
 io_max_workers            | 8
 io_min_workers            | 2
 io_worker_idle_timeout    | 1min
 io_worker_launch_interval | 100ms
(4 rows)
</pre></div>


<p>&#8220;io_min_workes&#8221; (as the name implies) controls how many workers are available by default, which is two:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; highlight: [1]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;DEV] ps -ef | grep postgres | grep worker | grep -v grep
postgres    8564    8562  0 06:34 ?        00:00:00 postgres: pgdev: io worker 0
postgres    8565    8562  0 06:34 ?        00:00:00 postgres: pgdev: io worker 1
</pre></div>


<p>&#8220;io_max_workers&#8221; (again, as the name implies) controls the maximum worker processes which can be launched for the whole instance.</p>



<p>To see that dynamic startup of workers in action lets create a simple table containing twenty million rows:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1,3]; title: ; notranslate">
postgres=# create table t ( a int, b text, c timestamptz );
CREATE TABLE
postgres=# insert into t select i, i::text, now() from generate_series(1,20000000) i;
INSERT 0 2000000
</pre></div>


<p>While watching the workers in a separate session:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; highlight: [1]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;DEV] watch &quot;ps -ef | grep postgres | grep worker | grep -v grep&quot;

Every 2.0s: ps -ef | grep postgres | grep worker | grep -v grep               pgbox.it.dbi-services.com: 06:52:20 AM
                                                                                                       in 0.022s (0)
postgres    8564    8562  0 06:34 ?        00:00:00 postgres: pgdev: io worker 0
postgres    8565    8562  0 06:34 ?        00:00:00 postgres: pgdev: io worker 1
</pre></div>


<p>&#8230; and doing a count(*) over the whole table in session one:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1]; title: ; notranslate">
postgres=# select count(*) from t;
  count   
----------
 20000000
(1 row)
</pre></div>


<p>&#8230; you&#8217;ll notice that an additional worker (io worker 2) shows up in the second session watching the processes (maybe you have to play a bit with the number of rows depending on your configuration of PostgreSQL):</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; highlight: [6]; title: ; notranslate">
Every 2.0s: ps -ef | grep postgres | grep worker | grep -v grep               pgbox.it.dbi-services.com: 07:02:40 AM
                                                                                                       in 0.018s (0)
postgres    8564    8562  0 06:34 ?        00:00:02 postgres: pgdev: io worker 0
postgres    8565    8562  0 06:34 ?        00:00:00 postgres: pgdev: io worker 1
postgres   11914    8562  0 07:02 ?        00:00:00 postgres: pgdev: io worker 2
</pre></div>


<p>Once this additional worker is idle for one minute it will disappear and we&#8217;re back to two worker processes:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; highlight: [1]; title: ; notranslate">
Every 2.0s: ps -ef | grep postgres | grep worker | grep -v grep               pgbox.it.dbi-services.com: 07:04:24 AM
                                                                                                       in 0.020s (0)
postgres    8564    8562  0 06:34 ?        00:00:02 postgres: pgdev: io worker 0
postgres    8565    8562  0 06:34 ?        00:00:00 postgres: pgdev: io worker 1
</pre></div>


<p>This is controlled by &#8220;io_worker_idle_timeout&#8221; and the default is one minute. </p>



<p>The remaining configuration knob is &#8220;io_worker_launch_interval&#8221;, and this is the interval at which additional workers can be launched. The reason behind this is, that not too many workers will be launched at once.</p>



<p>This will make tuning the workers easier, compared to PostgreSQL 18. Again, thanks to all involved, the commit is <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=d1c01b79d4ae90e52bf9db9c05c9de17b7313e85">here</a>.</p>



<p></p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-19-dynamically-adjust-the-i-o-worker-pool/">PostgreSQL 19: Dynamically adjust the I/O worker pool</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-19-dynamically-adjust-the-i-o-worker-pool/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PostgreSQL 19: pg_waldump can now read from archives</title>
		<link>https://www.dbi-services.com/blog/postgresql-19-pg_waldump-can-now-read-from-archives/</link>
					<comments>https://www.dbi-services.com/blog/postgresql-19-pg_waldump-can-now-read-from-archives/#respond</comments>
		
		<dc:creator><![CDATA[Daniel Westermann]]></dc:creator>
		<pubDate>Mon, 11 May 2026 04:48:04 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44025</guid>

					<description><![CDATA[<p>When PostgreSQL 18 introduced the ability to verify tar based (and compressed) backups with pg_verifybackup there was one limitation: The verification of the WAL files in the tars (or compressed files) had to be skipped (--no-parse-wal) because pg_waldump in that version of PostgreSQL is not able to cope with that (and pg_waldump is used by [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-19-pg_waldump-can-now-read-from-archives/">PostgreSQL 19: pg_waldump can now read from archives</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>When <a href="https://www.postgresql.org/docs/current/release-18.html#RELEASE-18-HIGHLIGHTS" target="_blank" rel="noreferrer noopener">PostgreSQL 18 introduced the ability to verify tar based (and compressed) backups with pg_verifybackup</a> there was one limitation: <a href="https://www.dbi-services.com/blog/postgresql-18-verify-tar-format-and-compressed-backups/" target="_blank" rel="noreferrer noopener">The verification of the WAL files in the tars (or compressed files) had to be skipped</a> (<code>--no-parse-wal</code>) because <a href="https://www.postgresql.org/docs/18/pgwaldump.html" target="_blank" rel="noreferrer noopener">pg_waldump</a> in that version of PostgreSQL is not able to cope with that (and pg_waldump is used by pg_verifybackup). This will change with PostgreSQL 19 because of this <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=b15c1513984e6eafd264bf6e84a08549905621f1" target="_blank" rel="noreferrer noopener">commit</a>: &#8220;pg_waldump: Add support for reading WAL from tar archives&#8221;.</p>



<p>This is maybe not a feature a lot of people have waited for but it makes two tasks a lot easier:</p>



<ul class="wp-block-list">
<li>As mentioned above: pg_verifybackup can now read from WAL in tar and compressed files and therefore can do WAL verification</li>



<li>When you have WAL in a tar or compressed file and you know what you&#8217;re looking for you do not need to manually extract those archives before using pg_waldump</li>
</ul>



<p>To see that in action once can create a tar or compressed backup with pb_basebackup:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; highlight: [1,2,3]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;pgdev] mkdir /var/tmp/dummy
postgres@:/home/postgres/ &#x5B;pgdev] pg_basebackup --checkpoint=fast --format=t --pgdata=/var/tmp/dummy
postgres@:/home/postgres/ &#x5B;pgdev] ls -la /var/tmp/dummy
total 128476
drwxr-xr-x. 1 postgres postgres        66 May 11 06:36 .
drwxrwxrwt. 1 root     root           762 May 11 06:33 ..
-rw-------. 1 postgres postgres    149515 May 11 06:36 backup_manifest
-rw-------. 1 postgres postgres 114619904 May 11 06:36 base.tar
-rw-------. 1 postgres postgres  16778752 May 11 06:36 pg_wal.tar
</pre></div>


<p>Looking at the PostgreSQL log file while the backup is running gives us a LSN we can give to pg_waldump:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; highlight: [3]; title: ; notranslate">
2026-05-11 06:36:18.397 CEST - 2 - 1731 -  - @ - 0LOG:  checkpoint complete: fast force wait: wrote 2 buffers (0.0%), wrote 3 SLRU buffers; 0 WAL file(s) added, 1 removed, 0 recycled; write=0.002 s, sync=0.005 s, total=0.019 s; sync files=4, longest=0.003 s, average=0.002 s; distance=16384 kB, estimate=16384 kB; lsn=0/0D000088, redo lsn=0/0D000028

postgres@:/home/postgres/ &#x5B;pgdev] pg_waldump --path=/var/tmp/dummy/pg_wal.tar -s &quot;0/0D000088&quot; 
rmgr: XLOG        len (rec/tot):    122/   122, tx:          0, lsn: 0/0D000088, prev 0/0D000050, desc: CHECKPOINT_ONLINE redo 0/0D000028; tli 1; prev tli 1; fpw true; wal_level replica; logical decoding false; xid 0:729; oid 16420; multi 1; offset 1; oldest xid 684 in DB 1; oldest multi 1 in DB 1; oldest/newest commit timestamp xid: 0/0; oldest running xid 729; checksums on; online
rmgr: Standby     len (rec/tot):     54/    54, tx:          0, lsn: 0/0D000108, prev 0/0D000088, desc: RUNNING_XACTS nextXid 729 latestCompletedXid 728 oldestRunningXid 729; dbid: 0
rmgr: XLOG        len (rec/tot):     34/    34, tx:          0, lsn: 0/0D000140, prev 0/0D000108, desc: BACKUP_END 0/0D000028
rmgr: XLOG        len (rec/tot):     24/    24, tx:          0, lsn: 0/0D000168, prev 0/0D000140, desc: SWITCH 
pg_waldump: error: could not find WAL &quot;00000001000000000000000E&quot; in archive &quot;pg_wal.tar
</pre></div>


<p>This helps pg_verifybackup fully verify a backup (in previous versions you had to use &#8220;&#8211;no-parse-wal&#8221;):</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; highlight: [1]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;pgdev] pg_verifybackup --progress /var/tmp/dummy/
111933/111933 kB (100%) verified
backup successfully verified
</pre></div>


<p>As usual, thanks to all involved.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-19-pg_waldump-can-now-read-from-archives/">PostgreSQL 19: pg_waldump can now read from archives</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-19-pg_waldump-can-now-read-from-archives/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PostgreSQL 19: Importing statistics from remote servers</title>
		<link>https://www.dbi-services.com/blog/postgresql-19-importing-statistics-from-remote-servers/</link>
					<comments>https://www.dbi-services.com/blog/postgresql-19-importing-statistics-from-remote-servers/#respond</comments>
		
		<dc:creator><![CDATA[Daniel Westermann]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 08:15:22 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=43948</guid>

					<description><![CDATA[<p>Usually we do not see many foreign data wrappers being used by our customers. Most of them use the foreign data wrapper for Oracle to fetch data from Oracle systems. Some of them use the foreign data wrapper for files but that&#8217;s mostly it. Only one (I am aware of) actually uses the foreign data [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-19-importing-statistics-from-remote-servers/">PostgreSQL 19: Importing statistics from remote servers</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Usually we do not see many foreign data wrappers being used by our customers. Most of them use the <a href="https://github.com/laurenz/oracle_fdw" target="_blank" rel="noreferrer noopener">foreign data wrapper for Oracle</a> to fetch data from Oracle systems. Some of them use the <a href="https://www.dbi-services.com/blog/external-tables-in-postgresql/">foreign data wrapper for files</a> but that&#8217;s mostly it. Only one (I am aware of) actually uses the <a href="https://www.postgresql.org/docs/18/postgres-fdw.html" target="_blank" rel="noreferrer noopener">foreign data wrapper for PostgreSQL</a> which obviously connects PostgreSQL to PostgreSQL. Some foreign data wrappers allow for collecting optimizer statistics on foreign tables and the foreign data wrappers for Oracle and PostgreSQL are examples for this. These local statistics are better than nothing but you need to take care that they are up to date and for that you need a fresh copy of the statistics over the remote data. PostgreSQL 19 will come with a solution for that when it comes to the foreign data wrapper for PostgreSQL. Actually, the solution is not in the foreign data wrapper for PostgreSQL but in the underlying framework and postgres_fdw uses can use that from version 19 on.</p>



<p>For looking at this we need a simple setup, so we initialize two new PostgreSQL 19 clusters and connect them with postgres_fdw:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; highlight: [1,3,4,5,6,7,8,9,11,13,15,17,19,21]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;pgdev] initdb --version
initdb (PostgreSQL) 19devel
postgres@:/home/postgres/ &#x5B;pgdev] initdb --pgdata=/var/tmp/pg1
postgres@:/home/postgres/ &#x5B;pgdev] initdb --pgdata=/var/tmp/pg2
postgres@:/home/postgres/ &#x5B;pgdev] echo &quot;port=8888&quot; &gt;&gt; /var/tmp/pg1/postgresql.auto.conf 
postgres@:/home/postgres/ &#x5B;pgdev] echo &quot;port=8889&quot; &gt;&gt; /var/tmp/pg2/postgresql.auto.conf 
postgres@:/home/postgres/ &#x5B;pgdev] pg_ctl --pgdata=/var/tmp/pg1/ start
postgres@:/home/postgres/ &#x5B;pgdev] pg_ctl --pgdata=/var/tmp/pg2/ start
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8888 -c &quot;create extension postgres_fdw&quot;
CREATE EXTENSION
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8889 -c &quot;create table t ( a int, b text, c timestamptz )&quot;
CREATE TABLE
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8889 -c &quot;insert into t select i, md5(i::text), now() from generate_series(1,1000000) i&quot;
INSERT 0 1000000
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8888 -c &quot;create server srv_pg2 foreign data wrapper postgres_fdw options(port &#039;8889&#039;, dbname &#039;postgres&#039;)&quot;
CREATE SERVER
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8888 -c &quot;create user mapping for postgres server srv_pg2 options (user &#039;postgres&#039;, password &#039;postgres&#039;)&quot;
CREATE USER MAPPING
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8888 -c &quot;create foreign table ft (a int, b text, c timestamptz) server srv_pg2 options (schema_name &#039;public&#039;, table_name &#039;t&#039;)&quot;
CREATE FOREIGN TABLE
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8888 -c &quot;select count(*) from ft&quot;
  count  
---------
 1000000
(1 row)
</pre></div>


<p>What we have now is one table in the cluster on port 8889 and this table is attached as a foreign table in the cluster on port 8888.</p>



<p>We already have statistics on the source table in the cluster on port 8889:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8889 -c &quot;select reltuples::bigint from pg_class  where relname = &#039;t&#039;&quot;

 reltuples 
-----------
   1000000
(1 row)
</pre></div>


<p>&#8230; but we do not have any statistics on the foreign table in the cluster on port 8888:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8888 -c &quot;select reltuples::bigint from pg_class  where relname = &#039;ft&#039;&quot;

 reltuples 
-----------
        -1

(1 row)
</pre></div>


<p>Only after manually analyzing the foreign table the statistics show up:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1,3]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;DEV] psql -p 8888 -c &quot;analyze ft&quot;
ANALYZE
postgres@:/home/postgres/ &#x5B;DEV] psql -p 8888 -c &quot;select reltuples::bigint from pg_class  where relname = &#039;ft&#039;&quot;

 reltuples 
-----------
   1000000
(1 row)
</pre></div>


<p>The issue that can arise with these local statistics is, that they probably become outdated when the source table is modified:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1,3,10]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8889 -c &quot;insert into t select i, md5(i::text), now() from generate_series(1000001,2000000) i&quot;
INSERT 0 1000000
postgres@:/home/postgres/ &#x5B;DEV] psql -p 8889 -c &quot;select reltuples::bigint from pg_class  where relname = &#039;t&#039;&quot;

 reltuples 
-----------
   2000000
(1 row)

postgres@:/home/postgres/ &#x5B;DEV] psql -p 8888 -c &quot;select reltuples::bigint from pg_class  where relname = &#039;ft&#039;&quot;

 reltuples 
-----------
   1000000
(1 row)
</pre></div>


<p>As you can see, the row counts do not match anymore. Once the local statistics are gathered we again have the same picture on both sides:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1,3]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;DEV] psql -p 8888 -c &quot;analyze ft&quot;
ANALYZE
postgres@:/home/postgres/ &#x5B;DEV] psql -p 8888 -c &quot;select reltuples::bigint from pg_class  where relname = &#039;ft&#039;&quot;

 reltuples 
-----------
   2000000
(1 row)
</pre></div>


<p>One way to avoid this issue even before PostgreSQL 19 is to tell postgres_fdw to run analyze on the remote table and to use those statistics:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8888 -c &quot;alter foreign table ft options ( use_remote_estimate &#039;true&#039; )&quot;
</pre></div>


<p>In this case the local statistics will not be used but of course this comes with the overhead of the additional analyze on the remote side.</p>



<p>From PostgreSQL 19 there is another option:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;pgdev] psql -p 8888 -c &quot;alter foreign table ft options ( restore_stats &#039;true&#039; )&quot;
ALTER FOREIGN TABLE
</pre></div>


<p>This option tells postgres_fdw to import the statistics from the remote side and store them locally. If that fails it will run analyze as above, the <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=28972b6fc3dcd1296e844246b635eddfa29c38e1" target="_blank" rel="noreferrer noopener">commit message</a> nicely explains this:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Add support for importing statistics from remote servers.

Add a new FDW callback routine that allows importing remote statistics
for a foreign table directly to the local server, instead of collecting
statistics locally.  The new callback routine is called at the beginning
of the ANALYZE operation on the table, and if the FDW failed to import
the statistics, the existing callback routine is called on the table to
collect statistics locally.

Also implement this for postgres_fdw.  It is enabled by &quot;restore_stats&quot;
option both at the server and table level.  Currently, it is the user&#039;s
responsibility to ensure remote statistics to import are up-to-date, so
the default is false.
</pre></div>


<p>As usual, thanks to all involved.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-19-importing-statistics-from-remote-servers/">PostgreSQL 19: Importing statistics from remote servers</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-19-importing-statistics-from-remote-servers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PostgreSQL 19: Online enabling of data checksums</title>
		<link>https://www.dbi-services.com/blog/postgresql-19-online-enabling-of-data-checksums/</link>
					<comments>https://www.dbi-services.com/blog/postgresql-19-online-enabling-of-data-checksums/#respond</comments>
		
		<dc:creator><![CDATA[Daniel Westermann]]></dc:creator>
		<pubDate>Fri, 17 Apr 2026 06:00:00 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=43935</guid>

					<description><![CDATA[<p>Since PostgreSQL 18 was released last year checksums are enabled by default when a new cluster is initialized. This also means, that you either need to explicitly disable that when you upgrade from a previous version of PostgreSQL or you need to enable this in the old version of PostgreSQL you want to upgrade from. [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-19-online-enabling-of-data-checksums/">PostgreSQL 19: Online enabling of data checksums</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Since PostgreSQL 18 was released last year checksums are enabled by default when a new cluster is initialized. This also means, that you either need to explicitly disable that when you upgrade from a previous version of PostgreSQL or you need to enable this in the old version of PostgreSQL you want to upgrade from. The reason is, that <a href="https://www.postgresql.org/docs/current/pgupgrade.html">pg_upgrade</a> will complain if the old and new version of PostgreSQL do not have the same setting for this.</p>



<p>Enabling and disabling checksums in offline mode can be done since several versions of PostgreSQL using <a href="https://www.postgresql.org/docs/current/app-pgchecksums.html" target="_blank" rel="noreferrer noopener">pg_checksums</a>, but as mentioned: This will not work if the cluster is running:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; highlight: [1,3,9]; title: ; notranslate">
postgres@:/home/postgres/ &#x5B;181] pg_checksums --version
pg_checksums (PostgreSQL) 18.1 
postgres@:/home/postgres/ &#x5B;181] pg_checksums --pgdata=$PGDATA
Checksum operation completed
Files scanned:   966
Blocks scanned:  2969
Bad checksums:  0
Data checksum version: 1  -&gt; This means &quot;enabled&quot;
postgres@:/home/postgres/ &#x5B;181] pg_checksums --pgdata=$PGDATA --disable
pg_checksums: error: cluster must be shut down
</pre></div>


<p>Even in PostgreSQL 19 this is still same: You cannot use pg_checksum to enable or disable checksums while the cluster is running.</p>



<p>What will change in version 19 is that two new functions have been added, one for enabling checksums and one for disabling checksums:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1]; title: ; notranslate">
postgres=# \dfS *checksums*
                                                        List of functions
   Schema   |           Name            | Result data type |                     Argument data types                      | Type 
------------+---------------------------+------------------+--------------------------------------------------------------+------
 pg_catalog | pg_disable_data_checksums | void             |                                                              | func
 pg_catalog | pg_enable_data_checksums  | void             | cost_delay integer DEFAULT 0, cost_limit integer DEFAULT 100 | func
(2 rows)
</pre></div>


<p>As mentioned in the <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=f19c0eccae9680f5785b11cdc58ef571998caec9" target="_blank" rel="noreferrer noopener">commit message</a> this is implemented by background workers and to actually see those processes on the operating system lets create some data so the workers really have something to do:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1,3]; title: ; notranslate">
postgres=# create table t ( a int, b text, c timestamptz );
CREATE TABLE
postgres=# insert into t select i, md5(i::text), now() from generate_series(1,10000000) i;
INSERT 0 10000000
</pre></div>


<p>As this is version 19 of PostgreSQL currently checksum are enabled:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1]; title: ; notranslate">
postgres=# show data_checksums;
 data_checksums 
----------------
 on
(1 row)
</pre></div>


<p>To disable that online, pg_disable_data_checksums is the function to use:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1,7]; title: ; notranslate">
postgres=# select * from pg_disable_data_checksums();
 pg_disable_data_checksums 
---------------------------
 
(1 row)

postgres=# show data_checksums;
 data_checksums 
----------------
 off
(1 row)
</pre></div>


<p>To enable checksums online pg_enable_data_checksums is the function to use. If you want to see the background workers you might grep for that in a second session on the operating system:</p>



<p></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [2,8,15]; title: ; notranslate">
-- first session, connected to PostgreSQL
postgres=# select pg_enable_data_checksums();
 pg_enable_data_checksums 
--------------------------
 
(1 row)

postgres=# show data_checksums ;
 data_checksums 
----------------
 on
(1 row)

-- second session, on the OS
postgres@:/home/postgres/postgresql/ &#x5B;pgdev] watch &quot;ps -ef | grep checksum | grep -v watch&quot;
Every 2.0s: ps -ef | grep checksum | grep -v watch                                                                                                                                                    pgbox.it.dbi-services.com: 09:49:20 AM
                                                                                                                                                                                                                               in 0.006s (0)
postgres    4931    2510  0 09:49 ?        00:00:00 postgres: pgdev: datachecksum launcher
postgres    4932    2510 25 09:49 ?        00:00:00 postgres: pgdev: datachecksum worker
postgres    4964    4962  0 09:49 pts/2    00:00:00 grep checksum
</pre></div>


<p>Because enabling the checksum comes with some overhead there is throttling control as it is already the case for autovacuum:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; highlight: [1]; title: ; notranslate">
postgres=# select pg_enable_data_checksums(cost_delay=&gt;1,cost_limit=&gt;3000);
 pg_enable_data_checksums 
--------------------------
 
(1 row)
</pre></div>


<p>Very nice, thanks to all involved.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-19-online-enabling-of-data-checksums/">PostgreSQL 19: Online enabling of data checksums</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-19-online-enabling-of-data-checksums/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-06-03 16:55:45 by W3 Total Cache
-->