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

<channel>
	<title>Database 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>Thu, 15 Oct 2026 15:38:42 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/05/cropped-favicon_512x512px-min-32x32.png</url>
	<title>Database management - dbi Blog</title>
	<link>https://www.dbi-services.com/blog/category/database-management/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>OCI-managed MCP Server: Troubleshooting ORA-12170 errors with Private Endpoint connectivity</title>
		<link>https://www.dbi-services.com/blog/oci-managed-mcp-server-troubleshooting-ora-12170-errors-with-private-endpoint-connectivity/</link>
					<comments>https://www.dbi-services.com/blog/oci-managed-mcp-server-troubleshooting-ora-12170-errors-with-private-endpoint-connectivity/#respond</comments>
		
		<dc:creator><![CDATA[Jérôme Witt]]></dc:creator>
		<pubDate>Thu, 15 Oct 2026 15:38:40 +0000</pubDate>
				<category><![CDATA[Cloud]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[OCI]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[MCP]]></category>
		<category><![CDATA[Oracle]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=47876</guid>

					<description><![CDATA[<p>We recently worked on a customer proof of concept (PoC) involving Oracle databases hosted in Oracle Cloud Infrastructure (OCI) within private VCN subnets. The goal was to evaluate OCI-managed MCP servers and determine whether extending their available toolsets with additional database operations could reduce reliance on agent skills. By exposing reusable operations as MCP tools, [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/oci-managed-mcp-server-troubleshooting-ora-12170-errors-with-private-endpoint-connectivity/">OCI-managed MCP Server: Troubleshooting ORA-12170 errors with Private Endpoint connectivity</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">We recently worked on a customer proof of concept (PoC) involving Oracle databases hosted in Oracle Cloud Infrastructure (OCI) within private VCN subnets.</p>



<p class="wp-block-paragraph">The goal was to evaluate OCI-managed MCP servers and determine whether extending their available toolsets with additional database operations could reduce reliance on agent skills. By exposing reusable operations as MCP tools, we aimed to reduce token consumption and associated costs while improving execution efficiency, without compromising the security of databases hosted in private VCN subnets.</p>



<h2 id="h-provision-oci-resources-when-rtfm-isn-t-enough" class="wp-block-heading">Provision OCI-resources: When RTFM Isn’t Enough</h2>



<p class="wp-block-paragraph">Provisioning an MCP server requires an underlying database connection that combines database credentials with network connectivity to the target database, typically through a private endpoint. This connection allows the MCP server to access the database securely while keeping it within the private VCN.</p>



<p class="wp-block-paragraph">The detailed setup procedure is beyond the scope of this article, as Oracle provides official documentation covering the prerequisites, configuration, and provisioning for <a href="https://docs.oracle.com/en-us/iaas/database-tools/doc/private-endpoints.html">Database Tools Private Endpoints</a></p>



<p class="wp-block-paragraph">In short, the documentation mentions using Zero Trust Packet Routing (ZPR) alongside, or instead of, Network Security Groups (NSGs) to control network access.</p>



<p class="wp-block-paragraph">So far, so good. We already use NSGs exclusively to manage network traffic.</p>



<p class="wp-block-paragraph">However, the documentation does not clearly spell out which specific NSG rules are required or whether ingress rules alone are sufficient. After digging deeper into the documentation, we found the missing piece of the puzzle:</p>



<p class="wp-block-paragraph"><a href="https://docs.oracle.com/en-us/iaas/database-management/doc/create-database-management-private-endpoint.html" target="_blank" rel="noreferrer noopener">Creating a Database Management Private Endpoint</a></p>



<p class="wp-block-paragraph">The key takeaway is that the Network Security Groups (NSGs) associated with the private endpoint must have appropriate ingress and egress rules to permit the required network traffic.</p>



<p class="wp-block-paragraph">This is where things got more interesting. Our environment already hosted several databases interconnected through Oracle database links, with ingress and egress rules in place for the relevant subnets, including the newly provisioned private endpoint. Yet, despite these existing network configurations, the MCP server still could not connect to the target database.</p>



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



<h2 id="h-ora-12170-cannot-connect-tcp-connect-timeout" class="wp-block-heading">ORA-12170: Cannot connect. TCP connect timeout</h2>



<figure class="wp-block-image size-full"><img fetchpriority="high" decoding="async" width="936" height="134" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/OCI-DBaaS-edp-conn-ORA-12170.png" alt="" class="wp-image-47905" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/OCI-DBaaS-edp-conn-ORA-12170.png 936w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/OCI-DBaaS-edp-conn-ORA-12170-300x43.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/OCI-DBaaS-edp-conn-ORA-12170-768x110.png 768w" sizes="(max-width: 936px) 100vw, 936px" /></figure>



<p class="wp-block-paragraph">Clearly, something was missing. We therefore dug deeper into the private endpoint’s specific network security requirements to identify the root cause of the connectivity issue.<br></p>



<h2 class="wp-block-heading">Still Getting ORA-12170, But Why?</h2>



<p class="wp-block-paragraph">Hold on a minute! We quickly went through the OCI Console to double-check our network configuration:</p>



<ul class="wp-block-list">
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Verified the private endpoint&#8217;s NSG ingress and egress rules.</li>



<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Confirmed that the databases and the private endpoint were deployed in the same subnet.</li>
</ul>



<p class="wp-block-paragraph">Everything looked correct at first glance, yet the dreaded <code>ORA-12170: TNS:Connect timeout occurred</code> error persisted.</p>



<p class="wp-block-paragraph"><strong>What were we missing? Time to bring in Oracle Support!</strong></p>



<p class="wp-block-paragraph">While working with Oracle Support, we took a closer look at the NSG configuration and the private endpoint&#8217;s VNIC. Something caught our attention: an entry was missing from the list, but we hadn&#8217;t initially considered it relevant.</p>



<p class="wp-block-paragraph">This prompted us to inspect the private endpoint configuration in greater detail using the OCI Command Line Interface (OCI CLI).</p>



<pre class="wp-block-code"><code>jerome_witt@cloudshell:~ (eu-zurich-1)$ oci dbtools private-endpoint get \
  --private-endpoint-id ${EDP_OCID}  \
  --query 'data.{"nsg-ids": "nsg-ids", "private-endpoint-vnic-id": "private-endpoint-vnic-id", "security-attributes": "security-attributes"}' \
  --output json
{
  <strong>"nsg-ids": null</strong>,
  "private-endpoint-vnic-id": "ocid1.vnic.oc1.eu-zurich-1.xxxxxxxxxxxxxxxxx",
  "security-attributes": {}
}</code></pre>



<p class="wp-block-paragraph"><br>Oh! The nsg-ids field is null! That was the missing piece of the puzzle.</p>



<p class="wp-block-paragraph">After updating the private endpoint with the appropriate NSG, we ran the following OCI CLI command:</p>



<pre class="wp-block-code"><code>oci dbtools private-endpoint update \ 
  --private-endpoint-id ${EDP_OCID} \
  --nsg-ids '&#091;"${NSG_ATTACHED_TO_DB_OCID}"]'</code></pre>



<p class="wp-block-paragraph">And then, the magic happened! The database connection succeeded, and the dreaded ORA-12170 error was gone.</p>



<p class="wp-block-paragraph">After explicitly associating the appropriate NSG with the Database Tools private endpoint, connectivity was restored. The OCI CLI output had revealed that the <code>nsg-ids</code> field was <code>null</code>, a detail we had overlooked when initially checking the configuration through the OCI Console.</p>



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



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



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="347" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/Screenshot-2026-10-15-at-17.23.54-1024x347.png" alt="" class="wp-image-47907" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/Screenshot-2026-10-15-at-17.23.54-1024x347.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/Screenshot-2026-10-15-at-17.23.54-300x102.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/Screenshot-2026-10-15-at-17.23.54-768x261.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/10/Screenshot-2026-10-15-at-17.23.54.png 1120w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">This behavior may be related to recent changes in OCI as Oracle continues to rapidly develop and roll out its core Deep Data Security capabilities.</p>



<p class="wp-block-paragraph">What had initially looked like a correctly configured network turned out to be missing a crucial piece: the NSG association explicitly configured on the Database Tools private endpoint itself.</p>



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



<h2 id="h-lessons-learned" class="wp-block-heading">Lessons Learned</h2>



<ul class="wp-block-list">
<li>Never blindly trust the console! Always verify the underlying configuration using the OCI CLI.</li>



<li>Have fun troubleshooting!<strong> </strong>Sometimes, the smallest configuration detail makes all the difference.</li>
</ul>



<p class="wp-block-paragraph">By the way, it seems we&#8217;re not alone. Others have apparently encountered similar database connectivity issues when configuring OCI Database Tools but none were using NSGs exclusively.</p>



<p class="wp-block-paragraph">For further reference, these Oracle Support knowledge base articles may be useful:</p>



<ul class="wp-block-list">
<li>KB64948 — <em>How to Connect to the Autonomous Database (Private Endpoint Access)</em></li>



<li>KB42277 — <em>OCI — Enable Database Management Fails</em></li>
</ul>



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



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



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



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



<p class="has-small-font-size wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/oci-managed-mcp-server-troubleshooting-ora-12170-errors-with-private-endpoint-connectivity/">OCI-managed MCP Server: Troubleshooting ORA-12170 errors with Private Endpoint connectivity</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/oci-managed-mcp-server-troubleshooting-ora-12170-errors-with-private-endpoint-connectivity/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server: Disabling FileTable/FileStream on an AlwaysOn high-availability instance</title>
		<link>https://www.dbi-services.com/blog/sql-server-disabling-filetable-filestream-on-an-alwayson-high-availability-instance/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-disabling-filetable-filestream-on-an-alwayson-high-availability-instance/#respond</comments>
		
		<dc:creator><![CDATA[Stéphane Haby]]></dc:creator>
		<pubDate>Thu, 15 Oct 2026 12:11:29 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Microsoft]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=47891</guid>

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph">I hope these steps will help anyone facing a similar issue! <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-disabling-filetable-filestream-on-an-alwayson-high-availability-instance/">SQL Server: Disabling FileTable/FileStream on an AlwaysOn high-availability instance</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/sql-server-disabling-filetable-filestream-on-an-alwayson-high-availability-instance/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Archiving 5 TB from Oracle into SQL Server: views, indexes and compression (part 2)</title>
		<link>https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-views-indexes-and-compression-part-2/</link>
					<comments>https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-views-indexes-and-compression-part-2/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Wed, 07 Oct 2026 06:20:08 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[Oracle]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[migration]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=47235</guid>

					<description><![CDATA[<p>Translating 4,000 Oracle views, rebuilding indexes offline on Standard Edition, and compressing clustered index to save 1T.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-views-indexes-and-compression-part-2/">Archiving 5 TB from Oracle into SQL Server: views, indexes and compression (part 2)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">At the end of&nbsp;<a href="https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-a-migration-logbook-part-1/">part 1</a>&nbsp;the data was across, at 42,170 rows per second into a database that could not answer a single question. Every nonclustered index disabled, the largest tables loaded as heaps with no primary key, every foreign key switched off, and 4,784 views that mostly did not compile.</p>



<p class="wp-block-paragraph">This part is about putting it back: <strong>translating the views</strong>, <strong>rebuilding the keys and indexes under a Standard Edition licence</strong> that makes every build offline, <strong>and compressing 3.2 TB of heaps</strong> that turned out to be mostly empty space.</p>



<p class="wp-block-paragraph">The logbook resumes at day 5.</p>



<h2 id="h-day-5-four-thousand-views-to-convert" class="wp-block-heading">Day 5. Four thousand views to convert</h2>



<p class="wp-block-paragraph">4,658 of the 4,784 views live in a single schema, and nobody needed all of them. We went through the list with the client first and kept the ones that actually matter to the business, which brought the scope down to about 710. SSMA converted around 600 of those on its own.</p>



<p class="wp-block-paragraph">SSMA fails mostly on <code>CONNECT BY</code>, <code>START WITH</code> and <code>SYS_CONNECT_BY_PATH</code>. For some easy cases, <code>DECODE</code> to <code>CASE</code> and <code>NVL</code> handles it well, for the record. For more complex use case, we needed to find an automatic solution process. So first, we discussed with the client to know which view were critical and needed to be migrated. Then, with this list, we leveraged the power of AI to help us converting the remaining views based on the following workflow:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="786" height="1024" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/09/image-45-786x1024.png" alt="" class="wp-image-47241" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/09/image-45-786x1024.png 786w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/09/image-45-230x300.png 230w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/09/image-45-768x1001.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/09/image-45.png 1120w" sizes="auto, (max-width: 786px) 100vw, 786px" /></figure>



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



<p class="wp-block-paragraph">SSMA converts what it can. The failures get extracted from <code>dba_views </code>into files. Then, and this is the step that changes the economics, they get<strong> classified by pattern in the Oracle source</strong> rather than one at a time by error message.</p>



<p class="wp-block-paragraph">Translation itself was done in batches of one family at a time, outside production, on extracted files, <strong>with an AI assistant</strong> and a context file describing the type mapping decisions, the column types of the referenced tables and the target version. Then human review, tests against Oracle and finally, deployment.</p>



<p class="wp-block-paragraph">The catalogue of families is the real deliverable. Eight of them closed the great majority of the cases:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Family</th><th>Verdict</th></tr></thead><tbody><tr><td><code>+</code> concatenation on numerics (<code>col + '_' + col</code>)</td><td><code>CONCAT</code></td></tr><tr><td>Untyped NULL in a UNION, cast to <code>numeric(38,10)</code> against a <code>datetime2</code> sibling</td><td>retype the cast</td></tr><tr><td>Call to a package function that was never migrated</td><td>inline it if possible</td></tr><tr><td><code>ssma_oracle.*</code> helper functions</td><td>install the extension pack</td></tr><tr><td>XML extraction (<code>.extract().getStringVal()</code>)</td><td>rewrite part of the query</td></tr><tr><td><code>TO_TIMESTAMP_TZ</code></td><td>rewrite part of the query</td></tr><tr><td>Date arithmetic landing in float</td><td>redefine type mapping</td></tr><tr><td><code>CONNECT BY</code> used as a generator</td><td>materialise the level connectors</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong><span style="text-decoration: underline">And a word on the AI</span></strong>, since that is the part people ask about. <strong>No data ever left the environment</strong>: only the view definitions were processed, and every test against the live database was run by hand so we kept control of the process. We could have added MCP connectors and let a local model run the tests itself, but with the number of days allocated to the project and the small number of views left to fix, doing it manually was simpler. Around 85 percent of the views <strong>wanted by the client</strong> had already come through SSMA.</p>



<h2 id="h-day-6-putting-the-indexes-back-and-making-the-database-smaller" class="wp-block-heading">Day 6. Putting the indexes back and making the database smaller</h2>



<h3 id="h-recovering-what-i-had-thrown-away" class="wp-block-heading">Recovering what I had thrown away</h3>



<p class="wp-block-paragraph">The primary keys I dropped to load into heaps had left no trace on the SQL Server side. Nothing in <code>sys.indexes</code>, no disabled definition, nothing.</p>



<h3 id="h-what-standard-edition-costs" class="wp-block-heading">What Standard Edition costs</h3>



<p class="wp-block-paragraph">Index builds are offline and single-threaded. The table is locked for the duration, metadata reads queue behind it, but we managed to run 4 builds in parallel. <code>SORT_IN_TEMPDB = OFF</code> for the clustered builds. The sort covers the entire table, and 36 GB of tempdb was never going to hold a 900 GB sort.</p>



<p class="wp-block-paragraph">Meanwhile <code>percent_complete</code> sits at 0 for the whole build, with the serene confidence of something that has never been asked to account for itself. It works for <code>BACKUP DATABASE</code>. It does not work here. Real progress lives in <code><a href="https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-objects/sys-dm-exec-query-profiles-transact-sql?view=sql-server-ver17">sys.dm_exec_query_profile</a>s</code>, operator by operator :</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT physical_operator_name, row_count, estimate_row_count,
       CAST(row_count*100.0/NULLIF(estimate_row_count,0) AS decimal(5,1)) AS pct
FROM sys.dm_exec_query_profiles WITH (NOLOCK)
WHERE session_id = &lt;spid of the build&gt; 
ORDER BY node_id;

</pre></div>

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


<p class="wp-block-paragraph">Those are not three parallel counters. They are three sequential phases of one pipeline, and only one of them moves at a time. The scan reads the source table, and while it runs its percentage is the only honest progress figure available. The sort is a blocking operator: it cannot emit a single row until it has consumed all 31.9 million, so it stays at zero through the whole scan and then through the whole sort. That is the blind spot. Once the scan reaches 100 percent you get a long stretch where all three counters report nothing while the server works flat out, and if the sort spills to disk that stretch can outlast the other two phases combined. Index Insert starts moving only when the sort begins handing rows over, and it is the first number that means the end is actually in sight.</p>



<h3 id="h-why-compress-and-what-it-returned" class="wp-block-heading">Why compress, and what it returned</h3>



<p class="wp-block-paragraph">On a read-only database, PAGE compression has no recurring write cost. You keep the read benefit and pay nothing back. The argument is rarely this clean.</p>



<p class="wp-block-paragraph">Compression trades CPU for I/O, and on an archive that trade is one-sided. These pages are read rarely, and when they are, nothing heavy is competing for the processor, so the decompression cost lands on a machine with nothing better to do. What you get back is density: more rows per page, which means fewer reads to find the one row somebody asked for and more of the table sitting in memory at once.</p>



<p class="wp-block-paragraph">The data was unusually well suited to it, for a reason worth spelling out. These tables carry roughly 200 numeric columns, mostly null, plus five <code>nvarchar(200)</code> aggregation labels that repeat endlessly. Here is the part that surprises people: <strong>a fixed-length numeric column that is NULL still occupies its full width</strong> in SQL Server. “Mostly null” saves nothing before compression. It is precisely why the heap was that large, and precisely what the page dictionary destroys.</p>



<p class="wp-block-paragraph">The allocated space per row tells the story:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Table</th><th>Rows</th><th>GB (heap)</th><th>bytes allocated per row</th></tr></thead><tbody><tr><td>TABLE1</td><td>251,777,518</td><td>960.5</td><td>~4,100</td></tr><tr><td>TABLE2</td><td>229,881,717</td><td>876.9</td><td>~4,100</td></tr><tr><td>TABLE3</td><td>343,997,550</td><td>874.8</td><td>~2,700</td></tr><tr><td>TABLE4</td><td>447,972,849</td><td>227.9</td><td>~550</td></tr><tr><td>TABLE5</td><td>519,727,931</td><td>120.2</td><td>~250</td></tr><tr><td>TABLE6</td><td>505,819,544</td><td>117.0</td><td>~250</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Three tables hold 2.7 TB of the 3.2 TB total</strong>. At around 4 KB of allocated space per row against an 8 KB page, the top two fit around two rows per page and waste most of what is left.</p>



<p class="wp-block-paragraph">Then the first build, on 505,819,544 rows with a five-column primary key:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
ALTER TABLE &lt;TABLE6&gt;
ADD CONSTRAINT PK_TABLE6 PRIMARY KEY CLUSTERED
    (&lt;COLUMNS_NAME&gt;)
WITH (DATA_COMPRESSION = PAGE, SORT_IN_TEMPDB = OFF);

</pre></div>


<p class="wp-block-paragraph"><strong>117 GB to 30.7 GB. A ratio of 3.81.</strong></p>



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



<p class="wp-block-paragraph">Finally, as you can see above, more than 2.5 T have been reclaimed (datafiles were full with the heaps). Compression has made huge benefits and that means less data on disks, which means fewer pages to take in the buffer (less IO).</p>



<h3 id="h-the-script-that-puts-everything-back" class="wp-block-heading">The script that puts everything back</h3>



<p class="wp-block-paragraph">Across every schema that is two cursors, one rebuilding the disabled indexes with compression and one re-enabling the foreign keys. It is driven by the same&nbsp;<code>#Schemas</code>&nbsp;table as the disable script in part 1:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SET NOCOUNT ON;
DECLARE @Execute bit = 0;   -- 0 = preview ; 1 = execute
  
DECLARE @cmd nvarchar(max);
DECLARE @n   int = 0;
  
DECLARE reb_cur CURSOR LOCAL FAST_FORWARD FOR
    SELECT N&#039;ALTER INDEX &#039; + QUOTENAME(i.name)
         + N&#039; ON &#039; + QUOTENAME(s.name) + N&#039;.&#039; + QUOTENAME(t.name)
         + N&#039; REBUILD WITH (DATA_COMPRESSION = PAGE);&#039;
    FROM   sys.indexes  i
    JOIN   sys.tables   t ON i.object_id = t.object_id
    JOIN   sys.schemas  s ON t.schema_id = s.schema_id
    WHERE  s.name IN (SELECT schema_name FROM #Schemas)
      AND  i.is_disabled = 1          
    ORDER BY s.name, t.name, i.name;
  
OPEN reb_cur;
FETCH NEXT FROM reb_cur INTO @cmd;
WHILE @@FETCH_STATUS = 0
BEGIN
    SET @n += 1;
    PRINT @cmd;
    IF @Execute = 1
    BEGIN
        BEGIN TRY EXEC sys.sp_executesql @cmd; END TRY
        BEGIN CATCH PRINT N&#039;   -- FAILED: &#039; + ERROR_MESSAGE(); END CATCH
    END
    FETCH NEXT FROM reb_cur INTO @cmd;
END
CLOSE reb_cur;
DEALLOCATE reb_cur;
  
PRINT N&#039;---&#039;;
PRINT CONVERT(nvarchar(10), @n) + N&#039; index &#039;
    + CASE WHEN @Execute = 1 THEN N&#039;rebuilt.&#039; ELSE N&#039;to be rebuilt (preview).&#039; END;
GO
  
  
SET NOCOUNT ON;
DECLARE @Execute bit = 1;   -- 0 = preview ; 1 = executer
  
DECLARE @cmd nvarchar(max);
DECLARE @n   int = 0;
  
DECLARE fkc_cur CURSOR LOCAL FAST_FORWARD FOR
    SELECT N&#039;ALTER TABLE &#039; + QUOTENAME(s.name) + N&#039;.&#039; + QUOTENAME(t.name)
         + N&#039; WITH NOCHECK CHECK CONSTRAINT &#039; + QUOTENAME(fk.name) + N&#039;;&#039;
    FROM   sys.foreign_keys fk
    JOIN   sys.tables  t ON fk.parent_object_id = t.object_id
    JOIN   sys.schemas s ON t.schema_id = s.schema_id
    WHERE  s.name IN (SELECT schema_name FROM #Schemas);
  
OPEN fkc_cur;
FETCH NEXT FROM fkc_cur INTO @cmd;
WHILE @@FETCH_STATUS = 0
BEGIN
    SET @n += 1;
    PRINT @cmd;
    IF @Execute = 1
    BEGIN
        BEGIN TRY EXEC sys.sp_executesql @cmd; END TRY
        BEGIN CATCH PRINT N&#039;   -- FAILED : &#039; + ERROR_MESSAGE(); END CATCH
    END
    FETCH NEXT FROM fkc_cur INTO @cmd;
END
CLOSE fkc_cur;
DEALLOCATE fkc_cur;
  
PRINT N&#039;---&#039;;
PRINT CONVERT(nvarchar(10), @n) + N&#039; FK &#039;
    + CASE WHEN @Execute = 1 THEN N&#039;activated and validated.&#039; ELSE N&#039;to be activated (preview).&#039; END;
GO

</pre></div>


<h3 id="h-the-foreign-keys-which-turned-out-to-be-free" class="wp-block-heading">The foreign keys, which turned out to be free</h3>



<p class="wp-block-paragraph">All 6,072 foreign keys were already back, active and marked not trusted, because the loading script re-enabled them with <code>NOCHECK</code>. Revalidating them with <code>WITH CHECK</code> would mean full joins against the largest tables in the database.</p>



<p class="wp-block-paragraph">On a read-only archive whose source integrity <strong>was enforced by Oracle</strong> for fifteen years, that buys one thing: the optimizer regains the right to eliminate redundant joins. Worth having on a schema of deeply nested views, not worth days of scanning here. They stayed not trusted.</p>



<h2 id="h-day-7-testing-600-views" class="wp-block-heading">Day 7. Testing 600 views</h2>



<p class="wp-block-paragraph">The principle is simple: run every view, record what happened, and never lose the record. Each one gets a <code>SELECT TOP (100) *</code> inside a <code>TRY/CATCH</code>, so a view that fails is logged rather than fatal, and the outcome lands in a table with its duration in milliseconds. The cursor only picks up views that are not already recorded, which means an interrupted run resumes where it stopped instead of starting over, and the duration column is what you sort on once it is done:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SET NOCOUNT ON;
 
IF OBJECT_ID(&#039;dbo.ViewTestResults&#039;) IS NULL
    CREATE TABLE dbo.ViewTestResults (
        schema_name  sysname,
        view_name    sysname,
        statut       varchar(20),
        duree_ms     int,
        message      nvarchar(2048),
        tested_at    datetime2 DEFAULT SYSDATETIME(),
        CONSTRAINT PK_ViewTestResults PRIMARY KEY (schema_name, view_name)
    );
 
DECLARE @sch sysname, @vue sysname, @sql nvarchar(max), @t0 datetime2, @ms int, @i int = 0, @n int;
 
SELECT @n = COUNT(*)
FROM   sys.views v JOIN sys.schemas s ON v.schema_id = s.schema_id
WHERE  s.name = &#039;SCDAT&#039;
  AND  NOT EXISTS (SELECT 1 FROM dbo.ViewTestResults r
                   WHERE r.schema_name = s.name AND r.view_name = v.name);
 
RAISERROR(&#039;=== %d view remaining ===&#039;, 0, 1, @n) WITH NOWAIT;
 
DECLARE c CURSOR FAST_FORWARD FOR
  SELECT s.name, v.name
  FROM   sys.views v JOIN sys.schemas s ON v.schema_id = s.schema_id
  WHERE  s.name = &#039;SCDAT&#039;
    AND  NOT EXISTS (SELECT 1 FROM dbo.ViewTestResults r
                     WHERE r.schema_name = s.name AND r.view_name = v.name)
  ORDER  BY v.name;
 
OPEN c;
FETCH NEXT FROM c INTO @sch, @vue;
WHILE @@FETCH_STATUS = 0
BEGIN
    SET @i += 1;
    RAISERROR(&#039;&#x5B;%d/%d] %s.%s&#039;, 0, 1, @i, @n, @sch, @vue) WITH NOWAIT;
 
    SET @t0 = SYSDATETIME();
    BEGIN TRY
        SET @sql = N&#039;SELECT TOP (100) * FROM &#039; + QUOTENAME(@sch) + N&#039;.&#039; + QUOTENAME(@vue) + N&#039;;&#039;;
        EXEC sp_executesql @sql;
        SET @ms = DATEDIFF(MILLISECOND, @t0, SYSDATETIME());
        INSERT dbo.ViewTestResults (schema_name, view_name, statut, duree_ms, message)
        VALUES (@sch, @vue, &#039;OK&#039;, @ms, NULL);
    END TRY
    BEGIN CATCH
        SET @ms = DATEDIFF(MILLISECOND, @t0, SYSDATETIME());
        INSERT dbo.ViewTestResults (schema_name, view_name, statut, duree_ms, message)
        VALUES (@sch, @vue, &#039;FAILED&#039;, @ms, ERROR_MESSAGE());
        RAISERROR(&#039;    -&gt; FAILED: %s&#039;, 0, 1, @vue) WITH NOWAIT;
    END CATCH
 
    FETCH NEXT FROM c INTO @sch, @vue;
END
CLOSE c; DEALLOCATE c;
 
RAISERROR(&#039;=== Done ===&#039;, 0, 1) WITH NOWAIT;
 
-- summary
SELECT statut, COUNT(*) AS nb, SUM(duree_ms) AS duree_totale_ms
FROM dbo.ViewTestResults GROUP BY statut;
 
-- failed
SELECT schema_name, view_name, duree_ms, message
FROM dbo.ViewTestResults WHERE statut=&#039;FAILED&#039; ORDER BY view_name;
 
-- all
SELECT schema_name, view_name, statut, duree_ms
FROM dbo.ViewTestResults ORDER BY duree_ms DESC;

</pre></div>


<p class="wp-block-paragraph">The result looked excellent: <strong>604 views OK, zero errors</strong>, against real data. Six skipped because they are not implemented to be run without an explicit filter added. <code>SELECT TOP 100 *</code> without a filter <strong>is not a representative test</strong> but still confirms that definition and joins are working fine. So: test with the filter the users will actually write. A view that only misbehaves when queried in a way nobody queries it is not a broken view, but you cannot know that from a green tick.</p>



<h2 id="h-day-8-closing-the-hatch" class="wp-block-heading">Day 8. Closing the hatch</h2>



<p class="wp-block-paragraph">Database set to read-only, final backup taken and user&#8217;s specific queries tested with success, the story now comes to a happy end.</p>



<h2 id="h-what-i-would-tell-the-next-person" class="wp-block-heading">What I would tell the next person</h2>



<p class="wp-block-paragraph">An archive migration is not a cheap application migration. It is a different decision set, and read-only is the input that drives most of it. 25,000 triggers are pointless, PAGE compression free, and untrusted foreign keys perfectly acceptable. It even tells you how to index: broadly and in advance, because nobody can tell you which row an auditor will want in three years. Before anything else, ask what the target will be used for. The answer rewrites the plan before the first byte moves.</p>



<p class="wp-block-paragraph">Measure, and distrust anything that presents itself as progress. The SSMA counter refreshes per batch and sits still for minutes at a time. <code>percent_complete </code>stays at zero for an entire index build. A green tick on<code> SELECT TOP 100</code> confirms that the joins resolve and nothing beyond that. In every one of those cases the real number was sitting in a DMV.</p>



<p class="wp-block-paragraph">On views, the conversion rate is the least useful number in the report. What made four thousand views a finite piece of work was not the translation but the two steps around it: agreeing the real scope with the client first, then classifying what remained by pattern in the Oracle source rather than one error message at a time. <strong>An AI assistant</strong> translates a family of twenty views very well. It validates nothing, and the test against the source stays yours.</p>



<p class="wp-block-paragraph">Compression on an archive is the rare decision with nothing to weigh against it. 117 GB down to 30.7 GB on the first table, <strong>more than 2.5 TB</strong> reclaimed across the set, and not a single write to pay it back with. If your heap is mostly null numeric columns, it is bigger than you think, and it will compress better than you expect.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-views-indexes-and-compression-part-2/">Archiving 5 TB from Oracle into SQL Server: views, indexes and compression (part 2)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-views-indexes-and-compression-part-2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Archiving 5 TB from Oracle into SQL Server: a migration logbook (part 1)</title>
		<link>https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-a-migration-logbook-part-1/</link>
					<comments>https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-a-migration-logbook-part-1/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Tue, 29 Sep 2026 19:37:18 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[Oracle]]></category>
		<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=47204</guid>

					<description><![CDATA[<p>Loading 5 TB from Oracle into SQL Server with SSMA: type overflows, heaps, and the settings that took 788 rows/s to 42,170.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-a-migration-logbook-part-1/">Archiving 5 TB from Oracle into SQL Server: a migration logbook (part 1)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">680,000 rows in 863 seconds. That is 788 rows per second, and the table I was loading holds 229,881,717 of them.</p>



<p class="wp-block-paragraph">Eighty-one hours. For one table. There were five more behind it.</p>



<p class="wp-block-paragraph">SSMA is well documented but migrations at this scale are not done often enough to have plenty of materials. Most of what you find is either a step-by-step guide against a sample database, or a vendor case study with no numbers in it. This post has the numbers, in the order I actually got them, including the ones I got wrong first.</p>



<p class="wp-block-paragraph">To keep my sanity through a long and intricate migration, I kept a logbook. This first part is about getting the data across: the brief, the type negotiation, and the settings that took the load from 788 rows per second to 42,170. Part 2 picks up exactly where this one stops, with 3.2 TB sitting in heaps and not a single index on it.</p>



<h2 id="h-day-1-the-brief-and-the-one-sentence-that-mattered" class="wp-block-heading">Day 1. The brief, and the one sentence that mattered</h2>



<p class="wp-block-paragraph">An Oracle 19c production database running an asset management package, to be archived into SQL Server 2025 Standard Edition. Roughly 5,600 tables, 4,800 views, a little under 5 TB depending on how you count (more on that later).</p>



<p class="wp-block-paragraph">Three facts shaped everything that followed.</p>



<p class="wp-block-paragraph">The target is<strong><span style="text-decoration: underline"> read-only</span></strong>. Nothing will ever write to it once the load is finished, because the whole point of the exercise is to keep fifteen years of records available to people who need to look something up rather than to keep an application running. Auditors come by, rarely and unpredictably, to find one specific row.</p>



<p class="wp-block-paragraph">That is the entire performance requirement, and it decided more of this project than any measurement did. It is why the indexes are broad rather than targeted at known queries, why PAGE compression costs nothing here, why 25,000 triggers went straight in the bin, and why two views that take thirty seconds are perfectly acceptable. Every one of those decisions comes back later in this series.</p>



<p class="wp-block-paragraph">The freeze has no deadline. This is unusual enough to state plainly. Most migrations are built around a cutover window measured in hours, and every technical decision bends to it. Here the source was frozen since the beginning and simply stayed frozen.</p>



<p class="wp-block-paragraph">The data travels in two hops, which becomes the story of the whole load:</p>



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



<h3 id="h-getting-in-without-asking-for-the-keys" class="wp-block-heading">Getting in without asking for the keys</h3>



<p class="wp-block-paragraph">The source is critical production, so the migration account got the minimum that actually works: <code>CREATE SESSION</code>, dictionary read, and <code>SELECT </code>on the tables in scope.</p>



<p class="wp-block-paragraph">SSMA also asks for <code>CREATE ANY PROCEDURE</code>, <code>CREATE ANY TYPE</code> and <code>CREATE ANY TRIGGER</code>. Those three serve server-side extraction and SSMA’s own testing feature and at the end of the project a single <code>DROP USER &lt;user_name&gt; CASCADE</code> removes the whole thing.</p>



<p class="wp-block-paragraph">There is a decision hiding here that is worth making deliberately.<code> SELECT ANY TABLE</code> is convenient and gives your account read access to the entire database, including schemas you are not migrating. Granting <code>SELECT </code>table by table on the schemas in scope is more work and produces an auditable perimeter.</p>



<h2 id="h-day-2-negotiating-with-numbers" class="wp-block-heading">Day 2. Negotiating with numbers</h2>



<p class="wp-block-paragraph">We agreed the type mapping with the client before converting anything, which I recommend for the political reasons as much as the technical ones:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Oracle</th><th>SQL Server</th></tr></thead><tbody><tr><td>NUMBER(38) to NUMBER(*)</td><td>FLOAT</td></tr><tr><td>NUMBER(19) to NUMBER(38)</td><td>DECIMAL[19-38]</td></tr><tr><td>NUMBER(11) to NUMBER(18)</td><td>BIGINT</td></tr><tr><td>NUMBER(5) to NUMBER(10)</td><td>INT</td></tr><tr><td>NUMBER(1) to NUMBER(4)</td><td>SMALLINT</td></tr><tr><td>DATE</td><td>DATETIME2(0)</td></tr><tr><td>XMLTYPE</td><td>XML</td></tr><tr><td>VARCHAR2(n CHAR)</td><td>NVARCHAR(n)</td></tr><tr><td>CHAR(n CHAR)</td><td>NCHAR(n)</td></tr></tbody></table></figure>



<h3 id="h-38-digits-against-51" class="wp-block-heading">38 digits against 51</h3>



<p class="wp-block-paragraph">Then the conversions started failing on decimal overflow, and the reason is more interesting than &#8220;<em>the numbers were too big</em>&#8220;.</p>



<p class="wp-block-paragraph">Oracle <code>NUMBER </code>without precision is a <strong>floating decimal:</strong> 38 significant digits, with an exponent range running from 1e-130 to just under 1e126. SQL Server <code>DECIMAL </code>is <strong>fixed point</strong>: the precision bounds the magnitude. <code>DECIMAL(38,0)</code> stops at 1e38 and there is no exponent to escape with.</p>



<p class="wp-block-paragraph">So the values that broke were not unusually precise, they were unusually <em>large</em>. A value carrying 51 digits and two significant figures is perfectly ordinary in Oracle, sails through every constraint it meets on the way out, and then has no <code>DECIMAL </code>representation waiting for it at the other end.</p>



<p class="wp-block-paragraph">The answer is <code>FLOAT</code>, which is also a floating type and reaches ±1.79e308. Eleven tables were reloaded after switching the mapping.</p>



<p class="wp-block-paragraph"><strong><span style="text-decoration: underline">Be careful</span></strong> what you promise the client here, because <code>FLOAT </code>keeps <span style="text-decoration: underline">15 significant digits, not 38</span>. For the values that actually overflowed, large magnitude and few significant figures, the loss is zero. For a value in those columns carrying more than 15 significant digits, the loss is real. That is countable rather than arguable, so count it before you write the reassuring email.</p>



<h3 id="h-25-000-triggers-none-converted" class="wp-block-heading">25,000 triggers, none converted</h3>



<p class="wp-block-paragraph">The source carried roughly 25,000 triggers, almost all in one schema. I converted none of them and unticked them all before <em>Convert Schema</em>.</p>



<p class="wp-block-paragraph">The reasoning fits in one line: on a read-only database, a trigger never fires. Converting and testing 25,000 objects that will never execute would have consumed weeks and produced nothing.</p>



<p class="wp-block-paragraph">Tables went first, with their types and sequences, to unblock the data migration. Packages, views and procedures came later. Indexes are not a separate step: SSMA converts them alongside their table, turning bitmap indexes into nonclustered ones, handling function-based indexes case by case, and skipping <code>XMLTYPE </code>indexes entirely.</p>



<p class="wp-block-paragraph">Then 103 tables failed extraction with <code>ORA-00904</code>, all because of hidden <code>SYS_C…$</code> columns left behind by function-based indexes. I had already set &#8220;<em>Ignore hidden system columns</em>&#8221; to <code>Yes</code>, which is why this took longer than it should have: in our project that option governed schema conversion and did not stop the extraction from asking for the column. The fix is a custom select that aliases <code>NULL </code>into the missing name (see my <a href="https://www.dbi-services.com/blog/fixing-ora-00904-on-oracle-hidden-columns-during-ssma-data-migration/">previous blog about this problem</a>).</p>



<h2 id="h-day-3-turning-everything-off" class="wp-block-heading">Day 3. Turning everything off</h2>



<p class="wp-block-paragraph">The loading method is three switches and a recovery model.</p>



<p class="wp-block-paragraph">Disable the nonclustered indexes. A disabled index stores nothing and is not maintained row by row during the load.</p>



<p class="wp-block-paragraph">Disable the foreign keys with <code>NOCHECK CONSTRAINT</code>.</p>



<p class="wp-block-paragraph">Drop the clustered primary keys on the largest tables and load into heaps.</p>



<p class="wp-block-paragraph">Then set the recovery model to <code>SIMPLE</code>. With heaps and bulk insert, logging is already minimal.</p>



<p class="wp-block-paragraph">The script that does this across every schema needs to be restartable and needs to log what it did. A load that runs for days will be interrupted, and you want to know exactly which of 22&#8217;000 indexes were disabled when it was.</p>



<p class="wp-block-paragraph">Script to disable all nonclustered indexes:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
USE &lt;DB_NAME&gt;;  
GO
  
  
IF OBJECT_ID(N&#039;tempdb..#Schemas&#039;) IS NOT NULL DROP TABLE #Schemas;
CREATE TABLE #Schemas (schema_name sysname PRIMARY KEY);
  
INSERT INTO #Schemas (schema_name) VALUES (N&#039;&lt;SCHEMA_NAME&gt;&#039;); --Insert your schema name here
    ;
  
SELECT schema_name AS schemas_a_traiter FROM #Schemas;
GO
  
  
-- Disable all NC Indexes
 
SET NOCOUNT ON;
DECLARE @Execute bit = 0;   -- 0 = preview ; 1 = execute
  
DECLARE @cmd nvarchar(max);
DECLARE @n   int = 0;
  
DECLARE idx_cur CURSOR LOCAL FAST_FORWARD FOR
    SELECT N&#039;ALTER INDEX &#039; + QUOTENAME(i.name)
         + N&#039; ON &#039; + QUOTENAME(s.name) + N&#039;.&#039; + QUOTENAME(t.name)
         + N&#039; DISABLE;&#039;
    FROM   sys.indexes  i
    JOIN   sys.tables   t ON i.object_id = t.object_id
    JOIN   sys.schemas  s ON t.schema_id = s.schema_id
    WHERE  s.name IN (SELECT schema_name FROM #Schemas)
      AND  i.type_desc            in (N&#039;NONCLUSTERED&#039;) 
      AND  i.is_disabled          = 0                
    ORDER BY s.name, t.name, i.name;
  
OPEN idx_cur;
FETCH NEXT FROM idx_cur INTO @cmd;
WHILE @@FETCH_STATUS = 0
BEGIN
    SET @n += 1;
    PRINT @cmd;
    IF @Execute = 1
    BEGIN
        BEGIN TRY
            EXEC sys.sp_executesql @cmd;
        END TRY
        BEGIN CATCH
            PRINT N&#039;   -- ECHEC : &#039; + ERROR_MESSAGE();
        END CATCH
    END
    FETCH NEXT FROM idx_cur INTO @cmd;
END
CLOSE idx_cur;
DEALLOCATE idx_cur;
  
PRINT N&#039;---&#039;;
PRINT CONVERT(nvarchar(10), @n) + N&#039; index nonclustered &#039;
    + CASE WHEN @Execute = 1 THEN N&#039;disabled.&#039; ELSE N&#039;te be disabled (preview).&#039; END;
GO
  
  
----------------------------------------------
 
 
SET NOCOUNT ON;
DECLARE @Execute bit = 0;   -- 0 = preview ; 1 = executer
  
DECLARE @cmd nvarchar(max);
DECLARE @n   int = 0;
  
DECLARE fk_cur CURSOR LOCAL FAST_FORWARD FOR
    SELECT N&#039;ALTER TABLE &#039; + QUOTENAME(s.name) + N&#039;.&#039; + QUOTENAME(t.name)
         + N&#039; NOCHECK CONSTRAINT &#039; + QUOTENAME(fk.name) + N&#039;;&#039;
    FROM   sys.foreign_keys fk
    JOIN   sys.tables  t ON fk.parent_object_id = t.object_id
    JOIN   sys.schemas s ON t.schema_id = s.schema_id
    WHERE  s.name IN (SELECT schema_name FROM #Schemas)
      AND  fk.is_disabled = 0;
  
OPEN fk_cur;
FETCH NEXT FROM fk_cur INTO @cmd;
WHILE @@FETCH_STATUS = 0
BEGIN
    SET @n += 1;
    PRINT @cmd;
    IF @Execute = 1
    BEGIN
        BEGIN TRY EXEC sys.sp_executesql @cmd; END TRY
        BEGIN CATCH PRINT N&#039;   -- FAILED: &#039; + ERROR_MESSAGE(); END CATCH
    END
    FETCH NEXT FROM fk_cur INTO @cmd;
END
CLOSE fk_cur;
DEALLOCATE fk_cur;
  
PRINT N&#039;---&#039;;
PRINT CONVERT(nvarchar(10), @n) + N&#039; FK &#039;
    + CASE WHEN @Execute = 1 THEN N&#039;disabled.&#039; ELSE N&#039;to be disabled (preview).&#039; END;
GO 
</pre></div>


<p class="wp-block-paragraph">There is a second script that puts all of this back, with PAGE compression, once the data is in. It belongs with the rebuild rather than with the load, so it is in part 2.</p>



<h2 class="wp-block-heading">Day 4. ×53, on the same architecture</h2>



<p class="wp-block-paragraph">The first load ran <strong>client-side</strong>: the hop server reads from Oracle and writes to SQL Server.</p>



<p class="wp-block-paragraph">On the SQL Server side the dominant wait was <code>ASYNC_NETWORK_IO</code>, which is the engine telling you it has finished its work and is sitting there waiting for the next batch to arrive. Meanwhile CPU and memory on the hop VM stayed low throughout. Nothing anywhere in the chain was short of power; the pipeline was simply underfed.</p>



<p class="wp-block-paragraph">The answer for this was not a different architecture (with for example a linked server directly from Oracle to SQL Server), it was three settings on the one I already had.</p>



<p class="wp-block-paragraph">Load into heaps with the nonclustered indexes disabled. Parallelise <strong>across tables</strong>, not within one, which is the part most people have backwards about SSMA: Thread Count 14 with <code>Table lock = No</code>, because <code>TABLOCK </code>plus multithreading deadlocks. Batch size at 200&#8217;000.</p>



<p class="wp-block-paragraph">Measured with the two-timestamp method: <strong>7,000,000</strong> <strong>rows in 166 seconds, about 42,170 rows per second</strong>. Against 788, that is ×53.</p>



<p class="wp-block-paragraph">It is worth being explicit about why write-side changes fixed a network wait, because the two do not obviously connect. Of the three levers only one touches writing. Parallelising across tables fills a pipe that was carrying a single stream and fast writes remove the back-pressure that was leaving the sender idle.</p>



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



<h3 class="wp-block-heading">Two volumetry traps</h3>



<p class="wp-block-paragraph">Ranking tables by <code>dba_segments </code>with <code>segment_type = 'TABLE' </code><strong>ignores LOBs</strong>, which live in their own segment. One table that looked unremarkable carried 123 GB of LOB. Redo the ranking with LOB segments included before you plan anything. And row count does not rank the same way as volume. One table held 2.8 billion rows in 67 GB. Another held a tenth of that in fourteen times the space.</p>



<h2 class="wp-block-heading">What I would tell the next person, about the load</h2>



<p class="wp-block-paragraph">The bottleneck was the feed rate rather than the hardware. 788 rows per second, with <code>ASYNC_NETWORK_IO </code>at the top of the wait list, on a chain where nothing was short of CPU or memory. Three settings took it to 42,170: heaps, disabled indexes and parallelism across tables. Reach for the settings before you reach for a new architecture, because the new architecture takes a week to build and may not be the thing that was slow.</p>



<p class="wp-block-paragraph">On types, the failure was one of magnitude and not of precision, which is worth knowing before you try to diagnose it. Oracle <code>NUMBER </code>is a floating decimal whose exponent runs far past anything <code>DECIMAL </code>can address, so a value with 51 digits and two significant figures breaks a conversion that a value with 38 significant digits survives without trouble. And when you move those columns to <code>FLOAT</code>, count what you are actually giving up before you write the email saying nothing was lost.</p>



<p class="wp-block-paragraph">At this point the data is across and the database is useless. Every nonclustered index is disabled, the largest tables have no primary key, the foreign keys are off, and none of the four thousand views compile. <strong>Part 2</strong> is about putting all of that back.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-a-migration-logbook-part-1/">Archiving 5 TB from Oracle into SQL Server: a migration logbook (part 1)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/archiving-5-tb-from-oracle-into-sql-server-a-migration-logbook-part-1/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Oracle LDAP Timeouts</title>
		<link>https://www.dbi-services.com/blog/oracle-ldap-timeouts/</link>
					<comments>https://www.dbi-services.com/blog/oracle-ldap-timeouts/#respond</comments>
		
		<dc:creator><![CDATA[Martin Bracher]]></dc:creator>
		<pubDate>Tue, 22 Sep 2026 13:21:39 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[Oracle]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46960</guid>

					<description><![CDATA[<p>Introduction, Setup You are using 2 or more LDAP servers for Oracle name resolution to be highly available? Do you kown what happens if an ldap server is down or not reachable? You probably expect that it tries the next one? Short answer: Yes, but probably not with a behavior you expect&#8230; Let&#8217;s see my [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/oracle-ldap-timeouts/">Oracle LDAP Timeouts</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 id="h-introduction-setup" class="wp-block-heading">Introduction, Setup</h2>



<p class="wp-block-paragraph">You are using 2 or more LDAP servers for Oracle name resolution to be highly available? <br>Do you kown what happens if an ldap server is down or not reachable? You probably expect that it tries the next one?</p>



<p class="wp-block-paragraph">Short answer: Yes, but probably not with a behavior you expect&#8230;</p>



<p class="wp-block-paragraph">Let&#8217;s see my test configuration There are 2 ldap servers ol9a and ol9b. For the setup, see my former blog post: <a href="https://www.dbi-services.com/blog/setup-an-ldap-server-for-oracle-naming-on-enterprise-linux-9/" data-type="post" data-id="46966">Setup an LDAP server for Oracle naming on Enterprise Linux 9</a></p>



<pre class="wp-block-code"><code># ldap.ora
DIRECTORY_SERVERS=(ol9a:389:636,ol9b:389:636)
DEFAULT_ADMIN_CONTEXT = "dc=dbi-services,dc=com"
DIRECTORY_SERVER_TYPE = OID</code></pre>



<pre class="wp-block-code"><code># sqlnet.ora
NAMES.DEFAULT_DOMAIN=dbi-services.com
NAMES.DIRECTORY_PATH=(LDAP)</code></pre>



<p class="wp-block-paragraph">With <code>NAMES.DIRECTORY_PATH=(LDAP)</code> we enforce to only use LDAP. Often, you will allow other methods like <code>(LDAP,TNSNAMES,EZCONNECT)</code>. Oracle tries the name resolution in this order. </p>



<h2 id="h-tests-with-an-oracle-client-installation-tnsping" class="wp-block-heading">Tests with an Oracle client installation (tnsping)</h2>



<p class="wp-block-paragraph">Now let&#8217;s see what happens if ldap is not reachable. Note the difference: &#8220;server&#8221; refers to the ldap server process, &#8220;host&#8221; refers to the machine on which the ldap server runs (If the server is up, then logically the host is also up). &#8220;down&#8221; of a host is from the Oracle-client&#8217;s point of view and means that it is unreachable (will not respond), regardless of whether it is a host problem or just a network interruption. &#8220;1st&#8221; means the 1st host/server in the DIRECTORY_SERVERS parameter (in the example ora9a).</p>


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


<p class="wp-block-paragraph">We remember: the order is <code>DIRECTORY_SERVERS=(ol9a:389:636,ol9b:389:636)</code></p>



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



<p class="wp-block-paragraph">If both ldap servers are up, we get an answer in a fraction of a second (as expected).</p>



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



<p class="wp-block-paragraph">If the 2nd server/host (ora9b) is down, we get an answer in a fraction of a second. I repeated this test several times, and it always responds immediately. That means, there is no round-robin selection of the servers in place.</p>



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



<p class="wp-block-paragraph">if the 1st ldap server on ora9a is down (the process, not the host), we get the answer within a fraction of a second from ora9b. the Oracle client immediately detects that there is no ldap server running (error &#8220;Connection refused&#8221; on ldap port). </p>



<p class="wp-block-paragraph">But if the host itself does not respond (stopped, network interruption), then the Oracle client has to wait for a timeout and that is usually more than 10 seconds. Did you expect that? Most customers have never noticed this problem as they have never had an LDAP host failure. </p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Used LDAP adapter to resolve the alias
Attempting to contact (DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=ltp14s-i)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=DB1970A.erfinder)))
OK (30 msec)

real    0m15.090s
</pre></div>


<p class="wp-block-paragraph">Is that acceptable for your application to wait for &gt;10 seconds? For an interactive login once a day, it is annoying, but probably OK. But with applications that make thousands of short-term logins? In the worst case, the application will get a timeout.</p>



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



<p class="wp-block-paragraph">Can we get around this long timeout? Yes, we can. Add the following parameter to sqlnet.ora:</p>



<pre class="wp-block-code"><code>NAMES.LDAP_CONN_TIMEOUT=1  #value in seconds</code></pre>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
time tnsping db1970

TNS Ping Utility for Linux: Version 19.0.0.0.0 - Production on 18-SEP-2026 16:46:37

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

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

Used LDAP adapter to resolve the alias
Attempting to contact (DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=ltp14s-i)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=DB1970A)))
OK (10 msec)

real    0m1.040s
</pre></div>


<p class="wp-block-paragraph">Note: the timeouts may have some variations (after a few calls, the timeout was reduced to about 3 seconds for me). <br>The value <code>OK (10 msec)</code> refers only to the time for the listener connect, not for the name resolution. The output of <code>time</code> includes the total time of the tnsping execution.</p>



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



<ul class="wp-block-list">
<li>We can configure the ldap failover timeout in seconds. Minimum is 1 second, default seems to be 10-15 sec.</li>



<li>Timeouts only happen when the host is unreachable, not when the LDAP server (software) is stopped.</li>



<li>LDAP servers are used in the order of the DIRECTORY_SERVERS parameter. There is no round-robin for load-balancing.</li>
</ul>



<h2 id="h-tests-with-java-jdbc" class="wp-block-heading">Tests with Java/JDBC</h2>



<p class="wp-block-paragraph">To do the tests with Java jdbc, I will use Oracle&#8217;s <a href="https://www.oracle.com/database/sqldeveloper/technologies/download/" data-type="link" data-id="https://www.oracle.com/database/sqldeveloper/technologies/download/">SQL Developer</a> . </p>



<p class="wp-block-paragraph">I create a new connection using &#8220;Custom JDBC&#8221; and add the following string:</p>



<pre class="wp-block-code"><code>
jdbc:oracle:thin:@ldap://ol9a:389/DB1970,cn=OracleContext,dc=dbi-services,dc=com ldap://ol9b:389/DB1970,cn=OracleContext,dc=dbi-services,dc=com</code></pre>



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



<p class="wp-block-paragraph">The behavior is like before. But wait&#8230; it isn&#8217;t! <br>I have now noticed that the ldap servers are used round-robin. Sometimes the connect is instantaneous, sometimes it takes the time of the timeout to failover to the other server. </p>



<p class="wp-block-paragraph">The jdbc:thin driver does not use our sqlnet.ora settings. So how can we configure this in Java? Fortunately there is a connection property<br><code> <a href="https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/constant-values.html#oracle.jdbc.OracleConnection.CONNECTION_PROPERTY_THIN_JNDI_LDAP_CONNECT_TIMEOUT"> com.sun.jndi.ldap.connect.timeout</a></code> <br>In sqldeveloper you will configure it in the &#8220;Advanced&#8221; tab:</p>



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



<p class="wp-block-paragraph">With JDBC, the values are in milliseconds. Thus, in contrast to the normal client, we can also specify values of less than 1 second (in the example 500 = 0.5 sec.).</p>



<p class="wp-block-paragraph">In sqldeveloper it works. Does it also work with a &#8220;normal&#8221; java program (thanks to duck.ai to generate me the code). The class is named <code>T</code> (like Test).</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: java; title: ; notranslate">
// T.java
import java.sql.*;
import java.util.Properties;

class T {
  public static void main(String&#x5B;] args) throws Exception {
    String url =
        &quot;jdbc:oracle:thin:@ldap://ol9a:389/DB1970,cn=OracleContext,dc=dbi-services,dc=com ldap://ol9b:389/DB1970,cn=OracleContext,dc=dbi-services,dc=com&quot;;

    Properties env = new Properties();
    env.put(&quot;user&quot;, &quot;system&quot;);
    env.put(&quot;password&quot;, &quot;SystemPassword&quot;);
    env.put(&quot;com.sun.jndi.ldap.connect.timeout&quot;, &quot;2000&quot;);

    try (Connection c = DriverManager.getConnection(url, env);
         Statement s = c.createStatement();
         ResultSet r = s.executeQuery(&quot;select host_name from v$instance&quot;)) {
      if (r.next())
        System.out.println(r.getString(1));
    }
  }
}
</pre></div>


<p class="wp-block-paragraph">Then run the program</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
 $ORACLE_HOME/jdk/bin/javac -cp $ORACLE_HOME/jdbc/lib/ojdbc8.jar T.java
 time $ORACLE_HOME/jdk/bin/java -cp $ORACLE_HOME/jdbc/lib/ojdbc8.jar:. T
</pre></div>


<p class="wp-block-paragraph">We will see the same round-robin behavior as in sqldeveloper. Sometimes it takes 0.04-0.6 seconds (tries ol9b first), sometimes it takes 2.05-2.6 seconds (tries ol9a, wait configured timeout of 2sec. , try ol9b and connect)</p>



<h3 id="h-conclusion-0" class="wp-block-heading">Conclusion:</h3>



<ul class="wp-block-list">
<li>We can configure the ldap failover timeout in milliseconds (in contrast to seconds with OCI client like sqlplus)</li>



<li>Timeouts only happen when the host is unreachable, not when the LDAP server (software) is stopped.</li>



<li>LDAP servers defined in the DIRECTORY_SERVERS parameter are used in a rond-robin order which is ideal for load-balancing.</li>
</ul>



<h2 id="h-general-remarks-and-conclusions" class="wp-block-heading">General remarks and conclusions</h2>



<ul class="wp-block-list">
<li>a timeout does not occur when the ldap service is stopped. The client immediately receives an error message from the host and can ask the other ldap</li>



<li>If the first ldap server does not know the requested name, it will not try other ldap servers, but will try (if configured in names.directory_path) other naming methods (e.g. tnsnames.ora)</li>



<li>The configured timeout is the maximum timeout. If it finds out that the host is not reachable, it tries the next one (e.g. the hostname is unknown).</li>
</ul>



<p class="wp-block-paragraph">For normal sql*net clients using the *.ora files (TNS_ADMIN / &#8230;/network/admin; see my <a href="https://www.dbi-services.com/blog/tns_admin-which-ora-files-oracle-will-use/">blog post</a>), you can configure the timeout in sqlnet.ora</p>



<pre class="wp-block-code"><code>NAMES.LDAP_CONN_TIMEOUT=1  #value in seconds</code></pre>



<p class="wp-block-paragraph">For Java JDBC clients, set the property</p>



<pre class="wp-block-code"><code>com.sun.jndi.ldap.connect.timeout=1000  #value in milliseconds</code></pre>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/oracle-ldap-timeouts/">Oracle LDAP Timeouts</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/oracle-ldap-timeouts/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Setup an LDAP server for Oracle naming on Enterprise Linux 9</title>
		<link>https://www.dbi-services.com/blog/setup-an-ldap-server-for-oracle-naming-on-enterprise-linux-9/</link>
					<comments>https://www.dbi-services.com/blog/setup-an-ldap-server-for-oracle-naming-on-enterprise-linux-9/#respond</comments>
		
		<dc:creator><![CDATA[Martin Bracher]]></dc:creator>
		<pubDate>Fri, 18 Sep 2026 12:27:08 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[Oracle]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46966</guid>

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



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



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



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



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



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


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


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



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



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



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


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

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

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

Enter port number &#x5B;389]:

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

Enter secure port number &#x5B;636]:

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

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

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

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

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

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

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


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


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


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



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

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



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



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



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



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


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


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


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

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

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

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


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


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


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


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


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



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



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



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


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

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

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

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

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

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

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

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


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



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



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



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



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



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/setup-an-ldap-server-for-oracle-naming-on-enterprise-linux-9/">Setup an LDAP server for Oracle naming on Enterprise Linux 9</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/setup-an-ldap-server-for-oracle-naming-on-enterprise-linux-9/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Fixing ORA-00904 on Oracle hidden columns during SSMA data migration</title>
		<link>https://www.dbi-services.com/blog/fixing-ora-00904-on-oracle-hidden-columns-during-ssma-data-migration/</link>
					<comments>https://www.dbi-services.com/blog/fixing-ora-00904-on-oracle-hidden-columns-during-ssma-data-migration/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Wed, 26 Aug 2026 08:54:51 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[Oracle]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[ssma]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46636</guid>

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



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



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


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


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



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



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



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


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


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


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


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



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



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



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



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



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



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



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



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



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



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



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


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


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



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


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


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



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



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



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



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



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


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


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



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



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



<p class="wp-block-paragraph"><code>SYS_C…$</code> is not an exotic Oracle feature, it&#8217;s an ordinary column someone deleted years ago, logically. Oracle keeps the name on file; SSMA finds it, insists on naming it, and Oracle refuses to hand it over. Aliasing a NULL satisfies both, then you drop the column on the target. No source DDL, no external tooling, everything inside SSMA, behind a project setting that&#8217;s hidden by default.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/fixing-ora-00904-on-oracle-hidden-columns-during-ssma-data-migration/">Fixing ORA-00904 on Oracle hidden columns during SSMA data migration</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/fixing-ora-00904-on-oracle-hidden-columns-during-ssma-data-migration/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Green SQL Server DBA Tips #3 – The hidden cost of forgotten databases</title>
		<link>https://www.dbi-services.com/blog/green-sql-server-dba-tips-3-the-hidden-cost-of-forgotten-databases/</link>
					<comments>https://www.dbi-services.com/blog/green-sql-server-dba-tips-3-the-hidden-cost-of-forgotten-databases/#comments</comments>
		
		<dc:creator><![CDATA[Stéphane Haby]]></dc:creator>
		<pubDate>Thu, 06 Aug 2026 13:10:24 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Microsoft]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46270</guid>

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



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



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



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



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



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



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



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



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



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



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



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



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

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

    MAX(ius.last_user_update) AS LastWriteActivity,

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

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/green-sql-server-dba-tips-3-the-hidden-cost-of-forgotten-databases/">Green SQL Server DBA Tips #3 – The hidden cost of forgotten databases</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/green-sql-server-dba-tips-3-the-hidden-cost-of-forgotten-databases/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>PostgreSQL Snapshot Backup and Restore with Proxmox ZFS (4/4)</title>
		<link>https://www.dbi-services.com/blog/postgresql-snapshot-backup-and-restore-with-proxmox-zfs-4-4/</link>
					<comments>https://www.dbi-services.com/blog/postgresql-snapshot-backup-and-restore-with-proxmox-zfs-4-4/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 22:13:44 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[proxmox]]></category>
		<category><![CDATA[ZFS]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46203</guid>

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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


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


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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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


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

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

Data page checksums are enabled.

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


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


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

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


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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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


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

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


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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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


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


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


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


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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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


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


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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-snapshot-backup-and-restore-with-proxmox-zfs-4-4/">PostgreSQL Snapshot Backup and Restore with Proxmox ZFS (4/4)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/postgresql-snapshot-backup-and-restore-with-proxmox-zfs-4-4/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server:  msdb is in Suspect State</title>
		<link>https://www.dbi-services.com/blog/sql-server-msdb-is-in-suspect-state/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-msdb-is-in-suspect-state/#comments</comments>
		
		<dc:creator><![CDATA[Stéphane Haby]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 10:00:17 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Microsoft]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=46190</guid>

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-msdb-is-in-suspect-state/">SQL Server:  msdb is in Suspect State</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/sql-server-msdb-is-in-suspect-state/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 
Lazy Loading (feed)

Served from: www.dbi-services.com @ 2026-10-24 01:48:58 by W3 Total Cache
-->