<?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>Louis Tochon, auteur/autrice sur dbi Blog</title>
	<atom:link href="https://www.dbi-services.com/blog/author/louistochon/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.dbi-services.com/blog/author/louistochon/</link>
	<description></description>
	<lastBuildDate>Fri, 15 May 2026 11:34:45 +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>Louis Tochon, auteur/autrice sur dbi Blog</title>
	<link>https://www.dbi-services.com/blog/author/louistochon/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>How Row Goal shapes your SQL Server query strategy by hunting for pierogis</title>
		<link>https://www.dbi-services.com/blog/how-row-goal-shapes-your-sql-server-query-strategy-by-hunting-for-pierogis/</link>
					<comments>https://www.dbi-services.com/blog/how-row-goal-shapes-your-sql-server-query-strategy-by-hunting-for-pierogis/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Fri, 15 May 2026 11:34:42 +0000</pubDate>
				<category><![CDATA[Development & Performance]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[rowgoal]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44511</guid>

					<description><![CDATA[<p>Mastering SQL Server Row Goals and how TOP, EXISTS, and FAST N influence execution plans and how to avoid performance traps in your queries</p>
<p>L’article <a href="https://www.dbi-services.com/blog/how-row-goal-shapes-your-sql-server-query-strategy-by-hunting-for-pierogis/">How Row Goal shapes your SQL Server query strategy by hunting for pierogis</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image is-style-rounded">
<figure class="aligncenter size-full is-resized"><img fetchpriority="high" decoding="async" width="475" height="433" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-58.png" alt="" class="wp-image-44512" style="aspect-ratio:1.0970763424726049;object-fit:cover;width:243px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-58.png 475w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-58-300x273.png 300w" sizes="(max-width: 475px) 100vw, 475px" /></figure>
</div>


<h2 class="wp-block-heading" id="h-the-wroclaw-connection">The Wroclaw Connection</h2>



<p class="wp-block-paragraph"><a href="https://sqlday.pl/en/sqlday-2026/">SQLDay 2026</a> took place this week, from May 11th to 13th, in Wroclaw. Among the featured speakers was <a href="https://erikdarling.com/" id="https://erikdarling.com/">Erik Darling</a>, who delivered both a main session and a full-day workshop dedicated to SQL Server performance. During his presentations, he emphasized a concept that is not always widely understood, known as the Row Goal.</p>



<p class="wp-block-paragraph">The purpose of this article is to recap Erik’s key observations and to introduce this topic, which can serve as a powerful lever for query optimization.</p>



<h2 class="wp-block-heading" id="h-a-quick-culinary-detour-and-why-pierogis-matter">A quick culinary detour and why pierogis matter</h2>



<p class="wp-block-paragraph">In order to understand the explanations below, one key concept must be understood: <strong>the Pierogi</strong>.</p>



<p class="wp-block-paragraph">&#8220;<em>Pierogi are filled dumplings made from unleavened dough, popular in Polish cuisine and enjoyed worldwide, with various savory and sweet fillings</em>&#8221; <a href="https://en.wikipedia.org/wiki/Pierogi" id="https://en.wikipedia.org/wiki/Pierogi">[1]</a>, <a href="https://www.booths.co.uk/recipe/polish-cheese-potato-pierogi/" id="https://www.booths.co.uk/recipe/polish-cheese-potato-pierogi/">[2]</a>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large is-resized"><img decoding="async" width="1024" height="576" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-60-1024x576.png" alt="" class="wp-image-44514" style="width:497px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-60-1024x576.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-60-300x169.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-60-768x432.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-60.png 1200w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">To be honest, this has nothing to do with our technical topic, but this dish discovered during this trip is so good that I simply had to include it in this blog.</p>



<h2 class="wp-block-heading" id="h-filling-the-aisles-and-designing-our-database">Filling the aisles and designing our database</h2>



<p class="wp-block-paragraph">In this article, we will use a custom-made database simulating a Polish supermarket selling pierogis. Unfortunately, there aren&#8217;t many left, and the product distribution is not uniform. In fact, pierogis account for <span style="text-decoration: underline">much less than 1%</span> of the supermarket&#8217;s total stock. <br>Here is the script to create the DB, along with its article reference table and inventory:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
USE master;
GO

IF EXISTS (SELECT * FROM sys.databases WHERE name = &#039;PierogiMart&#039;)
    DROP DATABASE PierogiMart;
GO

CREATE DATABASE PierogiMart;
GO

USE PierogiMart;
GO

CREATE TABLE Articles (
    ArticleID INT IDENTITY(1,1) PRIMARY KEY,
    ArticleName VARCHAR(50) NOT NULL,
    Price DECIMAL(10, 2) NOT NULL
);

CREATE TABLE Inventory (
    ReferenceID INT IDENTITY(1,1) PRIMARY KEY,
    ArticleID INT NOT NULL,
    ValidityDate DATETIME NOT NULL,
    Quantity INT NOT NULL,
    CONSTRAINT FK_Article FOREIGN KEY (ArticleID) REFERENCES Articles(ArticleID)
);
GO

INSERT INTO Articles (ArticleName, Price)
VALUES 
(&#039;Pierogi&#039;, 12.50),
(&#039;Pasta&#039;, 8.00),
(&#039;Sandwich&#039;, 6.50),
(&#039;Quiche&#039;, 9.00);
GO

INSERT INTO Inventory (ArticleID, ValidityDate, Quantity)
SELECT TOP 100000 
    2, 
    DATEADD(DAY, ABS(CHECKSUM(NEWID())) % 365, &#039;2025-01-01&#039;), 
    ABS(CHECKSUM(NEWID())) % 100
FROM sys.all_columns a CROSS JOIN sys.all_columns b;

INSERT INTO Inventory (ArticleID, ValidityDate, Quantity)
SELECT TOP 10000 
    3, 
    DATEADD(DAY, ABS(CHECKSUM(NEWID())) % 365, &#039;2025-01-01&#039;), 
    ABS(CHECKSUM(NEWID())) % 100
FROM sys.all_columns a CROSS JOIN sys.all_columns b;

INSERT INTO Inventory (ArticleID, ValidityDate, Quantity)
SELECT TOP 50000 
    4, 
    DATEADD(DAY, ABS(CHECKSUM(NEWID())) % 365, &#039;2025-01-01&#039;), 
    ABS(CHECKSUM(NEWID())) % 100
FROM sys.all_columns a CROSS JOIN sys.all_columns b;

INSERT INTO Inventory (ArticleID, ValidityDate, Quantity)
SELECT TOP 10 
    1, 
    &#039;2026-12-31&#039;, 
    5
FROM sys.all_columns;
GO
</pre></div>


<p class="wp-block-paragraph">We are also including a few indexes to simulate a real-world use case and to support our queries, ensuring we get realistic execution plans:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
CREATE NONCLUSTERED INDEX IDX_INV_QUANT ON &#x5B;dbo].&#x5B;Inventory] (&#x5B;Quantity]) include (ArticleID)

CREATE NONCLUSTERED INDEX IDX_INV_VALIDITY on &#x5B;dbo].&#x5B;Inventory] (&#x5B;ValidityDate]) include (ArticleID)

CREATE NONCLUSTERED INDEX IDX_INV_ART on &#x5B;dbo].&#x5B;Inventory] (ArticleID)
</pre></div>


<h2 class="wp-block-heading" id="h-what-exactly-is-a-row-goal">What exactly is a Row Goal?</h2>



<p class="wp-block-paragraph">Normally, the SQL Server optimizer seeks to minimize the total cost of processing <strong>all data</strong> for a query. However, if it knows that you only need a specific number of rows (for example, via a <code><a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/top-transact-sql?view=sql-server-ver17">TOP</a></code>, <code><a href="https://www.sqlshack.com/explore-sql-queries-hint-option-fast-n/">FAST(N)</a></code>, or <a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/exists-transact-sql?view=sql-server-ver17"><code>EXISTS</code> </a>clause), it changes its strategy.</p>



<p class="wp-block-paragraph">The <strong>Row Goal</strong> is this specific row target that pushes the optimizer to favor a plan capable of delivering the first few rows as quickly as possible, even if that same plan would be catastrophic for processing the entire table.</p>



<h2 class="wp-block-heading" id="h-top-n-hunting-for-the-best-pierogi">TOP(N): Hunting for the best Pierogi</h2>



<p class="wp-block-paragraph">To illustrate the definition above, let’s search for the pierogis with the furthest expiration dates. <br><span style="text-decoration: underline">Note that the IDX_INV_VALIDITY index supports this query</span>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SELECT 
    A.ArticleName, 
    A.Price, 
    I.ValidityDate
FROM Articles A
INNER JOIN Inventory I ON A.ArticleID = I.ArticleID
WHERE A.ArticleName = &#039;Pierogi&#039;
order by I.ValidityDate desc;

SELECT top 10
    A.ArticleName, 
    A.Price, 
    I.ValidityDate
FROM Articles A
INNER JOIN Inventory I ON A.ArticleID = I.ArticleID
WHERE A.ArticleName = &#039;Pierogi&#039;
order by I.ValidityDate desc
</pre></div>


<p class="wp-block-paragraph">The difference between these two queries is that one requests only the first 10 rows, while the other requests all matching rows. However, this simple distinction is not merely applied when displaying the results; this condition is pushed deeper into the execution plan to influence the choice of operators (Nested Loop, Hash Join, Merge Join) further down the tree.</p>



<p class="wp-block-paragraph">For the first query, here is the resulting plan:</p>



<figure class="wp-block-image size-full"><img decoding="async" width="762" height="342" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-61.png" alt="" class="wp-image-44517" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-61.png 762w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-61-300x135.png 300w" sizes="(max-width: 762px) 100vw, 762px" /></figure>



<p class="wp-block-paragraph">As we can see, the optimizer chose a <strong>Hash Join</strong> given the volume of data to be joined. A <strong>Hash Match</strong> implies that all the data must be read in order to produce the desired result.</p>



<p class="wp-block-paragraph">For the second query, here is the execution plan:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="435" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-64-1024x435.png" alt="" class="wp-image-44521" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-64-1024x435.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-64-300x127.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-64-768x326.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-64.png 1239w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">We can see that this time, the optimizer chose a <strong>Nested Loop</strong>, which takes each row from the reference table (Inventory) and joins them with the Articles table. This operation can be very time-consuming if a large number of rows must be processed. However, this is where <strong>EstimateRowsWithoutRowGoal</strong> comes into play. The value of this property is <strong>40&#8217;002.5</strong>; this means that in a case where a subset of rows was <strong>not </strong>specifically required, the optimizer would have estimated the number of rows returned by this operator at that value. We can see, however, that the estimation actually used is <strong>10 rows</strong> for one execution, a value clearly derived from the <code>TOP(10)</code>.</p>



<p class="wp-block-paragraph">In summary, adding the <code>TOP(10)</code> allowed the optimizer to use a less expensive join for a small amount of data, even though the <code>TOP</code> operator is located at the very end of the execution plan (since a plan is read from right to left).</p>



<h2 class="wp-block-heading" id="h-exists-the-search-for-the-first-match">EXISTS: The search for the first match</h2>



<p class="wp-block-paragraph">As explained previously, the <code>EXISTS</code> clause has a cardinality of <strong>1</strong> because the very first row meeting the internal condition is enough to validate the case. This triggers a <strong>Row Goal</strong>, as the optimizer must estimate how many rows it will need to read to satisfy (or not) this condition.</p>



<p class="wp-block-paragraph"><em>Note: In cases where the condition is never met, the optimizer’s plan can become highly inefficient; for full details, see Erik Darling&#8217;s blog <a href="https://erikdarling.com/learn-t-sql-with-erik-exists-not-exists-and-row-goals/" id="https://erikdarling.com/learn-t-sql-with-erik-exists-not-exists-and-row-goals/">[here]</a>.</em></p>



<p class="wp-block-paragraph">We will now observe this behavior with the following query, varying the internal condition of the <code>EXISTS</code> clause by testing one highly selective (discriminant) case and another much less so.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SELECT 
    A.ArticleName, 
    A.Price
FROM Articles A
WHERE not EXISTS (
    SELECT 1/0
    FROM Inventory I 
    WHERE I.ArticleID = A.ArticleID 
    AND I.Quantity &gt; 10 -- vs 98
);
</pre></div>


<p class="wp-block-paragraph">As you may have noticed, I am looking here for products that maintain a certain quantity for every possible consumption date. My goal, of course, is to avoid depleting the stocks of these excellent Polish pierogis so that everyone can enjoy them!</p>



<p class="wp-block-paragraph">The case where we want to ensure that all existing quantities for an item are greater than 10 is very difficult to satisfy; based on the statistics available to the optimizer, all items have 10 or more units in stock, except for the pierogis! <br>Since this condition is so widespread, the optimizer knows it will have to scan a large number of rows to find a single case where the condition is <em>not</em> met. This is why it opts for a <strong>Scan</strong>. This behavior is evidenced by the estimated number of rows to be read (<strong>160&#8217;010</strong>, which represents the entire table).</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="517" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-77-1024x517.png" alt="" class="wp-image-44550" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-77-1024x517.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-77-300x152.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-77-768x388.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-77.png 1156w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">On the other hand, for a very restrictive condition <code>(quantity &gt; 98)</code>, the optimizer recognizes that this condition is highly selective. This is why it favors a <strong>Nested Loop</strong>, estimating that only <strong>1&#8217;608 rows</strong> will be necessary to prove the non-existence of the condition.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="411" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-78-1024x411.png" alt="" class="wp-image-44551" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-78-1024x411.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-78-300x120.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-78-768x308.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-78.png 1151w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">In summary, <code>EXISTS</code> forces the optimizer to estimate the number of rows required to find a single occurrence that proves whether a condition is met or not, thereby triggering a local optimization of the execution plan.</p>



<h2 class="wp-block-heading" id="h-option-fast-n-manually-steering-the-engine">OPTION(FAST N): Manually steering the engine</h2>



<p class="wp-block-paragraph">The <code>OPTION(FAST N)</code> hint allows you to manually introduce the Row Goal concept into a query. This hint does not limit the total number of results returned; instead, it optimizes the execution plan to retrieve the first <strong>N</strong> rows as quickly as possible (potentially at the expense of performance for the remaining rows).</p>



<p class="wp-block-paragraph">In our example below, we have two identical queries retrieving items with a quantity greater than 10. However, the second one uses an execution plan optimized to return the first row as fast as possible (just to make sure no one steals the last available pierogi from the top of the pile!).</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
select * from Inventory i
where i.Quantity &gt; 10 
order by i.ArticleID

select * from Inventory i
where i.Quantity &gt; 10 
order by i.ArticleID option(fast 1)
</pre></div>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="575" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-79-1024x575.png" alt="" class="wp-image-44553" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-79-1024x575.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-79-300x168.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-79-768x431.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-79.png 1038w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Once again, the plans diverge. To retrieve a single row, the <code>IDX_INV_ART</code> index (which already contains sorted <code>ArticleIDs</code>) is used. It performs a <strong>Seek</strong> on the smallest <code>ArticleID</code> to check if it satisfies the condition of having a quantity greater than 10.</p>



<p class="wp-block-paragraph">However, by enabling <code>SET STATISTICS TIME ON</code>, we can see that the second execution plan is slower than the first<span style="text-decoration: underline"> when returning all requested rows</span> (<strong>250ms vs. 204ms</strong>). While the gap is not massive due to the small table size, the difference is nonetheless observable.</p>



<h2 class="wp-block-heading" id="h-wrapping-up-and-how-to-survive-the-row-goal-gamble">Wrapping up and how to survive the Row Goal gamble</h2>



<p class="wp-block-paragraph">To conclude, the <strong>Row Goal</strong> is a double-edged sword; brilliant when you only need a quick glimpse of your data, but it can become a real performance trap if the optimizer&#8217;s &#8220;bet&#8221; fails.</p>



<p class="wp-block-paragraph">Fortunately, if you find that SQL Server is making bad decisions by being too optimistic, you can take back control. By using the hint <code>OPTION (USE HINT ('DISABLE_OPTIMIZER_ROWGOAL'))</code>, you force the optimizer to stop daydreaming and focus on the actual cost of the query. It&#8217;s the ultimate tool to ensure your execution plan doesn&#8217;t end up as messy as a dropped plate of pierogis!</p>
<p>L’article <a href="https://www.dbi-services.com/blog/how-row-goal-shapes-your-sql-server-query-strategy-by-hunting-for-pierogis/">How Row Goal shapes your SQL Server query strategy by hunting for pierogis</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/how-row-goal-shapes-your-sql-server-query-strategy-by-hunting-for-pierogis/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server 2025 In-Memory: New Cleanup Features &#038; SQLBits 2026 Insights</title>
		<link>https://www.dbi-services.com/blog/in-memory-tables-in-sql-server-2025-and-sqlbits-2026/</link>
					<comments>https://www.dbi-services.com/blog/in-memory-tables-in-sql-server-2025-and-sqlbits-2026/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Sun, 10 May 2026 19:29:35 +0000</pubDate>
				<category><![CDATA[Development & Performance]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[inmemory]]></category>
		<category><![CDATA[SQLBits]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44077</guid>

					<description><![CDATA[<p>SQL Server 2025 finally allows dropping In-Memory filegroups, a breakthrough analyzed here alongside expert performance insights from SQLBits 2026.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/in-memory-tables-in-sql-server-2025-and-sqlbits-2026/">SQL Server 2025 In-Memory: New Cleanup Features &amp; SQLBits 2026 Insights</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large is-style-rounded"><img loading="lazy" decoding="async" width="1024" height="384" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-18-1024x384.png" alt="" class="wp-image-44196" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-18-1024x384.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-18-300x113.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-18-768x288.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-18-1536x576.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-18.png 1920w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Summer is already around the corner, but it&#8217;s not too late for some spring cleaning!<br>If you manage SQL Server databases with <a href="https://learn.microsoft.com/en-us/sql/relational-databases/in-memory-oltp/introduction-to-memory-optimized-tables?view=sql-server-ver17" id="https://learn.microsoft.com/en-us/sql/relational-databases/in-memory-oltp/introduction-to-memory-optimized-tables?view=sql-server-ver17">In-Memory tables</a>, you may have already tried to delete a <code>MEMORY_OPTIMIZED_DATA</code> file or <code>FILEGROUP</code>, only to find that SQL Server simply won&#8217;t let you. <br>This limitation has existed since the debut of In-Memory with SQL Server 2014, and the only workaround until now was to recreate the database from scratch.<br>It is with SQL Server 2025 that Microsoft finally lifts this restriction. In this article, we will analyze these behavioral differences before and after this version. <br>To conclude, we will draw on the key points presented by <a href="https://www.linkedin.com/in/thodoris-katsimanis-057a5711a/" id="https://www.linkedin.com/in/thodoris-katsimanis-057a5711a/">Thodoris Katsimanis</a>, DBA Team Technology Manager at Kaizen Gaming, during his session at <a href="https://sqlbits.com/" id="https://sqlbits.com/">SQLBits 2026</a> on In-Memory tables, in order to summarize the challenges and benefits this feature can bring to production.</p>



<h2 class="wp-block-heading" id="h-the-legacy-struggle-in-memory-limitations-from-2014-to-2022">The Legacy Struggle: In-Memory Limitations from 2014 to 2022</h2>



<p class="wp-block-paragraph">To demonstrate this difference in behavior, we will create a database under SQL Server 2022 with an In-Memory table loaded with data, and then attempt to delete the associated files and FILEGROUP:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
DECLARE @DataPath NVARCHAR(512) = &#039;&lt;YOUR_DATA_FOLDER&gt;&#039;;
DECLARE @LogPath  NVARCHAR(512) = &#039;&lt;YOUR_LOG_FOLDER&gt;&#039;;
DECLARE @SQL      NVARCHAR(MAX);

SET @SQL = N&#039;
CREATE DATABASE TestInMemory
ON PRIMARY (
    NAME = TestInMemory_data,
    FILENAME = &#039;&#039;&#039; + @DataPath + N&#039;TestInMemory.mdf&#039;&#039;
),
FILEGROUP XTP_FG CONTAINS MEMORY_OPTIMIZED_DATA (
    NAME = TestInMemory_XTP,
    FILENAME = &#039;&#039;&#039; + @DataPath + N&#039;TestInMemory_XTP&#039;&#039;
)
LOG ON (
    NAME = TestInMemory_log,
    FILENAME = &#039;&#039;&#039; + @LogPath + N&#039;TestInMemory_log.ldf&#039;&#039;
);&#039;;

EXEC sp_executesql @SQL;

USE TestInMemory;
GO

CREATE TABLE dbo.TestTable
(
    ID    INT          NOT NULL,
    Val   NVARCHAR(50) NOT NULL,
    CONSTRAINT PK_TestTable PRIMARY KEY NONCLUSTERED HASH (ID)
        WITH (BUCKET_COUNT = 1024)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
GO

INSERT INTO dbo.TestTable VALUES (1, &#039;Hello&#039;), (2, &#039;World&#039;);
GO
</pre></div>


<p class="wp-block-paragraph">Once our table is loaded (to ensure the table exists and is not just metadata), we can then delete it:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
DROP TABLE dbo.TestTable;
GO

SELECT name, type_desc 
FROM sys.tables 
WHERE is_memory_optimized = 1;
</pre></div>


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



<p class="wp-block-paragraph">The verification clearly shows that no more In-Memory objects exist. We can therefore proceed with the famous cleanup of the files linked to the table we deleted:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
ALTER DATABASE TestInMemory 
REMOVE FILE TestInMemory_XTP;
GO

ALTER DATABASE TestInMemory
REMOVE FILEGROUP XTP_FG;
GO
</pre></div>


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



<p class="wp-block-paragraph">And here is the famous error, impossible to bypass it and sort things out.<br><strong>Note:</strong> This cleanup challenge specifically affects tables using DURABILITY = SCHEMA_AND_DATA, as they are the only ones where data persists within physical files on disk.</p>



<h2 class="wp-block-heading" id="h-sql-server-2025-breaking-the-in-memory-cleanup-barrier">SQL Server 2025: Breaking the In-Memory Cleanup Barrier</h2>



<p class="wp-block-paragraph">SQL Server 2025 does not just lift the restriction: it also introduces a new DMV, <code><a href="https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-db-xtp-undeploy-status?view=sql-server-ver17">sys.dm_db_xtp_undeploy_status</a></code>, which exposes the precise reason why the deletion is not yet possible. <br>By querying it at the same stage as our previous example, here is what it returns:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
USE TestInMemory;
GO

SELECT
    deployment_state,
    deployment_state_desc,
    undeploy_lsn,
    start_of_log_lsn
FROM sys.dm_db_xtp_undeploy_status;
GO
</pre></div>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="285" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-13-1024x285.png" alt="" class="wp-image-44080" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-13-1024x285.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-13-300x84.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-13-768x214.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-13.png 1055w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Now we have a clear reason: the <code>start_of_log_lsn</code> is too old, which prevents SQL Server from releasing the FILEGROUP. To resolve this, the LSNs must be advanced. A FULL backup is first required to initialize the backup chain, followed by a LOG backup to effectively advance the position in the logs:</p>


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

BACKUP DATABASE TestInMemory TO DISK = &#039;NUL&#039;;
GO

BACKUP LOG TestInMemory TO DISK = &#039;NUL&#039;;
GO
</pre></div>


<p class="wp-block-paragraph">Once the LOG backup is executed and the LSNs are sufficiently advanced, the files can finally be deleted. The <code>sys.dm_db_xtp_undeploy_status</code> view confirms that the XTP engine is no longer deployed and that the cleanup has been successfully performed.</p>



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



<p class="wp-block-paragraph">SQL Server 2025 not only introduces the ability to purge empty files that were linked to an In-Memory object but also the ability to troubleshoot their deletion!</p>



<h2 class="wp-block-heading" id="h-from-milliseconds-to-microseconds-thodoris-katsimanis-at-sqlbits-2026">From Milliseconds to Microseconds: Thodoris Katsimanis at SQLBits 2026</h2>



<p class="wp-block-paragraph">To conclude this article, let&#8217;s look back at the key points covered by Thodoris Katsimanis during his session at SQLBits 2026, entitled <em>&#8220;Revolutionizing Database Performance: Deep Dive into SQL InMemory Technology&#8221;</em>.<br>His context is particularly telling: at Kaizen Gaming, SQL Server databases handle thousands of transactions per second in real time, in an environment where every millisecond has a direct impact on the user experience. It is precisely in this type of workload that In-Memory tables reveal their full potential.</p>



<h3 class="wp-block-heading" id="h-eliminating-page-contention-with-latch-free-architecture">Eliminating Page Contention with Latch-Free Architecture</h3>



<p class="wp-block-paragraph">The presentation exposed limitations of the SQL Server engine: in a high-performance system, latches on disk pages (<code><a href="https://www.sqlskills.com/help/waits/pagelatch_ex/" id="https://www.sqlskills.com/help/waits/pagelatch_ex/">PAGELATCH_EX</a></code>) create bottlenecks that can lead to Thread Pool exhaustion. The In-Memory architecture solves this problem at its root via a latch-free structure. By relying on optimistic concurrency control and multi-versioning (<a href="https://www.geeksforgeeks.org/dbms/what-is-multi-version-concurrency-control-mvcc-in-dbms/" id="https://www.geeksforgeeks.org/dbms/what-is-multi-version-concurrency-control-mvcc-in-dbms/">MVCC</a>), SQL Server no longer waits for locks. Each row has a Begin-Timestamp and an End-Timestamp, allowing transactions to read the valid version of the data without blocking writes.</p>



<h3 class="wp-block-heading" id="h-maximizing-performance-the-crucial-choice-between-hash-and-bw-tree">Maximizing Performance: The Crucial Choice between Hash and BW-Tree</h3>



<p class="wp-block-paragraph">The choice between a Hash index and a Nonclustered index is crucial. The Hash index is perfectly suited for Point Lookups (searches on an exact value): it points directly to the memory address via a hash function. Conversely, the Nonclustered index relies on a BW-Tree structure, which is essential for range scans and sorting, where Hash is of little use. To learn more about indexes for In-Memory tables, check the <a href="https://learn.microsoft.com/en-us/sql/relational-databases/in-memory-oltp/indexes-for-memory-optimized-tables?view=sql-server-ver17" id="https://learn.microsoft.com/en-us/sql/relational-databases/in-memory-oltp/indexes-for-memory-optimized-tables?view=sql-server-ver17">Microsoft&#8217;s documentation</a>.</p>



<h3 class="wp-block-heading" id="h-the-critical-impact-of-bucket-count-misconfiguration">The Critical Impact of BUCKET_COUNT Misconfiguration</h3>



<p class="wp-block-paragraph">As Thodoris points out, the success of a Hash index relies on tuning the <code>BUCKET_COUNT</code>. This parameter defines the number of entry points in the index. If it is too low, the system generates collision chains: multiple values end up in the same bucket, forcing the engine to scan a linked list, which degrades performance. If it is too high, it consumes memory unnecessarily. Thodoris also highlights a crucial observation: using a Nonclustered index for an equality search can consume significantly more memory than a properly sized Hash index.</p>



<h2 class="wp-block-heading" id="h-final-thoughts-embracing-the-in-memory-revolution">Final Thoughts: Embracing the In-Memory Revolution</h2>



<p class="wp-block-paragraph">SQL Server 2025 finally lifts a limitation that has hampered the lifecycle management of In-Memory databases for over ten years. Being able to cleanly delete associated files and FILEGROUPs, understanding why the engine blocks this operation thanks to <code>sys.dm_db_xtp_undeploy_status</code>, and having a clear procedure to remedy it: this is concrete progress for everyone operating this technology in production.</p>



<p class="wp-block-paragraph">Thodoris Katsimanis&#8217;s session at SQLBits 2026 reminds us that the care given to maintenance and monitoring matters just as much as the initial design. In-Memory tables are not a simple performance lever to be activated and forgotten: they require a mastery of their internal mechanisms, thread management to eliminate contention, and rigorous sizing of the <code>BUCKET_COUNT</code>. As he summarizes: the millisecond is no longer a sufficient unit of measurement. In-Memory OLTP aims for the microsecond and in hyper-transactional environments, that is precisely what makes the difference.</p>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/in-memory-tables-in-sql-server-2025-and-sqlbits-2026/">SQL Server 2025 In-Memory: New Cleanup Features &amp; SQLBits 2026 Insights</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/in-memory-tables-in-sql-server-2025-and-sqlbits-2026/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Beyond TDE and TLS: Bridging the Data Security Governance Gap in Lower Environments</title>
		<link>https://www.dbi-services.com/blog/tde-tls-data-security-governance-gap-in-lower-environments/</link>
					<comments>https://www.dbi-services.com/blog/tde-tls-data-security-governance-gap-in-lower-environments/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Mon, 27 Apr 2026 20:21:34 +0000</pubDate>
				<category><![CDATA[Delphix]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[delphix]]></category>
		<category><![CDATA[encryption]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=43973</guid>

					<description><![CDATA[<p>Why one layer is never enough, why dev environments are your biggest GDPR gap, and how to industrialize their governance.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/tde-tls-data-security-governance-gap-in-lower-environments/">Beyond TDE and TLS: Bridging the Data Security Governance Gap in Lower Environments</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full is-style-rounded"><img loading="lazy" decoding="async" width="1408" height="768" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/Gemini_Generated_Image_8vt09b8vt09b8vt0.png" alt="Conceptual diagram of a secure data pipeline showing production data passing through a governance engine to anonymized dev and staging environments." class="wp-image-44034" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/Gemini_Generated_Image_8vt09b8vt09b8vt0.png 1408w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/Gemini_Generated_Image_8vt09b8vt09b8vt0-300x164.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/Gemini_Generated_Image_8vt09b8vt09b8vt0-1024x559.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/Gemini_Generated_Image_8vt09b8vt09b8vt0-768x419.png 768w" sizes="auto, (max-width: 1408px) 100vw, 1408px" /></figure>



<h2 class="wp-block-heading" id="h-the-multi-layered-threat-why-one-tool-is-never-enough">The Multi-Layered Threat: Why One Tool is Never Enough</h2>



<p class="wp-block-paragraph">We’ve all left the key in our bike lock at least once. This simple human oversight makes the heaviest chain irrelevant and we often see the exact same logic applied to data environments. Most organizations spend months hardening their production core but leave the keys in the locks of the <em>dev</em> and <em>staging </em>systems that sit right next to it. </p>



<p class="wp-block-paragraph">The numbers back this up. While 91% of organizations are concerned about their exposure across lower environments, a staggering <span style="text-decoration: underline"><strong>86%</strong> <strong>still allow data compliance exceptions in non-production</strong></span>. This gap between concern and action has real consequences: more than half of these organizations have already experienced a breach or audit failure in their testing and development systems (<a href="https://www.prnewswire.com/news-releases/delphixs-state-of-data-compliance-and-security-report-reveals-54-of-organizations-have-experienced-data-breaches-or-theft-in-non-production-environments-302225897.html">PR Newswire</a>).</p>



<p class="wp-block-paragraph">Effective security is rarely a single-layer problem. Between the stolen backup that lands in the wrong hands, the analyst running a <code>SELECT</code> on a table they probably shouldn&#8217;t see, and the packet quietly crossing an unsecured network segment, <a href="https://www.fortinet.com/resources/cyberglossary/attack-surface">the attack surface</a> is wide, and no single mechanism covers it all.</p>



<p class="wp-block-paragraph">Transport Layer Security (TLS), Transparent Data Encryption (TDE), symmetric encryption, dynamic masking, row-level security, data anonymization: for most RDBMS, the options exist and they work. Most teams already have access to at least one of them. The real challenge isn&#8217;t finding a solution; it&#8217;s understanding what each one actually protects, where it breaks down, and whether it survives contact with a production environment.</p>



<h2 class="wp-block-heading" id="h-shadow-environments-the-weakest-link-in-your-data-chain">Shadow Environments: The Weakest Link in Your Data Chain</h2>



<p class="wp-block-paragraph">Here is the uncomfortable truth: non-production environments are often where security policies are quietly buried. It starts with a backup restored without encryption, or real customer data seeding a dev database <em>&#8220;just for a quick test</em>&#8220;.</p>



<p class="wp-block-paragraph">The fundamental problem is that most protections assume a controlled environment. Encryption can be bypassed by someone with the right credentials. Masking can be misconfigured. Row-level security doesn&#8217;t help much when the whole database is sitting on a developer&#8217;s laptop.</p>



<h2 class="wp-block-heading" id="h-technical-trade-offs-finding-your-strategic-fit">Technical Trade-offs: Finding Your Strategic Fit</h2>



<p class="wp-block-paragraph">To make this reasoning concrete, the table below maps six core techniques against the operational criteria that define their success. The goal isn&#8217;t to pick a favorite tool, but to identify which combination actually addresses your specific vulnerabilities.</p>



<figure class="wp-block-table alignfull is-style-stripes"><table><tbody><tr><td class="has-text-align-center" data-align="center"></td><td class="has-text-align-center" data-align="center">Physical File Theft</td><td class="has-text-align-center" data-align="center">Read Access (SELECT)</td><td class="has-text-align-center" data-align="center">Network Sniffing</td><td class="has-text-align-center" data-align="center">Performance Impact</td><td class="has-text-align-center" data-align="center">Granularity</td><td class="has-text-align-center" data-align="center">Applicable in Prod <br>(live data)</td><td class="has-text-align-center" data-align="center">Applicable in DEV</td></tr><tr><td class="has-text-align-center" data-align="center">TLS</td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center">Data packet</td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td></tr><tr><td class="has-text-align-center" data-align="center">TDE</td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center">Column<br>Tablespace<br>Datafile</td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td></tr><tr><td class="has-text-align-center" data-align="center">Symmetric encryption (applicative)</td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-center" data-align="center">Field<br>Value</td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td></tr><tr><td class="has-text-align-center" data-align="center">Dynamic Masking</td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center">Column</td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td></tr><tr><td class="has-text-align-center" data-align="center">Row-level security</td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center">Row</td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td></tr><tr><td class="has-text-align-center" data-align="center">Data anonymization</td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td><td class="has-text-align-center" data-align="center">Field<br>Column</td><td class="has-text-align-center" data-align="center"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-center" data-align="center"><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;" /></td></tr></tbody></table></figure>



<ul class="wp-block-list">
<li><a href="https://en.wikipedia.org/wiki/Transport_Layer_Security">TLS </a>protects data in motion. The moment a packet leaves a server, TLS ensures anyone intercepting it sees encrypted noise. What it doesn&#8217;t do is equally important: it has no opinion about who queries your database or what&#8217;s stored on disk. Once the data arrives, TLS&#8217;s job is done.<br>TLS is now the industry standard for securing data in motion.<br><em>(SQL Server technical blog about TLS <a href="https://www.dbi-services.com/blog/sql-server-how-to-see-your-enable-security-protocols-tls-ssl-dtls-with-a-tsql-query/">here</a>)</em></li>



<li><a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/asoag/introduction-to-transparent-data-encryption.html">TDE </a>encrypts the physical files that make up your database (data files, log files, backups), so that anyone who gets their hands on them without the encryption key can&#8217;t read them. The performance impact is a negligible overhead; in fact, Microsoft for example enables TDE <strong>by default</strong> for all its cloud-based databases.<br><em>(PostgreSQL technical blog about TDE <a href="https://www.dbi-services.com/blog/commercial-postgresql-distributions-with-tde-1-fujitsu-enterprise-postgres-1-setup/">here</a>)</em><br>However, deploying TDE in development is a security best practice, but it quickly becomes an operational nightmare for environment refreshes, especially if you want to use distinct certificates to avoid leaking production secrets into lower environments.</li>



<li><a href="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-database-engine?view=sql-server-ver17">Symmetric encryption</a> is field-level encryption applied directly in the application layer. Unlike TDE, it survives a legitimate SELECT; even a user with full read access sees <a href="https://en.wikipedia.org/wiki/Ciphertext">ciphertext </a>unless they hold the applicative key. The tradeoff is performance: encrypting and decrypting at scale adds up quickly.<br><em>(MongoDB technical blog about Client-side Field Level Encryption <a href="https://www.geeksforgeeks.org/mongodb/mongodb-client-side-field-level-encryption/">here</a>)</em></li>



<li><a href="https://learn.microsoft.com/en-us/sql/relational-databases/security/dynamic-data-masking?view=sql-server-ver17">Dynamic masking</a> doesn&#8217;t encrypt anything. It intercepts query results and replaces sensitive values with masked equivalents based on the user&#8217;s role. Fast, lightweight, zero application changes required. The catch: it only controls what&#8217;s displayed, not what&#8217;s stored. A user with sufficient privileges can bypass it entirely.<br><em>(SQL Server technical blog about dynamic masking <a href="https://www.mssqltips.com/sqlservertip/7887/dynamic-data-masking-in-sql-server-for-sensitive-data-protection/">here</a>)</em></li>



<li><a href="https://database.guide/understanding-row-level-security-rls/">Row-Level Security </a>enforces access at the row level directly inside the database engine. Users see only the rows they&#8217;re allowed to see, regardless of how the query is written. No application changes, no trust placed in the calling layer. The policy lives in the database and applies universally.<br><em>(Oracle technical blog about Virtual Private Database <a href="https://www.dbi-services.com/blog/oracle-virtual-private-database/">here</a>)</em></li>



<li><a href="https://en.wikipedia.org/wiki/Data_anonymization">Data anonymization</a> doesn&#8217;t protect sensitive data, <strong><span style="text-decoration: underline">it eliminates it</span></strong>. Real values are replaced with realistic but fictional equivalents (<a href="https://www.ibm.com/think/topics/synthetic-data">synthetic data</a>), permanently and irreversibly. No encryption key to steal, no masking rule to bypass. Whatever leaks simply isn&#8217;t sensitive anymore. This is why anonymization is the only control that makes unconditional sense in non-production environments. A stolen backup, a misconfigured SELECT, a sniffed packet: <strong>none</strong> of it matters if the data was anonymized before it ever reached a staging environment. We covered how to implement it in practice in <a href="https://www.dbi-services.com/blog/data-anonymization-as-a-service-with-delphix-continuous-compliance/" id="https://www.dbi-services.com/blog/data-anonymization-as-a-service-with-delphix-continuous-compliance/">a previous post</a></li>
</ul>



<h2 class="wp-block-heading" id="h-ownership-gaps-the-security-no-man-s-land">Ownership Gaps: The Security No Man&#8217;s Land</h2>



<p class="wp-block-paragraph">We are shifting from a technical challenge to a human and organizational one. The security landscape moves so fast that the struggle of mastering every layer has become overwhelming.</p>



<p class="wp-block-paragraph">This complexity is where governance goes to die. Infrastructure teams build the walls, developers write the code, and DBAs manage the house, but the accountability for the data itself often falls through the cracks. The most dangerous gap isn&#8217;t a missing feature; it’s the absence of a governance model strong enough to stop the game of hot potato and force a cross-domain ownership of security.</p>



<p class="wp-block-paragraph">The CISO&#8217;s role in this landscape is not to master every technical layer, it is to force the question of ownership into the open. Who signs off on what data enters a non-production environment? Who is accountable when a dev database is restored without encryption? Who audits that masking policies are still effective after a release?</p>



<p class="wp-block-paragraph">Without explicit answers to these questions, security becomes a game of assumptions. Every team assumes another layer is holding. And the gaps compound silently, until they don&#8217;t.</p>



<h2 class="wp-block-heading">From Handcrafted Scripts to Enterprise Platforms</h2>



<p class="wp-block-paragraph">Every technique in this table can be implemented on a spectrum, from a carefully written script to a fully automated enterprise solution. The right choice depends on your scale, your team, and how much operational overhead you can realistically absorb.</p>



<ul class="wp-block-list">
<li><strong>TLS certificate deployment:</strong> you can generate and rotate certificates manually, instance by instance. Or you can automate the entire lifecycle using <strong><span style="text-decoration: underline">Ansible</span></strong> against an internal PKI with a consistent and auditable way that is invisible to the teams consuming it. The security outcome is identical; the operational cost is not.</li>



<li><strong>Data anonymization:</strong> a custom script that detects <a href="https://www.cloudflare.com/learning/privacy/what-is-pii/">PII </a>columns and replaces values with masked data works well at small scale. The challenge appears when your data spans multiple database engines (SQL Server, Oracle, PostgreSQL, &#8230;) and when anonymized values need to remain consistent across foreign keys and referential constraints. Replacing a customer ID in one table while leaving it intact in another isn&#8217;t anonymization, it&#8217;s a GDPR incident waiting to happen. Solutions like <a href="https://help.delphix.com/cc/current/content/continuous_compliance_home.htm">Delphix Continuous Compliance</a> handle cross-DBMS consistency, constraint awareness, and sensitive field detection out of the box, turning a fragile hand-rolled process into a governed, repeatable and auditable one.</li>



<li><strong>Dynamic masking and row-level security:</strong> defining a handful of policies manually in SSMS is perfectly reasonable for a contained environment. Automating policy deployment across environments and instances is a different challenge entirely. It is a level of scale where ad-hoc scripts quickly become a liability.</li>
</ul>



<h2 class="wp-block-heading" id="h-conclusion-moving-beyond-security-by-accident">Conclusion: Moving Beyond Security by Accident</h2>



<p class="wp-block-paragraph">Security is not a one-time project. It is an operational discipline that requires the same rigor in a developer&#8217;s sandbox as it does in production, and that rigor has to be enforced <span style="text-decoration: underline"><strong>by design</strong></span>, not by goodwill.</p>



<p class="wp-block-paragraph">Most breaches in non-production environments don&#8217;t happen because a tool failed. They happen because nobody owned the decision to use it in the first place.</p>



<p class="wp-block-paragraph">At <strong>dbi services</strong>, we help organizations move from fragile, handcrafted scripts to governed, auditable architectures across every environment, every database engine, and every team. </p>



<p class="wp-block-paragraph"><strong>Because under GDPR, <span style="text-decoration: underline">one</span> incident is all it takes to make ownership everyone&#8217;s problem.</strong></p>
<p>L’article <a href="https://www.dbi-services.com/blog/tde-tls-data-security-governance-gap-in-lower-environments/">Beyond TDE and TLS: Bridging the Data Security Governance Gap in Lower Environments</a> 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/tde-tls-data-security-governance-gap-in-lower-environments/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Scaling SSRS Migrations: Multi-Threaded Automation for PBIRS 2025</title>
		<link>https://www.dbi-services.com/blog/scaling-ssrs-migrations-multi-threaded-automation-for-pbirs-2025/</link>
					<comments>https://www.dbi-services.com/blog/scaling-ssrs-migrations-multi-threaded-automation-for-pbirs-2025/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Tue, 07 Apr 2026 12:58:14 +0000</pubDate>
				<category><![CDATA[Business Intelligence]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[PBIRS]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[SQLServer]]></category>
		<category><![CDATA[SSRS]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=43802</guid>

					<description><![CDATA[<p>Migrate SSRS to PBIRS 2025: a PowerShell ETL to automate extraction, XML patching, and parallelized deployment.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/scaling-ssrs-migrations-multi-threaded-automation-for-pbirs-2025/">Scaling SSRS Migrations: Multi-Threaded Automation for PBIRS 2025</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">Modernizing a reporting platform is a pivotal milestone for any BI infrastructure. Whether it’s a standard upgrade or a forced transition to <strong><a href="https://learn.microsoft.com/en-us/power-bi/report-server/download-powerbi-report-server">Power BI Report Server (PBIRS)</a></strong> following the decommissioning of SSRS in SQL Server 2025, the operation is critical. For the purposes of our lab, we will use an SSRS 2017 source, but the logic remains universal: regardless of the original version, the goal is to ensure the continuity of your decision-making services without sacrificing your mental health in the process.</p>



<p class="wp-block-paragraph">As my colleague Amine Haloui explained in <a href="https://www.dbi-services.com/blog/sql-server-2025-retirement-of-sql-server-reporting-services-ssrs/" id="https://www.dbi-services.com/blog/sql-server-2025-retirement-of-sql-server-reporting-services-ssrs/">a recent blog post</a>, several strategies exist for migrating an instance. The &#8220;Lift and Shift&#8221; method (restoring the <code>ReportServer</code> database onto a new instance) is often the most attractive on paper. However, the reality on the ground can be more temperamental.</p>



<p class="wp-block-paragraph">In some production environments, the target PBIRS instance already exists, hosts its own content, or follows specific configurations that prohibit simply overwriting its underlying <code>ReportServer</code> database. Therefore, we are proceeding here on the premise of a selective and granular migration: we must inject the SSRS catalog into an active PBIRS environment without burning everything to the ground in the process.</p>



<p class="wp-block-paragraph">When faced with inventories exceeding hundreds or even thousands of reports (RDL), folders, and datasources, a manual approach via the web interface is not an option and automation becomes a necessity.</p>



<p class="wp-block-paragraph">This article analyzes a systematic approach based on the <code>ReportingServicesTools</code> PowerShell module. The objective is to provide a robust methodology to extract your catalog and redeploy it intelligently, while managing the necessary reconfigurations along the way.</p>



<h2 class="wp-block-heading" id="h-phase-1-smart-dumping-building-the-local-staging-area">Phase 1: Smart Dumping – Building the Local Staging Area</h2>



<p class="wp-block-paragraph">To migrate cleanly, objects must first be isolated. The idea is not to blindly vacuum everything, but to target the critical folders of your SSRS instance and transform them into flat files (.rdl and .rds) within a local staging area. If your SSRS instance contains specific object types, the scripts can easily be adapted to include them as well.</p>



<p class="wp-block-paragraph">This is where the power of the <strong><a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/new-webserviceproxy?view=powershell-5.1">SOAP Proxy</a></strong> comes into play. Rather than multiplying slow HTTP calls, we use the native service interface to list and extract our components:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$sourceUrl  = &quot;http://your-ssrs-server/ReportServer&quot;
$exportRoot = &quot;H:\Migration_Dump&quot;

$proxySource = New-RsWebServiceProxy -ReportServerUri $sourceUrl
</pre></div>


<p class="wp-block-paragraph">In a production environment, SSRS folders are often a messy mix of reports, data sources, images, and sometimes obsolete semantic models. To maintain total control over what we export, we isolate the filtering logic.</p>



<p class="wp-block-paragraph">This <code>Get-AllItemsByType</code> function allows us to retrieve only what truly matters to us, based on the <strong>TypeName</strong> and <strong>file extension</strong> returned by the API.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
function Get-AllItemsByType {
    param(
        &#x5B;string]$CurrentPath,
        $Proxy,
        &#x5B;string]$TypeName 
    )
    try {
        return $Proxy.ListChildren($CurrentPath, $true) | Where-Object { $_.TypeName -eq $TypeName }
    } catch {
        Write-Host &quot;  &#x5B;!] Error on $CurrentPath : $($_.Exception.Message)&quot; -ForegroundColor Red
        return $null
    }
}
</pre></div>


<p class="wp-block-paragraph">This mapping between the file type and its extension must be defined upfront in a dictionary:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$extensionMap = @{
    &quot;Report&quot; = &quot;.rdl&quot;
    &quot;DataSource&quot; = &quot;.rds&quot;
}
</pre></div>


<p class="wp-block-paragraph">A crucial point in extracting SSRS objects is preserving their context. To ensure a seamless import into PBIRS 2025, we must recreate the exact folder hierarchy of the source server locally.</p>



<p class="wp-block-paragraph">The trick lies in transforming the SSRS path (formatted as <code>/Folder/SubFolder/Report</code>) into a valid Windows path, while simultaneously handling the extension mapping (<code>.rdl</code> for reports, <code>.rds</code> for DataSources).</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
function Export-SsrsItems {
    param(
        &#x5B;string]$RootPath,
        $Proxy,
        &#x5B;string]$TypeName,
        &#x5B;string]$ExportRoot
    )

    $items = Get-AllItemsByType -CurrentPath $RootPath -Proxy $Proxy -TypeName $TypeName

    foreach ($item in $items) {
        $relativeItemPath = $item.Path.TrimStart(&#039;/&#039;).Replace(&quot;/&quot;, &quot;\&quot;)
        $localFilePath    = Join-Path $ExportRoot $relativeItemPath
        $localDirectory   = Split-Path -Path $localFilePath -Parent

        if (-not (Test-Path $localDirectory)) {
            New-Item -ItemType Directory -Path $localDirectory -Force | Out-Null
        }

        Out-RsCatalogItem -Path $item.Path -Destination $localDirectory -Proxy $Proxy
    }
}
</pre></div>


<p class="wp-block-paragraph">By doing this, your <code>H:\Migration_Dump</code> becomes the exact mirror of your SSRS portal. This structural rigor is what will allow us, in the next step, to remap our data sources without having to hunt down which report belongs to which department.</p>



<div class="wp-block-columns no-bottom-margin is-layout-flex wp-container-core-columns-is-layout-8f761849 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="284" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-7-1024x284.png" alt="" class="wp-image-43818" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-7-1024x284.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-7-300x83.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-7-768x213.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-7.png 1145w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="887" height="255" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-6.png" alt="" class="wp-image-43817" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-6.png 887w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-6-300x86.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-6-768x221.png 768w" sizes="auto, (max-width: 887px) 100vw, 887px" /></figure>
</div>
</div>



<p class="wp-block-paragraph">Finally, we define the folders we wish to export along with the document types they contain (since a migration is often the perfect time for a bit of spring cleaning):</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$exportTasks = @(
    @{ Path = &quot;/Migration_Source_2&quot;; Types = @(&quot;Report&quot;) },
    @{ Path = &quot;/Data Sources&quot;;  Types = @(&quot;DataSource&quot;) }
)

Write-Host &quot;--- Selective Export Started ---&quot; -ForegroundColor Cyan

foreach ($task in $exportTasks) {
    foreach ($typeName in $task.Types) {
        $ext = $extensionMap&#x5B;$typeName]
        Export-SsrsItems `
            -RootPath   $task.Path `
            -Proxy      $proxySource `
            -TypeName   $typeName `
            -Extension  $ext `
            -ExportRoot $exportRoot
    }
}
</pre></div>

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


<h2 class="wp-block-heading" id="h-phase-2-data-source-patching-mass-xml-transformation">Phase 2: Data Source Patching – Mass XML Transformation</h2>



<p class="wp-block-paragraph">Once the extraction is complete, you have a local mirror of your source instance, but the data sources still point to the legacy infrastructure.</p>



<p class="wp-block-paragraph">Instead of manually fixing each connection after the import (the best way to miss half of them), we will apply an automated transformation directly to our local XML files. This allows us to update connection strings in bulk before a single report even hits the target server.</p>



<p class="wp-block-paragraph">The idea is simple: use PowerShell to inject the new SQL instance wherever necessary, ensuring a functional deployment from the very first second:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$allDataSources = Get-ChildItem -Path $exportRoot -Filter &quot;*.rds&quot; -Recurse

Write-Host &quot;&#x5B;&gt;] Datasources updated in : $exportRoot&quot; -ForegroundColor Yellow

foreach ($dsFile in $allDataSources) {
    &#x5B;xml]$xmlContent = Get-Content $dsFile.FullName
 
    $node = $xmlContent.SelectSingleNode(&quot;//ConnectString&quot;)
    
    if ($null -ne $node) {
        $oldValue = $node.&quot;#text&quot; 
        if ($null -eq $oldValue) { $oldValue = $node.InnerText }

        $newValue = $oldValue -replace &quot;OLD_REPORTING_INSTANCE&quot;, &quot;NEW_REPORTING_INSTANCE&quot;
        
        if ($oldValue -ne $newValue) {
            $node.InnerText = $newValue
            $xmlContent.Save($dsFile.FullName)
            Write-Host &quot;  &#x5B;v] ConnectString updated in : $($dsFile.Name)&quot; -ForegroundColor Green
        }
    } else {
        Write-Host &quot;  &#x5B;!] ConnectString not found in file $($dsFile.Name)&quot; -ForegroundColor Red
    }
}
</pre></div>


<p class="wp-block-paragraph">Moreover, since we are interacting directly with the file’s XML structure, this logic isn&#8217;t limited to connection strings: you can apply the same principle to automate changes for any XML property, from timeouts to provider names.</p>



<h2 class="wp-block-heading" id="h-phase-3-mass-deployment-rebuilding-the-reporting-portal">Phase 3: Mass Deployment – Rebuilding the Reporting Portal</h2>



<p class="wp-block-paragraph">At this stage, the operation is purely mechanical. We once again leverage the <strong>ReportingServicesTools</strong> module to recreate the folder structure and upload the <code>.rds</code> and <code>.rdl</code> files. By following this specific order, PBIRS will automatically restore the links between your reports and their newly patched data sources.</p>



<p class="wp-block-paragraph">It is worth noting that the script allows for importing into a specific root folder (defined by the <code>$destroot</code> variable). This is particularly useful if you want to isolate the migrated assets into a dedicated directory, such as <code>SSRS_Folder</code> to keep them distinct from the existing hierarchy. Furthermore, this script is designed with safety in mind: it cannot overwrite or delete anything. If a report with the same name already exists in the same location, the <code>-Overwrite:$false</code> argument prevents replacement, ensuring that the import process never destroys existing content.</p>



<p class="wp-block-paragraph">Here is the final block to complete your migration:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$destUrl   = &quot;http://your-pbirs-server/ReportServer&quot; 
$localDump = &quot;H:\Migration_Dump&quot;
$destRoot  = &quot;/&quot; #Start import in the root folder
$proxyDest = New-RsWebServiceProxy -ReportServerUri $destUrl

$extensionMap = @{
    &quot;Report&quot;     = &quot;.rdl&quot;
    &quot;DataSource&quot; = &quot;.rds&quot;
}

function Ensure-RsFolderBruteForce {
    param($fullFolderPath, $Proxy)
    $parts = $fullFolderPath.Split(&#039;/&#039;) | Where-Object { $_ -ne &#039;&#039; }
    $currentPath = &#039;&#039;
    
    foreach ($part in $parts) {
        $parent = if ($currentPath -eq &#039;&#039;) { &quot;/&quot; } else { $currentPath }
        $target = if ($currentPath -eq &#039;&#039;) { &quot;/$part&quot; } else { &quot;$currentPath/$part&quot; }
        
        try {
            $Proxy.CreateFolder($part, $parent, $null) | Out-Null
            Write-Host &quot;  &#x5B;DIR] Created : $target&quot; -ForegroundColor Cyan
        } catch {
            if ($_.Exception.Message -match &quot;AlreadyExists&quot;) {
                # Folder already exists but we continue
            } else {
                Write-Host &quot;  &#x5B;!] Error for folder $target : $($_.Exception.Message)&quot; -ForegroundColor Red
            }
        }
        $currentPath = $target
    }
}

function Import-SsrsItem {
    param(
        &#x5B;System.IO.FileInfo]$File,
        &#x5B;string]$LocalDump,
        &#x5B;string]$DestRoot,
        $Proxy
    )

    $relativeDir       = $File.DirectoryName.Replace($LocalDump, &#039;&#039;).Replace(&quot;\&quot;, &quot;/&quot;)
    $targetFolderPath  = ($DestRoot + $relativeDir).Replace(&quot;//&quot;, &quot;/&quot;)
    $fullItemPath      = ($targetFolderPath + &quot;/&quot; + $File.BaseName).Replace(&quot;//&quot;, &quot;/&quot;)

    Ensure-RsFolderBruteForce -fullFolderPath $targetFolderPath -Proxy $Proxy

    try {
        Write-RsCatalogItem -Path $File.FullName -Destination $targetFolderPath -Proxy $Proxy -Overwrite:$false
        Write-Host &quot;  &#x5B;DONE] Imported: $fullItemPath&quot; -ForegroundColor Green
    }
    catch {
        if ($_.Exception.Message -match &quot;already exists&quot;) {
            Write-Host &quot;  &#x5B;SKIP] Already created : $fullItemPath&quot; -ForegroundColor Gray
        } else {
            Write-Host &quot;  &#x5B;FAIL] Error $fullItemPath : $($_.Exception.Message)&quot; -ForegroundColor Red
        }
    }
}

$importOrder = @(&quot;DataSource&quot;, &quot;Report&quot;)

foreach ($typeName in $importOrder) {
    $extension = $extensionMap&#x5B;$typeName]
    Write-Host &quot;`n&#x5B;PASS] Import of object with type : $typeName ($extension)&quot; -ForegroundColor Magenta
    
    $filesToImport = Get-ChildItem -Path $localDump -Filter &quot;*$extension&quot; -Recurse

    if ($filesToImport.Count -eq 0) {
        Write-Host &quot;  &#x5B;i] No file with $extension found.&quot; -ForegroundColor Gray
        continue
    }

    foreach ($file in $filesToImport) {
        Import-SsrsItem -File $file -LocalDump $localDump -DestRoot $destRoot -Proxy $proxyDest
    }
}

Write-Host &quot;`nImport done!&quot; -ForegroundColor Green
</pre></div>


<p class="wp-block-paragraph">Importing via SOAP is more resource-intensive than extraction, as the server must validate every piece of metadata and physically recreate the path for each report. On large volumes, this stage can become a bottleneck (averaging ~1 second per report).</p>



<p class="wp-block-paragraph">To overcome this, we can parallelize the import by folder, creating multiple background jobs running on separate threads. Here is the general skeleton to implement this multi-threaded approach:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$maxJobs = 5 

foreach ($file in $filesToImport) {
    while ((Get-Job -State Running).Count -ge $maxJobs) {
        Start-Sleep -Milliseconds 500
    }

    Start-Job -Name &quot;Import_$($file.Name)&quot; -ScriptBlock {
        param($f, $url, $targetPath)

        $Proxy = New-RsWebServiceProxy -ReportServerUri $url
        
        try {
            Write-RsCatalogItem -Path $f.FullName -Destination $targetPath -Proxy $Proxy -Overwrite:$false
            return &quot;SUCCESS: $($f.Name)&quot;
        } catch {
            return &quot;ERROR: $($f.Name) -&gt; $($_.Exception.Message)&quot;
        }
    } -ArgumentList $file, $destUrl, $targetFolderPath
}
</pre></div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="520" height="330" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image.png" alt="" class="wp-image-43806" style="width:522px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image.png 520w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-300x190.png 300w" sizes="auto, (max-width: 520px) 100vw, 520px" /></figure>
</div>


<p class="wp-block-paragraph"><strong>Note :</strong> The <code><a href="https://www.powershelladmin.com/wiki/PowerShell_foreach_loops_and_ForEach-Object.php" id="https://www.powershelladmin.com/wiki/PowerShell_foreach_loops_and_ForEach-Object.php">-Parallel</a></code> parameter is a feature of the <code>ForEach-Object</code> cmdlet introduced in PowerShell 7 to enable native multi-threading. While it allows for processing multiple objects simultaneously, it is not reliably supported by the <code>ReportingServicesTools</code> library as the underlying API is not thread-safe. To ensure stability and avoid session collisions, it is recommended to use the <code>Start-Job</code> method instead, as it provides better process isolation for each task.</p>



<h2 class="wp-block-heading" id="h-key-takeaways-for-a-seamless-cutover">Key Takeaways for a Seamless Cutover</h2>



<p class="wp-block-paragraph">Migrating to Power BI Report Server shouldn&#8217;t be a manual challenge. By adopting this <strong>PowerShell-driven ETL approach</strong>, you replace the uncertainty of manual intervention with industrial-grade rigor.</p>



<p class="wp-block-paragraph">The primary advantage lies in consistency: regardless of the report volume or folder complexity, the script guarantees an identical and predictable result every single time. By isolating extraction, XML transformation, and ordered importation, you maintain total control over your data integrity.</p>



<p class="wp-block-paragraph">Ultimately, automation is about securing your delivery and freeing up time for what truly matters: leveraging your data on your brand-new PBIRS 2025 platform.</p>



<p class="wp-block-paragraph">Happy migrating!</p>
<p>L’article <a href="https://www.dbi-services.com/blog/scaling-ssrs-migrations-multi-threaded-automation-for-pbirs-2025/">Scaling SSRS Migrations: Multi-Threaded Automation for PBIRS 2025</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/scaling-ssrs-migrations-multi-threaded-automation-for-pbirs-2025/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server 2022 CU23: Database Mail is broken, but your alerts shouldn’t be</title>
		<link>https://www.dbi-services.com/blog/sql-server-2022-cu23-database-mail-is-broken-but-your-alerts-shouldnt-be/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-2022-cu23-database-mail-is-broken-but-your-alerts-shouldnt-be/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Wed, 28 Jan 2026 08:35:42 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Cumulative Update]]></category>
		<category><![CDATA[Mail]]></category>
		<category><![CDATA[SQL Server 2022]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=42662</guid>

					<description><![CDATA[<p>SQL Server 2022 CU23 bug freezes your emails? Bypass Database Mail using dbatools and PowerShell to resend blocked alerts.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-2022-cu23-database-mail-is-broken-but-your-alerts-shouldnt-be/">SQL Server 2022 CU23: Database Mail is broken, but your alerts shouldn’t be</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"><em>&#8220;One single alerting email is missing, and all your monitoring feels deserted.&#8221;</em> </p>



<p class="wp-block-paragraph">This is the harsh lesson many SQL Server DBAs learned the hard way this week. If your inbox has been suspiciously quiet since your last maintenance window, don&#8217;t celebrate just yet: your instance might have simply lost its voice.</p>



<h3 class="wp-block-heading" id="h-cu23-the-poisoned-gift">CU23: The poisoned gift</h3>



<p class="wp-block-paragraph">The <strong><a href="https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate23">Cumulative Update 23</a></strong> for SQL Server 2022 (KB5074819), released on January 15 2026, quickly became the &#8220;hot&#8221; topic on technical forums. The reason? A major regression that purely and simply breaks <strong><a href="https://learn.microsoft.com/en-us/sql/relational-databases/database-mail/database-mail?view=sql-server-ver17">Database Mail</a></strong>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Could not load file or assembly &#039;Microsoft.SqlServer.DatabaseMail.XEvents, Version=17.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91&#039; or one of its dependencies. The system cannot find the file specified.
</pre></div>


<p class="wp-block-paragraph">This error message is the visible part of the iceberg. Beyond the technical crash, it is the silence of your monitoring that should worry you. The real danger here is that while the mailing engine fails, your SQL Agent jobs don&#8217;t necessarily report an error. Since the mail items are never even processed, they don&#8217;t appear in the <code>failed_items</code> or <code>unsent_items</code> views with typical error statuses. <strong>For most monitoring configurations, this means you stay completely unaware that your instance has lost its voice.</strong> You aren&#8217;t getting alerts, but everything looks fine on the surface.</p>



<h3 class="wp-block-heading">Database Mail: The (Too) Quiet Hero</h3>



<p class="wp-block-paragraph">We often tend to downplay the importance of Database Mail, treating it as a minor utility. Yet, for many of us, it is the <strong>backbone of our monitoring</strong>. From SQL Agent job failure notifications to corruption alerts or disk space thresholds, Database Mail is a critical component. When it fails, your infrastructure visibility evaporates, leaving you flying blind in a rather uncomfortable technical fog.</p>



<h3 class="wp-block-heading">Rollback or Status Quo?</h3>



<p class="wp-block-paragraph">While waiting for an official fix, the best way to protect your production remains a <code><strong>rollback</strong> </code>(guidelines available <a href="https://learn.microsoft.com/en-us/sql/sql-server/install/uninstall-a-cumulative-update-from-sql-server?view=sql-server-ver17" id="https://learn.microsoft.com/en-us/sql/sql-server/install/uninstall-a-cumulative-update-from-sql-server?view=sql-server-ver17">here</a>). Uninstalling a CU is never an easy task: it implies additional downtime and a fair amount of stress. However, staying in total darkness regarding your servers&#8217; health is a much higher risk. Microsoft has promised a hotfix &#8220;soon&#8221; but in the meantime, a server that reboots is better than a server that suffers in silence.</p>



<h3 class="wp-block-heading">The Light at the End of the Tunnel (is in the MSDB)</h3>



<p class="wp-block-paragraph">If a rollback is impossible due to SLAs or internal policies, remember that not all is lost. Even if the emails aren&#8217;t being sent, the information itself isn&#8217;t volatile. SQL Server continues to conscientiously log everything it wants to tell you inside the <code>msdb</code> system tables.</p>



<p class="wp-block-paragraph">You can query the following table to keep an eye on the alerts piling up:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SELECT mailitem_id, recipients, subject, body, send_request_date, sent_date
 FROM &#x5B;msdb].&#x5B;dbo].&#x5B;sysmail_mailitems]
 where sent_date is null
 and send_request_date &gt; &#039;2026-01-15 00:00:00.000&#039;
</pre></div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="218" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-34-1024x218.png" alt="" class="wp-image-42680" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-34-1024x218.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-34-300x64.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-34-768x164.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-34.png 1037w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">It’s less stylish than a push notification, but it’s your final safety net to ensure your Log Shipping hasn&#8217;t flatlined or your backups haven&#8217;t devoured the last available Gigabyte.</p>



<h2 class="wp-block-heading" id="h-how-to-build-a-solid-home-made-quick-fix">How to Build a Solid &#8220;Home-Made&#8221; Quick Fix</h2>



<p class="wp-block-paragraph">If you have an SMTP gateway reachable via PowerShell, you can bridge the gap using the native <code>Send-MailMessage</code> cmdlet. This approach effectively bypasses the broken <code>DatabaseMail.exe</code> by using the PowerShell network stack to ship your alerts.</p>



<h3 class="wp-block-heading">Testing with Papercut</h3>



<p class="wp-block-paragraph">In a Lab environment, you likely don&#8217;t have a full-blown Exchange server. To test this script, I recommend using <strong><a href="https://github.com/ChangemakerStudios/Papercut-SMTP/releases/tag/7.6.2">Papercut</a></strong>. It acts as a local SMTP gateway that catches any outgoing mail and displays it in a UI without actually sending it to the internet. Simply run the Papercut executable, and it will listen on <code>localhost:25</code>.</p>



<h3 class="wp-block-heading">The Recovery Script</h3>



<p class="wp-block-paragraph">For the purpose of this article&#8217;s demonstration, the following <a href="https://learn.microsoft.com/en-us/sql/relational-databases/administer-multiple-servers-using-central-management-servers?view=sql-server-ver17">Central Management Server (CMS)</a> setup is used:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="235" height="135" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-33.png" alt="" class="wp-image-42678" /></figure>
</div>


<p class="wp-block-paragraph">Run the following script as a scheduled task every 15 minutes to replay any messages stuck in the <code>msdb</code> queue:</p>



<pre class="wp-block-code"><code>Import-Module -Name dbatools;

Set-DbatoolsConfig -FullName 'sql.connection.trustcert' -Value $true;

$cmsServer = "SQLAGAD\LAB01"
$cmsGroupName = "LAB_AG"
$smtpServer = "localhost"

try {
    $instances = Get-DbaRegServer -SqlInstance $cmsServer -ErrorAction Stop | 
                 Where-Object group -match $cmsGroupName;

    if (-not $instances) {
        Write-Warning "No instances found in the group '$cmsGroupName' on $cmsServer."
        return
    }

    foreach ($server in $instances) {
        Write-Host "Processing: $($server.Name)" -ForegroundColor Cyan
        
        $query = "SELECT SERVERPROPERTY('ServerName') SQLInstance, * FROM &#091;msdb].&#091;dbo].&#091;sysmail_mailitems] where sent_date is null and send_request_date &gt; DATEADD(minute, -15, GETDATE())"
        
        $blockedMails = Invoke-DbaQuery -SqlInstance $server.Name -Database msdb -Query $query -ErrorAction SilentlyContinue

        if ($blockedMails) {
            foreach ($mail in $blockedMails) {
                Send-MailMessage -SmtpServer $smtpServer `
                                 -From "MSSQLSecurity@lto.com" `
                                 -To $mail.recipients `
                                 -Subject "&#091;CU23-RECOVERY] $($mail.subject)" `
                                 -Body $mail.body `
                                 -Priority High

                Write-Host "Successfully re-sent: $($mail.subject)" -ForegroundColor Green
            }
        }
    }
}
catch {
    Write-Error "Error accessing the CMS: $($_.Exception.Message)"
}</code></pre>



<p class="wp-block-paragraph">Resulting alerts in the Papercut mailbox:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1012" height="269" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-32.png" alt="" class="wp-image-42667" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-32.png 1012w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-32-300x80.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-32-768x204.png 768w" sizes="auto, (max-width: 1012px) 100vw, 1012px" /></figure>
</div>


<p class="wp-block-paragraph">By leveraging the <a href="https://dbatools.io/">dbatools</a> module and the native <code>Send-MailMessage</code> function, we can create a &#8220;recovery bridge.&#8221; The following script is designed to be run as a scheduled task (e.g., every 15 minutes). It scans your entire infrastructure via your <code>CMS</code>, identifies any messages that failed to send in the last 15-minute window, and replays them through PowerShell’s network stack instead of SQL Server’s.</p>



<p class="wp-block-paragraph"><strong>Note on Data Integrity:</strong> You will notice that this script intentionally performs no <code>UPDATE</code> or <code>DELETE</code> operations on the <code>msdb</code> tables. We chose to treat the system database as a read-only &#8216;source of truth&#8217; to avoid any further corruption or inconsistency while the instance is already in an unstable state.</p>



<h2 class="wp-block-heading" id="h-let-s-wrap-up">Let&#8217;s wrap-up !</h2>



<p class="wp-block-paragraph">Until Microsoft releases an official fix for this capricious CU23, this script acts as a life support system for your monitoring alerts. It is simple, effective, and most importantly, it prevents the Monday morning nightmare of discovering failed weekend backups that went unnoticed because the notification engine was silent.</p>



<p class="wp-block-paragraph">So, if your SQL alerts prefer staying cozy in the <code>msdb</code> rather than doing their job, you now have the bridge to get them moving. Set up the scheduled task, run the script, and go grab a coffee, your emails are finally back on track.</p>



<p class="wp-block-paragraph"><strong>Until the hotfix lands, keep your scripts ready!</strong><br></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-2022-cu23-database-mail-is-broken-but-your-alerts-shouldnt-be/">SQL Server 2022 CU23: Database Mail is broken, but your alerts shouldn’t be</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-2022-cu23-database-mail-is-broken-but-your-alerts-shouldnt-be/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Scaling Data Governance: Why ABAC is My Favorite Delphix DCT Feature</title>
		<link>https://www.dbi-services.com/blog/scaling-data-governance-why-abac-is-my-favorite-delphix-dct-feature/</link>
					<comments>https://www.dbi-services.com/blog/scaling-data-governance-why-abac-is-my-favorite-delphix-dct-feature/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Tue, 13 Jan 2026 14:58:36 +0000</pubDate>
				<category><![CDATA[Delphix]]></category>
		<category><![CDATA[delphix]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=42390</guid>

					<description><![CDATA[<p>ABAC in Delphix Data Control Tower automates governance via tags, providing granular control and full autonomy for developers.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/scaling-data-governance-why-abac-is-my-favorite-delphix-dct-feature/">Scaling Data Governance: Why ABAC is My Favorite Delphix DCT Feature</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="h-delphix-a-quick-refresher">Delphix: A Quick Refresher</h2>



<p class="wp-block-paragraph"><a href="https://www.perforce.com/products/delphix"><strong>Delphix</strong></a> is a software-defined data platform designed to automate and secure the data lifecycle. To handle data at scale without the usual operational friction, it relies on three pillars:</p>



<ul class="wp-block-list">
<li><strong><a href="https://www.perforce.com/products/delphix/continuous-data">Continuous Data</a> </strong><em>(Virtualization)</em> : This <strong>virtual appliance</strong> ingests production sources to create compressed <strong>dSources</strong>. You can then provision virtual clones (<strong>VDBs</strong>) in minutes. It’s not just about optimizing storage via block-sharing, it’s about drastically slashing refresh times, moving from hours of manual DB cloning to a few minutes of automation.</li>



<li><strong><a href="https://www.perforce.com/products/delphix/continuous-compliance">Continuous Compliance</a> </strong><em>(Masking)</em> : Acting as the compliance safeguard, this <strong>virtual appliance</strong> anonymizes sensitive data (GDPR, etc.) while keeping referential integrity intact across applications. You get usable test data without the risk of a data leak or a legal headache. For more information, you can read my other blog <a href="https://www.dbi-services.com/blog/data-anonymization-as-a-service-with-delphix-continuous-compliance/">Data Anonymization as a Service with Delphix Continuous Compliance</a>.</li>



<li><strong><a href="https://www.perforce.com/products/delphix/data-control-tower">Data Control Tower</a> </strong><em>(DCT)</em> : The central Control Plane. Unlike the engines above, DCT is a platform that unifies everything through a single API. It also provides a dedicated interface where developers can manually manage their own resources in total autonomy, without needing a DBA behind them for every refresh. It effectively turns isolated instances into a programmable, self-service infrastructure.</li>
</ul>



<h2 class="wp-block-heading" id="h-the-abac-approach-beyond-static-roles">The ABAC Approach: Beyond static roles</h2>



<p class="wp-block-paragraph">Access management usually relies on the <a href="https://en.wikipedia.org/wiki/Role-based_access_control"><strong>RBAC</strong></a> (<em>Role-Based Access Control</em>) model. In this setup, permissions are tied to roles (like &#8220;Developer&#8221; or &#8220;Analyst&#8221;) and users simply inherit whatever their role allows. While this works fine at the start, it quickly becomes a rigid mess as projects multiply, usually leading to a &#8220;role explosion&#8221; that’s a nightmare to maintain.</p>



<p class="wp-block-paragraph"><a href="https://en.wikipedia.org/wiki/Attribute-based_access_control"><strong>ABAC</strong></a> (<em>Attribute-Based Access Control</em>) takes a different path: rights aren&#8217;t frozen into a static role anymore. Instead, they are determined dynamically based on attributes (characteristics of the resource, the user, and the environment). It’s a shift from <em>Who are you?</em> to <em>What are you trying to access and does it match your current context?</em>.</p>



<h2 class="wp-block-heading" id="h-delphix-dct-leveraging-tags-for-smarter-data-governance">Delphix DCT: Leveraging Tags for Smarter Data Governance</h2>



<p class="wp-block-paragraph">The real strength of <strong>Delphix Data Control Tower (DCT)</strong> lies in its ability to leverage <strong>tags</strong> for strict resource segregation. In a sprawling enterprise environment, global visibility is rarely a feature; it’s more of a liability. By mapping specific tags to your resources, you define isolated management boundaries that actually make sense for the business.</p>



<p class="wp-block-paragraph">This setup ensures that teams only see the infrastructure they are responsible for. It’s a clean way to enforce the <strong><a href="https://en.wikipedia.org/wiki/Principle_of_least_privilege">Least Privilege</a></strong> principle without the administrative overhead of manual role mapping. By restricting a user&#8217;s scope to their specific tags, you eliminate the risk of someone interact with an environment they weren&#8217;t even supposed to know existed. It’s about creating a workspace where developers can be autonomous within their own perimeter, while the rest of the infrastructure remains safely out of sight (and out of reach).</p>



<h2 class="wp-block-heading" id="h-abac-in-action-configuring-dynamic-access-control-in-delphix-dct">ABAC in Action: Configuring Dynamic Access Control in Delphix DCT</h2>



<p class="wp-block-paragraph">Even when managing a massive Delphix environment, you can strictly limit a developer&#8217;s or DevOps engineer&#8217;s visibility to only the resources they actually own. Through the unified DCT platform, they get a consolidated view of their specific objects only :</p>



<figure data-wp-context="{&quot;imageId&quot;:&quot;6a2d9ff0dd9ae&quot;}" data-wp-interactive="core/image" data-wp-key="6a2d9ff0dd9ae" class="wp-block-image wp-lightbox-container"><img loading="lazy" decoding="async" width="1024" height="306" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-10-1024x306.png" alt="" class="wp-image-42468" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-10-1024x306.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-10-300x90.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-10-768x229.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-10-1536x458.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-10.png 1887w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button></figure>



<figure data-wp-context="{&quot;imageId&quot;:&quot;6a2d9ff0de251&quot;}" data-wp-interactive="core/image" data-wp-key="6a2d9ff0de251" class="wp-block-image size-large wp-lightbox-container"><img loading="lazy" decoding="async" width="1024" height="282" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-7-1024x282.png" alt="" class="wp-image-42464" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-7-1024x282.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-7-300x83.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-7-768x212.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-7-1536x423.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-7.png 1877w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button></figure>


<div class="wp-block-image">
<figure data-wp-context="{&quot;imageId&quot;:&quot;6a2d9ff0dea50&quot;}" data-wp-interactive="core/image" data-wp-key="6a2d9ff0dea50" class="aligncenter size-large wp-lightbox-container"><img loading="lazy" decoding="async" width="1024" height="282" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/dsources-1024x282.png" alt="" class="wp-image-42469" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/dsources-1024x282.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/dsources-300x83.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/dsources-768x212.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/dsources-1536x423.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/dsources.png 1876w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button></figure>
</div>


<p class="has-text-align-left wp-block-paragraph">In the three images above, each component is identified by the tag <code>Team: BlogTeam</code>. This key-value pair is directly tied to the permissions assigned to the current user. In this specific scenario, a user belonging to the <code>BlogTeam </code>access group is granted the following roles:</p>



<figure data-wp-context="{&quot;imageId&quot;:&quot;6a2d9ff0df2d9&quot;}" data-wp-interactive="core/image" data-wp-key="6a2d9ff0df2d9" class="wp-block-image size-large wp-lightbox-container"><img loading="lazy" decoding="async" width="1024" height="298" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-11-1024x298.png" alt="" class="wp-image-42471" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-11-1024x298.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-11-300x87.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-11-768x224.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-11-1536x448.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-11.png 1891w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button></figure>



<p class="has-text-align-left wp-block-paragraph">While these roles might seem overly permissive at first glance, the <strong>ABAC</strong> framework in Delphix allows for granular control. It ensures that these permissions are dynamically scoped, applying only to the specific resources that match the user&#8217;s attributes.</p>



<figure data-wp-context="{&quot;imageId&quot;:&quot;6a2d9ff0dfae7&quot;}" data-wp-interactive="core/image" data-wp-key="6a2d9ff0dfae7" class="wp-block-image size-large wp-lightbox-container"><img loading="lazy" decoding="async" width="1024" height="214" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-12-1024x214.png" alt="" class="wp-image-42472" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-12-1024x214.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-12-300x63.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-12-768x161.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-12-1536x321.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/01/image-12.png 1869w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button></figure>



<p class="wp-block-paragraph">It is important to understand that ABAC in Delphix DCT isn&#8217;t just a &#8220;view-only&#8221; filter ; it doesn’t just dictate <em>who sees what</em>, it defines <em>who can do what</em>.</p>



<p class="wp-block-paragraph">By combining roles with tag-based scopes, you can achieve surgical precision in your permissions. For instance, a developer might be granted the <code>Provision VDBs</code> right on all resources tagged with <code>Team : BlogTeam</code>, while only having <code>Refresh</code> or <code>Snapshot</code> capabilities on VDBs tagged with <code>Environment: Integration</code>. This ensures that even within the same project, critical actions are restricted to the right people at the right time, without ever needing to touch a single manual access list.</p>



<p class="wp-block-paragraph">The true power of Delphix DCT lies in its ability to transform a basic administrative task (tagging) into a robust automated governance engine. It provides administrators with absolute control while ensuring developers operate within a streamlined interface, strictly confined to their specific workspace.</p>



<h2 class="wp-block-heading" id="h-dct-moving-from-gui-management-to-api-first-governance">DCT: Moving from GUI Management to API-First Governance</h2>



<p class="wp-block-paragraph">Integrating Delphix into a modern CI/CD pipeline means saying a final goodbye to manual clicks. Thanks to DCT’s <strong>API-First architecture</strong>, every resource is treated as a programmable object. By using tags as dynamic filters, you stop managing environments by their names or IP addresses and start orchestrating them by their business properties.</p>



<p class="wp-block-paragraph">The real beauty here is the ability to script complex workflows in just a few lines of code. Instead of manually triggering a refresh for ten different test databases (a task as tedious as it is prone to error) your pipeline can simply query the API to identify every resource tagged with <code>Team : BlogTeam</code> and fire off the operation simultaneously. This approach doesn&#8217;t just save precious time; it ensures total reproducibility while eliminating the <em>human factor</em>.</p>



<p class="wp-block-paragraph">To give you a concrete example, the following Python snippet shows how to authenticate with the DCT API and retrieve details for all VDBs associated with the specific tag <code>Team : BlogTeam</code> :</p>



<pre class="wp-block-code"><code>import requests, urllib3, logging
from typing import List, Dict, Any

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)

class Config:
    dct_url = "https://&lt;DELPHIX_ENGINE_URL&gt;/dct/v3"
    username = "YOUR_USERNAME"
    password = "YOUR_PASSWORD"

class DctApiClient:
    def __init__(self, cfg):
        self.cfg = cfg
        self.token = None

    def authenticate(self):
        try:
            r = requests.post(f"{self.cfg.dct_url}/login", 
                json={"username": self.cfg.username, "password": self.cfg.password}, verify=False)
            self.token = r.json()&#091;"access_token"]
            logger.info(f"Connected (expires in {r.json().get('expires_in')}s)")
            return True
        except Exception as e:
            logger.error(f"Authentication failed: {e}")
            return False

    def headers(self):
        return {"Authorization": f"Bearer {self.token}", "Accept": "application/json"}

    def get_vdbs_by_tag(self, tag_key: str, tag_value: str) -&gt; List&#091;Dict&#091;str, Any]]:
        vdbs = requests.get(f"{self.cfg.dct_url.replace('/dct/v3', '/v3')}/vdbs", 
                           headers=self.headers(), verify=False).json().get('items', &#091;])
        
        filtered = &#091;v for v in vdbs if any(t.get('key')==tag_key and t.get('value')==tag_value 
                                           for t in v.get('tags', &#091;]))]
        logger.info(f"{len(filtered)} VDB(s) found")
        
        for v in filtered:
            status_color = '\033&#091;92m' if v.get('status','').lower()=="running" else '\033&#091;91m'
            print(f"\n{'='*35}\n{v.get('name'):20} | {status_color}{v.get('status')}\033&#091;0m\n"
                  f"ID: {v.get('id')}\nType: {v.get('database_type')}\n{'='*35}")
        return filtered

if __name__ == "__main__":
    client = DctApiClient(Config)
    if client.authenticate():
        client.get_vdbs_by_tag("Team", "BlogTeam")</code></pre>



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



<pre class="wp-block-code"><code>INFO: Connected (expires in 86400s)
INFO: 1 VDB(s) found

===================================
VDB_BLOG             | RUNNING
ID: 1-MSSQL_DB_CONTAINER-190
Type: MSSql
===================================</code></pre>



<p class="wp-block-paragraph">While Python offers great flexibility for complex logic, the same result can be achieved more concisely using the <a href="https://dct.delphix.com/docs/latest/dct-toolkit">dct-toolkit</a>. This native CLI tool simplifies interactions with the DCT engine, allowing you to execute the same resource filtering with a single command:</p>



<pre class="wp-block-code"><code>dct-toolkit search_vdbs -c name,tags filter_expression "tags CONTAINS {key EQ 'team' AND value EQ 'BlogTeam'} "</code></pre>



<h2 class="wp-block-heading" id="h-conclusion-bridging-governance-and-agility"><strong>Conclusion: Bridging Governance and Agility</strong></h2>



<p class="wp-block-paragraph">In summary, implementing <strong>ABAC</strong> within <strong>Delphix Data Control Tower</strong> marks the end of an era where security was synonymous with administrative friction. By using tags as the foundation of your governance, you transform a complex data infrastructure into a granular, secure self-service ecosystem. It is no longer the tool dictating your processes, but your business requirements dynamically driving access control.</p>



<p class="wp-block-paragraph">A major advantage of this model is the seamless onboarding of new developers. Instead of manually configuring complex permissions for every newcomer, simply assigning the appropriate tags allows them to be productive instantly, with immediate and secure visibility over their specific scope.</p>



<p class="wp-block-paragraph">This approach opens the door to advanced use cases that we have only scratched the surface of here:</p>



<ul class="wp-block-list">
<li><strong>Automated Self-Service:</strong> Integrating tag creation directly into your provisioning processes so that resources are immediately isolated within the correct perimeter.</li>



<li><strong>FinOps &amp; Showback:</strong> Leveraging this same tagging logic to accurately allocate storage and compute costs by project or team.</li>



<li><strong>Dynamic Data Masking:</strong> Automating masking jobs based on tag criticality (e.g., <code>Criticality : Confidential</code>), ensuring no sensitive data leaves the production environment without protection.</li>
</ul>



<p class="wp-block-paragraph" id="h-wrap-up">And you? How are you managing access in your non-production environments for developers? Is API automation already at the heart of your <strong>DataOps</strong> strategy? Let’s discuss it in the comments, or feel free to reach out to explore these topics further.</p>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/scaling-data-governance-why-abac-is-my-favorite-delphix-dct-feature/">Scaling Data Governance: Why ABAC is My Favorite Delphix DCT Feature</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/scaling-data-governance-why-abac-is-my-favorite-delphix-dct-feature/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server Always-On: Centralizing Backup History Across Replicas</title>
		<link>https://www.dbi-services.com/blog/sql-server-always-on-centralizing-backup-history-across-replicas/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-always-on-centralizing-backup-history-across-replicas/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Thu, 08 Jan 2026 17:19:10 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[AlwaysOn]]></category>
		<category><![CDATA[Backup]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=42169</guid>

					<description><![CDATA[<p>Replace brittle T-SQL links and manual checks with a central PowerShell orchestrator to reliably manage Always On backup data at scale.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-always-on-centralizing-backup-history-across-replicas/">SQL Server Always-On: Centralizing Backup History Across Replicas</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="h-context">Context</h2>



<p class="wp-block-paragraph">Monitoring backups in a SQL Server Always On Availability Group can often feel like a game of hide and seek where the rules change every time you flip a failover switch. On paper, your backup strategy is solid ; you’ve configured your replica priorities and your jobs are running like clockwork. But when you query the backup history to ensure everything is under control, you realize the truth is fragmented.</p>



<p class="wp-block-paragraph">Because the <code><strong>msdb</strong></code> database is local to each instance, each replica only knows about the backups it performed itself. If your backups happen on the secondary today and the primary tomorrow, no single node holds the complete story. This leads to inconsistent monitoring reports, &#8220;false positive&#8221; alerts, and a lot of manual jumping between instances just to answer a simple question: <em>&#8220;Are we actually protected with consistent backups?&#8221;</em>.</p>



<p class="wp-block-paragraph">Before we dive deeper, a quick disclaimer for the lucky ones: If you are running SQL Server 2022 and have implemented <em><strong><a href="https://learn.microsoft.com/en-us/sql/database-engine/availability-groups/windows/contained-availability-groups-overview?view=sql-server-ver17">Contained Availability Groups</a></strong></em> you can stop reading here, you are safe. In a Contained AG, the system databases are replicated alongside your data, meaning the backup history is finally unified and follows the group. </p>



<p class="wp-block-paragraph">For the others, in this post, we’re going to explore how to stop chasing metadata across your cluster. We’ll look at the limitations of the local <code><strong>msdb</strong></code>, compare different ways to consolidate a unified backup view using <em>Linked Servers</em>, <em>OPENDATASOURCE</em>, and PowerShell, and see how to build a monitoring query that finally tells the whole truth, regardless of where the backup actually ran.</p>



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



<p class="wp-block-paragraph">The traditional approach is therefore to query the <code>msdb</code> through the AG listener, pointing to the primary replica. Using a simple TSQL query to find the most recent backup, we get the following results.</p>



<pre class="wp-block-code"><code>SELECT
bs.database_name,
MAX(bs.backup_finish_date) AS LastBackup
FROM msdb.dbo.backupset bs
WHERE bs.database_name = 'StackOverflow2010'
GROUP BY bs.database_name;</code></pre>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="432" height="62" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-32.png" alt="" class="wp-image-42173" style="aspect-ratio:6.969246281825057;width:412px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-32.png 432w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-32-300x43.png 300w" sizes="auto, (max-width: 432px) 100vw, 432px" /></figure>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="420" height="62" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-30.png" alt="" class="wp-image-42171" style="aspect-ratio:6.775901184774389;width:406px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-30.png 420w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-30-300x44.png 300w" sizes="auto, (max-width: 420px) 100vw, 420px" /></figure>



<p class="wp-block-paragraph">As we can see, the two backup dates differ (14:39 being the time the database was created in the AG and 15:15 being the time of the last backup). As a result, if a failover occurs, the main <code>msdb</code> will contain incomplete data because the primary <code>msdb</code> will be the one on <em>SQLAGVM2</em>.</p>



<p class="wp-block-paragraph">This traditional approach is perfectly acceptable for standalone instances because there is only one source of truth, but in the case of Always-On, we need a more robust solution. Let&#8217;s examine the different possibilities.</p>



<h2 class="wp-block-heading" id="h-the-good-old-linked-servers">The good old Linked Servers</h2>



<p class="wp-block-paragraph">If you want to keep everything within the SQL Engine, Linked Servers are your go-to tool. The idea is simple: from your Primary replica, you reach out to the Secondary, query its <code>msdb</code>, and union the results with your local data. </p>



<pre class="wp-block-code"><code>SELECT
@@SERVERNAME AS &#091;PrimaryServer],
bs.database_name,
MAX(bs.backup_finish_date) AS LastBackup
FROM msdb.dbo.backupset bs
WHERE bs.database_name = 'StackOverflow2010'
GROUP BY bs.database_name

UNION ALL

SELECT
<strong>'REMOTE_MSDB'</strong> AS &#091;ReportingServer],
bs.database_name,
MAX(bs.backup_finish_date) AS LastBackup
FROM <strong>&#091;REMOTE_MSDB]</strong>.msdb.dbo.backupset bs
WHERE bs.database_name = 'StackOverflow2010'
GROUP BY bs.database_name;</code></pre>



<p class="wp-block-paragraph">On paper, the best way to secure a Linked Server is to use the <strong><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/linked-servers/create-linked-servers-sql-server-database-engine?view=sql-server-ver17">Self </a></em></strong>option, ensuring that your own permissions are carried over to the next node. It’s the most transparent approach for auditing and security. However, this is where we often hit a silent wall: the <strong><a href="https://techcommunity.microsoft.com/blog/askds/understanding-kerberos-double-hop/395463">Double-Hop</a></strong>. Unless Kerberos delegation is perfectly configured in your domain, NTLM will prevent your identity from traveling to the second replica. You’ll end up with a connection error, not because of a lack of permissions, but because your identity simply couldn&#8217;t make the trip. To determine the type of authentication protocol you are using, use the following query.</p>



<pre class="wp-block-code"><code>SELECT auth_scheme 
FROM sys.dm_exec_connections 
WHERE session_id = @@SPID;</code></pre>



<p class="wp-block-paragraph">To bypass this hurdle, it is common to see <strong><em>Fixed Logins</em></strong> being used as a pragmatic workaround. But by hardcoding credentials to make the bridge work, we create a permanent, pre-authenticated tunnel. From a security standpoint, this can facilitate <a href="https://www.crowdstrike.com/en-us/cybersecurity-101/cyberattacks/lateral-movement/">lateral movement</a> if one instance is ever compromised, a major concern in modern <strong>Zero Trust</strong> architectures. Furthermore, it obscures your audit logs, as the remote server only sees the service account instead of the actual user. These hidden complexities and security risks are precisely why many DBAs are now moving toward more decoupled, scalable alternatives.</p>



<h2 class="wp-block-heading" id="h-the-on-the-fly-connection-opendatasource">The &#8220;On-the-Fly&#8221; Connection: OPENDATASOURCE</h2>



<p class="wp-block-paragraph">To address the frustrations of Linked Servers, another T-SQL path often explored is the use of <em>ad hoc </em>queries via <strong><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/opendatasource-transact-sql?view=sql-server-ver17">OPENDATASOURCE</a></em></strong>.<br>The concept is tempting: instead of building a permanent bridge (Linked Server), you use a temporary ladder ; <code>OPENDATASOURCE</code> allows you to define a connection string directly inside your T-SQL statement, reaching out to a remote replica only for the duration of that specific query. It feels lightweight and dynamic because it requires no pre-configured server objects.</p>



<p class="wp-block-paragraph">In theory, you would use it like this to pull backup history from your secondary node:</p>



<pre class="wp-block-code"><code>SELECT 
    @@SERVERNAME AS &#091;Source Server],
    bs.database_name, 
    MAX(bs.backup_finish_date) AS LastBackup
FROM OPENDATASOURCE('MSOLEDBSQL', 'Data Source=SQLAGVM1,1432;Integrated Security=SSPI').msdb.dbo.backupset bs
WHERE bs.database_name = 'StackOverflow2010'
GROUP BY bs.database_name;</code></pre>



<p class="wp-block-paragraph">However, if the environment is not properly configured, an error will occur immediately.</p>



<pre class="wp-block-code"><code>Msg 15281, Level 16, State 1, Line 1
SQL Server blocked access to STATEMENT 'OpenRowset/OpenDatasource' of component 'Ad Hoc Distributed Queries' because this component is turned off as part of the security configuration for this server. A system administrator can enable the use of 'Ad Hoc Distributed Queries' by using sp_configure. </code></pre>



<p class="wp-block-paragraph">By default, SQL Server locks this door. It is a protective measure to prevent users from using the SQL instance as a jumping point to query any other server on the network. To get past this error, a sysadmin must explicitly enable <strong>Ad Hoc Distributed Queries</strong>. This requires tweaking the advanced configuration of the instance.</p>



<pre class="wp-block-code"><code>EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
GO
EXEC sp_configure 'Ad Hoc Distributed Queries', 1;
RECONFIGURE;
GO</code></pre>



<p class="wp-block-paragraph">Once these options have been reconfigured, access to the remote <code>msdb</code> is finally possible.</p>



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



<p class="wp-block-paragraph">While we successfully made <code>OPENDATASOURCE</code> work, the trade-offs are significant. We have ended up with a solution that is brittle, difficult to maintain, and a potential liability:</p>



<ul class="wp-block-list">
<li><strong>Hardcoding &amp; Maintenance:</strong> Every replica requires a manually adapted connection string. If a server is renamed, a port is migrated, or a password expires, the entire monitoring logic collapses.</li>



<li><strong>Security &amp; Shadow IT Risk:</strong> Enabling <em>Ad Hoc Distributed Queries</em> opens a permanent hole in your instance. You aren&#8217;t just allowing your script to run; you are allowing any sysadmin to connect to any external server, creating a <em>ghost </em>feature that can be easily misused.</li>
</ul>



<p class="wp-block-paragraph">In short, we are fighting the SQL engine’s security defaults just to get a simple timestamp. For a truly robust and scalable solution, it is time to look beyond T-SQL.</p>



<h2 class="wp-block-heading" id="h-here-comes-the-superhero-powershell">Here comes the superhero Powershell</h2>



<p class="wp-block-paragraph">PowerShell steps in as the ultimate lifesaver, delivering high-level automation and pure execution simplicity. It allows you to centralize and control your scripts from an external management server, piloting your entire fleet remotely without cluttering your SQL instances.</p>



<p class="wp-block-paragraph">By leveraging the power of <strong><a href="https://dbatools.io/">dbatools</a></strong>, we shatter the limitations of traditional T-SQL. We gain dynamic flexibility and enhanced security by bypassing risky configurations, all while maintaining total control over how we manipulate the retrieved data. The security gain does not come from PowerShell itself, but from removing risky SQL Server surface area features and centralizing access control on a hardened management host. This works because PowerShell establishes <strong>direct connections</strong> from your management host to each replica. This bypasses the NTLM double-hop issue entirely, as your identity is never passed between servers, removing any need for complex Kerberos delegation or risky fixed logins.</p>



<p class="wp-block-paragraph">Here is how to put this into practice. By using the Availability Group <strong>Listener</strong> as your unique gateway, you can dynamically discover the cluster topology and query all member nodes.</p>



<pre class="wp-block-code"><code>$Listener = 'SQLLISTENER,1432'
$TargetDB = 'StackOverflow2010'

try {
    $Nodes = Invoke-DbaQuery -SqlInstance $Listener -Database 'master' -Query "SELECT replica_server_name FROM sys.dm_hadr_availability_replica_cluster_states" 
    $BackupQuery = "SELECT SERVERPROPERTY('ServerName') as &#091;InstanceName], MAX(backup_finish_date) as &#091;LastBackup] FROM msdb.dbo.backupset WHERE database_name = '$TargetDB' GROUP BY database_name"
    write-output $Nodes
    $Results = foreach ($Node in $Nodes.replica_server_name) {
        write-output $Node
        Invoke-DbaQuery -SqlInstance $Node -Database 'msdb' -Query $BackupQuery -ErrorAction SilentlyContinue
    }

    $Results | Where-Object { $_.LastBackup } | Sort-Object LastBackup -Descending | Select-Object -First 1 | Format-Table -AutoSize
}
catch {
    Write-Error "Error : $($_.Exception.Message)"
}</code></pre>



<p class="wp-block-paragraph">This script is just a first step, but it lays the foundation for truly scalable infrastructure management. By shifting the logic to PowerShell instead of overloading our SQL instances, we achieve a robust and extensible method capable of handling large Always-On ecosystems without additional manual effort.</p>



<p class="wp-block-paragraph">In this example, the listener name is hardcoded for the sake of clarity. However, the true strength of this approach lies in its ability to work behind the scenes with a dynamic inventory. In a <strong>Production </strong>environment, you would typically query a <em>CMDB </em>or a centralized configuration file to automatically populate the list of instances. This transforms a simple check into a silent, reliable automation that adapts seamlessly as your SQL environment evolves.</p>



<p class="wp-block-paragraph">While we wrote the T-SQL manually in this example for the sake of clarity, it is worth noting that <em>dbatools </em>offers an even more streamlined approach with the <code>Get-DbaBackupHistory</code> command. This native function eliminates the need for manual queries entirely, returning rich metadata objects that are ready to be filtered and aggregated across your entire fleet.</p>



<pre class="wp-block-code"><code>$Nodes.replica_server_name | Get-DbaBackupHistory -Database 'StackOverflow2010' | Sort-Object End -Descending | Select-Object -First 1</code></pre>



<h2 class="wp-block-heading" id="h-final-thoughts-taking-control-of-the-always-on-fleet">Final Thoughts: Taking Control of the Always-On Fleet</h2>



<p class="wp-block-paragraph">To wrap up, remember that technical skills are only as good as the management strategy behind them. Transitioning away from legacy methods toward a PowerShell-driven approach is about gaining control over your environment. Here is what you should keep in mind:</p>



<ul class="wp-block-list">
<li><strong>Beyond T-SQL Boundaries:</strong> While <em>Linked Servers</em> or <em>OPENDATASOURCE</em> might work for quick fixes, they quickly become bottlenecks and security risks in hardened infrastructures.</li>



<li><strong>Object-Oriented Efficiency:</strong> By using PowerShell and <em>dbatools</em>, you stop managing raw text and start handling objects. This allows you to effortlessly filter, sort, and aggregate data from multiple Always-On nodes to extract a single, reliable source of truth.</li>



<li><strong>Smarter Security:</strong> Running queries externally via dedicated management shells ensures you maintain a high security posture without needing to enable high-risk surface area features on your SQL instances.</li>
</ul>



<p class="wp-block-paragraph"><strong>The real game-changer is the Central Management Server.</strong> By centralizing your logic on a dedicated administration machine, you stop scattering scripts across every instance. This server becomes your orchestrator: it pulls from your inventory (<em>CMDB</em>, central tables), broadcasts tasks across your entire fleet, and consolidates the results. This is exactly the approach we take at <strong>dbi services</strong> to manage the extensive SQL Server environments under our care. We leverage PowerShell and the <em>dbatools </em>module as the backbone of our scripting architecture. This allows us to collect data in the most comprehensive and optimized way possible, ensuring we deliver top-tier service to our clients.</p>



<p class="wp-block-paragraph">This is how you move from artisanal, server-by-server management to an industrial-grade automation capable of piloting hundreds of instances with the same simplicity as a single one.</p>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-always-on-centralizing-backup-history-across-replicas/">SQL Server Always-On: Centralizing Backup History Across Replicas</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/sql-server-always-on-centralizing-backup-history-across-replicas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Data Anonymization as a Service with Delphix Continuous Compliance</title>
		<link>https://www.dbi-services.com/blog/data-anonymization-as-a-service-with-delphix-continuous-compliance/</link>
					<comments>https://www.dbi-services.com/blog/data-anonymization-as-a-service-with-delphix-continuous-compliance/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Mon, 22 Dec 2025 10:43:15 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[Delphix]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[anonymization]]></category>
		<category><![CDATA[compliance]]></category>
		<category><![CDATA[delphix]]></category>
		<category><![CDATA[GDPR]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=41856</guid>

					<description><![CDATA[<p>Context In the era of digital transformation, attack surfaces are constantly evolving and cyberattack techniques are becoming increasingly sophisticated. Maintaining the confidentiality, integrity, and availability of data is therefore a critical challenge for organizations, both from an operational and a regulatory standpoint (GDPR, ISO 27001, NIST). Therefore, data anonymization is crucial today. Contrary to a [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/data-anonymization-as-a-service-with-delphix-continuous-compliance/">Data Anonymization as a Service with Delphix Continuous Compliance</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="h-context">Context</h2>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph">In the era of digital transformation, attack surfaces are constantly evolving and cyberattack techniques are becoming increasingly sophisticated. Maintaining the confidentiality, integrity, and availability of data is therefore a critical challenge for organizations, both from an operational and a regulatory standpoint (GDPR, ISO 27001, NIST). Therefore, data anonymization is crucial today.</p>



<p class="wp-block-paragraph">Contrary to a widely held belief, the risk is not limited to the production environment. Development, testing, and pre-production environments are prime targets for attackers, as they often benefit from weaker security controls. The use of production data that is neither anonymized nor pseudonymized directly exposes organizations to data breaches, regulatory non-compliance, and legal sanctions.</p>
</div></div>
</div></div>



<h2 class="wp-block-heading" id="h-why-and-how-to-anonymize-data">Why and How to Anonymize Data</h2>



<p class="wp-block-paragraph">Development teams require realistic datasets in order to:</p>



<ul class="wp-block-list">
<li>Test application performance</li>



<li>Validate complex business processes</li>



<li>Reproduce error scenarios</li>



<li>Train Business Intelligence or Machine Learning algorithms</li>
</ul>



<p class="wp-block-paragraph">However, the use of real data requires the implementation of anonymization or pseudonymization mechanisms ensuring:</p>



<ul class="wp-block-list">
<li>Preservation of functional and referential consistency</li>



<li>Prevention of data subject re-identification</li>
</ul>



<p class="wp-block-paragraph">Among the possible anonymization techniques, the main ones include:</p>



<ul class="wp-block-list">
<li><strong>Dynamic Data Masking</strong>, applied on-the-fly at access time but which does not anonymize data physically</li>



<li><strong>Tokenization</strong>, which replaces a value with a surrogate identifier</li>



<li><strong>Cryptographic hashing</strong>, with or without salting</li>
</ul>



<h2 class="wp-block-heading" id="h-direct-data-copy-from-production-to-development">Direct data copy from Production to Development</h2>



<p class="wp-block-paragraph">In this scenario, a full backup of the production database is restored into a development environment. Anonymization is then applied using manually developed SQL scripts or ETL processes.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="848" height="267" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-7.png" alt="" class="wp-image-41868" style="width:592px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-7.png 848w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-7-300x94.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-7-768x242.png 768w" sizes="auto, (max-width: 848px) 100vw, 848px" /></figure>
</div>


<p class="wp-block-paragraph">This approach presents several critical weaknesses:</p>



<ul class="wp-block-list">
<li>Temporary exposure of personal data in clear text</li>



<li>Lack of formal traceability of anonymization processes</li>



<li>Risk of human error in scripts</li>



<li>Non-compliance with GDPR requirements</li>
</ul>



<p class="wp-block-paragraph">This model should therefore be avoided in regulated environments.</p>



<h2 class="wp-block-heading" id="h-data-copy-via-a-staging-database-in-production">Data copy via a Staging Database in Production</h2>



<p class="wp-block-paragraph">This model introduces an intermediate staging database located within a security perimeter equivalent to that of production. Anonymization is performed within this secure zone before replication to non-production environments.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="887" height="337" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-8.png" alt="" class="wp-image-41869" style="width:621px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-8.png 887w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-8-300x114.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-8-768x292.png 768w" sizes="auto, (max-width: 887px) 100vw, 887px" /></figure>
</div>


<p class="wp-block-paragraph">This approach makes it possible to:</p>



<ul class="wp-block-list">
<li>Ensure that no sensitive data in clear text leaves the secure perimeter</li>



<li>Centralize anonymization rules</li>



<li>Improve overall data governance</li>
</ul>



<p class="wp-block-paragraph">However, several challenges remain:</p>



<ul class="wp-block-list">
<li>Versioning and auditability of transformation rules</li>



<li>Governance of responsibilities between teams (DBAs, security, business units)</li>



<li>Maintaining inter-table referential integrity</li>



<li>Performance management during large-scale anonymization</li>
</ul>



<h2 class="wp-block-heading" id="h-integration-of-delphix-continuous-compliance">Integration of Delphix Continuous Compliance</h2>



<p class="wp-block-paragraph">In this architecture, <code><a href="https://cd.delphix.com/docs/latest/overview">Delphix</a></code> is integrated as the central engine for data virtualization and anonymization. The <em><a href="https://help.delphix.com/cc/">Continuous Compliance</a></em> module enables process industrialization through:</p>



<ul class="wp-block-list">
<li>An automated data profiler identifying sensitive fields</li>



<li>Deterministic or non-deterministic anonymization algorithms</li>



<li>Massively parallelized execution</li>



<li>Orchestration via REST APIs integrable into CI/CD pipelines</li>



<li>Full traceability of processing for audit purposes</li>
</ul>



<p class="wp-block-paragraph">This approach enables the rapid provisioning of compliant, reproducible, and secure databases for all technical teams.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="915" height="394" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-27.png" alt="" class="wp-image-42155" style="aspect-ratio:2.3224468636599274;width:653px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-27.png 915w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-27-300x129.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-27-768x331.png 768w" sizes="auto, (max-width: 915px) 100vw, 915px" /></figure>
</div>


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



<p class="wp-block-paragraph">Database anonymization should no longer be viewed as a one-time constraint but as a structuring process within the data lifecycle. It is based on three fundamental pillars:</p>



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



<li>Pipeline industrialization</li>



<li>Regulatory compliance</li>
</ul>



<p class="wp-block-paragraph">An in-house implementation is possible, but it requires a high level of organizational maturity, strong skills in anonymization algorithms, data engineering, and security, as well as a strict audit framework. Solutions such as Delphix provide an industrialized response to these challenges while reducing both operational and regulatory risks. </p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="535" height="438" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-28.png" alt="" class="wp-image-42156" style="aspect-ratio:1.2215047625842816;width:388px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-28.png 535w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-28-300x246.png 300w" sizes="auto, (max-width: 535px) 100vw, 535px" /></figure>
</div>


<p class="wp-block-paragraph">To take this further, Microsoft&#8217;s article explaining the integration of Delphix into Azure pipelines analyzes the same issues discussed above, but this time in the context of the cloud : <a href="https://learn.microsoft.com/en-us/azure/architecture/databases/guide/data-obfuscation-with-delphix-in-azure-data-factory">Use Delphix for Data Masking in Azure Data Factory and Azure Synapse Analytics</a></p>



<h2 class="wp-block-heading" id="h-what-s-next">What&#8217;s next ?</h2>



<p class="wp-block-paragraph">This use case is just one example of how Delphix can be leveraged to optimize data management and compliance in complex environments. In upcoming articles, we will explore other recurring challenges, highlighting both possible in-house approaches and industrialized solutions with Delphix, to provide a broader technical perspective on data virtualization, security, and performance optimization.</p>



<h2 class="wp-block-heading" id="h-what-about-you">What about you ?</h2>



<p class="wp-block-paragraph">How confident are you about the management of your confidential data? <br>If you have any doubts, please don’t hesitate to reach out to me to discuss them !</p>
<p>L’article <a href="https://www.dbi-services.com/blog/data-anonymization-as-a-service-with-delphix-continuous-compliance/">Data Anonymization as a Service with Delphix Continuous Compliance</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/data-anonymization-as-a-service-with-delphix-continuous-compliance/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The truth about nested transactions in SQL Server</title>
		<link>https://www.dbi-services.com/blog/the-truth-about-nested-transactions-in-sql-server/</link>
					<comments>https://www.dbi-services.com/blog/the-truth-about-nested-transactions-in-sql-server/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Mon, 15 Dec 2025 10:42:51 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[nested transactions]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=41965</guid>

					<description><![CDATA[<p>Nested transactions in SQL Server don’t provide real isolation; @@TRANCOUNT rises, but only the main transaction controls locks.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/the-truth-about-nested-transactions-in-sql-server/">The truth about nested transactions in SQL Server</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Working with transactions in SQL Server can feel like navigating a maze blindfolded. On paper, nested transactions look simple enough, start one, start another, commit them both, but under the hood, SQL Server plays by a very different set of rules. And that’s exactly where developers get trapped.</p>



<p class="wp-block-paragraph">In this post, we’re going to look at what really happens when you try to use nested transactions in SQL Server. We’ll walk through a dead-simple demo, expose why <code>@@TRANCOUNT</code> is more illusion than isolation, and see how a single rollback can quietly unravel your entire call chain. If you’ve ever assumed nested transactions can behave the same way as in Oracle for example, this might clarify a few things you didn’t expect !</p>



<h2 class="wp-block-heading" id="h-practical-example">Practical example</h2>



<p class="wp-block-paragraph">Before diving into the demonstration, let’s set up a simple table in <code>tempdb </code>and illustrate how nested transactions behave in SQL Server.</p>



<pre class="wp-block-code"><code>IF OBJECT_ID('tempdb..##DemoLocks') IS NOT NULL
    DROP TABLE ##DemoLocks;

CREATE TABLE ##DemoLocks (id INT IDENTITY, text VARCHAR(50));

BEGIN TRAN MainTran;

BEGIN TRAN InnerTran;
INSERT INTO ##DemoLocks (text) VALUES ('I''m just a speedy insert ! Nothing to worry about');
COMMIT TRAN InnerTran;

WAITFOR DELAY '00:00:10';

ROLLBACK TRAN MainTran;</code></pre>



<p class="wp-block-paragraph">Let’s see how locks behave after committing the nested transaction and entering the <code>WAITFOR </code>phase. If nested transactions provided isolation between each other, no locks should remain since the transaction no longer works on any object. The following query shows all locks associated with my query specifically and the <code>##Demolocks</code> table we are working on. </p>



<pre class="wp-block-code"><code>SELECT 
    l.request_session_id AS SPID,
    r.blocking_session_id AS BlockingSPID,
    resource_associated_entity_id,
    DB_NAME(l.resource_database_id) AS DatabaseName,
    OBJECT_NAME(p.object_id) AS ObjectName,
    l.resource_type AS ResourceType,
    l.resource_description AS ResourceDescription,
    l.request_mode AS LockMode,
    l.request_status AS LockStatus,
    t.text AS SQLText
FROM sys.dm_tran_locks l
LEFT JOIN sys.dm_exec_requests r
    ON l.request_session_id = r.session_id
LEFT JOIN sys.partitions p
    ON l.resource_associated_entity_id = p.hobt_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t
where t.text like 'IF OBJECT%'
    and OBJECT_NAME(p.object_id) = '##DemoLocks'
ORDER BY l.request_session_id, l.resource_type;</code></pre>



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



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="90" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-26-1024x90.png" alt="" class="wp-image-41967" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-26-1024x90.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-26-300x26.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-26-768x67.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-26.png 1371w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">All of this was just smoke and mirrors !<br>We clearly see in the image 2 persistent locks of different types:</p>



<ul class="wp-block-list">
<li>LockMode IX: Intent lock on a data page of the <code>##DemoLocks</code> table. This indicates that a lock is active on one of its sub-elements to optimize the engine’s lock checks.</li>



<li>LockMode X: Exclusive lock on a <code>RID </code>(Row Identifier) for data writing (here, our <code>INSERT</code>).<br><br>For more on locks and their usage : <a href="https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-tran-locks-transact-sql?view=sql-server-ver17">sys.dm_tran_locks (Transact-SQL) &#8211; SQL Server | Microsoft Learn</a></li>
</ul>



<p class="wp-block-paragraph">In conclusion, SQL Server does not allow nested transactions to maintain isolation from each other, and causes nested transactions to remain dependent on their main transaction, which prevents the release of locks. Therefore, the rollback of <code>MainTran</code> causes the above query to leave the table <span style="text-decoration: underline"><strong>empty</strong></span>, even with a <code>COMMIT</code> at the nested transaction level. This behavior still respects the ACID properties (Atomicity, Consistency, Isolation, and Durability), which are crucial for maintaining data validity and reliability in database management systems.</p>



<p class="wp-block-paragraph">Now that we have shown that nested transactions have no useful effect on lock management and isolation, let’s see if they have even worse consequences. To do this, let’s create the following code and observe how SQL Server behaves under intensive nested transaction creation. This time, we will add SQL Server’s native <code><a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/trancount-transact-sql?view=sql-server-ver17">@@TRANCOUNT</a></code> variable, which allows us to analyze the number of open transactions currently in progress.</p>



<pre class="wp-block-code"><code> CREATE PROCEDURE dbo.NestedProc
    @level INT
AS
BEGIN
    BEGIN TRANSACTION;

    PRINT 'Level ' + CAST(@level AS VARCHAR(3)) + ', @@TRANCOUNT = ' + CAST(@@TRANCOUNT AS VARCHAR(3));

    IF @level &lt; 100
    BEGIN
        SET @level += 1
        EXEC dbo.NestedProc @level;
    END

    COMMIT TRANSACTION;
END
GO

EXEC dbo.NestedProc 1;</code></pre>



<p class="wp-block-paragraph">This procedure recursively creates 100 nested transactions, if we manage to go that far… Let’s look at the output.</p>



<pre class="wp-block-code"><code>Level 1, @@TRANCOUNT = 1
&#091;...]
Level 32, @@TRANCOUNT = 32

Msg 217, Level 16, State 1, Procedure dbo.NestedProc, Line 12 &#091;Batch Start Line 15]
Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32).</code></pre>



<p class="wp-block-paragraph">Indeed, SQL Server imposes various <a href="https://learn.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server?view=sql-server-ver17">limitations </a>on nested transactions which imply that if they are mismanaged, the application may suddenly suffer a killed query, which can be very dangerous. These limitations are in place to act as safeguards against infinite nesting loops of nested transactions.<br>Furthermore, we see that <code>@@TRANCOUNT</code> increments with each new <code>BEGIN TRANSACTION</code>, but it does not reflect the true number of active main transactions; i.e., there are 32 transactions ongoing but only 1 can actually release locks.</p>



<h2 class="wp-block-heading" id="h-ok-but-we-still-didn-t-see-what-a-real-nested-transaction-would-look-like">Ok, but we still didn&#8217;t see what a real nested transaction would look like !</h2>



<p class="wp-block-paragraph" id="h-ok-but-we-still-didn-t-see-any-real-nested-transaction">I understand, we cannot stop here. I need to go get my old Oracle VM from my garage and fire it up.<br>Oracle has a pragma called <a href="https://docs.oracle.com/cd/B13789_01/appdev.101/b10807/13_elems002.htm">AUTONOMOUS_TRANSACTION</a> that allows creating independent transactions inside a main transaction. Let’s see this in action with a small code snippet.</p>



<pre class="wp-block-code"><code>CREATE TABLE test_autonomous (
    id NUMBER PRIMARY KEY,
    msg VARCHAR2(100)
);
/

CREATE OR REPLACE PROCEDURE auton_proc IS
    PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
    INSERT INTO test_autonomous (id, msg) VALUES (2, 'Autonomous transaction');
    COMMIT;
END;
/

CREATE OR REPLACE PROCEDURE main_proc IS
BEGIN
    INSERT INTO test_autonomous (id, msg) VALUES (1, 'Main transaction');
    auton_proc;
    ROLLBACK;
END;
/</code></pre>



<p class="wp-block-paragraph">In this code, we create two procedures:</p>



<ul class="wp-block-list">
<li><code>main_proc</code>, the main procedure, inserts the first row into the table.</li>
</ul>



<ul class="wp-block-list">
<li><code>auton_proc</code>, called by main_proc, adds a second row to the table.</li>
</ul>



<p class="wp-block-paragraph"><code>auton_proc </code>is committed while <code>main_proc </code>is rolled back. Let’s observe the result:</p>



<pre class="wp-block-code"><code>SQL&gt; SELECT * FROM test_auton;

        ID MSG
---------- --------------------------------------------------
         2 Autonomous transaction</code></pre>



<p class="wp-block-paragraph">Now, that is true isolation between transactions! Here, the inner transaction achieves transactional independence and can persist regardless of the fate of its main transaction. While autonomous transactions in Oracle are not qualified as purely nested because they do not share a common locking context, they serve the same architectural purpose as the one we are demonstrating in this article: allowing a sub-unit of work to decouple its success from the parent flow.</p>



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



<p class="wp-block-paragraph">In summary, SQL Server and Oracle can handle transactions in different ways. In SQL Server, <em>nested transactions</em> do not create real isolation: <code>@@TRANCOUNT</code> may increase, but a single main transaction actually controls locks and the persistence of changes. Internal limits, like the maximum nesting of 32 procedures, show that excessive nested transactions can cause critical errors.</p>



<p class="wp-block-paragraph">In contrast, Oracle, through <code>PRAGMA AUTONOMOUS_TRANSACTION</code>, allows for truly independent transactions launched from within a main flow. These autonomous transactions can be committed or rolled back without affecting the parent transaction, providing a practical mechanism to decouple specific tasks from the main transactional outcome.</p>



<p class="wp-block-paragraph">As <a href="https://www.brentozar.com/archive/2023/02/can-you-nest-transactions-in-sql-server/">Brent Ozar</a> points out, SQL Server also has a <code>SAVE TRANSACTION</code> <a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/save-transaction-transact-sql?view=sql-server-ver17">command</a>, which allows you to save a state after a nested transaction has been committed, for example. This command therefore provides more flexibility in managing nested transactions but does not provide complete isolation of sub-transactions. Furthermore, as Brent Ozar emphasizes, this command is complex and requires careful analysis of its behavior and the consequences it entails.<br>Another approach to bypass SQL Server’s nested-transaction limitations is to manage transaction coordination directly at the application level, where each logical unit of work can be handled independently.</p>



<p class="wp-block-paragraph">The lesson is clear: appearances can be deceiving! Understanding the actual behavior of transactions in each DBMS is crucial for designing reliable applications and avoiding unpleasant surprises.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/the-truth-about-nested-transactions-in-sql-server/">The truth about nested transactions in SQL Server</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/the-truth-about-nested-transactions-in-sql-server/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Understanding XML performance pitfalls in SQL Server</title>
		<link>https://www.dbi-services.com/blog/sql-server-xml-performance-explained/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-xml-performance-explained/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Thu, 11 Dec 2025 20:38:55 +0000</pubDate>
				<category><![CDATA[Development & Performance]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[XML]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=41897</guid>

					<description><![CDATA[<p>A deep dive into how SQL Server processes XML, the performance impact of XQuery methods, and why proper indexing matters.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-xml-performance-explained/">Understanding XML performance pitfalls in SQL Server</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="h-context">Context</h2>



<p class="wp-block-paragraph">Working with XML in SQL Server can feel like taming a wild beast. It’s undeniably flexible and great for storing complex hierarchical data, but when it comes to querying efficiently, many developers hit a wall. That’s where things get interesting.</p>



<p class="wp-block-paragraph">In this post, we’ll dive into a real-world scenario with half a million rows, put two XML query methods head-to-head <code>.exist()</code> vs <code>.value()</code>, and uncover how SQL Server handles them under the scenes.</p>



<h2 class="wp-block-heading" id="h-practical-example">Practical example</h2>



<p class="wp-block-paragraph">To demonstrate this, we’ll use SQL Server 2022 Developer Edition and create a table based on the open-source <a href="https://www.brentozar.com/archive/2021/03/download-the-current-stack-overflow-database-for-free-2021-02/">StackOverflow2010 database</a>, derived from the Posts table, but storing part of the original data in XML format. We will also add a few indexes to simulate an environment with a minimum level of optimization.</p>



<pre class="wp-block-code"><code>CREATE TABLE dbo.PostsXmlPerf
(
    PostId        INT           NOT NULL PRIMARY KEY,
    PostTypeId    INT           NOT NULL,
    CreationDate  DATETIME      NOT NULL,
    Score         INT           NOT NULL,
    Body          NVARCHAR(MAX) NOT NULL,
    MetadataXml   XML           NOT NULL
);

INSERT INTO dbo.PostsXmlPerf (PostId, PostTypeId, CreationDate, Score, Body, MetadataXml)
SELECT TOP (500000)
       p.Id,
       p.PostTypeId,
       p.CreationDate,
       p.Score,
       p.Body,
       (
           SELECT  
               p.OwnerUserId     AS &#091;@OwnerUserId],
               p.LastEditorUserId AS &#091;@LastEditorUserId],
               p.AnswerCount     AS &#091;@AnswerCount],
               p.CommentCount    AS &#091;@CommentCount],
               p.FavoriteCount   AS &#091;@FavoriteCount],
               p.ViewCount       AS &#091;@ViewCount],
               (
                   SELECT TOP (5)
                          c.Id           AS &#091;Comment/@Id],
                          c.Score        AS &#091;Comment/@Score],
                          c.CreationDate AS &#091;Comment/@CreationDate]
                   FROM dbo.Comments c
                   WHERE c.PostId = p.Id
                   FOR XML PATH(''), TYPE
               )
           FOR XML PATH('PostMeta'), TYPE
       )
FROM dbo.Posts p
ORDER BY p.Id;

CREATE nonclustered INDEX IX_PostsXmlPerf_CreationDate
ON dbo.PostsXmlPerf (CreationDate);

CREATE nonclustered INDEX IX_PostsXmlPerf_PostTypeId
ON dbo.PostsXmlPerf (PostTypeId);</code></pre>



<p class="wp-block-paragraph">Next, let’s create two queries designed to interrogate the column that contains XML data, in order to extract information based on a condition applied to a value stored within that XML.</p>



<pre class="wp-block-code"><code>SET STATISTICS IO ON;
SET STATISTICS TIME ON;

DBCC FREEPROCCACHE;

SELECT PostId, Score
FROM dbo.PostsXmlPerf
WHERE MetadataXml.exist('/PostMeta&#091;@OwnerUserId="8"]') = 1;</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="406" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-22-1024x406.png" alt="" class="wp-image-41915" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-22-1024x406.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-22-300x119.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-22-768x305.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-22.png 1457w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<pre class="wp-block-code"><code>DBCC FREEPROCCACHE;

SELECT PostId, Score
FROM dbo.PostsXmlPerf
WHERE MetadataXml.value('(/PostMeta/@OwnerUserId)&#091;1]', 'INT') = 8</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="284" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-23-1024x284.png" alt="" class="wp-image-41916" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-23-1024x284.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-23-300x83.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-23-768x213.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-23.png 1450w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Comparing logical and physical reads, we notice something interesting:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td></td><td>Logical Reads</td><td>CPU Time</td></tr><tr><td>.exist()</td><td>125&#8217;912</td><td>00:00:05.954</td></tr><tr><td>.value()</td><td>125&#8217;912</td><td>00:00:03.125</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">At first glance, the number of pages read is identical, but .exist() is clearly taking more time. Why? Execution plans reveal that .exist() sneaks in a Merge Join, adding overhead.<br>Additionally, on both execution plans we can see a small yellow bang icon. On the first plan, it’s just a memory grant warning, but the second one is more interesting:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="483" height="179" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-19.png" alt="" class="wp-image-41910" style="width:450px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-19.png 483w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-19-300x111.png 300w" sizes="auto, (max-width: 483px) 100vw, 483px" /></figure>
</div>


<p class="wp-block-paragraph">Alright, a bit strange ; let’s move forward with some tuning and maybe this warning will disappear.<br>To help with querying, it can be useful to create a more targeted index for XML queries.<br>Let’s create an index on the column that contains XML. However, as you might expect, it’s not as straightforward as indexing a regular column. For an XML column, you first need to create a primary XML index, which physically indexes the overall structure of the column (similar to a clustered index), and then a secondary XML index, which builds on the primary index and is optimized for a specific type of query (value, path, or property) &#8211; to know more about XML indexes : <a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/create-xml-index-transact-sql?view=sql-server-ver17">Microsoft Learn</a>, <a href="http://mssqltips.com/tutorial/sql-server-xml-i">MssqlTips</a>. <br>So, let&#8217;s create these indexes !</p>



<pre class="wp-block-code"><code>CREATE PRIMARY XML INDEX IX_XML_Primary_MetadataXml
ON dbo.PostsXmlPerf (MetadataXml);

CREATE XML INDEX IX_XML_Value_MetadataXml
ON dbo.PostsXmlPerf (MetadataXml)
USING XML INDEX IX_XML_Primary_MetadataXml FOR Value;</code></pre>



<p class="wp-block-paragraph">Let’s rerun the performance tests with our two queries above, making sure to flush the buffer cache between each execution.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="310" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-24-1024x310.png" alt="" class="wp-image-41917" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-24-1024x310.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-24-300x91.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-24-768x232.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-24.png 1379w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="296" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-25-1024x296.png" alt="" class="wp-image-41918" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-25-1024x296.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-25-300x87.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-25-768x222.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/12/image-25.png 1458w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>﻿</td><td>Logical Reads</td><td>CPU Time</td></tr><tr><td>.exist()</td><td>4</td><td>00:00:00.031</td></tr><tr><td>.value()</td><td>125&#8217;912</td><td>00:00:03.937</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The inevitable happened: the implicit conversion makes it impossible to use the secondary XML index due to a data type mismatch, preventing an actual seek on it. We do see a seek in the second execution plan, but it occurs for every row in the table (500&#8217;000 executions) and is essentially just accessing the underlying physical structure stored in the clustered index. In reality, this &#8216;seek&#8217; is SQL Server scanning the XML to retrieve the exact value of the requested field (in this case, <em>OwnerUserId</em>).<br>This conversion issue occurs because the <a href="https://learn.microsoft.com/en-us/sql/t-sql/xml/exist-method-xml-data-type?view=sql-server-ver17">function .exist()</a> returns a BIT, while the <a href="https://learn.microsoft.com/en-us/sql/t-sql/xml/value-method-xml-data-type?view=sql-server-ver17">function .value()</a> returns a SQL type.<br>This difference in return type can lead to significant performance problems when tuning queries that involve XML. <br>As explained by <a href="https://learn.microsoft.com/en-us/sql/t-sql/xml/value-method-xml-data-type?view=sql-server-ver17">Microsoft</a>: <em>&#8220;For performance reasons, instead of using the&nbsp;<code>value()</code>&nbsp;method in a predicate to compare with a relational value, use&nbsp;<code>exist()</code>&nbsp;with&nbsp;<code>sql:column()</code>&#8220;</em> </p>



<h2 class="wp-block-heading" id="h-key-take-aways">Key take-aways</h2>



<p class="wp-block-paragraph">Working with XML in SQL Server can be powerful, but it can quickly become tricky to manage. <code>.exist()</code> and <code>.value()</code> might seem similar, but execution differences and type conversions can have a huge performance impact. Proper XML indexing is essential, and knowing your returned data types can save you from hours of head-scratching. Most importantly, before deciding to store data as XML, consider whether it’s truly necessary ; relational databases are not natively optimized for XML and can introduce complexity and performance challenges.</p>



<p class="wp-block-paragraph">Sometimes, a simpler and highly effective approach is to extract frequently queried XML fields at the application level and store them in separate columns. This makes them much easier to index and query, reducing overhead while keeping your data accessible.</p>



<p class="wp-block-paragraph">If your application relies heavily on semi-structured data or large volumes of XML/JSON, it’s worth considering alternative engines. For instance, <strong>MongoDB</strong> provides native document storage and fast queries on JSON/BSON, while <strong>PostgreSQL</strong> offers XML and JSONB support with powerful querying functions. Choosing the right tool for the job can simplify your architecture and significantly improve performance.</p>



<p class="wp-block-paragraph">And to dive even deeper into the topic, with a forthcoming article focused this time on XML storage, keep an eye on the <strong>dbi services</strong> blogs !</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-xml-performance-explained/">Understanding XML performance pitfalls in SQL Server</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/sql-server-xml-performance-explained/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 
Lazy Loading (feed)

Served from: www.dbi-services.com @ 2026-06-13 20:22:40 by W3 Total Cache
-->