<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marcus</title>
    <description>The latest articles on DEV Community by Marcus (@marcus1968).</description>
    <link>https://dev.to/marcus1968</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4009616%2F12158dea-c717-41b8-be68-130eacdf65ed.jpg</url>
      <title>DEV Community: Marcus</title>
      <link>https://dev.to/marcus1968</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcus1968"/>
    <language>en</language>
    <item>
      <title>Reading Complex XML in SQL Server — Shredding a Thousand Attributes Generically Instead of Looping for Hours</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sat, 31 Oct 2026 18:32:48 +0000</pubDate>
      <link>https://dev.to/marcus1968/reading-complex-xml-in-sql-server-shredding-a-thousand-attributes-generically-instead-of-looping-5be0</link>
      <guid>https://dev.to/marcus1968/reading-complex-xml-in-sql-server-shredding-a-thousand-attributes-generically-instead-of-looping-5be0</guid>
      <description>&lt;p&gt;A catalog of around 3000 product configurations arrives as XML. Depending on the equipment, a configuration carries different and additional attributes, around a thousand across all variants. In an earlier project, the evaluation of such a catalog ran in application code: a loop over the object types, inside it a loop over the attributes, plus lists in memory that grew with every iteration. The program took several hours. Where exactly the time went can no longer be said in hindsight. What is certain is what the design demanded: code per type, a separate lookup per attribute, and a schema that had to be in your head beforehand. This article shows how to &lt;strong&gt;shred XML in SQL Server&lt;/strong&gt; without knowing the attributes in advance: load it raw, shred it into a path/value table that holds one row per value with its path in the document, read the schema off the data, and let the database generate the views per type. In the measurement of this article that takes not hours but around 15 seconds for a catalog of the same size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why loops over object types and attributes get so expensive with a thousand attributes, and what the database route does differently: the attributes become data rows instead of code.&lt;/li&gt;
&lt;li&gt;How the catalog is loaded into SQL Server as a single XML value and then split into 3000 table rows, one per configuration. From these rows, a query in which no attribute name appears fetches every value together with its path in the document, for example &lt;code&gt;/Drivetrain/Gears&lt;/code&gt; with the value &lt;code&gt;14&lt;/code&gt;. The result is 1.5 million such rows of path and value, and the whole route takes around 15 seconds in the measurement instead of hours.&lt;/li&gt;
&lt;li&gt;How an inventory built from these rows makes required, optional and variant attributes per type readable, without anyone having known the attributes beforehand.&lt;/li&gt;
&lt;li&gt;How the database generates views with more than 600 columns from the inventory itself, and where the limits of the method are: column limits, typing, very large files. What it requires is little: the element of the sub-documents, an upper bound for the depth, and lists that repeat uniformly in at least one configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Docker and SQL Server 2022 as a container (Developer Edition). The scripts run with &lt;code&gt;sqlcmd&lt;/code&gt;, the command-line client that ships with the container. The call is at the start of the section "Load raw". The sample catalog, its generator and the scripts come in four downloads at the end of the article: the XML route and the JSON route, each for SQL Server and for Postgres. Postgres 18.6 serves for the bridge at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catalog: three types, one drivetrain with two faces
&lt;/h2&gt;

&lt;p&gt;The example is a bicycle configurator with 3000 configurations. There are three object types: road bike, trekking bike and cargo bike. The three types share the groups &lt;code&gt;General&lt;/code&gt;, &lt;code&gt;Frame&lt;/code&gt;, &lt;code&gt;Brakes&lt;/code&gt; and &lt;code&gt;Wheels&lt;/code&gt;, and already here they resemble each other more than they differ. What the &lt;code&gt;Drivetrain&lt;/code&gt; contains depends on its XML attribute &lt;code&gt;kind&lt;/code&gt;. With &lt;code&gt;kind="chain"&lt;/code&gt; it holds chainrings, sprockets, rear derailleur, front derailleur, gear ratios and chainline. With &lt;code&gt;kind="hub"&lt;/code&gt; it holds gears, gear range, belt, hub type and coaster brake. The same group, but different attributes depending on the variant. The &lt;code&gt;Brakes&lt;/code&gt; also carry their design as an XML attribute, &lt;code&gt;type&lt;/code&gt;, and not as a child element. &lt;code&gt;Lighting&lt;/code&gt; and &lt;code&gt;Suspension&lt;/code&gt; are optional groups that a road bike rarely has and a cargo bike almost always. Two groups are lists. &lt;code&gt;Accessories&lt;/code&gt; contains zero to four &lt;code&gt;Part&lt;/code&gt; elements with name, weight and a standard-equipment flag. &lt;code&gt;Wheels&lt;/code&gt; always contains two &lt;code&gt;Wheel&lt;/code&gt; elements, one for the front and one for the rear, with rim, tyre and spoke count.&lt;/p&gt;

&lt;p&gt;"Attribute" in this article means a business characteristic of a configuration, such as the number of gears or the material of the frame. In the XML such a characteristic is usually an element and only rarely an XML attribute. Where the XML construct is meant, the text says XML attribute explicitly.&lt;/p&gt;

&lt;p&gt;To reach the order of a thousand attributes, every configuration additionally carries twelve of twenty parameter groups with 48 values each, every single one of which is present with a probability of eighty percent. These groups are simply called &lt;code&gt;G01&lt;/code&gt; to &lt;code&gt;G20&lt;/code&gt; and are filler. They are there so that the measurements have the right order of magnitude, not because they mean anything. Which type carries which groups is distributed so that the types overlap but do not coincide.&lt;/p&gt;

&lt;p&gt;This is what one configuration looks like, shortened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;  1: &lt;span class="nt"&gt;&amp;lt;Configuration&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"C000001"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"cargo_bike"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  2:   &lt;span class="nt"&gt;&amp;lt;General&amp;gt;&lt;/span&gt;
  3:     &lt;span class="nt"&gt;&amp;lt;Model&amp;gt;&lt;/span&gt;CargoBike-214&lt;span class="nt"&gt;&amp;lt;/Model&amp;gt;&lt;/span&gt;
  4:     &lt;span class="nt"&gt;&amp;lt;ModelYear&amp;gt;&lt;/span&gt;2019&lt;span class="nt"&gt;&amp;lt;/ModelYear&amp;gt;&lt;/span&gt;
  5:     &lt;span class="nt"&gt;&amp;lt;Weight&amp;gt;&lt;/span&gt;13.25&lt;span class="nt"&gt;&amp;lt;/Weight&amp;gt;&lt;/span&gt;
  6:     &lt;span class="nt"&gt;&amp;lt;Price&amp;gt;&lt;/span&gt;4576.94&lt;span class="nt"&gt;&amp;lt;/Price&amp;gt;&lt;/span&gt;
  7:   &lt;span class="nt"&gt;&amp;lt;/General&amp;gt;&lt;/span&gt;
  8:   &lt;span class="nt"&gt;&amp;lt;Frame&amp;gt;&lt;/span&gt;
  9:     &lt;span class="nt"&gt;&amp;lt;Material&amp;gt;&lt;/span&gt;Aluminum&lt;span class="nt"&gt;&amp;lt;/Material&amp;gt;&lt;/span&gt;
 10:     &lt;span class="nt"&gt;&amp;lt;Size&amp;gt;&lt;/span&gt;56&lt;span class="nt"&gt;&amp;lt;/Size&amp;gt;&lt;/span&gt;
 11:   &lt;span class="nt"&gt;&amp;lt;/Frame&amp;gt;&lt;/span&gt;
 12:   &lt;span class="nt"&gt;&amp;lt;Drivetrain&lt;/span&gt; &lt;span class="na"&gt;kind=&lt;/span&gt;&lt;span class="s"&gt;"hub"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
 13:     &lt;span class="nt"&gt;&amp;lt;Gears&amp;gt;&lt;/span&gt;14&lt;span class="nt"&gt;&amp;lt;/Gears&amp;gt;&lt;/span&gt;
 14:     &lt;span class="nt"&gt;&amp;lt;GearRange&amp;gt;&lt;/span&gt;371&lt;span class="nt"&gt;&amp;lt;/GearRange&amp;gt;&lt;/span&gt;
 15:     &lt;span class="nt"&gt;&amp;lt;Belt&amp;gt;&lt;/span&gt;yes&lt;span class="nt"&gt;&amp;lt;/Belt&amp;gt;&lt;/span&gt;
 16:   &lt;span class="nt"&gt;&amp;lt;/Drivetrain&amp;gt;&lt;/span&gt;
 17:   &lt;span class="nt"&gt;&amp;lt;Brakes&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"Disc hydraulic"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
 18:     &lt;span class="nt"&gt;&amp;lt;Rotor_front&amp;gt;&lt;/span&gt;160&lt;span class="nt"&gt;&amp;lt;/Rotor_front&amp;gt;&lt;/span&gt;
 19:     &lt;span class="nt"&gt;&amp;lt;Pads&amp;gt;&lt;/span&gt;sintered&lt;span class="nt"&gt;&amp;lt;/Pads&amp;gt;&lt;/span&gt;
 20:   &lt;span class="nt"&gt;&amp;lt;/Brakes&amp;gt;&lt;/span&gt;
 21:   &lt;span class="nt"&gt;&amp;lt;Wheels&amp;gt;&lt;/span&gt;
 22:     &lt;span class="nt"&gt;&amp;lt;Wheel&amp;gt;&amp;lt;Position&amp;gt;&lt;/span&gt;front&lt;span class="nt"&gt;&amp;lt;/Position&amp;gt;&amp;lt;Rim&amp;gt;&lt;/span&gt;584x23&lt;span class="nt"&gt;&amp;lt;/Rim&amp;gt;&amp;lt;Tyre&amp;gt;&lt;/span&gt;32-622&lt;span class="nt"&gt;&amp;lt;/Tyre&amp;gt;&amp;lt;Spokes&amp;gt;&lt;/span&gt;24&lt;span class="nt"&gt;&amp;lt;/Spokes&amp;gt;&amp;lt;/Wheel&amp;gt;&lt;/span&gt;
 23:     &lt;span class="nt"&gt;&amp;lt;Wheel&amp;gt;&amp;lt;Position&amp;gt;&lt;/span&gt;rear&lt;span class="nt"&gt;&amp;lt;/Position&amp;gt;&amp;lt;Rim&amp;gt;&lt;/span&gt;559x25&lt;span class="nt"&gt;&amp;lt;/Rim&amp;gt;&amp;lt;Tyre&amp;gt;&lt;/span&gt;47-622&lt;span class="nt"&gt;&amp;lt;/Tyre&amp;gt;&amp;lt;Spokes&amp;gt;&lt;/span&gt;36&lt;span class="nt"&gt;&amp;lt;/Spokes&amp;gt;&amp;lt;/Wheel&amp;gt;&lt;/span&gt;
 24:   &lt;span class="nt"&gt;&amp;lt;/Wheels&amp;gt;&lt;/span&gt;
 25:   &lt;span class="nt"&gt;&amp;lt;Lighting&amp;gt;&lt;/span&gt;
 26:     &lt;span class="nt"&gt;&amp;lt;Front_Lumen&amp;gt;&lt;/span&gt;70&lt;span class="nt"&gt;&amp;lt;/Front_Lumen&amp;gt;&lt;/span&gt;
 27:     &lt;span class="nt"&gt;&amp;lt;PowerSource&amp;gt;&lt;/span&gt;HubDynamo&lt;span class="nt"&gt;&amp;lt;/PowerSource&amp;gt;&lt;/span&gt;
 28:   &lt;span class="nt"&gt;&amp;lt;/Lighting&amp;gt;&lt;/span&gt;
 29:   &lt;span class="nt"&gt;&amp;lt;Accessories&amp;gt;&lt;/span&gt;
 30:     &lt;span class="nt"&gt;&amp;lt;Part&amp;gt;&amp;lt;Name&amp;gt;&lt;/span&gt;BottleCage&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&amp;lt;Weight&amp;gt;&lt;/span&gt;275&lt;span class="nt"&gt;&amp;lt;/Weight&amp;gt;&amp;lt;Standard&amp;gt;&lt;/span&gt;yes&lt;span class="nt"&gt;&amp;lt;/Standard&amp;gt;&amp;lt;/Part&amp;gt;&lt;/span&gt;
 31:     &lt;span class="nt"&gt;&amp;lt;Part&amp;gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Speedometer&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&amp;lt;Weight&amp;gt;&lt;/span&gt;441&lt;span class="nt"&gt;&amp;lt;/Weight&amp;gt;&amp;lt;Standard&amp;gt;&lt;/span&gt;yes&lt;span class="nt"&gt;&amp;lt;/Standard&amp;gt;&amp;lt;/Part&amp;gt;&lt;/span&gt;
 32:   &lt;span class="nt"&gt;&amp;lt;/Accessories&amp;gt;&lt;/span&gt;
 33:   &lt;span class="nt"&gt;&amp;lt;Parameter&amp;gt;&lt;/span&gt;
 34:     &lt;span class="nt"&gt;&amp;lt;G09&amp;gt;&amp;lt;P02&amp;gt;&lt;/span&gt;552.04&lt;span class="nt"&gt;&amp;lt;/P02&amp;gt;&amp;lt;P04&amp;gt;&lt;/span&gt;861.71&lt;span class="nt"&gt;&amp;lt;/P04&amp;gt;&amp;lt;P06&amp;gt;&lt;/span&gt;B&lt;span class="nt"&gt;&amp;lt;/P06&amp;gt;&amp;lt;/G09&amp;gt;&lt;/span&gt;
 35:   &lt;span class="nt"&gt;&amp;lt;/Parameter&amp;gt;&lt;/span&gt;
 36: &lt;span class="nt"&gt;&amp;lt;/Configuration&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catalog does not come from a real project but from a Python script, the generator in the download. The script rolls the dice for the type, the equipment and the values of every configuration, but starts the random generator with the same seed on every run. That is why every run produces exactly the same catalog, and all numbers in this article can be reproduced with it. The XML is 28 MB in size and contains a good 1.5 million values. The same data exists as JSON Lines, one line per configuration, in the notation produced by the Python package &lt;code&gt;xmltodict&lt;/code&gt;: attributes as keys with &lt;code&gt;@&lt;/code&gt;, repeated elements as arrays. Each of the two files comes with the generator in a download of its own, XML and JSON separately. The German version of this article uses the same catalog with German element names. The structure and all numbers are identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why loops over variable attributes get expensive
&lt;/h2&gt;

&lt;p&gt;The obvious design in application code looks like this: for every object type there is a class or a mapping table that knows its attributes. For every configuration, the code walks through the attributes of that type, looks up the node in the document, reads the value and stores it in a list. If a value turns up that the list does not know yet, the list grows. With three types and twenty attributes that is manageable. With a thousand attributes, some of which overlap and some of which exist in only one variant, it becomes code that rebuilds the structure of the catalog, line by line. Every new attribute is a code change. Every access to a node is a separate search through the document. And every list that grows with progress is searched a little more slowly with every iteration.&lt;/p&gt;

&lt;p&gt;Where exactly the hours came from in that project can no longer be proven, and this article does not claim to know. It only claims that the design multiplies the costs: types times attributes times configurations, plus lists whose size depends on the order of processing. The general argument for why a loop per element loses against a query over the set is made in the article &lt;a href="https://sql.marcus-belz.de/en/set-based-vs-row-by-row/" rel="noopener noreferrer"&gt;Set-based vs. row by row&lt;/a&gt; on another case study. Here it is about the special case where the loop does not even know what it is looping over, because the schema only exists in the data. The programming language is not the problem. A program that walks the tree once and emits every leaf with its path needs no schema and stays fast, and that is exactly what the twin of this article shows in Python. It only gets expensive through a design that searches the document again for every attribute.&lt;/p&gt;

&lt;p&gt;The database route turns the task around. It does not know the attributes and does not need to. What it requires is little: the element under which the configurations sit, and an upper bound for the depth of the tree. Added to that is an assumption about lists: it recognises them only when an element name repeats within them in at least one configuration. It loads the document raw, shreds it with generic queries in which no attribute name appears into rows of path and value, and only afterwards does anyone look at which paths exist at all. The thousand attributes are then a thousand different values in one column, not a thousand lines of code. The schema is counted, not known. And the views that a consumer wants to see at the end are built by the database from this count. Architecturally this is ELT: load raw, then transform in SQL. What distinguishes that from classic ETL is explained in &lt;a href="https://sql.marcus-belz.de/en/etl-vs-elt-explained/" rel="noopener noreferrer"&gt;ETL vs. ELT&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The following diagram shows the whole route with the tables that come into being in the next sections, and with the numbers from the sample catalog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwn3bbex4rot0or3fjns8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwn3bbex4rot0or3fjns8.png" alt="Flow diagram of the database route: the file configs.xml (28 MB) is loaded as one xml value into stage.raw_xml and split into 3000 rows in stage.config, one per configuration. Three passes read from it: pass 1 fetches 1,516,476 leaf elements, pass 2 computes ordinals for 12,095 repeated elements, a third pass fetches 6000 XML attributes. The three results flow together into stage.leaf with 1,522,476 rows and 1024 paths, from which stage.inventory and the generated view dbo.vw_trekking_bike with 621 columns are built." width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Load raw: one row per configuration
&lt;/h2&gt;

&lt;p&gt;All SQL Server scripts of this article run against a Docker container. The catalog and the scripts are copied with &lt;code&gt;docker cp&lt;/code&gt; to &lt;code&gt;/tmp&lt;/code&gt; in the container, which is where the paths in the scripts point. Every file is run with &lt;code&gt;sqlcmd&lt;/code&gt;, which ships with the image: the switch &lt;code&gt;-i&lt;/code&gt; names the file, &lt;code&gt;-C&lt;/code&gt; accepts the self-signed certificate of the container, and &lt;code&gt;-I&lt;/code&gt; turns on &lt;code&gt;QUOTED_IDENTIFIER&lt;/code&gt;, which the XML methods require. For the remaining files only the file name changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;147-mssql-xml-en&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-e&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ACCEPT_EULA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-e&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MSSQL_SA_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'S3cret!Pass'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;mcr.microsoft.com/mssql/server:2022-latest&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;3:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;147-mssql-xml-en&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/opt/mssql-tools18/bin/sqlcmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-S&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;localhost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sa&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-P&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'S3cret!Pass'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;4:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nt"&gt;-Q&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREATE DATABASE catalog"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\configs.xml&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;147-mssql-xml-en:/tmp/configs.xml&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\147001-mssql-xml-load.sql&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;147-mssql-xml-en:/tmp/147001-mssql-xml-load.sql&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;147-mssql-xml-en&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/opt/mssql-tools18/bin/sqlcmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-S&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;localhost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sa&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-P&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'S3cret!Pass'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;8:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;catalog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/tmp/147001-mssql-xml-load.sql&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first step loads the whole file as a single value of type &lt;code&gt;xml&lt;/code&gt;. &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/openrowset-transact-sql" rel="noopener noreferrer"&gt;&lt;code&gt;OPENROWSET&lt;/code&gt;&lt;/a&gt; in &lt;code&gt;SINGLE_BLOB&lt;/code&gt; mode returns the file as &lt;code&gt;varbinary&lt;/code&gt;, and &lt;code&gt;CONVERT&lt;/code&gt; to &lt;code&gt;xml&lt;/code&gt; parses it once into SQL Server's internal representation. Right after that the document is split: &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/xml/nodes-method-xml-data-type" rel="noopener noreferrer"&gt;&lt;code&gt;nodes()&lt;/code&gt;&lt;/a&gt; returns one row per &lt;code&gt;Configuration&lt;/code&gt; element, &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/xml/value-method-xml-data-type" rel="noopener noreferrer"&gt;&lt;code&gt;value()&lt;/code&gt;&lt;/a&gt; fetches the two XML attributes as columns, and &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/xml/query-method-xml-data-type" rel="noopener noreferrer"&gt;&lt;code&gt;query('.')&lt;/code&gt;&lt;/a&gt; stores the fragment as an &lt;code&gt;xml&lt;/code&gt; value of its own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 147001: Load the catalog and split it into one row per configuration (SQL Server)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- The XML methods require QUOTED_IDENTIFIER ON; sqlcmd starts with OFF (switch -I).&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;QUOTED_IDENTIFIER&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GO&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;SCHEMA_ID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stage'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CREATE SCHEMA [stage]'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GO&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- The whole document as a single xml value&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;raw_xml&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;raw_xml&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;doc&lt;/span&gt;   &lt;span class="n"&gt;xml&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;raw_xml&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BulkColumn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;OPENROWSET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BULK&lt;/span&gt; &lt;span class="s1"&gt;'/tmp/configs.xml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SINGLE_BLOB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- One row per configuration: key, type and the fragment as xml&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'@id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'varchar(20)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'@type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'varchar(20)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;raw_xml&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/Catalog/Configuration'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;configs&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;config_type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- cargo_bike      1025&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- road_bike        995&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- trekking_bike    980&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The split is the most important step of the whole method, for a reason that only becomes visible later. All subsequent XQuery expressions then run against fragments of around 9 KB instead of a document of 28 MB. What that is worth is shown more drastically by Postgres than by SQL Server: there, the same generic query over the whole document was still running after 36 minutes and could not even be cancelled. More on that in the &lt;strong&gt;Postgres bridge&lt;/strong&gt; section below.&lt;/p&gt;

&lt;p&gt;One trap right at the start: the XML methods &lt;code&gt;nodes()&lt;/code&gt;, &lt;code&gt;value()&lt;/code&gt; and &lt;code&gt;query()&lt;/code&gt; require &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/set-quoted-identifier-transact-sql" rel="noopener noreferrer"&gt;&lt;code&gt;QUOTED_IDENTIFIER ON&lt;/code&gt;&lt;/a&gt;, alongside further session options such as &lt;code&gt;ANSI_NULLS&lt;/code&gt;, &lt;code&gt;ANSI_PADDING&lt;/code&gt; and &lt;code&gt;ANSI_WARNINGS&lt;/code&gt;, which are already set in &lt;code&gt;sqlcmd&lt;/code&gt;. The tool &lt;code&gt;sqlcmd&lt;/code&gt;, however, starts with &lt;code&gt;QUOTED_IDENTIFIER OFF&lt;/code&gt;, and the error message does name the option, but does not say which construct in the statement needs it. Instead it lists all the features that depend on it. The switch &lt;code&gt;-I&lt;/code&gt; on the &lt;code&gt;sqlcmd&lt;/code&gt; call takes care of that, alternatively the first line in the script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shred generically: every leaf with its path
&lt;/h2&gt;

&lt;p&gt;The goal is a table with four columns: configuration, type, path and value. In the SQL Server world this breaking of a document into rows is called XML shredding, and the tools for it are the methods &lt;code&gt;nodes()&lt;/code&gt; and &lt;code&gt;value()&lt;/code&gt; of the &lt;code&gt;xml&lt;/code&gt; type. The values sit in the leaves of the tree, that is, in elements without child elements, and additionally in XML attributes. An XML attribute can hang on any element, including one with children, like &lt;code&gt;kind&lt;/code&gt; on the &lt;code&gt;Drivetrain&lt;/code&gt;. The path is the chain of element names from the &lt;code&gt;Configuration&lt;/code&gt; element down to the leaf. For an XML attribute it ends with &lt;code&gt;@&lt;/code&gt; and the attribute name. From the excerpt above this yields rows such as &lt;code&gt;/Frame/Material&lt;/code&gt; with the value &lt;code&gt;Aluminum&lt;/code&gt;, &lt;code&gt;/Drivetrain/Gears&lt;/code&gt; with the value &lt;code&gt;14&lt;/code&gt; or &lt;code&gt;/Drivetrain/@kind&lt;/code&gt; with the value &lt;code&gt;hub&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This happens in two passes over all 3000 fragments, called pass 1 and pass 2 in what follows. The reason for the split are the lists. A configuration with two accessory parts has two elements with the same path &lt;code&gt;/Accessories/Part/Name&lt;/code&gt;. To tell them apart, every element would have to carry its position among its same-named siblings, that is, whether it is the first or the second part. This position is called ordinal in what follows and appears in square brackets in the path, for example &lt;code&gt;/Accessories/Part[2]/Name&lt;/code&gt;. Computing it is expensive in SQL Server: for each of the 1.5 million leaves the XQuery engine would have to count the siblings before it, and that raises the runtime in the measurement from around two to around 40 seconds. Pass 1 therefore fetches all values with their path, but without positions. From its result, an ordinary grouping reveals at which positions elements repeat at all. In the catalog these are &lt;code&gt;Part&lt;/code&gt; under &lt;code&gt;Accessories&lt;/code&gt; and &lt;code&gt;Wheel&lt;/code&gt; under &lt;code&gt;Wheels&lt;/code&gt;. Pass 2 then computes the positions only for these positions, that is, for around 12,000 elements instead of 1.5 million. XML attributes are not elements and are skipped by both passes. They get a short pass of their own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 1: all leaves, no positions
&lt;/h3&gt;

&lt;p&gt;The path expression &lt;code&gt;/Configuration//*[not(*)]&lt;/code&gt; matches every element in the fragment that has no child element, at any depth. The names of its ancestors come from the parent axis: &lt;code&gt;local-name(..)&lt;/code&gt; is the name of the parent element, &lt;code&gt;local-name(../..)&lt;/code&gt; that of the grandparent, and so on. In XQuery, SQL Server supports &lt;a href="https://learn.microsoft.com/en-us/sql/xquery/path-expressions-specifying-axis" rel="noopener noreferrer"&gt;the axes&lt;/a&gt; &lt;code&gt;child&lt;/code&gt;, &lt;code&gt;descendant&lt;/code&gt;, &lt;code&gt;descendant-or-self&lt;/code&gt;, &lt;code&gt;parent&lt;/code&gt;, &lt;code&gt;attribute&lt;/code&gt; and &lt;code&gt;self&lt;/code&gt;, but not &lt;code&gt;ancestor&lt;/code&gt;. That is why the depth has to be chosen as a fixed number. The catalog has at most three levels below the configuration, so four name columns suffice with one column in reserve: the fourth always holds &lt;code&gt;Configuration&lt;/code&gt;. That is exactly what can be checked. A leaf for which &lt;code&gt;Configuration&lt;/code&gt; appears in none of the ancestor columns lies deeper than the reserve, and from the fifth level on its path would be cut off at the front without any error message pointing it out. Whoever has deeper documents adds columns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 147002: Pass 1 - every leaf element with the names of its ancestors (SQL Server)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- One pass over all configurations. The parent axis (..) yields the ancestor&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- names; positions are deliberately NOT computed here.&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;QUOTED_IDENTIFIER&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GO&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_raw&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local-name(.)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'varchar(100)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name0&lt;/span&gt;   &lt;span class="c1"&gt;-- the leaf itself&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local-name(..)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="s1"&gt;'varchar(100)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name1&lt;/span&gt;   &lt;span class="c1"&gt;-- parent&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local-name(../..)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="s1"&gt;'varchar(100)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name2&lt;/span&gt;   &lt;span class="c1"&gt;-- grandparent&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local-name(../../..)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'varchar(100)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name3&lt;/span&gt;   &lt;span class="c1"&gt;-- great-grandparent&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="s1"&gt;'varchar(200)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_raw&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/Configuration//*[not(*)]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;leaves&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_raw&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 1516476&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single pass delivers all 1,516,476 leaf elements of the 3000 configurations in around two seconds. Lines 12 to 15 fetch the four names, line 16 the value, and that is all. What is deliberately missing is the position of an element among its siblings. It would be needed to tell the two &lt;code&gt;Part&lt;/code&gt; elements in the accessories apart. The obvious XQuery expression for it is &lt;code&gt;let $i := . return count(../*[. &amp;lt;&amp;lt; $i]) + 1&lt;/code&gt;: count the siblings that precede me in the document. With two such expressions, one for the leaf and one for its parent element, the same query takes twenty times as long instead of around two seconds, measured 1.8 against 40.2 seconds. For each of the 1.5 million leaves SQL Server then walks the sibling list and compares document positions, and exactly this evaluation is expensive in the XQuery engine. Computing the position across the board is therefore not an option. It is computed in the second pass only where it is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  XML attributes
&lt;/h3&gt;

&lt;p&gt;XML attributes are not elements and do not appear in pass 1, regardless of whether they hang on a leaf or on an element with children. They get a short pass of their own. In SQL Server 2022 the method &lt;code&gt;nodes()&lt;/code&gt; also returns attribute nodes, the path &lt;code&gt;/Configuration//@*&lt;/code&gt; matches all XML attributes in the fragment, and the parent axis leads from the XML attribute to the element that carries it. The two XML attributes of the &lt;code&gt;Configuration&lt;/code&gt; element itself are already columns and are excluded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 147003: Attributes - nodes() also returns attribute nodes (SQL Server)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- The attributes of the configuration itself (id, type) are already columns in&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- [stage].[config] and are excluded here.&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;QUOTED_IDENTIFIER&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GO&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_attr&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local-name(.)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'varchar(100)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;attr_name&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local-name(..)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="s1"&gt;'varchar(100)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name1&lt;/span&gt;   &lt;span class="c1"&gt;-- element carrying the attribute&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local-name(../..)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="s1"&gt;'varchar(100)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name2&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local-name(../../..)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'varchar(100)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name3&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="s1"&gt;'varchar(200)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_attr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/Configuration//@*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local-name(..)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'varchar(100)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Configuration'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;name1&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;attr_name&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_attr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;name1&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;attr_name&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;name1&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;attr_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Brakes       type   3000&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Drivetrain   kind   3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catalog has two further XML attributes, &lt;code&gt;kind&lt;/code&gt; on the drivetrain and &lt;code&gt;type&lt;/code&gt; on the brakes, and the pass takes half a second.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 2: ordinals only where something repeats
&lt;/h3&gt;

&lt;p&gt;Without positions, the two &lt;code&gt;Part&lt;/code&gt; elements of a configuration fall onto the same path &lt;code&gt;/Accessories/Part/Name&lt;/code&gt;. For the inventory in the next section that is actually fine, for a view with one column per path it is not. The solution has two steps. First, a set-based query reads off pass 1 which parent elements repeat at all: if, within one configuration, more than one leaf with the same name sits under the same parent path, then either the parent element repeats, or the leaf itself repeats within the parent element. Both need ordinals. Which of the two cases applies, the grouping does not reveal, because in pass 1 both look the same. The template below solves the first one. The second one is shown by the inventory after pass 2: the path then carries more than one value in a single configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 147004: Which parents repeat? Set-based, from pass 1 (SQL Server)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- A parent element repeats when more than one leaf of the same name sits under&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- the same parent path within one configuration. The same query also finds&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- leaves that repeat within a single parent.&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;repeated_parent&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CTE_leaf_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name3&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name2&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name1&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name0&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_raw&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name3&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name2&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name1&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name0&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;name2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Configuration'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;name3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Configuration'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name2&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name2&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;parent_path&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;parent_name&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;repeated_parent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;CTE_leaf_count&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;parent_path&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;parent_name&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;repeated_parent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;parent_path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Accessories   Part&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Wheels        Wheel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query finds two positions, &lt;code&gt;/Accessories&lt;/code&gt; with the child &lt;code&gt;Part&lt;/code&gt; and &lt;code&gt;/Wheels&lt;/code&gt; with the child &lt;code&gt;Wheel&lt;/code&gt;, and takes under a second for that. Only now does the expensive position computation come into play, and only for the nodes that this detection has delivered. For every position found, a template yields a &lt;code&gt;SELECT&lt;/code&gt; that fetches the affected parent elements with &lt;code&gt;nodes()&lt;/code&gt;, computes their position among their same-named siblings and reads the leaves below them with their own position. At such a position every element gets its ordinal, even when a configuration has only a single element there. A configuration with three parts gets &lt;code&gt;/Accessories/Part[1]/Name&lt;/code&gt; to &lt;code&gt;/Accessories/Part[3]/Name&lt;/code&gt;, one with exactly one part gets &lt;code&gt;/Accessories/Part[1]/Name&lt;/code&gt;. If the single part stayed without an ordinal, the first element of the same list would sit on two different paths depending on the configuration. The inventory would then count one list as two things, and the path without ordinal would end up as a column in the type's view, filled only for the configurations with exactly one part. The two wheels of every configuration are accordingly called &lt;code&gt;/Wheels/Wheel[1]/Rim&lt;/code&gt; and &lt;code&gt;/Wheels/Wheel[2]/Rim&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 147005: Pass 2 - ordinals only for the parents that repeat (SQL Server)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- For every row of [stage].[repeated_parent] the template yields one SELECT.&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- At such a position every element gets its ordinal, even when a configuration&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- has only a single element there: a list stays a list, and its first element&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- sits on the same path in every configuration.&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- The expensive position expression runs only over the few affected nodes,&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- not over all leaves.&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- If leaves repeat directly within one parent element, the template gets the&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- same expression once more on leaf level (T03). That costs a multiple of the&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- runtime of pass 2 and is not needed here. Whether the case exists is shown&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- by the inventory: n_values greater than n_configs.&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;QUOTED_IDENTIFIER&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GO&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'
 17: SELECT
 18:     T01.config_id
 19:    ,T01.config_type
 20:    ,&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;{path}/{name}&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;
 21:     + T02.n.value(&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;let $p := .
 22:                     return concat("[", string(count(../*[local-name() = "{name}"][. &amp;lt;&amp;lt; $p]) + 1), "]")&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;varchar(10)&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;)
 23:     + &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; + T03.n.value(&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;local-name(.)&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;varchar(100)&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;) AS path
 24:    ,T03.n.value(&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;varchar(200)&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;) AS value
 25: FROM
 26:    [stage].[config] T01
 27:    CROSS APPLY T01.node.nodes(&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;/Configuration{path}/{name}&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;) AS T02(n)
 28:    CROSS APPLY T02.n.nodes(&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;./*[not(*)]&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;) AS T03(n)'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;STRING_AGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'{path}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'{name}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;' UNION ALL '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;repeated_parent&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_pass2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_pass2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;config_id&lt;/span&gt;     &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;   &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;          &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;         &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_pass2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="n"&gt;sp_executesql&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
 &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_pass2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%/Name'&lt;/span&gt;
 &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%/Rim'&lt;/span&gt;
 &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Accessories/Part[1]/Name   2406&lt;/span&gt;
 &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Accessories/Part[2]/Name   1831&lt;/span&gt;
 &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Accessories/Part[3]/Name   1243&lt;/span&gt;
 &lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Accessories/Part[4]/Name    615&lt;/span&gt;
 &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Wheels/Wheel[1]/Rim        3000&lt;/span&gt;
 &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Wheels/Wheel[2]/Rim        3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass 2 runs over 6095 &lt;code&gt;Part&lt;/code&gt; elements and 6000 &lt;code&gt;Wheel&lt;/code&gt; elements instead of 1.5 million leaves and takes under a second. The template applies at every position the detection reports, no matter how deep the repeated element sits in the fragment. Below it, however, it reads only the direct leaf children, the way &lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;Weight&lt;/code&gt; and &lt;code&gt;Standard&lt;/code&gt; sit directly under &lt;code&gt;Part&lt;/code&gt;. If leaves repeat directly within one parent element, the template gets the same expression once more on leaf level. That costs around 15 seconds instead of one in the measurement, because navigating to the parent element from every leaf is expensive, but it stays limited to the positions found. Two forms the template does not cover. If there is a further level between the repeated element and its leaves, say &lt;code&gt;Part/Product/Name&lt;/code&gt;, then the detection attributes the repetition to the intermediate element &lt;code&gt;Product&lt;/code&gt;, and pass 2 assigns every &lt;code&gt;Product&lt;/code&gt; the ordinal 1, because every &lt;code&gt;Part&lt;/code&gt; has only one &lt;code&gt;Product&lt;/code&gt;. The two names then land on the same path &lt;code&gt;/Accessories/Part/Product[1]/Name&lt;/code&gt;. The same applies to lists within lists. Both cases need a further &lt;code&gt;CROSS APPLY&lt;/code&gt; stage with the same pattern, and whether they occur is revealed by the inventory in the next section through a single metric. A third form escapes the detection itself, because it is a heuristic over the leaf names: it finds a list only when at least one leaf name repeats within it. If of two &lt;code&gt;Part&lt;/code&gt; elements one carries only a &lt;code&gt;Name&lt;/code&gt; and the other only a &lt;code&gt;Weight&lt;/code&gt;, no leaf name repeats, and the two parts look like a single one in the path/value table. The inventory's metric does not show this case. It stays hidden, however, only as long as not a single configuration carries the list uniformly. As soon as one does, the detection reports the position, and pass 2 assigns the ordinals in all configurations.&lt;/p&gt;

&lt;p&gt;One shortcut looks tempting and is not one. &lt;code&gt;ROW_NUMBER() OVER (ORDER BY (SELECT NULL))&lt;/code&gt; over the output of &lt;code&gt;nodes()&lt;/code&gt; delivers the document order in practice, and in the measurement the ordinals derived from it matched the XQuery computation in all 12,095 cases. The documentation, however, does not guarantee this order, and the window function forces the optimizer into a plan that raises the runtime of pass 1 from around two to around 40 seconds. Both together are enough to leave the shortcut alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merge
&lt;/h3&gt;

&lt;p&gt;The path/value table comes from three sources: pass 1 without the leaves under the repeated parent elements, plus pass 2 and the XML attributes. The path is assembled from the name columns, up to the name &lt;code&gt;Configuration&lt;/code&gt;, which marks the beginning. An index on type and path helps all the queries that now follow. At the end of the script stands a reconciliation: the number of leaves and XML attributes in the fragments must match the number of rows in the path/value table. This reconciliation only counts. Whether every value sits on the right path, it does not show.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 147006: Merge - the path/value table (SQL Server)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Pass 1 without the leaves under repeated parents, plus pass 2 and the&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- attributes. Result: one row per leaf value with its path.&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- The reconciliation at the end counts with XML methods and needs QUOTED_IDENTIFIER ON.&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;QUOTED_IDENTIFIER&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GO&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Configuration'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Configuration'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name1&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Configuration'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name1&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name1&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_raw&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;repeated_parent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name1&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Configuration'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                     &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Configuration'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name2&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                     &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name2&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_pass2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;name2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Configuration'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;name3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Configuration'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name2&lt;/span&gt;
 &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name2&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
 &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/@'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;attr_name&lt;/span&gt;
 &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
 &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_attr&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_leaf_type_path&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;leaves&lt;/span&gt;
 &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;config_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;configs&lt;/span&gt;
 &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;
 &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 1522476   3000   1024&lt;/span&gt;
 &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Reconciliation: leaves and attributes in the source against the rows of the table&lt;/span&gt;
 &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- The attributes of the configuration itself (id, type) do not count. If the sum&lt;/span&gt;
 &lt;span class="mi"&gt;69&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- of the first two columns differs from the third, something was lost or&lt;/span&gt;
 &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- duplicated while merging.&lt;/span&gt;
 &lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'count(/Configuration//*[not(*)])'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'int'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;source_leaves&lt;/span&gt;
 &lt;span class="mi"&gt;73&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'count(/Configuration/*//@*)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="s1"&gt;'int'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;source_attributes&lt;/span&gt;
 &lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;                           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;leaf_rows&lt;/span&gt;
 &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 1516476   6000   1522476&lt;/span&gt;
 &lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;79&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
 &lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;83&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;84&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;config_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'C000001'&lt;/span&gt;
 &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'/Drivetrain%'&lt;/span&gt;
 &lt;span class="mi"&gt;87&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;89&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/@kind          hub&lt;/span&gt;
 &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/Belt           yes&lt;/span&gt;
 &lt;span class="mi"&gt;91&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/CoasterBrake   no&lt;/span&gt;
 &lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/GearRange      371&lt;/span&gt;
 &lt;span class="mi"&gt;93&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/Gears          14&lt;/span&gt;
 &lt;span class="mi"&gt;94&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/HubType        N8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is 1,522,476 rows from 3000 configurations with 1024 distinct paths. The reconciliation adds up: 1,516,476 leaves and 6000 XML attributes in the source give the same number. The assignment of value to path was checked separately for this article, against a single pass that computes the position for each of the 1.5 million leaves. A comparison with &lt;code&gt;EXCEPT&lt;/code&gt; over configuration, path and value yields zero differences in both directions. The ordinals of the parts turn three paths into twelve, those of the wheels four paths into eight, the rest are the 1004 paths of the catalog including the XML attributes &lt;code&gt;/Drivetrain/@kind&lt;/code&gt; and &lt;code&gt;/Brakes/@type&lt;/code&gt;. For the configuration from the excerpt, the table delivers under &lt;code&gt;/Drivetrain&lt;/code&gt; exactly the six rows one would expect: the XML attribute and the five hub attributes.&lt;/p&gt;

&lt;p&gt;In total, the XML route in SQL Server looks like this. The runtime per script was measured in a Docker container under Docker Desktop on a current desktop machine: Intel Core i7-14700KF with 28 logical cores, 62 GB RAM and an NVMe SSD under Ubuntu, of which 28 cores and 8 GB go to the Docker VM, the SQL Server container limited to 4 GB of memory, SQL Server 2022 build 16.0.4265.3 without further tuning, in the second run in the same container. The numbers are orders of magnitude on this one system, not benchmarks. For perspective: a three-year-old laptop with an Intel Core i7-1165G7 needed, in an earlier measurement with a smaller version of the catalog, around three times as long for the whole route and around ten times as long for the XQuery steps pass 1 and pass 2.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Load as &lt;code&gt;SINGLE_BLOB&lt;/code&gt; and split into 3000 configurations&lt;/td&gt;
&lt;td&gt;8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass 1: all leaves with ancestor names&lt;/td&gt;
&lt;td&gt;1.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XML attributes&lt;/td&gt;
&lt;td&gt;0.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection of repeated parent elements&lt;/td&gt;
&lt;td&gt;0.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass 2: ordinals for the repetitions&lt;/td&gt;
&lt;td&gt;0.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge, index and reconciliation&lt;/td&gt;
&lt;td&gt;2.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;around 15 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The same with JSON and OPENJSON
&lt;/h3&gt;

&lt;p&gt;If the catalog arrives as JSON, there is no detour through pass 1 and pass 2, because an array brings its ordinals with it. The file is loaded as &lt;code&gt;SINGLE_CLOB&lt;/code&gt; and split with &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql" rel="noopener noreferrer"&gt;&lt;code&gt;STRING_SPLIT&lt;/code&gt;&lt;/a&gt; at the line breaks into one row per configuration. That &lt;code&gt;STRING_SPLIT&lt;/code&gt; guarantees no order does not matter here, because every line is a self-contained configuration. After that, a recursive CTE opens every node with &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql" rel="noopener noreferrer"&gt;&lt;code&gt;OPENJSON&lt;/code&gt;&lt;/a&gt;: the anchor is the document, the recursive part opens everything that is an object or an array and appends the key or the index to the path. Whatever is neither object nor array is a leaf.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 147007: The same catalog as JSON Lines - recursively with OPENJSON (SQL Server)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- One row per configuration; attributes follow the xmltodict convention (@kind),&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- repeated elements are arrays. Array indexes are counted from 1, as with XML.&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;raw_json&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;raw_json&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;OPENROWSET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BULK&lt;/span&gt; &lt;span class="s1"&gt;'/tmp/configs.jsonl'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SINGLE_CLOB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;STRING_SPLIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BulkColumn&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nb"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;LEN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Open recursively: objects (type 5) and arrays (type 4) are shredded further,&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- everything else is a leaf. OPENJSON returns key/value in Latin1_General_BIN2,&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- hence COLLATE DATABASE_DEFAULT on both sides of the recursion.&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_json&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CTE_node&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$."@id"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;DATABASE_DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$."@type"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;DATABASE_DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                         &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;DATABASE_DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                         &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;DATABASE_DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;json_type&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;raw_json&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_id&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;DATABASE_DEFAULT&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;DATABASE_DEFAULT&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;DATABASE_DEFAULT&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'['&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;']'&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;DATABASE_DEFAULT&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;CTE_node&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;OPENJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'[]'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
 &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_json&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;CTE_node&lt;/span&gt;
 &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;json_type&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;leaves&lt;/span&gt;
 &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;
 &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf_json&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 1528476   1026&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make the difference between a query that runs and one that does not even compile. First, &lt;code&gt;OPENJSON&lt;/code&gt; returns the columns &lt;code&gt;key&lt;/code&gt; and &lt;code&gt;value&lt;/code&gt; in the collation &lt;code&gt;Latin1_General_BIN2&lt;/code&gt;, while the columns of the anchor carry the collation of the database. In a recursive CTE, anchor and recursive part must have identical types, collation included, otherwise SQL Server reports "Types don't match between the anchor and the recursive part". The &lt;code&gt;COLLATE DATABASE_DEFAULT&lt;/code&gt; on both sides is therefore mandatory. Second, &lt;code&gt;OPENJSON&lt;/code&gt; counts array indexes from zero, XML ordinals count from one. The &lt;code&gt;+ 1&lt;/code&gt; in the path aligns the two, so that both routes deliver the same paths.&lt;/p&gt;

&lt;p&gt;The JSON route needs around 25 seconds for loading and recursion together. It delivers 1,528,476 rows with 1026 paths, that is, 6000 rows and two paths more than the XML route. The difference are &lt;code&gt;@id&lt;/code&gt; and &lt;code&gt;@type&lt;/code&gt;, which JSON carries as paths of their own, while they are columns on the XML route. All 1,522,476 rows of the XML route reappear with configuration, path and value in the JSON result, the ordinals of the lists included. The code, a single script, is much shorter than the five scripts of the XML route from pass 1 to the merge, but at around 25 seconds the runtime is above the around 15 seconds of the XML route. Whoever has the choice, because the catalog is converted anyway, can skip the XML shredding. What the conversion looks like in Python is shown by the twin of this article, which follows shortly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inventory: reading the schema off the data
&lt;/h2&gt;

&lt;p&gt;With the path/value table, the actual question of the project becomes a grouping: which type carries which paths, and how often? Per type and path, the values are counted, as are the configurations in which the path occurs. The share of all configurations of the type then says what one is dealing with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 147009: Inventory - which type carries which paths, and how often (SQL Server)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CTE_configs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;config_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;configs&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_values&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_configs&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;share_pct&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;CTE_configs&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Required or optional: from a share of 99 percent a path counts as required for&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- the type, everything below is optional or belongs to a variant.&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- The threshold is deliberately not 100 percent. In real data a required value is&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- missing in a few configurations, and the path should stay required so that&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- exactly these gaps show up. For strict counting use 100.0.&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;share_pct&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;required_paths&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;share_pct&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;optional_paths&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;config_type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- cargo_bike      640   34   606&lt;/span&gt;
 &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- road_bike       634   29   605&lt;/span&gt;
 &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- trekking_bike   640   29   611&lt;/span&gt;
 &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;n_configs&lt;/span&gt;
 &lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;share_pct&lt;/span&gt;
 &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;config_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'trekking_bike'&lt;/span&gt;
 &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'/Drivetrain/%'&lt;/span&gt;
 &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/@kind             980   100.0&lt;/span&gt;
 &lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/Belt              367    37.4&lt;/span&gt;
 &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/Chainline         613    62.6&lt;/span&gt;
 &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/Chainrings        613    62.6&lt;/span&gt;
 &lt;span class="mi"&gt;69&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/CoasterBrake      367    37.4&lt;/span&gt;
 &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/FrontDerailleur   613    62.6&lt;/span&gt;
 &lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/GearRange         367    37.4&lt;/span&gt;
 &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/GearRatio_max     613    62.6&lt;/span&gt;
 &lt;span class="mi"&gt;73&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/GearRatio_min     613    62.6&lt;/span&gt;
 &lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/Gears             367    37.4&lt;/span&gt;
 &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/HubType           367    37.4&lt;/span&gt;
 &lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/RearDerailleur    613    62.6&lt;/span&gt;
 &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- /Drivetrain/Sprockets         613    62.6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A path with a share close to one hundred percent is a required attribute of the type. The query draws the line at 99 percent and not at 100, so that a required attribute missing in a few configurations stays required and exactly these gaps show up. The classification is a finding from the data and not a schema statement. Whether an attribute is required in the business sense is decided by the business side, and an element that is present but empty counts as present here. A path with a small share is optional. And a path that occurs in a fixed fraction of the configurations belongs to a variant. The drivetrain of the trekking bike shows this without any prior knowledge: &lt;code&gt;/Drivetrain/@kind&lt;/code&gt; appears in all 980 configurations, the chain attributes appear in 62.6 percent of them, the hub attributes in the remaining 37.4 percent. The two variants of the drivetrain can be read off the count, and so can the fact that they exclude each other. The inventory takes under half a second.&lt;/p&gt;

&lt;p&gt;The same count is, as a side effect, a data quality tool. A path that occurs in only three out of a thousand configurations is either a rare extra or a typo in the element name. A required attribute at 99.8 percent has two configurations in which it is missing. And a path that counts more values than configurations is either a repetition that pass 2 did not resolve, or a pair of element names in the same configuration that differ only in upper and lower case and collapse under the database's default collation. This metric, &lt;code&gt;n_values&lt;/code&gt; greater than &lt;code&gt;n_configs&lt;/code&gt;, is a plausibility check for paths that are assigned more than once within a configuration. In the catalog it applies to no path. Whether the shredding was complete, it does not answer. That is what the reconciliation of the row counts at the end of the merge is for. Whoever wants to derive check rules from such metadata finds the idea continued in &lt;a href="https://sql.marcus-belz.de/en/derive-data-quality-rules-from-schema/" rel="noopener noreferrer"&gt;Deriving data quality rules from the schema&lt;/a&gt;, there for relational schemas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate views instead of writing them
&lt;/h2&gt;

&lt;p&gt;Nobody writes a view with six hundred columns by hand, and nobody should have to. The inventory knows every path per type, and every path becomes a column: &lt;code&gt;MAX(CASE WHEN path = '…' THEN value END)&lt;/code&gt; grouped by configuration. &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql" rel="noopener noreferrer"&gt;&lt;code&gt;STRING_AGG&lt;/code&gt;&lt;/a&gt; assembles the column list, &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/quotename-transact-sql" rel="noopener noreferrer"&gt;&lt;code&gt;QUOTENAME&lt;/code&gt;&lt;/a&gt; turns the path into a valid column name and the type into a valid view name, and &lt;a href="https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-executesql-transact-sql" rel="noopener noreferrer"&gt;&lt;code&gt;sp_executesql&lt;/code&gt;&lt;/a&gt; runs the DDL. Because paths and type come from the data, they pass through &lt;code&gt;QUOTENAME&lt;/code&gt; as identifiers and through a &lt;code&gt;REPLACE&lt;/code&gt; that doubles single quotes as literals. With XML names that is mere caution, because they cannot contain single quotes. With keys from JSON it is necessary. List paths with an ordinal stay out. They get views of their own with one row per list element, otherwise columns like &lt;code&gt;accessories_part_3_name&lt;/code&gt; would arise that stay empty for anything but four parts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- 147010: Generate the views from the inventory instead of writing them (SQL Server)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- One view per object type with one column per path. List paths (with [n]) stay&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- out; they get views of their own with one row per list element.&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Run the script once per object type: first as it stands with 'trekking_bike',&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- then with 'road_bike' and with 'cargo_bike' in @config_type. Every run creates&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- the view of its type (vw_trekking_bike, vw_road_bike, vw_cargo_bike).&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- CREATE VIEW must be alone in its batch, hence the DDL runs through&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- sp_executesql. The typed view and the sample query at the end belong to the&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- trekking bike and assume that its run was the first.&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Identifiers pass through QUOTENAME, literals through REPLACE('''') - paths from&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- JSON, unlike XML names, may contain single quotes.&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'trekking_bike'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- then 'road_bike', then 'cargo_bike'&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;         &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'CREATE OR ALTER VIEW [dbo].'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;QUOTENAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'vw_'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;' AS'&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;' SELECT config_id'&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;STRING_AGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;', MAX(CASE WHEN path = &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; THEN value END) AS '&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;QUOTENAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUBSTRING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'@'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'attr_'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;' FROM [stage].[leaf] WHERE config_type = &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; GROUP BY config_id;'&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;config_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%[[]%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="n"&gt;sp_executesql&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;columns_generated&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;INFORMATION_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'vw_'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- XML route:  trekking_bike 621, road_bike 615, cargo_bike 621&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- JSON route: trekking_bike 623, road_bike 617, cargo_bike 623&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GO&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Second layer: typing where it is needed. All values of the generated view are&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- text; only here do price, weight and lumen get their types.&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;vw_trekking_bike_typed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
 &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;general_model&lt;/span&gt;                                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;
 &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;TRY_CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;smallint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;general_modelyear&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;model_year&lt;/span&gt;
 &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;TRY_CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;general_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
 &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;TRY_CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="n"&gt;general_weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;weight_kg&lt;/span&gt;
 &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;frame_material&lt;/span&gt;                                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;frame_material&lt;/span&gt;
 &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;drivetrain_attr_kind&lt;/span&gt;                                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;drivetrain_kind&lt;/span&gt;
 &lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;TRY_CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;tinyint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="n"&gt;drivetrain_gears&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;gears&lt;/span&gt;
 &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;TRY_CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;smallint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;lighting_front_lumen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lighting_front_lumen&lt;/span&gt;
 &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;vw_trekking_bike&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GO&lt;/span&gt;
 &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;frame_material&lt;/span&gt;
 &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
 &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price_avg&lt;/span&gt;
 &lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;vw_trekking_bike_typed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;69&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;frame_material&lt;/span&gt;
 &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;frame_material&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Aluminum   246   3178&lt;/span&gt;
 &lt;span class="mi"&gt;73&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Carbon     246   3443&lt;/span&gt;
 &lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Steel      248   3426&lt;/span&gt;
 &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Titanium   240   3116&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The view for the trekking bike has 621 columns and comes into being in under half a second. The script creates the view of one type per run. For the road bike and the cargo bike it runs once more with the respective type in &lt;code&gt;@config_type&lt;/code&gt; and delivers views with 615 and 621 columns. A query over it, say the average price per frame material over 980 configurations, runs in around 40 milliseconds. This is the moment the approach pays off: a new attribute in the catalog requires no change to shredding, inventory and view generator. It is a new row in the inventory and, on the next run, a new column in the view. Manual work remains only for the typed view, if the attribute is to get a type there.&lt;/p&gt;

&lt;p&gt;All values of the generated view are text, just as they stood in the XML. They get types in a second layer, and only the columns that someone needs typed. &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/try-convert-transact-sql" rel="noopener noreferrer"&gt;&lt;code&gt;TRY_CONVERT&lt;/code&gt;&lt;/a&gt; is the right tool for that, because a single faulty value must not bring the whole view down. What to watch out for per data type is covered in &lt;a href="https://sql.marcus-belz.de/en/safe-type-conversion-t-sql/" rel="noopener noreferrer"&gt;Safe type conversion with T-SQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Three limits belong to this. A column name is an identifier of at most 128 characters. For longer input &lt;code&gt;QUOTENAME&lt;/code&gt; returns &lt;code&gt;NULL&lt;/code&gt;, and because &lt;code&gt;STRING_AGG&lt;/code&gt; skips &lt;code&gt;NULL&lt;/code&gt; values, the column silently disappears from the view. The column count at the end of the script is therefore more than a control output: 620 paths without ordinal plus &lt;code&gt;config_id&lt;/code&gt; must give 621 columns. Furthermore, the mapping of path to column name is not unique. &lt;code&gt;/A/B&lt;/code&gt; and &lt;code&gt;/A_B&lt;/code&gt; both become &lt;code&gt;a_b&lt;/code&gt;, and the view fails with the message that column names must be unique. The same happens with &lt;code&gt;/A/@b&lt;/code&gt; and &lt;code&gt;/A/attr_b&lt;/code&gt;, which both become &lt;code&gt;a_attr_b&lt;/code&gt;, and with &lt;code&gt;Gears&lt;/code&gt; and &lt;code&gt;gears&lt;/code&gt; as soon as the path column carries a binary collation and the two spellings sit in the inventory as separate paths. Whoever has such names appends a running number from the inventory. SQL Server allows &lt;a href="https://learn.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server" rel="noopener noreferrer"&gt;1024 columns per view&lt;/a&gt;, Postgres &lt;a href="https://www.postgresql.org/docs/current/limits.html" rel="noopener noreferrer"&gt;1600&lt;/a&gt;, where in Postgres a materialized table additionally requires the row to fit into an 8-KB block. With a thousand attributes and one view per type that still works out, with two thousand it no longer does. Then views are generated per assembly instead of per type, that is, one view for the frame, one for the drivetrain, and the consumers join through the configuration ID. And the path/value table itself is an intermediate format, not a target model. It has no types, no constraints, and every query has to pivot first. Whoever uses it as the data model for applications builds the entity-attribute-value pattern with all its well-known drawbacks. As a landing zone for a document with an unknown schema it is exactly right, and the generated views are the layer the consumers see. If a view is queried often, it can be materialized as a table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres bridge
&lt;/h2&gt;

&lt;p&gt;The same route leads through JSONB in Postgres, and there it is even the fastest. &lt;a href="https://www.postgresql.org/docs/current/sql-copy.html" rel="noopener noreferrer"&gt;&lt;code&gt;COPY&lt;/code&gt;&lt;/a&gt; loads the JSON Lines file as one row per configuration, where CSV mode with impossible delimiter and quote characters makes sure that backslashes in the text are not read as escapes. The recursive CTE opens objects with &lt;a href="https://www.postgresql.org/docs/current/functions-json.html" rel="noopener noreferrer"&gt;&lt;code&gt;jsonb_each&lt;/code&gt;&lt;/a&gt; and arrays with &lt;a href="https://www.postgresql.org/docs/current/functions-json.html" rel="noopener noreferrer"&gt;&lt;code&gt;jsonb_array_elements&lt;/code&gt;&lt;/a&gt; including the ordinality, and both happen in one lateral join, so that one recursion serves both cases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Load JSON Lines: one row per configuration. CSV mode with impossible quote and&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- delimiter characters keeps backslashes in the text from being read as escapes.&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_json&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;doc&lt;/span&gt;   &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_json&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="s1"&gt;'/tmp/configs.jsonl'&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QUOTE&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\x&lt;/span&gt;&lt;span class="s1"&gt;01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELIMITER&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\x&lt;/span&gt;&lt;span class="s1"&gt;02'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Flatten recursively: objects via jsonb_each, arrays via jsonb_array_elements&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- with ordinality; anything that is neither object nor array is a leaf.&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leaf_json&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- CASCADE: the views of an earlier run depend on it&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leaf_json&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CTE_node&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'@id'&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'@type'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_json&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;CTE_node&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;jsonb_each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;jsonb_typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="s1"&gt;'['&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;ordinality&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;']'&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;jsonb_typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'[]'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;ORDINALITY&lt;/span&gt;
 &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;config_id&lt;/span&gt;
 &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
 &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
 &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;CTE_node&lt;/span&gt;
 &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;jsonb_typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loading takes under half a second, the recursion just under four, and the result is row-identical to the &lt;code&gt;OPENJSON&lt;/code&gt; route: 1,528,476 rows, 1026 paths. More on &lt;code&gt;COPY&lt;/code&gt; and its variants is in &lt;a href="https://sql.marcus-belz.de/en/transfer-data-sql-server-to-postgresql/" rel="noopener noreferrer"&gt;Transferring data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;XML directly works in Postgres as well, with &lt;a href="https://www.postgresql.org/docs/current/functions-xml.html#FUNCTIONS-XML-PROCESSING-XMLTABLE" rel="noopener noreferrer"&gt;&lt;code&gt;xmltable&lt;/code&gt;&lt;/a&gt; instead of &lt;code&gt;nodes()&lt;/code&gt;, and with a trap that is bigger than anything in SQL Server. The function &lt;code&gt;xmltable&lt;/code&gt; over the whole 27-MB document with the row path &lt;code&gt;/Catalog/Configuration//*[not(*)]&lt;/code&gt; was still running after 36 minutes in the measurement with Postgres 18.6. It reacted neither to &lt;a href="https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SIGNAL" rel="noopener noreferrer"&gt;&lt;code&gt;pg_cancel_backend&lt;/code&gt;&lt;/a&gt; nor to &lt;code&gt;pg_terminate_backend&lt;/code&gt; and only ended with the container. That was no outlier, as a follow-up measurement with partial catalogs shows: for 50, 100 and 200 configurations the same query takes 4, 17 and 99 seconds, that is, a good five times as long per doubling, and for 3000 configurations that would be hours. The obvious explanation is a call into the XML library libxml2 that does not check for interrupts. What the measurement proves is only the behaviour, not the cause. After the split into one row per configuration, the same &lt;code&gt;xmltable&lt;/code&gt; runs as a lateral join over the fragments in around four seconds, and it does so even when it computes the positions right away, because &lt;code&gt;count(preceding-sibling::*)&lt;/code&gt; is cheap on a 9-KB fragment. The ordinals only for repeated siblings then come set-based with &lt;code&gt;dense_rank()&lt;/code&gt; over configuration, parent path and name. The script for it is in the XML download for Postgres. Postgres generates the views from the paths with &lt;a href="https://www.postgresql.org/docs/current/functions-string.html#FUNCTIONS-STRING-FORMAT" rel="noopener noreferrer"&gt;&lt;code&gt;format()&lt;/code&gt;&lt;/a&gt; and runs the DDL with &lt;a href="https://www.postgresql.org/docs/current/app-psql.html" rel="noopener noreferrer"&gt;&lt;code&gt;\gexec&lt;/code&gt;&lt;/a&gt;, three views with around 620 columns each, each in around ten milliseconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Generate the views from the paths: format() builds the DDL, \gexec runs it.&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- List paths (with [n]) and the two attributes of the configuration stay out.&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="s1"&gt;'CREATE OR REPLACE VIEW stage.vw_%s AS SELECT config_id%s FROM stage.leaf_json WHERE config_type = %L GROUP BY config_id;'&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;string_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;   ,max(value) FILTER (WHERE path = %L) AS %I'&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;both&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'[^a-z0-9]+'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'g'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;config_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;config_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leaf_json&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'/@%'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%[%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;config_type&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;gexec&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;columns_generated&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'vw&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;%'&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- vw_cargo_bike      621&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- vw_road_bike       615&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- vw_trekking_bike   621&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limits and decisions
&lt;/h2&gt;

&lt;p&gt;The method has a fixed frame, and that frame should be known before using it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Depth:&lt;/strong&gt; The ancestor names are columns, and their number is fixed. Four levels cover the catalog, a document with ten levels needs ten columns or a different way of building the path, and the check from pass 1 shows whether the reserve is enough. The recursive CTE of the JSON route does not know this limit, but in SQL Server it knows another one: after 100 recursion levels it stops with an error, until &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/with-common-table-expression-transact-sql" rel="noopener noreferrer"&gt;&lt;code&gt;OPTION (MAXRECURSION 0)&lt;/code&gt;&lt;/a&gt; lifts the limit. Postgres recurses without a limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Namespaces:&lt;/strong&gt; &lt;code&gt;local-name()&lt;/code&gt; ignores namespaces, which is intended here. Two elements with the same local name from different namespaces thus fall onto one path. Whoever has to tell them apart adds &lt;code&gt;namespace-uri()&lt;/code&gt; as a further column. A &lt;code&gt;name()&lt;/code&gt; with prefix does not exist in SQL Server's XQuery, and the prefix would only be a spelling of the URI anyway. To address a namespace in the path on purpose, there is &lt;code&gt;WITH XMLNAMESPACES&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spelling and collation:&lt;/strong&gt; XML names are case-sensitive, the text columns of the database with the default collation &lt;code&gt;SQL_Latin1_General_CP1_CI_AS&lt;/code&gt; are not. Two elements &lt;code&gt;Gears&lt;/code&gt; and &lt;code&gt;gears&lt;/code&gt; thus collapse onto one path in the inventory, and the detection of repetitions wrongly reports their parent element as repeated. Whoever has such names creates the name and path columns with a binary collation such as &lt;code&gt;Latin1_General_100_BIN2&lt;/code&gt;. If both spellings sit in the same configuration, the inventory shows the case through the same metric as a missing ordinal: more values than configurations. If they are spread over different configurations, the metric stays silent, and the case only shows up once the paths are counted under a binary collation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Column widths and character set:&lt;/strong&gt; Names with &lt;code&gt;varchar(100)&lt;/code&gt;, values with &lt;code&gt;varchar(200)&lt;/code&gt; and paths with &lt;code&gt;varchar(400)&lt;/code&gt; are tailored to the catalog, whose longest path has 32 and whose longest value has 19 characters. Longer strings are silently truncated by &lt;code&gt;value()&lt;/code&gt; to the target width, without error and without warning. Just as silently, characters that the code page of &lt;code&gt;varchar&lt;/code&gt; does not know are lost: &lt;code&gt;Łódź&lt;/code&gt; becomes &lt;code&gt;Lódz&lt;/code&gt;. Whoever expects such values takes &lt;code&gt;nvarchar&lt;/code&gt;. For foreign documents the widths are adjusted to the data. A first pass with wide columns and &lt;code&gt;MAX(LEN(…))&lt;/code&gt; shows what is needed. A blanket &lt;code&gt;varchar(max)&lt;/code&gt; is no option for the path: it is the key of the index on the path/value table, and a &lt;code&gt;max&lt;/code&gt; type cannot be an index key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed content:&lt;/strong&gt; Text alongside child elements falls through &lt;code&gt;[not(*)]&lt;/code&gt;. Such documents need an additional pass over &lt;code&gt;text()&lt;/code&gt; nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elements with XML attributes only:&lt;/strong&gt; An element like &lt;code&gt;&amp;lt;Warranty years="2"/&amp;gt;&lt;/code&gt; has neither text nor children. Pass 1 matches it nonetheless and delivers a row with an empty value, because &lt;code&gt;[not(*)]&lt;/code&gt; only asks about child elements. The XML attribute is collected correctly by its own pass as &lt;code&gt;/General/Warranty/@years&lt;/code&gt;. The empty row disappears if the predicate is extended by &lt;code&gt;[not(@*) or text()]&lt;/code&gt;: leaves without XML attributes are kept even when empty, leaves with XML attributes only if they have text. A plain &lt;code&gt;[text()]&lt;/code&gt;, by contrast, would also discard deliberately empty elements such as &lt;code&gt;&amp;lt;Belt/&amp;gt;&lt;/code&gt;. Whoever reaches for &lt;code&gt;normalize-space()&lt;/code&gt; for this gets an error, because the function is missing in SQL Server's XQuery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Types and localization:&lt;/strong&gt; All values are text. Decimal point or comma, date formats and yes/no spellings are decided only in the typed view, and there with &lt;code&gt;TRY_CONVERT&lt;/code&gt;, not with &lt;code&gt;CONVERT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File size:&lt;/strong&gt; An &lt;code&gt;xml&lt;/code&gt; value holds up to 2 GB, and &lt;code&gt;SINGLE_BLOB&lt;/code&gt; reads the whole file at once. With documents in the gigabyte range the split is the bottleneck, and from a certain size a streaming parser outside the database is the better choice. That is the topic of the twin article on Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known, small schema:&lt;/strong&gt; Whoever has twenty attributes and knows them needs none of this. A &lt;code&gt;nodes()&lt;/code&gt; with twenty &lt;code&gt;value()&lt;/code&gt; columns is then shorter and faster. The generic route pays off when the schema is large, variable or unknown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a decision aid:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Starting point&lt;/th&gt;
&lt;th&gt;Recommended route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;XML, schema large or unknown, file fits into memory&lt;/td&gt;
&lt;td&gt;Split, pass 1, detection, pass 2, inventory, generated views&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON, schema large or unknown&lt;/td&gt;
&lt;td&gt;One row per document, recursive CTE with &lt;code&gt;OPENJSON&lt;/code&gt; or JSONB, inventory, generated views&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XML, but conversion to JSON planned anyway&lt;/td&gt;
&lt;td&gt;Convert, then the JSON route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema small and known&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;nodes()&lt;/code&gt; with fixed &lt;code&gt;value()&lt;/code&gt; columns, no path/value table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File in the gigabyte range&lt;/td&gt;
&lt;td&gt;Streaming parser outside the database, load as JSON Lines, rest in SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The hours in that project did not come from the programming language but from the design: code per type, access per attribute, schema in your head.&lt;/li&gt;
&lt;li&gt;The database route does not know the attributes in advance. It loads raw, shreds generically into path and value, and afterwards counts what exists. What it requires are the element of the sub-documents, an upper bound for the depth, and lists in which an element name repeats in at least one configuration.&lt;/li&gt;
&lt;li&gt;In SQL Server, the split into one row per configuration is the decisive step. After that, one pass over &lt;code&gt;nodes()&lt;/code&gt; with the parent axis delivers all leaves in around two seconds.&lt;/li&gt;
&lt;li&gt;Computing positions for all 1.5 million leaves raises the runtime from around two to around 40 seconds. They are therefore computed only for the elements that repeat according to pass 1.&lt;/li&gt;
&lt;li&gt;The inventory makes required, optional and variant attributes per type visible, and from it the database generates the views with hundreds of columns itself.&lt;/li&gt;
&lt;li&gt;JSON does without the second pass, in Postgres JSONB is the fastest route, and &lt;code&gt;xmltable&lt;/code&gt; over a whole large document is a trap there.&lt;/li&gt;
&lt;li&gt;The path/value table is a landing zone, not a data model. The generated and typed views are the layer for the consumers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I read XML with an unknown structure in SQL Server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Load the document as an &lt;code&gt;xml&lt;/code&gt; value, split it with &lt;code&gt;nodes()&lt;/code&gt; into one row per sub-document, and then fetch every leaf element with &lt;code&gt;nodes('//*[not(*)]')&lt;/code&gt;. The parent axis &lt;code&gt;local-name(..)&lt;/code&gt; delivers the names of the ancestors, from which the path is built, and &lt;code&gt;value('.')&lt;/code&gt; delivers the value. The result is a path/value table from which a grouping makes the schema readable. Required are the element of the sub-documents and an upper bound for the depth of the tree. The method recognises lists only when an element name repeats within them in at least one configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do repeated elements get their position in SQL Server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The XQuery expression &lt;code&gt;let $p := . return count(../*[local-name() = "Part"][. &amp;lt;&amp;lt; $p]) + 1&lt;/code&gt; counts the same-named siblings that precede the current element in the document, and thus delivers its position. For all 1.5 million leaves this expression is too expensive. It raises the runtime from around two to around 40 seconds. So pass 1 fetches all leaves without position, a grouping reads off at which positions elements repeat at all, and pass 2 computes the position only there. A &lt;code&gt;ROW_NUMBER()&lt;/code&gt; over the output of &lt;code&gt;nodes()&lt;/code&gt; looks simpler, but does not guarantee the document order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isn't the path/value table exactly the EAV model everyone warns about?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, in form. No, in role. The entity-attribute-value pattern is a problem when it is the permanent data model of an application, because types, constraints and simple queries are missing. Here the table is an intermediate stage between the raw document and the generated views. It is filled once per load run, counted once, and the consumers see views with real columns. Whoever queries the views often materializes them and is back at an ordinary table model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XML or JSON: what should I load if I have the choice?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JSON, if the ordinals of lists matter and the conversion happens anyway. An array brings its order with it, and the recursive CTE needs no second pass. XML directly, if the file arrives that way and should not be touched again. In SQL Server the XML route took around 15 and the JSON route around 25 seconds for 1.5 million values. In Postgres, JSONB was clearly ahead at around four seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not OPENXML?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/openxml-transact-sql" rel="noopener noreferrer"&gt;&lt;code&gt;OPENXML&lt;/code&gt;&lt;/a&gt; with &lt;code&gt;sp_xml_preparedocument&lt;/code&gt; delivers an edge table of the whole document in which every node is a row with an ID and a parent ID, in document order. That sounds like exactly the generic access this article is looking for. In the measurement, the edge table alone for 3 million nodes took around 55 seconds, the text column arrives as &lt;code&gt;ntext&lt;/code&gt; and cannot be indexed, and the prepared document occupies memory until it is explicitly released. The route through &lt;code&gt;nodes()&lt;/code&gt; was around thirty times faster in this measurement and needs no handles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just &lt;code&gt;nodes()&lt;/code&gt; with fixed paths per attribute?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because that is exactly the loop in the code, only in SQL: one &lt;code&gt;value()&lt;/code&gt; call per attribute, a thousand calls per type, and a change for every new attribute. For a known schema with twenty attributes that is the right way. For a thousand attributes that nobody knows completely, the generic shredding with a subsequent inventory in SQL is the version that does not have to change with every catalog change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Upstream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/set-based-vs-row-by-row/" rel="noopener noreferrer"&gt;Set-Based vs. Row by Row: PostgreSQL Refresh from 114 to 12 Seconds&lt;/a&gt; (the general argument against the loop per element)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/etl-vs-elt-explained/" rel="noopener noreferrer"&gt;ETL vs. ELT — How to Tell Which Pattern You Actually Built&lt;/a&gt; (load raw, then transform in SQL)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/design-pattern-the-architecture-of-an-etl-process-how-to-isolate-bad-data-cleanly/" rel="noopener noreferrer"&gt;Design Pattern // The Architecture of an ETL Process&lt;/a&gt; (the layers into which the path/value table fits)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Downstream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/derive-data-quality-rules-from-schema/" rel="noopener noreferrer"&gt;Deriving Data Quality Rules from the Schema&lt;/a&gt; (the inventory as a data quality tool)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/safe-type-conversion-t-sql/" rel="noopener noreferrer"&gt;Design Pattern // Safe Type Conversion with T-SQL&lt;/a&gt; (the typed second view layer)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/transfer-data-sql-server-to-postgresql/" rel="noopener noreferrer"&gt;Transferring Data: bcp, COPY, pgloader, ETL&lt;/a&gt; (loading on the Postgres side)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Downloads:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/download/3975/" rel="noopener noreferrer"&gt;XML route for SQL Server: catalog as XML, generator and scripts (ZIP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/download/3978/" rel="noopener noreferrer"&gt;XML route for Postgres: catalog as XML, generator and script (ZIP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/download/3981/" rel="noopener noreferrer"&gt;JSON route for SQL Server: catalog as JSON Lines, generator and scripts (ZIP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/download/3984/" rel="noopener noreferrer"&gt;JSON route for Postgres: catalog as JSON Lines, generator and script (ZIP)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>sqlserver</category>
      <category>xml</category>
      <category>database</category>
    </item>
    <item>
      <title>The Context Window Is Full Before the First Prompt — When Project Rules in Claude Code Become a Fixed Cost of Every Session</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:23:31 +0000</pubDate>
      <link>https://dev.to/marcus1968/the-context-window-is-full-before-the-first-prompt-when-project-rules-in-claude-code-become-a-4p7c</link>
      <guid>https://dev.to/marcus1968/the-context-window-is-full-before-the-first-prompt-when-project-rules-in-claude-code-become-a-4p7c</guid>
      <description>&lt;p&gt;A subagent in Claude Code is supposed to answer a one-line question about the tool's documentation. It aborts before taking its first step: &lt;code&gt;Prompt is too long&lt;/code&gt;, 347,814 tokens against a limit of 200,000. The question itself had loaded nothing. What had loaded were the project instructions that every session in this repository receives at start. The subagent ran on a model with a context window of 200,000 tokens, and the request that was meant to start it was already almost twice the size of that window. The main session that had started it ran with a window of one million tokens. It did not abort, it was half full before anyone had typed anything.&lt;/p&gt;

&lt;p&gt;This article describes how these project instructions in the private DI² project could grow to that size. It shows what a measurement with &lt;code&gt;/context&lt;/code&gt; found and what three levers achieved that were applied afterwards. The mechanics and all numbers apply to Claude Code as of October 15, 2026. The measurement was taken with version 2.1.145, the subagent's failure comes from a session with version 2.1.292. The cause, however, is not limited to Claude Code. It affects every tool that hands an agent project rules at start. In such rule files, two kinds of text accumulate over time: instructions for the next step, say where a new test file belongs, and evidence of past steps, say which test file secures which bug. This article calls the first a &lt;strong&gt;rule&lt;/strong&gt; and the second a &lt;strong&gt;record&lt;/strong&gt;. Both belong in the repository. Into the context that every session loads at start belongs only the rule, because only the rule is needed for the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project rules are a fixed cost of every session:&lt;/strong&gt; Everything in the project's &lt;code&gt;CLAUDE.md&lt;/code&gt;, its imports and the rule files without a file scope loads in Claude Code at start, before the first prompt arrives. In the DI² repository that was a measured 454,900 tokens on October 15, 2026, just under half of a one-million-token context window and more than twice a 200,000-token window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dividing line is rule versus record, not long versus short:&lt;/strong&gt; A rule says where a file belongs and which pattern it uses, a record proves what was checked back then and why. After a good six months of maintenance, the largest rule file consisted to 84 percent of an inventory table with one row per test file. Nobody needs it while writing a test, yet every session paid for it at start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three levers, measured before and after:&lt;/strong&gt; Bind rules with a file scope to paths, move inventories into a neighboring file, and phrase instructions so that the agent looks things up instead of preloading everything. Afterwards the project instructions stood at 64,700 instead of 454,900 tokens, and the subagent that had aborted before started again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binding to paths has a limit:&lt;/strong&gt; A bound rule arrives only with the result of the tool call, and for a file that is just being created, that is too late. Three safety nets help against that, and without a guardrail in CI the growth comes back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Claude Code basics are explained briefly where they are needed. All token numbers come from &lt;code&gt;/context&lt;/code&gt; in a fresh session, with date and version. Where a number is only estimated or extrapolated, it says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Session Costs Before Anyone Types
&lt;/h2&gt;

&lt;p&gt;At the start of a session, Claude Code reads three kinds of project instructions. The project's &lt;code&gt;CLAUDE.md&lt;/code&gt; in the root directory of the repository. Everything this file pulls in via &lt;code&gt;@&lt;/code&gt; import, up to four levels deep. And every rule file under &lt;code&gt;.claude/rules/&lt;/code&gt; that carries no &lt;code&gt;paths:&lt;/code&gt; frontmatter. How a repository ends up with 30 such rule files and what is in them is described in &lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;27 Rule Files Instead of One CLAUDE.md&lt;/a&gt;. The point here is that these three items sit in the context before the first prompt is typed, and they do so in every session, regardless of whether the task takes one line or a whole day.&lt;/p&gt;

&lt;p&gt;How much that is, the &lt;code&gt;/context&lt;/code&gt; command shows. It lists every loaded instruction file individually with a token count that the tool determines itself, rounded to the hundred. The count depends on the session's model: A probe counted the same files noticeably lower under Haiku 4.5 than under Opus 4.7. The reason is the tokenizer: Models from Claude 4.7 onwards use a newer one that, &lt;a href="https://platform.claude.com/docs/en/models/haiku-5-5/whats-new-haiku-5-5" rel="noopener noreferrer"&gt;according to the documentation&lt;/a&gt;, counts the same text at roughly 30 percent more tokens than the older one, somewhat more or less depending on the text. It also depends on the content, per file between 1.9 and 2.2 bytes sat on one token, and it is reproducible for the same state and the same model. For a repeatable measurement, a start without the interactive interface is enough, with &lt;code&gt;/context&lt;/code&gt; as the only input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude -p "/context"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the DI² repository, this gave the following on October 15, 2026, with the Opus 5.5 model, before any change:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt;, its imports and 27 rule files without &lt;code&gt;paths:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;454,900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-memory (user-local memory file, not in the repository)&lt;/td&gt;
&lt;td&gt;4,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System prompt, tools, skills, agents&lt;/td&gt;
&lt;td&gt;27,900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Occupied before the first prompt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;487,500&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row is the fixed cost this article talks about: the project instructions that load at every start and that the project itself controls. The two other items come on top but do not depend on the repository.&lt;/p&gt;

&lt;p&gt;Which window this number fills depends on the model. The main session ran on Fable 5.1 and Opus 5. Both have a window of one million tokens in Claude Code, and both count text like Opus 5.5: A probe with the same two files gave the same values under all three models. There, 487,500 tokens meant 49 percent occupied before anyone had typed anything. For a model with a 200,000-token window, it is 244 percent. After deducting the reserve of 33,000 tokens that Claude Code, in the measured version 2.1.145, keeps free for automatic compaction, nothing is left there, and that is the kind of model the subagent from the opening of this article ran on. Its error message named 347,814 tokens, not 487,500, because the two do not count the same thing: The message counts the subagent's request with the Haiku 4.5 tokenizer, &lt;code&gt;/context&lt;/code&gt; counts the main session's start with the Opus 5.5 tokenizer. This article therefore compares only within &lt;code&gt;/context&lt;/code&gt; with the same model, the error message proves the yes or no. Three files carried just under half: the test rule with 124,000 tokens, the rule for the application's register layout with 70,700, and the brand rule with 28,600.&lt;/p&gt;

&lt;p&gt;The request that led to this measurement came the evening before, with the test rule open in the editor. It comes from the session transcript, was dictated, and is smoothed here for language and translated from German:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I've noticed that the context window fills up pretty quickly when I
develop new features or fix a bug. I just opened the file testing.md
and saw that it is pretty long. It probably gets loaded into the
context window in full whenever a unit test runs, or whenever, and
uses up a lot of it that way. Can you please analyze where and during
which actions the context window fills up quickly, and make
suggestions for how we deal with it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requested was an analysis with suggestions, not a measurement. The agent answered with “I'll first measure what actually loads at session start”, and what came next was an estimate: the size of the loaded files in bytes divided by 3.7, a rule of thumb for bytes per token. The files totaled 902,576 bytes that day, the result was 243,939, rounded 244,000 tokens. Observed rather than estimated was only one part, namely which files were actually loaded. That the agent could look up in its own instructions. Exactly three rule files were missing there, the ones for backend, frontend and security, and those are not the three largest just named, but the three that were already bound to file paths at the time and therefore do not load at start. How that works is shown by the lever “Binding rules to files with &lt;code&gt;paths:&lt;/code&gt;”. The measurement came only the day after, and it gave 454,900 tokens for 909,920 bytes, that is 2.0 bytes per token. The rule of thumb had underestimated the token count of German-language Markdown with tables, paths and code identifiers by almost half. All numbers in this article are therefore either measured or explicitly marked as an estimate or extrapolation. Whoever wants to know their own fixed cost measures it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grown With the Work
&lt;/h2&gt;

&lt;p&gt;The 454,900 tokens were the result of a good six months of maintenance. The first rule file was created at the end of March 2026, and the repository's history makes it possible to trace how many tokens the rule files would have cost on the first of each month. These values are not measured, that would have taken a separate session in a separate worktree for every month. They are extrapolated from the file sizes with the ratio of 2.0 bytes per token measured on October 15. Whether the historical states had the same ratio is not checked, per file it lay between 1.9 and 2.2 on the day of measurement. As an order of magnitude that is enough, and the table claims no more.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Rule files total (tokens, extrapolated)&lt;/th&gt;
&lt;th&gt;Of which the test rule (tokens, extrapolated)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April 1, 2026&lt;/td&gt;
&lt;td&gt;8,600&lt;/td&gt;
&lt;td&gt;does not exist yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 1, 2026&lt;/td&gt;
&lt;td&gt;26,600&lt;/td&gt;
&lt;td&gt;does not exist yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;June 1, 2026&lt;/td&gt;
&lt;td&gt;117,300&lt;/td&gt;
&lt;td&gt;does not exist yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;August 1, 2026&lt;/td&gt;
&lt;td&gt;219,000&lt;/td&gt;
&lt;td&gt;13,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;September 1, 2026&lt;/td&gt;
&lt;td&gt;250,900&lt;/td&gt;
&lt;td&gt;22,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;October 1, 2026&lt;/td&gt;
&lt;td&gt;391,100&lt;/td&gt;
&lt;td&gt;94,600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;October 14, 2026&lt;/td&gt;
&lt;td&gt;441,100&lt;/td&gt;
&lt;td&gt;119,800&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Since June the volume has almost quadrupled, and between September and October alone roughly 140,000 tokens came in, 72,000 of them in the test rule. Behind this jump is a maintenance duty. The test rule required that every new test file add a row to its inventory: which file checks what, why the test came about, and which counter-check proved that it catches the bug. Every single one of these rows makes sense. Whoever later wants to know whether a function already has a test finds the answer there. In sum, however, the duty turned the rule file into a record that every session pays for.&lt;/p&gt;

&lt;p&gt;And every one of these rules came out of a real bug, and every one prevents the same bug from coming back. Their value is not in question. What makes them expensive is the moment they load, which is the same for all of them, namely always, and the fact that in the same file two kinds of text grow that are needed at different times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule or Record: The 84 Percent Case
&lt;/h2&gt;

&lt;p&gt;The DI² project's test rule was, at 124,000 tokens, the largest rule file, measured with &lt;code&gt;/context&lt;/code&gt;. Of that, around 104,000 tokens went to the inventory table with one row per test file, 162 rows on the day of measurement. That is 84 percent of the file, calculated via the table's share of the bytes, because &lt;code&gt;/context&lt;/code&gt; counts per file, not per section. The measurement after extracting the table confirmed the arithmetic: Without it, the file cost 20,300 tokens. The remaining 16 percent were the actual rules: where test files go, which test environment to choose, mock patterns, time budgets, load tests, and a list of things one does not do. The longest single table row had 15,129 characters, more than the entire rule file for functions or the one for triggers.&lt;/p&gt;

&lt;p&gt;What the inventory rows answer is a record question: What was checked back then, why, and with which counter-check? What is needed when writing a new test is a rule question: Where does the file go, which environment, which mock pattern? The rows are not bad. They are in the wrong place, and in the wrong place they are paid for at every session start, including in the sessions in which nobody writes a test.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Answers&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rule&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the &lt;strong&gt;next&lt;/strong&gt; decision: where, how, what not&lt;/td&gt;
&lt;td&gt;“Test files sit next to the source file and end in &lt;code&gt;.test.ts&lt;/code&gt;.”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Record&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;a &lt;strong&gt;past&lt;/strong&gt; one: what was checked, why, with which counter-check&lt;/td&gt;
&lt;td&gt;“This file checks the rounding because an amount was rounded wrongly. Negative control: test red without the fix.”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same pattern showed up in four more rule files, there at harmless size. The rule for dropdown menus consisted to 35 percent of an inventory of its users, the one for naming toasts to 33 percent, the one for confirmation dialogs to 33 percent, the one for dialogs to 28 percent. In absolute terms that was only a few thousand tokens each, which is why it never stood out. The test rule differs from them not in pattern but only in scale: one row per test file, with growing test coverage.&lt;/p&gt;

&lt;p&gt;The dividing line runs along the function of a text, not its form. The same test rule holds a list of known gaps that are deliberately not covered by tests. It looks like an inventory but is a rule, because it is meant to act at the next intervention: Whoever touches one of these places should know that no test will catch them. This list stayed in the rule file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Gets Added While Working
&lt;/h2&gt;

&lt;p&gt;The fixed cost is only the beginning. As soon as a session works, reads come on top, and the largest of them is almost half the size of the fixed cost itself. The following values are extrapolated, not measured with &lt;code&gt;/context&lt;/code&gt;, because the command shows only the start. They are based on the file sizes and the measured ratio of 2.0 bytes per token.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Tokens (extrapolated)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reading the bug register to find the next free number&lt;/td&gt;
&lt;td&gt;around 210,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading the application's largest component once&lt;/td&gt;
&lt;td&gt;around 155,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loading the tool guide for the bug workflow&lt;/td&gt;
&lt;td&gt;around 22,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row is the interesting one. What is needed there is a single number, the highest number assigned. The table is sorted in descending order, so the first data row carries the answer. That the entire register wanders into the context instead, more than a full context window, is not the tool's fault. It comes from the instruction “read the register and determine the next number”. The agent does what it says.&lt;/p&gt;

&lt;p&gt;Together with the fixed cost, this explains the observation that stood at the beginning: start, tool guide, one large file, and the session is full before the first line of code is written. For subagents it is more than an annoyance. A subagent usually receives the project instructions, only the built-in &lt;code&gt;Explore&lt;/code&gt; and &lt;code&gt;Plan&lt;/code&gt; agents start without them. The main session's context window it does not inherit: Its window follows its own model, and some of the built-in agents run on smaller models than the main session. The agent that answers questions about the Claude Code documentation ran, in version 2.1.292, on Haiku 4.5 with a 200,000-token window. With 454,900 tokens of instructions, it could not start in this repository, while an agent that inherits the main session's model still had room with one million tokens. The failure from the opening of this article was exactly that, here the message verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt is too long · the request is ~347814 tokens (limit 200000) but this
conversation is only ~227482 tokens — the rest is system prompt, tool
definitions, and attachment content. A single-exchange conversation cannot
be compacted; reduce attached files/tools or start with less context.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The failure even prevented its own investigation: The attempt to clarify the tool's loading rules from the documentation via a subagent failed at the very context limit it was supposed to explain. Since version 2.1.293, the same agent runs on Haiku 5.5 with a window of one million tokens. Reproduced on October 20, 2026, with version 2.1.301 on the state before the rework: The agent started, its request had 485,056 tokens, counted with that model's newer tokenizer, and it answered the probe question. The fixed cost has not become smaller by that. Only the threshold at which it becomes visible has moved back.&lt;/p&gt;

&lt;p&gt;The table's second row, the largest component, applies to the day of measurement. Since then it has resolved itself, for a reason that had nothing to do with the context window. The largest component had 7,103 lines. In September, a bug fix had failed on it because the interaction of two load runs was not visible in any diff, and since then the file was under a lock for any non-trivial change. On the same day as the rework of the rules, its decomposition therefore began, behavior-neutral and in cuts, each its own commit. On October 19, 2026, it consisted of an orchestrator with 1,607 lines and 23 units for load paths, locks, mutations and dialogs, none above 1,000 lines, with 21 test files of their own alongside. The code did not get smaller in the process but grew by a third, because every unit needs a header, an interface and imports. What dropped is what a single task has to load. Whoever changes a load path today reads a hook with 900 lines, extrapolated around 22,000 tokens, instead of the whole file at around 155,000. That is the same dividing line as with the rules, only in source code: Nothing was shortened, what changed is what enters the context and when. The cut was needed first by the human who wants to see an interaction in the diff. The agent that reads the file gets the same benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Levers, and What They Cost
&lt;/h2&gt;

&lt;p&gt;The same rule applies to all three: No rule is shortened in substance. Each came from a real bug. What changes is only when it loads and what sits next to it in the same file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Binding Rules to Files With &lt;code&gt;paths:&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Claude Code knows a &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;&lt;code&gt;paths:&lt;/code&gt; frontmatter&lt;/a&gt; for rule files. A rule that carries it does not load at start but only when the agent reads, writes or edits a file that matches one of the patterns. For Write and Edit this has applied since version 2.1.288 of October 2, 2026, before that only a read loaded the rule. The measurement in this article with 2.1.145 is unaffected, it counts only the start. The patterns are glob patterns, relative to the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
paths:
  - "src/components/**"
  - "src/app/**"
  - "src/lib/**"
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the DI² repository, 3 of 30 rule files used this on the day of measurement, although most rules are tied to a file type. The SQL rules apply to files under &lt;code&gt;db/&lt;/code&gt;, the rules for register layout, dialogs and toasts to components. After the rework, 24 of 30 files carried a &lt;code&gt;paths:&lt;/code&gt; frontmatter. That was 264,300 tokens and thus a good two thirds of the total savings.&lt;/p&gt;

&lt;p&gt;The lever has a limit. A bound rule arrives only with the result of the tool call, because the call on a matching file is what loads it. A probe on October 15, 2026, in Claude Code 2.1.292 showed it: In a session whose context did not yet hold the frontend rule, a read of a component loaded it only in the result of that read. When editing an existing file this is harmless, because Claude Code's Edit tool requires a prior read of the file and the rule is then already there. If the agent changes the file through the shell instead, it does not load the rule. When writing a new file, the content is written before the rule arrives. A second probe on October 20, 2026, showed this in two versions: A rule required a specific first line for every new file under &lt;code&gt;src/&lt;/code&gt;. In 2.1.145 the Write call did not load the rule at all, in 2.1.301 it arrived after the call's result, and in both cases the file came into being without that line. Whether the agent notices that afterwards depends on the run: In a third probe with 2.1.301 it again wrote the file without the line, saw the rule with the result of the call, and added the line with a second call. That cannot be relied on. Precisely when a convention is meant to prevent something from being created wrongly, it is not there.&lt;/p&gt;

&lt;p&gt;Three safety nets close this gap:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Creation rules stay global:&lt;/strong&gt; The test rule and the rule for page scaffolds apply when creating files that do not exist yet. Both stay without &lt;code&gt;paths:&lt;/code&gt; and carry a comment at the top of the file that explains why. Without that comment, the next cleanup session tidies them away as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow skills read explicitly beforehand:&lt;/strong&gt; The skill that creates backend code reads the SQL rules before it writes the first file. The skills for frontend and interface design got the same step for the interface rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rule map in &lt;code&gt;CLAUDE.md&lt;/code&gt;:&lt;/strong&gt; One line per bound rule with file name and scope, without content. The map costs 1,800 tokens and deliberately sits in &lt;code&gt;CLAUDE.md&lt;/code&gt;, because that file is read in again after an automatic compaction of the context. Without it the agent does not know that a rule exists, and therefore does not ask for it either. Then binding would not be a saving but a loss of substance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The comment at the top of the test rule, translated from German:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- Deliberately WITHOUT paths: frontmatter. A bound rule loads only with
     the result of the Read/Write/Edit on a matching file. For a file that
     does not exist yet, that is too late. Applies when CREATING a test
     file (location, suffix, mocks, budgets). Do not bind. --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines from the map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- .claude/rules/sql.md — overarching SQL style guide (naming, alignment), db/**
- .claude/rules/tables.md — table DDL, constraints, comments, db/**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two pitfalls come with it. The frontmatter is YAML, and Claude Code reads it at the start of every session to decide which rules are bound. A glob pattern that begins with an asterisk and stands without quotes, say &lt;code&gt;- **/*.tsx&lt;/code&gt;, is read by YAML as a reference to an anchor, not as text. The parser aborts, and Claude Code then loads the rule as if it had no &lt;code&gt;paths:&lt;/code&gt;, that is, at every start. In normal operation it does not report this. A probe in Claude Code 2.1.145 confirmed it: Of three rules with &lt;code&gt;paths:&lt;/code&gt;, exactly the one with the unquoted asterisk appeared among the files loaded at start, and only &lt;code&gt;claude --debug&lt;/code&gt; reported the parser error, without naming the file. The saving is then gone, and nobody notices. Patterns in the frontmatter therefore always belong in quotes.&lt;/p&gt;

&lt;p&gt;The second pitfall is a binding that is too narrow. The brand rule, which among other things governs quotation marks and punctuation in user-facing text, was first bound to the dictionary files and one further single file. User-facing text, however, also lives in other files next to them, and from exactly one such file came the bug that had once triggered the rule. It is now bound to the whole directory. A binding that is too wide costs something only when the rule loads. One that is too narrow never loads it where it is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separating Rule and Record
&lt;/h3&gt;

&lt;p&gt;The test rule's inventory table moved into a file of its own under &lt;code&gt;docs/&lt;/code&gt;, with all 162 rows unchanged. The test rule itself kept its rules and two sentences about the inventory: where it lives and that every new test file adds a row there. Without this maintenance duty in the rule, the inventory becomes orphaned. The rule file thus shrank from 124,000 to 20,300 tokens. That is 103,700 tokens saved, a good quarter of the total.&lt;/p&gt;

&lt;p&gt;This is what the separation looks like in concrete terms. Since then, only the reference remains in the test rule, here shortened and translated from German:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Inventory

The inventory of test files (one row per file: what it checks,
triggering bug, negative control) lives outside this rule file in
docs/inventories/unit-tests.md. Do not read the whole inventory,
search for the file name instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extracted file begins with a note on what it is and how to read it. Then follows one row per test file, here one of the shorter ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Inventory of Unit Test Files

&amp;gt; Record, not rule. One row per test file: what it checks and why,
&amp;gt; often with triggering bug and negative control. The rules for unit
&amp;gt; tests live in .claude/rules/testing.md.
&amp;gt;
&amp;gt; Read selectively, not in full: The file name is at the start of
&amp;gt; every row, a search for it finds exactly that one row.
&amp;gt;
&amp;gt; Maintenance duty: add a row for every new test file.

| Test file                  | Project | Tests                                      |
|----------------------------|---------|--------------------------------------------|
| src/lib/topo-order.test.ts | unit    | computeProcessingOrder + refineWithinLevel |
|                            |         | (levels, cycles, tie-break, isStale)       |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What matters is how the extracted file is built. Moving alone would only have relocated the problem, because reading 104,000 tokens in one go as soon as the agent needs the inventory would hardly be better. The file therefore has one row per test file, and the file name is at the start of the row. A search for a file name finds the row without the whole file being read. The file's header says so explicitly: do not read, search.&lt;/p&gt;

&lt;p&gt;In the DI² project this has become a convention, which itself costs 1,500 tokens because it sits in a globally loaded rule. A new inventory is created under &lt;code&gt;docs/&lt;/code&gt;, never as a section of a rule file. The rule names the location and the maintenance duty in one sentence. And large files are not pulled into &lt;code&gt;CLAUDE.md&lt;/code&gt; via &lt;code&gt;@&lt;/code&gt; import, because what it imports loads with it at every start. Splitting a large file across several imports therefore saves nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Looking Up Instead of Preloading
&lt;/h3&gt;

&lt;p&gt;The third lever changes no rule but instructions. The skill that creates a new bug read the bug register in full to find the next number. Now it reads at most the first 30 lines, because the register is sorted in descending order and the first data row carries the highest number. Two further places that searched the register for a specific entry now search with &lt;code&gt;grep&lt;/code&gt; instead of reading. The same thought led to two changes to &lt;code&gt;CLAUDE.md&lt;/code&gt;: The roadmap table of the product description, 89 percent of that file, moved into a file of its own, and the feature index is no longer pulled in via import. The agent reads both when it needs them, and the skills that need them say so explicitly. Together 25,500 tokens.&lt;/p&gt;

&lt;p&gt;Not every full read is waste. The review of all skills found three that stay. The feature index is read in full by eight skills because the context there is actually needed. The product description is small after the rework. The test-case index is appended where it is read. The lever applies to instructions of the form “read the file to find a single value”. For a file whose entire content carries the decision, it does not apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and After: 454,900 to 64,700 Tokens
&lt;/h2&gt;

&lt;p&gt;The rework ran in five steps, each its own commit and individually revertible. First came the purely relocating ones, then binding to paths as the riskiest step, last the guardrail. There was no written-out implementation request. The agent had proposed a spec from the analysis and asked questions about it, which the maintainer answered by selection. Questions and answers are in the transcript verbatim, shown here translated from German:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How far should I take the context rework now?
  -&amp;gt; Everything, including the scoping pass
One feature for all four levers, or split up?
  -&amp;gt; One feature, four building blocks
Scope the register and layout rules? That is the big item,
but the risky one.
  -&amp;gt; Delegate the decision to /architecture
Detach the PRD roadmap and the feature index from the always-includes
of CLAUDE.md?
  -&amp;gt; Yes, with a lean remainder
Should a guard against relapse be created? Without it the next rule
file grows into a record dump again without anyone noticing.
  -&amp;gt; Yes, as a CI guard with baseline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The four levers in this count are the three of this article and the guard, which this article treats separately as the guardrail. The implementation request after that consisted of two words: “sofort umsetzen”, implement now. During implementation, after the loading semantics had been clarified, one last question came: whether the register and layout rules should be bound as well, by the estimate at the time 115,000 tokens together, among them the register rule with a measured 70,700. The answer was yes, and that became the largest item of the savings.&lt;/p&gt;

&lt;p&gt;The acceptance criteria, among them the measurement with &lt;code&gt;/context&lt;/code&gt; in a fresh session and the subagent that has to start again, had been written into the spec by the agent. The maintainer accepted them with the spec rather than formulating them, writes no Node, and could not have written the check script. The rework of the rule files, the script, and the decision which rules stay global were worked out by the agent. From the maintainer came the problem description, the answers above, and at the end the measurement itself: &lt;code&gt;/context&lt;/code&gt; and the subagent probe in a fresh session, started by hand, because a running session cannot re-measure its own start state. What mattered was that the measurement stood in the spec as acceptance from the beginning and was not set after the result.&lt;/p&gt;

&lt;p&gt;The measurement was taken with the tool itself. Both states were available as separate Git worktrees, &lt;code&gt;/context&lt;/code&gt; was the first and only input of a new session without the interactive interface, Claude Code 2.1.145 with the Opus 5.5 model, both runs on October 15, 2026, minutes apart on the same machine. A counter-check in an interactive session counted file by file the same.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Project instructions (&lt;code&gt;CLAUDE.md&lt;/code&gt;, imports, rule files)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;454,900&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;64,700&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-memory (user-local)&lt;/td&gt;
&lt;td&gt;4,700&lt;/td&gt;
&lt;td&gt;4,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System prompt, tools, skills, agents&lt;/td&gt;
&lt;td&gt;27,900&lt;/td&gt;
&lt;td&gt;27,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Occupied before the first prompt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;487,500&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96,600&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share of a one-million-token context window&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;49 %&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share of a 200,000-token context window&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;244 %&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free in a 200,000-token window after the 33,000-token reserve for automatic compaction&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;70,400&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Broken down by lever:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lever&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Binding rules to files&lt;/td&gt;
&lt;td&gt;21 more rule files with &lt;code&gt;paths:&lt;/code&gt;, 3 → 24 of 30&lt;/td&gt;
&lt;td&gt;−264,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separating rule and record&lt;/td&gt;
&lt;td&gt;Test rule 124,000 → 20,300, inventory alongside&lt;/td&gt;
&lt;td&gt;−103,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Looking up instead of preloading&lt;/td&gt;
&lt;td&gt;Roadmap out of the product description, feature index no longer an import&lt;/td&gt;
&lt;td&gt;−25,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost of the implementation&lt;/td&gt;
&lt;td&gt;Rule map (+1,800), convention “rule versus record” (+1,500)&lt;/td&gt;
&lt;td&gt;+3,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;files loaded at start 30 → 8: &lt;code&gt;CLAUDE.md&lt;/code&gt;, imports 2 → 1, unbound rule files 27 → 6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−390,200 (−85.8 %)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second proof is a yes or no. The same subagent that had aborted before started after the rework in a fresh session with 66,887 tokens of context and answered the probe question correctly.&lt;/p&gt;

&lt;p&gt;Three things the numbers do not say.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measured is the fixed cost, not the working cost:&lt;/strong&gt; Bound rules are not gone. A session on the register interface still loads the register rule with its 70,700 tokens as soon as it touches the first component. A session on SQL or documentation no longer loads it. Measured is the start, not a typical working day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The limit of binding is measurable:&lt;/strong&gt; Of the 64,700 tokens, 31,300 belong to the two rules that deliberately stay global because they have to act before a file exists. That is just under half of the remainder, and it cannot be removed without shortening the rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binding shifts who sees a rule:&lt;/strong&gt; Before the rework, every skill had all rules in its context because all of them loaded at start. After the rework, a bound rule loads only when the agent reads or edits a matching file. The skill that reviews a diff does not do that. It reads the diff via &lt;code&gt;git diff&lt;/code&gt; in the shell and touches not a single file in the process. Its context therefore holds none of the bound rules, for instance not the SQL conventions for files under &lt;code&gt;db/&lt;/code&gt;. It would have checked SQL against conventions it does not know, and found nothing. This came up only in the quality assurance of the rework, not during the rework itself. The skill now matches the paths in the diff against the map and reads the corresponding rules explicitly before it reviews. Behind that is the same reflex as in &lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click&lt;/a&gt;: do not assume what the agent sees, go and look.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Without a Guardrail, the Growth Comes Back
&lt;/h2&gt;

&lt;p&gt;The growth from the section “Grown With the Work” came from a maintenance duty without a cap. Without a cap, the savings would be a snapshot, and the next well-meant inventory would produce the same number again in half a year. With the rework, a check script therefore went into CI, without dependencies, as a blocking step. It calculates with bytes divided by 2.0, the measured ratio, and on the state of October 15 that put it about two percent off &lt;code&gt;/context&lt;/code&gt;. That is an approximation with one day's ratio, not a measurement. Measuring continues with &lt;code&gt;/context&lt;/code&gt;, the guard only draws the line, and the budget of 80,000 tokens leaves room against the measured 64,700 for the error of the approximation. It checks seven things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Reports&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed cost&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt;, its imports and all unbound rules above the budget of 80,000 tokens. The total is printed on every run so that a new import does not add fixed cost unnoticed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;a single rule file above the threshold, with separate thresholds for bound and unbound rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory share&lt;/td&gt;
&lt;td&gt;more than 25 percent of a rule file's lines sit under an inventory heading. This check runs separately from size: A large file of pure rule prose is no finding, a small file that is half inventory is. The guard thus does not force rules to be shortened, but records to be moved.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontmatter&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;paths:&lt;/code&gt; block that cannot be parsed, unquoted patterns included. Such a rule counts toward the fixed cost, because that is how the tool loads it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map&lt;/td&gt;
&lt;td&gt;a bound rule without a line in the map, or a map line without a file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orphaned inventory&lt;/td&gt;
&lt;td&gt;number of test files against number of inventory rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;a tolerated legacy that no longer occurs. The baseline may only shrink.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The guard checks files, not behavior. Whether a skill actually reads a bound rule before it reviews, it cannot see. The review skill without SQL rules from the section before was found by quality assurance, not by the guard.&lt;/p&gt;

&lt;p&gt;The first run immediately produced a finding nobody had expected. The orphaned-inventory check found five test files without an inventory row, all from the time before the rework. The maintenance duty “one row for every new test file” had thus let the inventory grow to 104,000 tokens and still had not been kept. A duty that nobody checks therefore keeps neither the inventory complete nor the rule file small.&lt;/p&gt;

&lt;p&gt;The decomposition of the largest component ended with a second guard on the same pattern, this time for source files. It checks every file under &lt;code&gt;src/&lt;/code&gt; for at most 1,000 lines, tests included, with a baseline for the legacy that may only shrink, and a margin of two percent so that a bug fix on an old file gets through and a feature does not. Its first run found 33 files above the limit, six were expected. The rule for it does not sit in a file that loads at every start but in the rule for components, pages and hooks, which is bound to exactly these paths. It therefore enters only the sessions in which such a file can grow. For all other files, the guard speaks up itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Solution Is Not
&lt;/h2&gt;

&lt;p&gt;Shortening rules until they say nothing is not it. After the rework, 20,300 tokens of pure rule prose remained in the test rule, and the acceptance criterion had demanded “at most 12,000”. That number came from the rule of thumb of the day before, with the same error as all the estimates. The number was revised, not the rule. What a rule says, it has earned from a bug, and a token budget is no reason to forget that again.&lt;/p&gt;

&lt;p&gt;A product critique is not it either. The binding mechanism existed the whole time, it was documented, and it demonstrably worked. That 3 of 30 files used it is a matter of maintenance discipline. That an agent has a rule in its context when it needs it is, moreover, the precondition for it to follow the rule at all.&lt;/p&gt;

&lt;p&gt;And a check that calculates with the same wrong assumption as what it checks is least of all. The guard from the section “Without a Guardrail, the Growth Comes Back”, the check script in CI, was at first calibrated not with the 3.7 of the first estimate but with a second wrong number: The failed subagent's error message named, next to the 347,814 tokens of the whole request, around 227,000 tokens for the conversation, and the file size divided by that number gave 4.0 bytes per token. Only, the message did not count the same thing as &lt;code&gt;/context&lt;/code&gt;. It came from a different model and referred to a part of the request. Measured, it is 2.0. Quality assurance and code review calculated with the same factor, confirmed “33,000 of 45,000 tokens” and considered the goal reached. Correct was 64,700 of 80,000. That came to light only when &lt;code&gt;/context&lt;/code&gt; ran in a fresh session. Two checks that report green prove nothing if they take the same number from the same source. Why a green test can confirm, in exactly this way, the very bug it was supposed to find is shown in &lt;a href="https://sql.marcus-belz.de/en/tests-pass-but-bug-still-there/" rel="noopener noreferrer"&gt;The Green Test That Proves Nothing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One caveat belongs here. The analysis that triggered this article ran in the same session whose context window it examined. Measuring itself costs context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code loads project instructions before the first prompt, in every session. In the DI² repository that was 454,900 tokens, just under half of a one-million window and more than twice a 200,000 window, grown over a good six months with the work on the project.&lt;/li&gt;
&lt;li&gt;The dividing line is rule versus record. A rule governs the next decision, a record proves a past one. Both belong in the repository, only the rule in the context at start. 84 percent of the largest rule file was record.&lt;/li&gt;
&lt;li&gt;Three levers, none of which shortens a rule: bind rules with a file scope to paths, move inventories into a neighboring file with the file name at the start of each row, write instructions to look up rather than preload. Result 64,700 tokens, and the subagent starts again.&lt;/li&gt;
&lt;li&gt;Binding to paths comes too late for new files and shifts who sees a rule. Against that help creation rules that stay global, skills that read explicitly, and a map in &lt;code&gt;CLAUDE.md&lt;/code&gt;. Against the growth helps a guard in CI, because a duty that nobody checks keeps neither the inventory complete nor the rule file small.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do you measure what a Claude Code session loads at start?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With the &lt;code&gt;/context&lt;/code&gt; command as the first input of a new session. It lists every loaded instruction file individually with its token count. For a repeatable measurement, &lt;code&gt;claude -p "/context"&lt;/code&gt; without the interactive interface is enough, checked with Claude Code 2.1.145. In Git Bash, path conversion has to be off for this (&lt;code&gt;MSYS_NO_PATHCONV=1&lt;/code&gt;), otherwise &lt;code&gt;/context&lt;/code&gt; reaches the model as a file path. The fresh session matters: A running session or a subagent carries the instruction state from its own start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isn't it enough to count characters and divide by four?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For English prose that is roughly right, for German-language Markdown with tables, paths and code identifiers it is not. Measured, the rule files sat at 2.0 bytes per token, per file between 1.9 and 2.2. An estimate with the factor 3.7 came in almost half too low, and a guard that calculated with 4.0 halved all results without anyone noticing. On top, the number depends on the model: The newer tokenizer from Claude 4.7 onwards counts the same text at roughly 30 percent more tokens than the older one, somewhat more or less depending on the text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a rule with &lt;code&gt;paths:&lt;/code&gt; load in time when the agent creates a new file?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. The rule arrives at the earliest with the result of the tool call, for a new file the content is already written by then, and before version 2.1.288 a Write call did not load it at all. Whether the agent then fixes the file on its own depends on the run. When editing an existing file there is no gap, because the Edit tool requires a prior read. For rules that have to act at creation, three ways remain: leave them global without &lt;code&gt;paths:&lt;/code&gt;, read them explicitly beforehand in the creating skill, and keep a map in &lt;code&gt;CLAUDE.md&lt;/code&gt; so that the agent knows the rule exists. Checked in Claude Code 2.1.145, 2.1.292 and 2.1.301.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not simply shorten the rules?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because every rule came from a bug that comes back without it. The DI² project's test rule kept 20,300 tokens of pure rule prose after the rework, although the target read “at most 12,000”: The target number came from a wrong estimate, and the number was revised. The savings come from when a rule loads and from no longer mixing it with record text, not from its content. Whoever still wants to shorten first checks whether a section proves a past decision. Then it is record and can move out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the agent still see a bound rule when it touches no matching file?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only if it knows the rule exists. A question about the SQL naming convention in conversation does not load the SQL rule, because no file under &lt;code&gt;db/&lt;/code&gt; is read. The same goes for a review that reads the diff via &lt;code&gt;git diff&lt;/code&gt; rather than through a file. That is why &lt;code&gt;CLAUDE.md&lt;/code&gt; holds a map with one line per bound rule, and skills that review without file access read the relevant rules explicitly. Otherwise binding would be a loss of conventions that goes unnoticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Parent articles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/agentic-coding-experience/" rel="noopener noreferrer"&gt;Agentic Coding from a User's Perspective&lt;/a&gt; — the sub-hub of this branch: The work does not disappear, it shifts, among other places into the maintenance of rules.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code&lt;/a&gt; — the entry point to rules, skills and agents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sibling articles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click&lt;/a&gt; — there the agent measures the product, here its own working material. The method is the same: measure first, then interpret.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/verify-ai-generated-code/" rel="noopener noreferrer"&gt;The Control Loop&lt;/a&gt; — rules are the means by which code can be checked that one does not read oneself. This article shows what that means costs at session start.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Downstream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;27 Rule Files Instead of One CLAUDE.md&lt;/a&gt; — how the rule files are built whose load time is shifted here.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/deriving-sql-conventions-with-claude-code/" rel="noopener noreferrer"&gt;Deriving SQL Conventions with Claude Code&lt;/a&gt; — how the rules come about whose running cost is measured here.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/tests-pass-but-bug-still-there/" rel="noopener noreferrer"&gt;The Green Test That Proves Nothing&lt;/a&gt; — why two checks with the same wrong assumption confirm the goal they miss.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Agent Measures Where I Click — Debugging Postgres, Docker and a Swapping Server with a Coding Agent</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:27:13 +0000</pubDate>
      <link>https://dev.to/marcus1968/the-agent-measures-where-i-click-debugging-postgres-docker-and-a-swapping-server-with-a-coding-3n59</link>
      <guid>https://dev.to/marcus1968/the-agent-measures-where-i-click-debugging-postgres-docker-and-a-swapping-server-with-a-coding-3n59</guid>
      <description>&lt;p&gt;A bug gets reported the way it feels: the message does not appear, the page barely responds, a tab no longer loads after a restart. A user can only describe what the interface shows, and the interface is the end of a long chain. A coding agent with access to the database, the container runtime and a shell does not see the interface and starts debugging somewhere else. What it sees instead is what has been stored, what has been started and what has been swapped out.&lt;/p&gt;

&lt;p&gt;This article describes four debugging sessions from the private DI² project in which exactly this difference decided the outcome. In three of them, a measurement settled early what a guess would have left open: which part of the chain is affected, whether the new code can be to blame at all, and whether a protection actually takes effect. In one case, the same method spent an hour ruling out explanations without hitting the cause. The same agent then built an explanation from the remaining signs that was internally consistent and still wrong, and presented it as a finding with more confidence than the evidence allowed. Both sides belong in the same article because they show the same way of working, once at its strength and once at its limit.&lt;/p&gt;

&lt;p&gt;All four cases arose during development, not while running an application with users. Development here does include the maintainer setting up the project’s four environments and keeping them running, and a database developer does not come with a server background for that. The agent proposed the commands for it, and they were only executed after the maintainer had approved them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The first question is not “why” but “is the suspicion even right”:&lt;/strong&gt; Refuting a suspicion is easier than confirming a cause. Confirmation requires tracing the whole chain. Refutation is often a single query, and an agent with access to the database and the server should start exactly there. In one of the cases, a comparison across four environments disposed of the suspicion that the newly deployed version was to blame within a few minutes, without anyone reading a line of code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The database tells you whether something was never written or just not displayed:&lt;/strong&gt; Many applications store messages in the database first and only display them from there. If a message is missing from the interface, it may have been lost while being written to the database or only on its way to the display. A look at the record and its timestamp settles that immediately. If the message is in the database, everything before storage is ruled out, and the search starts behind it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools have blind spots you need to know:&lt;/strong&gt; A service whose memory has been swapped out to disk almost entirely shows up in the container statistics with a small value, because they only report the share in RAM. Anyone who knows the service’s normal footprint may pause at that, but a small value easily reads like a healthy one. How much has actually been swapped out only shows up in the machine’s memory overview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A step that ran is not a step that worked:&lt;/strong&gt; A brake on automated password guessing had sat correctly in the configuration for months. It counted requests on the old login path, however, which no longer existed after the switch to an external login service, and the actual login route ran unthrottled. A different fix removed a symptom without hitting the cause, so that everything was back the next day. In both cases the only thing that helped was measuring the effect directly instead of trusting the configuration file or the success message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The flip side belongs here too:&lt;/strong&gt; A language model effortlessly tells an explanation that sounds internally consistent even when the evidence does not support it. That is exactly what happened in one of the four cases. The most useful follow-up question to an agent is therefore not “why” but which concrete measurement supports this statement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Basic database knowledge and a rough idea of what a container is are enough. The cases are told so that the diagnostic path stays easy to follow, not the application behind it. Commands appear as patterns, not as instructions to copy onto someone else’s system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Agent Sees and the Browser Does Not
&lt;/h2&gt;

&lt;p&gt;An error report from the browser is an observation at the end of the chain. Between the user’s action and what they see lie a form, a server action, a database transaction, a notification path and a rendering step. Any of these stations can be the cause, and the report does not say which one it was. Anyone who only has the interface has to guess at which station to start looking.&lt;/p&gt;

&lt;p&gt;An agent working in the same environment has different instruments. It can ask the database directly whether a record exists and when it was written. It can check which containers are running and which of their ports are actually reachable from outside. It can open a shell on the machine and look at how much memory is really available. This access does not make it smarter than the human. It makes certain questions cheap that used to be expensive, and cheap questions get asked earlier.&lt;/p&gt;

&lt;p&gt;The difference shows most clearly in refuting. Confirming a cause is laborious because you have to trace the whole chain. Falsifying a hypothesis is often a single query. That is the lever in the four cases that follow. In none of them did the agent guess the cause. In three, a measurement decided early where to keep looking, and in one, the same method spent an hour only ruling things out without hitting the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: Is the Message Missing, or Just Not Displayed?
&lt;/h2&gt;

&lt;p&gt;The maintainer reported that an expected error message did not appear in the interface. The application first stores such messages as a record in the database, and the interface displays them from there. The observation from the browser therefore leaves two entirely different causes open. Either the message was never written to the database, in which case the fault lies in the write path. Or the record is there and just does not reach the interface, in which case the fault lies on the way from the database to the display, in the notification path. Both guesses are equally plausible, and they lead to completely different places in the source code.&lt;/p&gt;

&lt;p&gt;Instead of following the bug description, the agent looked in the database first. The message was there. It had been written 22 seconds after the triggering change, in two write operations 348 milliseconds apart. That settled the question before anyone had read a line of application code. The write had worked, the display had not, and the search moved to the notification path. That is where the cause was.&lt;/p&gt;

&lt;p&gt;The pattern behind it is simpler than it sounds. If a bug is reported as “something is missing from the display” and the application stores the affected data before displaying it, there is a measurement that immediately decides whether to look before or behind the storage step. For that, the query has to reach the same database the application reads from. Case 2 shows that even this can go wrong. As a rule of thumb, the measurement looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ask the database whether the expected record exists,
and read its timestamp.

It does not exist  -&amp;gt; the fault lies in the write path.
It exists          -&amp;gt; the fault lies between storage and display.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timestamps are more than a by-product, even if on their own they do not explain anything. What the agent had measured was only that the record was written twice, 348 milliseconds apart, and that this happened 22 seconds after the triggering change. The timestamps do not say why. Two write operations can stem from a second write path or from a retry, and a delay from a background job that only runs at intervals. Gaps like these, though, are often the cheapest way to reconstruct a sequence nobody remembers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: Two Hours Down the Wrong Rabbit Hole
&lt;/h2&gt;

&lt;p&gt;After the development machine was restarted, one of the application’s tabs no longer loaded. The error messages alternated between “relation does not exist” and “column does not exist”, and on top of that a test project was missing that had still been there the day before. Everything about it looked like damage to the database or a bug in the application.&lt;/p&gt;

&lt;p&gt;The agent formed hypotheses one after another and checked each one with a measurement. Among other things, it compared the instance identifier (&lt;code&gt;system_identifier&lt;/code&gt;) across both access paths, a number PostgreSQL generates when a database instance is set up and that the instance keeps from then on. It was identical on both paths. The database log showed not a single error since startup, the activity view showed no connection from the application, and the agent read the connection address directly from the environment of the running process rather than from a configuration file. Each of these checks was quick, and apart from the identifier comparison each one ruled out an obvious explanation. Even so, after an hour no cause had been found.&lt;/p&gt;

&lt;p&gt;It was the human who found it, and only in passing. The maintainer threw in two observations that no tool shows: the test project had still been there the day before, and the database normally ran on its default port. That turned attention to the container’s port forwarding. A container is sealed off from the outside, and for the application to reach its database, the machine forwards one of its own ports to the container. This forwarding was configured but not active. The port was held by two forwarding processes that looked like leftovers from an earlier run. They were terminated, the application worked again, and the case was considered solved.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix Worked and Still Missed the Cause
&lt;/h3&gt;

&lt;p&gt;The next day everything was back. The same symptoms, the same hour of debugging, and this time the investigation went deeper. Two complete container worlds were running side by side on the machine, meaning two Docker installations, each with its own engine, the service that actually runs the containers. One was a system engine, the relic of a first installation whose removal had remained incomplete, and it was set up to start with every boot. The other was Docker Desktop, the installation that was actually intended. Both brought up a stack with the same name, including a database with the same container name.&lt;/p&gt;

&lt;p&gt;That creates a race for the port. Only one process can hold a given port on the machine, and whoever starts first gets it. In this setup, the container of the other world still started, just without forwarding, and its database was then unreachable from outside. The application connects through the machine’s port and ends up in whichever world holds it. After a boot, that was the system engine, because it came up first.&lt;/p&gt;

&lt;p&gt;The cross-checks the agent used to inspect the data ran along a different path. They were queries via &lt;code&gt;docker exec&lt;/code&gt;, which run directly inside the database container and bypass the port. Which engine a &lt;code&gt;docker&lt;/code&gt; command talks to is normally determined by the active Docker context, a setting of the command-line tool, and the active context was Docker Desktop’s. There was a container with exactly the same name there, and the cross-checks ran against its database without anything indicating it. The system engine was not hidden. It was simply not addressed by any &lt;code&gt;docker&lt;/code&gt; command, and that is why its containers did not show up in any list. So the two data sets slowly drifted apart, because clicks and scripts hit one database or the other depending on which world held the port.&lt;/p&gt;

&lt;p&gt;That turns the supposed fix from the day before into something other than what it seemed. Terminating the orphaned processes did work, because the application ran again afterwards. It did not hit the cause, because the second installation started again with the next boot. The fix even concealed the actual problem, because it removed the symptom reliably enough to end the search. Only the relapse showed that the supposedly orphaned processes belonged to the system engine and were therefore a symptom, not the culprit. The case was only resolved once the system engine had been shut down and uninstalled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spotting Two Docker Engines Earlier: The Rules From the Second Incident
&lt;/h3&gt;

&lt;p&gt;Two rules made it into the project documentation. The first concerns the order: the question of which engine the active Docker context addresses belongs at the start of every local diagnosis, before any listing of containers. &lt;code&gt;docker context ls&lt;/code&gt; shows all configured contexts with their endpoint and marks the active one with an asterisk. If the &lt;code&gt;DOCKER_HOST&lt;/code&gt; environment variable is set, it overrides the context, and the list then says so explicitly. Several contexts are only a hint, because a context can also point to a remote machine or to the same endpoint as another. If the asterisk is on the wrong context, though, everything measured afterwards is worthless.&lt;/p&gt;

&lt;p&gt;The second rule concerns the proof of identity: when the interface and a cross-check show contradictory data, the instance identifier can be used to check whether both paths reach the same database instance at all. An instance carries exactly one identifier. If it differs between the two paths, the paths demonstrably reach two different instances, and every further cross-check along the wrong path checks the wrong database. The reverse does not hold. A physical copy of the data directory, for example from a backup with &lt;code&gt;pg_basebackup&lt;/code&gt;, carries the same identifier as its original, so an identical identifier does not rule out two instances. If a logical dump is restored instead, the target instance keeps its own identifier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query the instance identifier over both paths and compare:

  Path 1: docker exec directly in the container (engine of the active context)
  Path 2: through the machine's port, the way the application connects

  SELECT system_identifier FROM pg_control_system();

Different values  -&amp;gt; two different instances, proven.
Same values       -&amp;gt; no proof, a physical copy carries the same identifier.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail cost a lot of time and is worth mentioning. As long as the other engine held the port, restarting the container did not bring the forwarding back either. Anyone who tries that takes the container for broken, while the problem lies with the engine that occupies the port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 3: “The Page Is Barely Responding”
&lt;/h2&gt;

&lt;p&gt;Immediately after three deployments in a row, the maintainer reported a vague symptom. The prompt was a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the page is barely responding. can we measure anything
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One suspicion was obvious: new code had just gone out to three environments, so presumably the newly deployed version had broken something that used to work. The agent did not chase that suspicion in the code but compared the environments. It first called a static version route across all four environments and then the start page and the login page in each environment. The result disposed of the suspicion at once, because all environments were affected, including the production environment, which was not running the newly deployed version at all. That ruled out the newly deployed version as the cause, and the search moved to what all four environments share.&lt;/p&gt;

&lt;p&gt;The decisive step lay outside the application, in a shell on the machine itself. The system load, which &lt;code&gt;uptime&lt;/code&gt; reports as a measure of how much the machine currently has to do, was low. That made it unlikely that processing power was the bottleneck. At the same time, 2.9 gigabytes sat in swap space on disk, although 5.6 gigabytes of RAM were free. The identity provider, the service that handles login, was hit particularly hard. Of its 802 megabytes, the system had swapped 754 out to disk, and only 48 were still in RAM.&lt;/p&gt;

&lt;p&gt;Swapped-out memory only comes back when a process needs it again. Linux does not bring it back on its own just because space has become free. That is why it stayed on disk even though there had long been room again. That fits the symptom: the first login request took 9.64 seconds, every further one only 0.19 seconds. On first access, the system has to fetch the swapped-out parts back from disk, and after that they are in RAM again. An ordinary cold start produces the same pattern, though. What pointed to swapping as the cause here was that almost all of this service’s memory was demonstrably on disk.&lt;/p&gt;

&lt;p&gt;A tool trap is an inseparable part of this case. The container statistics that &lt;code&gt;docker stats&lt;/code&gt; displays only report the share in RAM, not the swapped-out share. The affected service appeared there with 48 megabytes. Anyone who knows that it normally uses around 800 megabytes could have paused at that value, because it is not inconspicuous but suspiciously low. With a sluggish service, however, you look for a value that is too high, and a small one then reads more like a healthy one. Even then, pausing only gets you a suspicion. The gap to the normal footprint can have other causes too, such as a service that has just been restarted. How much has actually been swapped out on the machine only shows up in its memory overview, for example with &lt;code&gt;free -h&lt;/code&gt;, because the &lt;code&gt;docker stats&lt;/code&gt; table has no column for it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For sluggish services despite low load:

  docker stats   -&amp;gt; shows only the share in RAM
  free -h        -&amp;gt; also shows the machine's swap space

The telltale sign is all three together:
  first request takes seconds, second is instant
  low system load
  swap space in use while RAM is free
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent did not carry out the fix itself. Bringing the swap space back is a system-wide intervention, and the maintainer’s private data lives on the same machine. The agent handed over the command sequence and put a pre-check in front of it that calculates whether the swapped-out memory fits into the free RAM at all. If it does not, the kernel mechanism that kills processes when memory runs out kicks in, and a sluggish service becomes a dead one. The request looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Give me the command sequence to bring the swap space back.
Put a calculation in front of it that checks whether the swap in use
fits into the free RAM, and that aborts if the reserve is too small.
Also tell me what prevents this state permanently.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(The second prompt is reconstructed from the task and its result, not taken from a log. The first one appears above as it was typed, translated from German.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The maintainer then ran the command sequence, and only its result confirmed the explanation. Login dropped from 9.64 to 0.19 seconds and swap usage to zero, without a single line of application code. To make a repeat at the next memory spike less likely, a kernel parameter was adjusted as well. It weighs whether the system, under memory pressure, prefers to swap out process memory or to drop the file cache, and it was lowered from its default of 60 to 10. That makes the kernel more inclined to sacrifice the file cache instead of pushing a service out to disk. It is a weighting, not a guarantee. If RAM runs short during a spike, the kernel swaps anyway, and whether the same value helps on another machine depends on what occupies the memory there.&lt;/p&gt;

&lt;p&gt;One detail of this case is uncomfortable and still belongs in the story. The agent had long since produced the first hint itself without following it up. Its own first measurement call had taken 5.16 seconds, the second 0.10. That is exactly the pattern it later found again on the machine. At first it dismissed it as a cold start. That was possible, but it never checked that interpretation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 4: The Rate Limit That Never Kicked In
&lt;/h2&gt;

&lt;p&gt;The reverse proxy configuration contained a limit on requests per minute for the login path. Anyone reading it concluded that login was protected against automated password guessing. The line was there, it was written correctly, and it had been active for months.&lt;/p&gt;

&lt;p&gt;The limit sat on the endpoint of a login procedure the application used to handle itself. Since the switch to an external identity provider, that endpoint no longer exists. The throttled path was therefore never called. There was no effective throttling of login anywhere, and the dangerous part is not the missing throttle. It is the existing one, because it reassures every check that looks at the configuration.&lt;/p&gt;

&lt;p&gt;The fix put the limit on the identity provider’s login path, and this time its effect was checked independently of the configuration. The endpoint was actually hammered with requests, and after only a few attempts it answered with the status for too many requests. A cross-check against a neighboring endpoint that must not be throttled kept returning regular responses. That established that the throttle takes effect on the login path and does not slow down the neighboring endpoint.&lt;/p&gt;

&lt;p&gt;In addition to the throttle in the proxy, there is a second line of defense in the identity provider itself. It temporarily locks an account when too many logins for that account fail in a row. Since the fix, the setting for this is in the project’s setup script. Whether it is active was nevertheless not inferred from that script but queried directly from the running identity provider for all four environments. A script says what is supposed to be set, not what actually is set.&lt;/p&gt;

&lt;p&gt;The two lines complement each other because they hit different attacks. The throttle in the proxy counts per source address and slows down the simple case in which someone tries many passwords from one place. If the attempts come from many addresses, it is not enough on its own. The lockout in the identity provider counts per account instead, and therefore also takes effect when the attempts against the same account come from many addresses. Either one alone would have looked like a solution and left one of the two cases open.&lt;/p&gt;

&lt;p&gt;On the same day, the same class of error occurred a second time, this time while rolling out the fix. The rollout run printed two screens full of checkmarks, and afterwards the configuration appeared to be active in all four environments. In fact, the copy command came to nothing because it ran in the wrong directory, and the setup script ran in its old version. The effect was zero. The script prints its checkmark regardless of which status the call before it returned. The checkmark testifies that the line after it was reached, and nothing else.&lt;/p&gt;

&lt;p&gt;It was only noticed because one specific expected line was &lt;strong&gt;missing&lt;/strong&gt; from the output. It was not the overall impression that revealed the error but a targeted search for a single sentence that should have been there. The agent caused this third instance itself. It had handed over the rollout commands without pushing the corresponding commit first. The commands could not work on the server at all, because the code there did not yet contain the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Step That Ran Is Not a Step That Worked
&lt;/h2&gt;

&lt;p&gt;At first glance, Case 2 and Case 4 do not look related. One plays out on a development machine and is about containers, the other on a server and is about security configuration. They still share the same structure, and that structure is the real yield of the four cases.&lt;/p&gt;

&lt;p&gt;In both cases there was a signal that testified to the step, and nobody checked the effect. The orphaned processes were terminated, so the problem was solved. The throttle was in the configuration, so login was protected. The rollout script printed its checkmarks, so the fix was active everywhere. Each of these signals was true in itself. The process &lt;strong&gt;was&lt;/strong&gt; terminated, the line &lt;strong&gt;was&lt;/strong&gt; in the file, the script &lt;strong&gt;had&lt;/strong&gt; run to completion. Between signal and effect, the chain had still broken, and none of the signals noticed.&lt;/p&gt;

&lt;p&gt;What helped in every case was the same recipe. You look for a second, independent route to the same question, ideally one that turns out differently when the effect is missing than when it has occurred. For a configuration, that means hammering the endpoint and reading the setting back from the running system instead of reading the file. For a fix, it means restoring the condition under which the error occurred, and after a startup problem that includes another restart. For a rollout, it means searching for one specific expected line instead of judging the overall impression.&lt;/p&gt;

&lt;p&gt;This kind of cross-check is easy to automate, which makes it one of the places where an agent can do more than a visual check. It has to be asked to, though. On its own, it does not replace the success signal with an independent measurement, and an agent that is only asked whether the change has been rolled out reads the same checkmarks as the human.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Flip Side: When the Agent Tells a Story
&lt;/h2&gt;

&lt;p&gt;Case 2 holds a second lesson, and it is less flattering. In the middle of the fruitless search, the agent built an explanation from the available signs in which everything fit together, and presented it as a finding. The explanation was internally consistent, it contradicted none of the measurements, and it was wrong. Only when the maintainer asked what exactly it meant did the agent take it apart and separate which part had been measured and which part interpreted.&lt;/p&gt;

&lt;p&gt;That was not a slip but lies in how a language model is built. It produces fluent, plausible text, and an explanation of a cause is exactly that: fluent text. Forming a fourth statement from three observations that feels like a conclusion costs a language model practically nothing. Backing up the same statement costs it just as much as anyone else. The whole difference lies between these two steps, and it cannot be seen in the finished text.&lt;/p&gt;

&lt;p&gt;For the user, that means splitting every finding into two parts. The first part is what was measured, and it can be named: a query, a return value, a timestamp, a status code. The second part is what was inferred from it, and that is a claim. The most useful follow-up question to an agent is therefore rarely “why”. It is which concrete measurement supports this statement, meaning which query, which return value, which timestamp. If the answer points to a concrete measurement, the finding holds. If it points to a synthesis of several clues, the finding is a hypothesis and should be treated as one.&lt;/p&gt;

&lt;p&gt;How easily an estimate passes for a measurement is shown by a later case from the same project. The agent was asked to analyze where the context window fills up quickly and announced that it would first measure what loads at session start. What it then delivered was an extrapolation from file sizes and a rule of thumb. Its result came in almost half below the value the measurement gave the following day. The case is described in &lt;a href="https://sql.marcus-belz.de/en/context-window-full-before-first-prompt/" rel="noopener noreferrer"&gt;The Context Window Is Full Before the First Prompt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A second limit concerns not interpreting but acting. The swap space from Case 3 would have been technically within the agent’s reach. It still did not carry out the intervention, because the same machine holds data that does not belong to the project. Whether a system-wide intervention runs on someone else’s data is not a decision an agent makes alone, and that holds even when the proposed command is correct. The agent delivered the proposal along with the safety calculation, and the human executed it. That is exactly how the work should be divided.&lt;/p&gt;

&lt;p&gt;A related side finding belongs here too, because it came up in the same week. When two sessions share a working tree, a failed test run is at first only a hypothesis and not yet a bug in the product, because it may just as well stem from the other session’s work. What follows from that for parallel operation is covered in &lt;a href="https://sql.marcus-belz.de/en/parallel-claude-code-sessions-same-repo/" rel="noopener noreferrer"&gt;Two Agents, One Working Tree&lt;/a&gt;. And that, conversely, a green test run is also only a signal and not proof is shown in the article &lt;a href="https://sql.marcus-belz.de/en/tests-pass-but-bug-still-there/" rel="noopener noreferrer"&gt;The Green Test That Proves Nothing&lt;/a&gt;. What follows from that for manual test cases is covered in &lt;a href="https://sql.marcus-belz.de/en/how-to-write-manual-test-cases/" rel="noopener noreferrer"&gt;How to Write Manual Test Cases&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Remains
&lt;/h2&gt;

&lt;p&gt;Together the four cases cost about four hours of debugging, and most of that went to the two incidents with the two Docker engines. Four things connect them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The first measurement should narrow the search, not deliver the cause.&lt;/strong&gt; Does the record exist? Are all environments affected? Do two access paths land in different database instances? Questions like these do not yet say what is broken, but they rule out whole areas. They cost almost nothing and therefore belong at the start of every search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The most obvious suspicion gets a cross-check first and an investigation only afterwards.&lt;/strong&gt; Whether the last change is to blame at all can be settled in minutes. Anyone who skips that question and goes straight to the code risks hours in the wrong place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A success signal shows that something ran, not that it worked.&lt;/strong&gt; Terminated processes, a line in the configuration and a screen full of checkmarks all testify only to a step. Whether the effect has occurred has to be shown by a second route that is independent of that step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An agent’s finding contains measurement and interpretation, and both sound the same.&lt;/strong&gt; They sit in the same paragraph and in the same tone. Separating them is up to the user, and it costs only one follow-up question: which concrete measurement supports this statement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these cases was solved because the model understood the application particularly well. They were solved because questions that used to be too expensive suddenly cost only a minute, and in one case because a human contributed two observations that no tool shows. The first is what an agent brings when it can query the database and the server directly. The second stays with the human.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where do you start with a vague symptom like “the page is slow”?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not with a hypothesis but with a comparison. If the same application runs in several environments, the quickest question is whether all of them are affected. If an environment that does not even have the suspect version is affected too, the suspect version is ruled out as the cause, and the search moves to what the environments share. That can be the machine, the network or a shared service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why doesn’t &lt;code&gt;docker stats&lt;/code&gt; show a memory problem?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;docker stats&lt;/code&gt; only reports the share in RAM, not the swapped-out share. A service whose memory has been swapped out to disk almost entirely shows up there with a very small value. Anyone who knows how much the service normally needs may pause at that, because the value is not inconspicuous but too low. How much has actually been swapped out, however, only becomes visible in the operating system’s memory overview, for example with &lt;code&gt;free -h&lt;/code&gt;. The telltale sign is the combination of three things: pages that take seconds on the first request and respond instantly on the second, a low system load, and swap space in use while RAM is free. Each of these alone has other explanations too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you notice that two Docker engines are running in parallel?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most conspicuous sign is a port that answers although, according to &lt;code&gt;docker ps&lt;/code&gt;, the container has no forwarding at all. Add to that data that does not match between the interface and a cross-check, and changes that have disappeared again after reloading the page. The check itself is short: first list the existing Docker contexts with &lt;code&gt;docker context ls&lt;/code&gt; and see which one is active and where it points, then determine which process actually holds the port. Both belong before any further container diagnosis, because a wrong context invalidates every measurement that follows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should an agent be allowed to run system commands itself?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Within the project, there is little against it as long as the effect can be reversed. System-wide interventions are a different matter, especially when the same machine holds data that does not belong to the project. The division of labor that has proven itself here is that the agent delivers the command sequence together with a pre-check and the human executes it. That costs a minute and keeps the responsibility where it belongs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you stop an agent from making up a cause?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not completely, because producing plausible text is how it is built. What helps is asking for the measurement that led to the statement. Whoever asks for the concrete query and its return value gets either solid evidence or the information that it is a synthesis. Both are useful as long as they are kept apart. It also helps to have the agent actively look for the opposite, meaning the measurement that would refute its own explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Parent articles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/agentic-coding-experience/" rel="noopener noreferrer"&gt;Agentic Coding from a User’s Perspective&lt;/a&gt; — the sub-hub of this branch, where the control loop and the limits come together.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code&lt;/a&gt; — the entry point to rules, skills and agents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sibling articles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner&lt;/a&gt; — the setup in which the four environments share one machine and therefore also its RAM.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/parallel-claude-code-sessions-same-repo/" rel="noopener noreferrer"&gt;Two Agents, One Working Tree&lt;/a&gt; — why a red test run in a shared working tree is a hypothesis at first.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/context-window-full-before-first-prompt/" rel="noopener noreferrer"&gt;The Context Window Is Full Before the First Prompt&lt;/a&gt; — here the agent measures the product, there its own working material: what the project rules cost at the start of every session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Downstream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/set-based-vs-row-by-row/" rel="noopener noreferrer"&gt;Set-Based vs. Row by Row: PostgreSQL Refresh from 114 to 12 Seconds&lt;/a&gt; — the fully measured case from the same project: a loop over 1,000 tables, its execution plan and what the rebuild cost.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>debugging</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Set-Based vs. Row by Row: PostgreSQL Refresh from 114 to 12 Seconds</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Mon, 28 Sep 2026 11:03:37 +0000</pubDate>
      <link>https://dev.to/marcus1968/set-based-vs-row-by-row-postgresql-refresh-from-114-to-12-seconds-p7p</link>
      <guid>https://dev.to/marcus1968/set-based-vs-row-by-row-postgresql-refresh-from-114-to-12-seconds-p7p</guid>
      <description>&lt;p&gt;One click on "Refresh schema", and after 60 seconds the browser shows the error message 504 Gateway Timeout, because the web server in front of the application will not wait any longer for a response. On the server, the work continues undisturbed, only nobody can see it. So a second click follows, then a third, and shortly afterwards four runs are computing at the same time. A single run across 1,000 tables took a measured 114 seconds. The requirement was less than three seconds.&lt;/p&gt;

&lt;p&gt;The cause was not a slow query but a loop that asked the source database for each table individually and stored the result individually. This article describes how that loop was measured and replaced with set-based statements, that is, statements that read and write all 1,000 tables at once, and which options were weighed against each other. It also describes what went wrong along the way. The first of those statements was slower than 1,000 single queries combined, and the finished result missed its targets despite a gain by a factor of 9 to 19.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A loop in application code costs more per iteration than the same loop inside the database:&lt;/strong&gt; Whoever processes tables one at a time instead of with one statement over all of them pays a fixed overhead for every iteration, no matter how little data the iteration moves. If the loop lives in application code and opens its own database connection for each table, connection setup, authentication and a transaction are added per iteration. In the case described, one iteration cost around 114 milliseconds, which for all 1,000 tables adds up to the 114 seconds mentioned at the start. The same loop, rebuilt for this article as a procedure inside Postgres, cost around three milliseconds per table and thus around three seconds for the same 1,000 tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The loop was not carelessness, and its strengths had to be rebuilt:&lt;/strong&gt; For each table it called a function that already existed for activating a single table and that worked correctly there. If one table failed, the others carried on, and even four concurrent runs left no contradictory data behind. With the 15 to 70 tables that the real databases of this application have, the runtime never stood out. The solution that processes all tables at once had to restore this error isolation explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One statement over all tables is not automatically faster than a thousand single ones:&lt;/strong&gt; The first query that read the keys of all 1,000 tables at once from &lt;code&gt;information_schema&lt;/code&gt;, the views in which Postgres exposes its own metadata, took around five seconds on Postgres 17. Issued once per table instead, the same query adds up to an extrapolated half a second. The execution plan showed why: Postgres 17 had chosen a plan for this query that re-evaluated one of the two views involved for every table, and so it had rebuilt the loop internally. A query directly on the system tables in &lt;code&gt;pg_catalog&lt;/code&gt; behind those views took milliseconds, but it also showed every database user the keys of tables they are not allowed to read, and therefore needed a condition of its own for access privileges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;114 seconds became 12, and the targets were still missed:&lt;/strong&gt; Those twelve seconds apply to a run that rewrites all 1,000 tables. The more common case is a run over an unchanged set of tables, and for that one the target was three seconds, which allows three milliseconds per table. The new logic takes around 3.7 milliseconds per table and is thus just under a quarter over budget. Across 1,000 tables that is 3.7 seconds. The run still takes up to 6.4 seconds. The target was missed somewhere else, because the missing 2.7 seconds are what every call on the shared server costs before it touches the first table, among other things for authentication, permission checks and reading the table selection. Next to 114 seconds of loop, this share had never stood out, and the estimate before the rebuild did not include it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Basic SQL and an idea of what a stored procedure is. The examples are PL/pgSQL and were run on Postgres 17 and 18 in throwaway containers. The case described comes from an application written in TypeScript, whose code is not shown here because the mechanics do not depend on the programming language.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Starting Point: A Schema Snapshot Across 1,000 Tables
&lt;/h2&gt;

&lt;p&gt;The private project DI² generates ETL pipelines from the metadata of a source database. To know what it is working with, it reads the columns, the primary and unique keys and the foreign keys of every activated table and stores them as a snapshot in its own database. A click on "Refresh schema" repeats this read for all tables of a connection.&lt;/p&gt;

&lt;p&gt;This refresh was implemented as a loop. For each table, the application code called a function that already existed. It read the metadata of a single table from the source and replaced that table's snapshot in one transaction by deleting the old rows and inserting the new ones. The function had been written for a different occasion, activating a single table, and for that occasion it was correct. The loop on top of it was the obvious next step.&lt;/p&gt;

&lt;p&gt;The specification had carried a target for the refresh for months: less than three seconds for 1,000 tables. Nobody had measured it, because the real databases in the project had between 15 and 70 tables. The measurement was made up for with a synthetic source database of 1,000 tables with 50 columns each. The tables were empty, because the refresh reads only the catalog, that is, the views and system tables in which Postgres describes which tables, columns and keys exist. The measurement ran on the project's development server, a shared machine with four virtual cores and 7.6 gigabytes of memory.&lt;/p&gt;

&lt;p&gt;Measurement, code analysis and rebuild were done with a coding agent, which had also written the application code. The trade-offs and decisions this article tells about were made by the maintainer. What that division of labor looks like day to day is described in the article &lt;a href="https://sql.marcus-belz.de/en/agentic-coding-experience/" rel="noopener noreferrer"&gt;Agentic Coding from a User's Perspective&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The result of the measurement:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Duration of the loop, single run without concurrent runs&lt;/td&gt;
&lt;td&gt;114.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Difference between two runs&lt;/td&gt;
&lt;td&gt;6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration per table&lt;/td&gt;
&lt;td&gt;around 114 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading the table list, the only set-based step&lt;/td&gt;
&lt;td&gt;22 to 43 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;under 3 s, missed by a factor of 38&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of this result reached the browser. The web server in front of the application ended the request after 60 seconds with a gateway timeout while the loop kept running on the server. Whoever clicked again started a second run next to the first. In the measurement session, up to four loops were running at the same time this way, and one of them delivered its response only after about seven minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring Runtime When the Web Server Times Out
&lt;/h2&gt;

&lt;p&gt;Because the timeout cut off every measurement in the browser, the duration had to come from the data itself. The refresh replaces the snapshot of every table, and every newly written row receives a timestamp in &lt;code&gt;created_on&lt;/code&gt;. After a run without concurrent runs, the span between the first and the last written row therefore gives the duration of the loop to within about one iteration, because the timestamp records the start of an iteration and not its end. The query for it needs no instrumentation at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;table_count&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_row&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_row&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column_snapshot&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;schema_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'perf'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This calculation has a prerequisite that is easy to overlook. A default such as &lt;code&gt;now()&lt;/code&gt; returns in Postgres the moment the current transaction began, not the moment of the insert. In the case study, every table ran in its own transaction, so every table got its own timestamp. If a procedure instead writes all tables in a single transaction, every row carries the same value, and the span is zero. A rebuild of the loop as a Postgres procedure, shown in the section "The Same Loop Inside the Database", confirmed exactly that. With one &lt;code&gt;COMMIT&lt;/code&gt; per table, the query returned the duration of the loop, 5 milliseconds less than the stopwatch on the call, which is the last iteration, whose end no timestamp records anymore. Without &lt;code&gt;COMMIT&lt;/code&gt; it returned &lt;code&gt;00:00:00&lt;/code&gt;. Whoever needs the moment of every single row sets &lt;code&gt;clock_timestamp()&lt;/code&gt; as the default. &lt;code&gt;statement_timestamp()&lt;/code&gt; does not help here, because it returns the start of the statement the client sent, and for the whole procedure that is the &lt;code&gt;CALL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two independent runs came to 114.439 and 114.445 seconds and were thus practically identical. A statement about the distribution would have needed more runs, but for finding the cause, the order of magnitude was enough. Since the rebuild, the application measures its own duration and writes it to the audit log, so the detour via the timestamps is no longer needed. That instrumentation was itself one of the consequences of the analysis. Why a query on the stored data often leads to a finding faster than clicking through the user interface is shown in the article &lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the 114 Seconds Went
&lt;/h2&gt;

&lt;p&gt;Per table, the following happened in sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Three connections to the source database:&lt;/strong&gt; The code opened one connection each for columns, keys and foreign keys. Before each of these connections it loaded the connection profile from its own database and decrypted the password. Then it established a fresh TCP connection, authenticated, issued a single catalog query for exactly this table and closed the connection again. For 1,000 tables, that is 3,000 connection setups per refresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Up to four procedure calls in its own database:&lt;/strong&gt; One after the other, the column snapshot was replaced, the derived validation rules were reconciled, the key snapshot was replaced and the foreign-key snapshot was replaced.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The source database ran on the same machine, and the network between the containers costs less than a millisecond. The 114 milliseconds per table therefore consisted almost entirely of costs that every iteration pays regardless of the amount of data. How exactly they split between connection, profile and procedure calls, the measurement did not record. A separate measurement with &lt;code&gt;pgbench&lt;/code&gt; between two containers gives the order of magnitude. There, one connection setup with password authentication cost 17 milliseconds on average, while a simple query over an existing connection cost 0.36 milliseconds. Postgres starts a separate server process for every new connection, and exactly this overhead was incurred three times per table in the loop. Three connection setups thus explain around 50 of the 114 milliseconds, and the rest is spread over loading and decrypting the profile and the four procedure calls.&lt;/p&gt;

&lt;p&gt;The counter-evidence comes from the only step of the refresh that was already set-based, meaning it asked its question in a single statement for all tables instead of once per table. The list of all 1,000 tables came back in 22 to 43 milliseconds, which is less than 0.04 percent of the total time.&lt;/p&gt;

&lt;p&gt;The target also yields a budget, and this calculation settles the question earlier than any optimization attempt. Three seconds for 1,000 tables come to three milliseconds per table. A single connection setup alone costs a multiple of that. As long as every iteration opens a connection, the target is therefore out of reach, no matter how fast the queries inside it are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same Loop Inside the Database
&lt;/h2&gt;

&lt;p&gt;The case study plays out in application code, but the pattern is the same as with a cursor in a procedure that works through a result set row by row. Among SQL developers it goes by the mocking name RBAR, short for "Row By Agonizing Row". In the world of ORMs it is known as the N+1 problem: first one query for the list, then another one for every element. To see which part of the cost is tied to the loop itself and which to the connections, the snapshot was rebuilt inside Postgres for this article. It reads from &lt;code&gt;information_schema&lt;/code&gt;, the standardized set of views through which most SQL databases describe their tables and columns. The target table takes one row per column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column_snapshot&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;schema_name&lt;/span&gt;               &lt;span class="nb"&gt;text&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;                &lt;span class="nb"&gt;text&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;               &lt;span class="nb"&gt;text&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;                 &lt;span class="nb"&gt;text&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;          &lt;span class="nb"&gt;int&lt;/span&gt;           &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;                &lt;span class="n"&gt;timestamptz&lt;/span&gt;   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;created_by&lt;/span&gt;                &lt;span class="nb"&gt;text&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;current_user&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sequential version reads the tables of a schema and writes each table's snapshot in its own transaction, the way the case study did. The &lt;code&gt;COMMIT&lt;/code&gt; inside the loop requires that the &lt;code&gt;CALL&lt;/code&gt; is not inside an open transaction block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Parameters&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;--    p_schema_name        text&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;--       Schema whose tables are taken into the snapshot&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sp_load_column_snapshot_loop&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;IN&lt;/span&gt;    &lt;span class="n"&gt;p_schema_name&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;procedure&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DECLARE&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;l_table_name&lt;/span&gt;              &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;BEGIN&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;l_table_name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;LOOP&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;-- One table: delete the old snapshot, write the new one&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column_snapshot&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="k"&gt;schema_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l_table_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column_snapshot&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="k"&gt;schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;table_schema&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;
 &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;
 &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
 &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l_table_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;-- Each table is written in its own transaction&lt;/span&gt;
 &lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;procedure&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lines in detail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lines 17 to 54:&lt;/strong&gt; The loop runs over the table list from &lt;code&gt;information_schema.tables&lt;/code&gt;, and for each table a &lt;code&gt;DELETE&lt;/code&gt; and an &lt;code&gt;INSERT&lt;/code&gt; follow. That is the N+1 pattern: one query for the list, N more for the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line 57:&lt;/strong&gt; The &lt;code&gt;COMMIT&lt;/code&gt; ends the transaction after every table and thus rebuilds per-table atomicity. Without this line, the whole loop would run in a single transaction, and an error at table 500 would also roll back the 499 finished tables. The timestamp measurement from the previous section would then give zero, because &lt;code&gt;now()&lt;/code&gt; returns the start of the transaction and stays constant within it. Procedures may &lt;code&gt;COMMIT&lt;/code&gt; since Postgres 11, functions may not, and even a procedure may do so only as long as the caller holds no transaction open. If the caller opens one, for instance through a library that wraps every call in &lt;code&gt;BEGIN&lt;/code&gt; and &lt;code&gt;COMMIT&lt;/code&gt;, the procedure fails at the first &lt;code&gt;COMMIT&lt;/code&gt; with &lt;code&gt;invalid transaction termination&lt;/code&gt;. What the &lt;code&gt;COMMIT&lt;/code&gt; does not provide is the error isolation of the case study. This procedure has no error handling, and an error at table 500 ends the call. The 499 finished tables stay written, the remaining 500 stay unprocessed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The set-based version does the job with two statements over the whole schema and therefore needs no loop. The difference sits in the &lt;code&gt;WHERE&lt;/code&gt; clause: only the schema remains there, no longer the single table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- Parameters&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;--    p_schema_name        text&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;--       Schema whose tables are taken into the snapshot&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- --------------------------------------------------------------------------------&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sp_load_column_snapshot&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;IN&lt;/span&gt;    &lt;span class="n"&gt;p_schema_name&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;procedure&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;BEGIN&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column_snapshot&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;schema_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_schema_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column_snapshot&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;table_schema&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_schema_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;procedure&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also removes the &lt;code&gt;COMMIT&lt;/code&gt; per table, because both statements run in the same transaction. In return, it gives up per-table atomicity: an error rolls back the whole run and not just the table being processed. The two versions have a second difference: the set-based one also deletes rows for tables that no longer exist, while the loop leaves them in place, because it only runs over the tables that are present.&lt;/p&gt;

&lt;p&gt;On Postgres 18.6 in a Docker container on a laptop, the set-based version took around 0.6 seconds in three runs on a freshly created table, the loop around 3 seconds. Both versions wrote exactly the same 50,000 rows, and a comparison with &lt;code&gt;EXCEPT ALL&lt;/code&gt; over the five business columns, that is, without the timestamps, showed no difference in either direction. On Postgres 17 the times varied more, and the ratio was similar. The laptop numbers serve as an order of magnitude, not as a benchmark, because with repeated overwriting without &lt;code&gt;VACUUM&lt;/code&gt; both versions became noticeably slower.&lt;/p&gt;

&lt;p&gt;A factor of five (3 seconds ÷ 0.6 seconds) is significant. It is still a long way from what the case study experienced, because there one iteration cost 114 milliseconds instead of three. The difference is made by the overhead that every iteration there paid on top of the actual work: three connection setups, three authentications, loading and decrypting the profile and four separate procedure calls. So a loop is not expensive simply because it is a loop. It becomes expensive in proportion to what an iteration costs beyond the work, and that share grows with every connection, every process and every transaction that sits between the loop and the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Spoke for the Loop
&lt;/h2&gt;

&lt;p&gt;Before the rebuild, the loop deserves a fair assessment, because it was not carelessness. Its advantages were real, and some of them the new solution had to rebuild explicitly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reuse:&lt;/strong&gt; The function for a single table already existed and was tested. A loop on top of it brought neither a new code path nor a new class of errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-table error isolation:&lt;/strong&gt; If table 738 fails because of a missing permission or a concurrent &lt;code&gt;DROP TABLE&lt;/code&gt;, the application code catches the error, increments an error counter, and the remaining 999 tables carry on. This property comes from the loop's error handling and not from the transaction per table that the next point describes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-table atomicity:&lt;/strong&gt; Delete and re-insert ran in a transaction of their own for every table, so that even under concurrency no half snapshot of a single table ever came about. It does not guarantee a common state of all 1,000 tables from one and the same run, but the snapshot does not need that either, because every table is read on its own and used on its own. In the measurement session this proved itself: of seven runs, five overlapped, not one reported an error, and in the end exactly 50,000 rows for 1,000 tables were in place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constant memory footprint:&lt;/strong&gt; The application never held more than one table in memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readability:&lt;/strong&gt; The control flow was an ordinary loop that anyone understands on first reading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inconspicuous at small scale:&lt;/strong&gt; A database with 48 tables would have taken an extrapolated five to six seconds. That is a noticeable wait, but not an alarming one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem came with scale, not with the original decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Spoke Against the Loop and What the Switch Costs
&lt;/h2&gt;

&lt;p&gt;Four points spoke against the loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed cost times count:&lt;/strong&gt; Connection, authentication, profile and procedure call are incurred in every iteration, while the actual amount of data per table is tiny. More than 99.9 percent of the runtime sat in the loop over the tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The catalog answers set questions at almost the price of a single question:&lt;/strong&gt; In the rebuild, &lt;code&gt;information_schema.columns&lt;/code&gt; delivered all 50,000 columns in around 0.3 seconds. The columns of a single table took 6 to 15 milliseconds as a query of their own, and repeated a thousand times that is 6 to 15 seconds. The largest part of that is planning the view query, on Postgres 18.6 around 5 milliseconds against around one millisecond of execution. In a PL/pgSQL procedure this planning does not happen on every iteration: the procedure creates a prepared statement on the first iteration and switches after a few iterations to a cached plan that does not depend on the concrete values, provided that plan is not much worse. That is why the loop in the rebuild gets by with around three milliseconds per table. With the setting &lt;code&gt;plan_cache_mode = force_custom_plan&lt;/code&gt;, which re-plans on every iteration, the same loop took around two and a half to three seconds longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set-based writing spreads the cost over many rows:&lt;/strong&gt; In the same project, a single &lt;code&gt;INSERT&lt;/code&gt; into a different table wrote around 91,000 rows in about one second, as an order of magnitude and not as a comparison under equal conditions. The loop moved less data with its 50,000 rows and took 114 seconds for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collision with timeouts:&lt;/strong&gt; A synchronous operation that takes longer than the timeout of the web server in front of it produces an error message behind which the work continues. Users click again, and the runs pile up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The switch to set-based processing, however, has a price of its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The batch size becomes a decision in its own right:&lt;/strong&gt; A batch is a single call of a procedure that accepts the data of many tables at once, in the case described as a JSON document with all columns, keys and foreign keys of those tables. How many tables belong in it, someone has to decide, and before the switch the question did not arise at all. The amount of data per call has to fit the memory limits of the application and the parameter limits of the database drivers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors hit larger units:&lt;/strong&gt; If a batch fails, that affects all tables in it. The per-table count has to be restored actively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactions take longer:&lt;/strong&gt; A batch holds its locks longer than many small transactions do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More code has to be right at once:&lt;/strong&gt; Each of the three supported engines, PostgreSQL, SQL Server and MySQL, needs its own set-based queries against its catalog. The new batch procedures are new objects with tests of their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Equivalence has to be proven:&lt;/strong&gt; The rebuild must not change the stored result, so a before-and-after comparison of the stored data becomes mandatory. Every difference has to be explainable: unexplained differences are errors of the rebuild, explained ones may be errors of the old code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Decision: Four Options and a Budget Calculation
&lt;/h2&gt;

&lt;p&gt;With the budget of three milliseconds per table, the options were quick to sort:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Expectation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A: Set-based queries and batch writes&lt;/td&gt;
&lt;td&gt;three to four catalog queries for all tables over one connection, batch procedures instead of single calls&lt;/td&gt;
&lt;td&gt;2.5 to 5 seconds, the only way towards the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: Reuse the connection&lt;/td&gt;
&lt;td&gt;one connection and one profile per run or a connection pool, the loop stays&lt;/td&gt;
&lt;td&gt;factor 2 to 4, that is 30 to 60 seconds, three queries and four procedure calls per table remain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: Parallelize&lt;/td&gt;
&lt;td&gt;several workers work through the loop&lt;/td&gt;
&lt;td&gt;linear gain at higher load on the source, no target without A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D: Drop the target&lt;/td&gt;
&lt;td&gt;the refresh is rare and triggered by hand&lt;/td&gt;
&lt;td&gt;does not solve the five to six seconds on real databases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Option A was chosen, supplemented by detection of unchanged tables. A table whose columns, keys and foreign keys have not changed since the last refresh is not rewritten at all.&lt;/p&gt;

&lt;p&gt;Two rulings were part of the decision. First, the loop was replaced and not left standing as a second path next to the new solution. Activating a single table has since run through the set-based path as well, with a set of exactly one element. The function from whose reuse the loop once arose no longer exists. Second, the blanket target was replaced with three separate targets after the server's hardware had been checked: under three seconds when nothing has changed, under ten seconds when all 1,000 tables are rewritten, and under two seconds for a realistic 50 tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  How a Refresh Runs Today
&lt;/h3&gt;

&lt;p&gt;This is what the whole process looks like today in one piece, described for a click on "Refresh schema" across all 1,000 tables. For a single table, exactly the same path runs, then with a set of one element.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5okn2jvfl05le3l3204r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5okn2jvfl05le3l3204r.png" alt="Flowchart of a refresh across 1,000 tables in five steps: read the catalog from the source database, read the stored checksums, compare, stamp the unchanged tables and write the changed ones in ten batches, update the validation rules." width="800" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the catalog:&lt;/strong&gt; The application opens a single connection to the source database and asks four questions, each over all 1,000 tables at once: the columns, the type facts it needs to classify unknown data types, the unique and primary keys, and the foreign keys. After that, the complete catalog sits in the application's memory. Tables for which the catalog returns not a single column are sorted out here and reported as failed, instead of producing an empty snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the stored checksums:&lt;/strong&gt; A single &lt;code&gt;SELECT&lt;/code&gt; on the application's own database fetches the last stored checksum for all of these tables. That is a different server than in step 1. There the foreign source was queried, here the application's own data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare:&lt;/strong&gt; For each table, the application computes a SHA-256 checksum over exactly the data it would store and compares it with the stored one. If no stored checksum exists, the table counts as changed. The result is two groups.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unchanged:&lt;/strong&gt; Nothing is written for these tables. They only receive the stamp "last verified against the source", through a single procedure call for the whole group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changed:&lt;/strong&gt; These tables are cut into batches of 100. The number is a constant in the application code and not a setting, and if it does not divide evenly, the last batch is smaller. For each batch, one call goes to the database, with the complete data of the 100 tables as a JSON document in the parameter. The procedure behind it contains no loop of its own. It unpacks the document and writes columns, keys and foreign keys of all tables in the batch with one &lt;code&gt;DELETE&lt;/code&gt; and one &lt;code&gt;INSERT&lt;/code&gt; each. Every batch is a transaction of its own. For 1,000 changed tables, that is ten calls.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catch errors:&lt;/strong&gt; If a batch fails, the others carry on. The tables of the failed batch are then re-run one by one, through the same path with a set of one element. Only if the error would hit every further call anyway, for instance because the data model has meanwhile been frozen or the connection has been deleted, does the run abort instead of trying again for every single table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update the validation rules:&lt;/strong&gt; Finally, the validation rules derived from keys are updated, and only for the tables in the "changed" group.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From 3,000 connection setups and 4,000 single calls, this leaves one connection, four catalog queries and roughly ten to eleven procedure calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the New Solution Brings Back the Loop's Strengths
&lt;/h3&gt;

&lt;p&gt;Of the six strengths from the section "What Spoke for the Loop", the new solution had to rebuild two explicitly. For the others, the balance is mixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-table atomicity&lt;/strong&gt; comes back through the batches. Every batch runs in a transaction of its own, and because the boundary between two batches always lies between two tables and never in the middle of one, no half snapshot of a table comes about even under concurrency. A batch does, however, hold its locks longer than the many small transactions of the loop. So that two overlapping runs do not end up in a deadlock, with each waiting for rows the other holds, the procedure locks the table entries of a batch at the start in a fixed order, with &lt;code&gt;SELECT … ORDER BY id FOR UPDATE&lt;/code&gt;. The second run still has to wait until the first one's batch is complete, and it then works on that state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-table error isolation&lt;/strong&gt; comes back as a fallback level, as step 4 of the process shows: if a batch fails, the others carry on, and its tables are re-run one by one, so that in the end the count is per table again. The first version did not distinguish two cases here. It treated a single faulty table the same as a cause that hits all tables, such as a connection that has meanwhile been locked. In that case all ten batches failed, and the re-runs produced another 1,000 calls that failed just the same. The finished version recognizes such errors by their message and aborts the re-run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The constant memory footprint&lt;/strong&gt; is changed, not preserved. After step 1, the catalog of all tables sits completely in the application's memory, whereas the loop never held more than one table. What is bounded instead is the size of a single write call: the 100 tables per batch are the result of a trade-off between the size of the document passed in, the duration of the transaction and the time a batch holds locks. A particularly wide table merely makes its batch larger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reuse and readability&lt;/strong&gt; the new solution has given up. The function for a single table no longer exists, and the set-based path is more code, spread across three engines. The &lt;strong&gt;inconspicuousness at small scale&lt;/strong&gt; is preserved but turns out weaker than hoped: 48 tables now take 2.9 to 4.4 seconds instead of an extrapolated five to six. The largest part of that is a fixed base cost, which the results section breaks down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skipping Unchanged Tables
&lt;/h3&gt;

&lt;p&gt;For each table, the application computes a checksum over exactly the data it would store and compares it with the checksum of the last run. If both match, the table is not rewritten. It only receives a stamp with the time of the check, so that it stays visible that it was verified against the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set-Based Is Not Automatically Fast: Why a Query Over All Tables Can Be Slower
&lt;/h2&gt;

&lt;p&gt;The first set-based version read the keys of all tables through the two standard views &lt;code&gt;information_schema.table_constraints&lt;/code&gt; and &lt;code&gt;information_schema.key_column_usage&lt;/code&gt;. That is the portable version over the standard views, and filtered to a single table it had run in the project for years without attracting attention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_type&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_constraints&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column_usage&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'perf'&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PRIMARY KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UNIQUE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The join runs over schema and constraint name and additionally over the table name in line 13. That condition is necessary because in Postgres a foreign key may carry the same name as the unique constraint of another table in the same schema. Without it, &lt;code&gt;key_column_usage&lt;/code&gt; would attribute the columns of that foreign key to the key. Between primary and unique keys the case cannot occur, because the index behind them claims the name within the schema.&lt;/p&gt;

&lt;p&gt;Before deployment, the query was measured on Postgres 17. For 1,000 tables it took 4.7 to 5.1 seconds. The same query, issued for each table individually, came to 0.35 to 0.51 seconds extrapolated from a sample of 25 tables. The set-based query was thus around ten times slower than the single queries it was meant to replace, and it would have used up the gain of the entire catalog read.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; showed why. Both views are themselves queries over several system tables, with privilege checks and type conversions. The planner, that is, the part of Postgres that chooses the execution path for every query, had chosen a Nested Loop for this query, a join that walks through the other side again for every row of one side. In this plan, the join condition sat on the Nested Loop itself and not inside the subplan of the view &lt;code&gt;key_column_usage&lt;/code&gt;, and so it executed that subplan 1,000 times, once per constraint, which for 1,000 tables with one primary key each means once per table, and each time it read the entire system table &lt;code&gt;pg_constraint&lt;/code&gt;. In the plan, that shows up as &lt;code&gt;loops=1000&lt;/code&gt; on exactly this subplan. The timings on such a node are averages per iteration and have to be multiplied by the &lt;code&gt;loops=&lt;/code&gt; value, otherwise the most expensive node in the plan looks harmless. So the loop had not disappeared from this plan, it had moved from the application code into the execution plan. As a cross-check, &lt;code&gt;SET enable_nestloop = off&lt;/code&gt; brought the same query down from 4.7 seconds to 48 milliseconds. That is a diagnosis and not a solution for production. It does not prove that a Nested Loop would be wrong in principle, only that the chosen plan was expensive for these data volumes.&lt;/p&gt;

&lt;p&gt;The rebuild for this article confirms the behavior and at the same time shows that it depends on the version. On Postgres 17.10 the query took 3.9 to 4.8 seconds, on Postgres 18.6 only 29 to 34 milliseconds, because the planner chooses a Hash Join there. That is no blank check for &lt;code&gt;information_schema&lt;/code&gt;, though, because a join of the columns view with &lt;code&gt;information_schema.tables&lt;/code&gt; ran into the same pattern on 18.6 as well and took over 99 seconds. That is why the procedure in the previous section does without that join.&lt;/p&gt;

&lt;p&gt;The solution in the project was a query directly on &lt;code&gt;pg_catalog&lt;/code&gt;, the Postgres system tables from which the &lt;code&gt;information_schema&lt;/code&gt; views themselves read. It connects &lt;code&gt;pg_constraint&lt;/code&gt; through the column array &lt;code&gt;conkey&lt;/code&gt; with &lt;code&gt;pg_attribute&lt;/code&gt; and, like the view version, restricts itself to primary and unique keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conname&lt;/span&gt;                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;constraint_name&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'p'&lt;/span&gt;                 &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_primary_key&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attname&lt;/span&gt;                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;column_name&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ordinal_position&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_constraint&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conrelid&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conkey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;ORDINALITY&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attnum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_attribute&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conrelid&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attnum&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attnum&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relnamespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'perf'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regnamespace&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contype&lt;/span&gt;      &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'p'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'u'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;-- visibility as in information_schema.columns&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;pg_has_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relowner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USAGE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;has_column_privilege&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attnum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'SELECT, INSERT, UPDATE, REFERENCES'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conname&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The condition in lines 20 to 24 belongs to the next section. The query returned the same rows, the comparison with &lt;code&gt;EXCEPT ALL&lt;/code&gt; showed no difference, and the runtime dropped in a direct comparison run from 4.0 seconds to 13.6 milliseconds. In the rebuild it took around 30 to 60 milliseconds on both Postgres versions. Whoever joins catalog views should therefore look at the execution plan before taking the set-based version for the faster one. The telltale sign is a subplan that reads an entire system table and carries a &lt;code&gt;loops=&lt;/code&gt; value equal to the number of rows on the outer side, here the number of tables. The high &lt;code&gt;loops=&lt;/code&gt; value alone is not yet a finding, because a Nested Loop that hits an index on the inner side is often the fastest plan. The query on &lt;code&gt;pg_catalog&lt;/code&gt; applies only to Postgres. For SQL Server and MySQL, the project has its own catalog queries anyway, there through the &lt;code&gt;sys&lt;/code&gt; views and through &lt;code&gt;information_schema&lt;/code&gt; respectively, and the problem did not occur there: on both engines, the set-based version was faster than the single queries from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Privilege Trap When Switching to pg_catalog
&lt;/h2&gt;

&lt;p&gt;The switch to &lt;code&gt;pg_catalog&lt;/code&gt; has a side effect worth knowing. &lt;code&gt;information_schema.table_constraints&lt;/code&gt; shows only tables that the current role owns or on which it has some privilege other than &lt;code&gt;SELECT&lt;/code&gt;. A pure read-only role sees not a single key there, while &lt;code&gt;information_schema.columns&lt;/code&gt; shows it all columns. The source connections in the project work with exactly such read-only roles, and that is why the key snapshots of all Postgres sample connections had been empty from the start. Nobody had noticed, because an empty list does not look like an error. The rebuild fixed this defect as a side effect.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pg_catalog&lt;/code&gt; has the opposite problem: the system tables &lt;code&gt;pg_constraint&lt;/code&gt; and &lt;code&gt;pg_attribute&lt;/code&gt; do not filter by the role's privileges. A role therefore sees the constraint definitions there even for tables whose data it is not allowed to read. That is why the new query carries, in lines 20 to 24, a condition of its own that reproduces the visibility rule of &lt;code&gt;information_schema.columns&lt;/code&gt;, and it did not become measurably slower for it. It takes only the privilege check from that view. The view's remaining filters, such as those against dropped columns or temporary schemas of other sessions, the key query does not need, because a key cannot contain a dropped column and the schema is fixed in advance. So the condition is not a general replacement for the visibility logic of &lt;code&gt;information_schema&lt;/code&gt;, and whoever rebuilds a different catalog view on &lt;code&gt;pg_catalog&lt;/code&gt; has to carry over its conditions one by one. Whoever derives validation rules from keys, as the article &lt;a href="https://sql.marcus-belz.de/en/derive-data-quality-rules-from-schema/" rel="noopener noreferrer"&gt;Deriving Data Quality Rules from the Schema&lt;/a&gt; describes, should know which role the query runs under.&lt;/p&gt;

&lt;p&gt;A second finding belongs to the set-based read itself. The catalog reader creates an entry for every requested table. If the query returns no row for a table, because it no longer exists or the role is not allowed to see its columns, that entry stays empty, without any error message. The code would have turned that into a snapshot without columns and, during reconciliation, deleted the derived validation rules of that table. The code analysis before deployment found this. In a set, a missing element does not stand out, and whoever reads a set therefore has to check for themselves whether something is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result: Factor 9 to 19, Targets Still Missed
&lt;/h2&gt;

&lt;p&gt;The measurement after deployment ran on the same server and against the same source database with 1,000 tables:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000 tables, all rewritten&lt;/td&gt;
&lt;td&gt;114.4 s, timeout after 60 s&lt;/td&gt;
&lt;td&gt;12.1 s&lt;/td&gt;
&lt;td&gt;under 10 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000 tables, unchanged&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;5.8 to 6.4 s&lt;/td&gt;
&lt;td&gt;under 3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;48 tables, all rewritten&lt;/td&gt;
&lt;td&gt;extrapolated 5 to 6 s&lt;/td&gt;
&lt;td&gt;4.4 s&lt;/td&gt;
&lt;td&gt;under 2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;48 tables, unchanged&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;2.9 to 3.1 s&lt;/td&gt;
&lt;td&gt;under 2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle, no stored tables&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;2.7 s&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Instead of 3,000 connections and 4,000 single calls, the refresh now makes three to four catalog queries over a single connection and about ten batch calls. The run with a full rewrite is around nine times faster than before, the run over an unchanged set of tables around 19 times, where this factor measures both changes together, the set-based processing and the skipping of unchanged tables. No run reaches the timeout anymore, and in all nine measurement runs not a single snapshot failed.&lt;/p&gt;

&lt;p&gt;All three targets were still missed. The budget estimate before the rebuild had predicted 0.5 to 1.5 seconds for the unchanged run and 2.5 to 5 seconds for the full rewrite. The explanation is in the last row of the table. A refresh over a connection without stored tables reads only the table list and writes nothing, and it still takes 2.7 seconds. That is the fixed base cost of every call to the application server. It covers, among other things, authentication and permission checks, reading the table selection twice, before and after writing, and preparing the response, on a machine that shares its memory with other services. Only the sum was measured, the shares were not recorded individually. On top of this base, the new logic adds around 3.7 milliseconds per table in the unchanged run, across 1,000 tables therefore 3.7 seconds. Both parts together give the upper end of the measured 5.8 to 6.4 seconds (2.7 + 3.7). The base is a lower bound taken from the idle run, because reading the table selection grows with the number of tables. A run that rewrites all 1,000 tables takes more per table than these 3.7 milliseconds, because it additionally deletes and re-inserts 50,000 rows.&lt;/p&gt;

&lt;p&gt;Before this explanation was allowed to stand, an obvious alternative was ruled out. Two gigabytes were sitting in swap on the machine. Swap was cleared and the measurement repeated, with practically the same result. That largely ruled out swap as the cause, while other influences of the shared machine remained open. That swap on this same server had once before been the cause of a sluggish application is described in the article &lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click&lt;/a&gt;. The shared server itself is introduced in the article &lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As long as the loop took 114 seconds, 2.7 seconds of base cost made up a good two percent of the runtime, and nobody noticed them. After the rebuild, in the unchanged run, they are a good 40 percent. Whoever removes the cost per element exposes the fixed costs, and a budget calculation should therefore include them from the start. Lowering the base further would have meant work outside the snapshot code. The decision was to document the deviation and not to optimize further, because the actual problem was gone: there are no more runs lasting minutes, no timeout and no piled-up clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  When an Optimization Changes What a Display Means
&lt;/h2&gt;

&lt;p&gt;Skipping unchanged tables has two consequences that have nothing to do with speed.&lt;/p&gt;

&lt;p&gt;The first concerns the timestamp. Every snapshot row carries the time at which it was written, and the table register derives a table's "as of" date from it. As long as every refresh rewrote every table, that moment was also the moment of the last check against the source, because both happened at the same instant. With skipping, the two come apart. An example: a table was last rewritten three weeks ago and has not changed in the source since. Today's refresh checks it, finds it unchanged and skips it. Its snapshot rows therefore keep the timestamp from three weeks ago. If the register kept showing that timestamp as the "as of" date, a table that was checked a minute ago would display a three-week-old date, and the user would have to assume the refresh had passed it over. That is why there have been two points in time since the rebuild. The snapshot timestamp now moves only when something has actually changed at the source, and so it becomes the change date. The time of the last check is written by the refresh as a stamp of its own, also for every skipped table.&lt;/p&gt;

&lt;p&gt;The second concerns the success message. Before, it reported how many snapshots had been updated, and with 1,000 tables it always said 1,000. After the rebuild, "nothing changed" is the normal case, the number of rewritten tables is then zero, and the existing message would have shown "no changes". The most expensive kind of check, in which 1,000 tables are verified live against the source, would thus have produced exactly the same message as a run over an empty connection. The message therefore states both numbers, that is, how many tables stayed unchanged and how many were rewritten.&lt;/p&gt;

&lt;p&gt;Whoever replaces a loop with set-based statements and skips work in the process should also check the displays and messages that rely on the old behavior. Otherwise "a lot was checked" silently turns into "nothing was found", and the user loses exactly the confirmation they clicked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Remains
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An iteration costs its work plus everything it additionally crosses.&lt;/strong&gt; Inside the database, an iteration cost around three milliseconds, across connection and application 114. The useful question is therefore not whether a loop is allowed, but what every iteration pays beyond the actual work and how often it runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The budget per element comes before any optimization.&lt;/strong&gt; Three milliseconds per table settled connection reuse as the sole solution before anyone built it. The budget has to include the fixed costs, otherwise it misses the target in a place where nobody is looking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After the rebuild, the execution plan goes on the table.&lt;/strong&gt; The first set-based query was slower than the single queries because the planner executed it as a Nested Loop over an entire system table. A &lt;code&gt;loops=&lt;/code&gt; value equal to the number of outer rows is the cue to look at the affected subplan, and it gets expensive when that subplan reads an entire system table on every iteration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An equivalence proof also finds errors in the old code.&lt;/strong&gt; The comparison before and after the rebuild uncovered a privilege gap through which empty key snapshots had been stored for years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipped work changes what displays mean.&lt;/strong&gt; Timestamp and success message had to be redefined, although nothing in their code had changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is a loop in SQL always slower than a set-based statement?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Set-based means the same as set-oriented: one statement over all rows instead of processing them row by row. It is often faster, because the database can plan the work over the whole set together and fewer fixed costs are incurred per element, but that is no guarantee. How large the gap is depends on what an iteration costs. Inside the database, the loop in the rebuild was around five times slower, and with a few dozen elements that hardly matters. It gets considerably more expensive when every iteration brings its own connection, transaction or network call. If the planner chooses a plan for the set-based statement that internally executes it element by element again, it is even slower than the loop, as the case study shows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does the N+1 problem have to do with cursor loops?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both process a set element by element and differ in how many system boundaries each element crosses along the way. With the N+1 problem, an ORM first loads a list and then issues a separate query for every element, each of them a separate trip to the database. A cursor loop in a procedure, by contrast, repeats the work inside the database, without additional queries from outside. The case study shows a third variant, in which application code even opens a new connection per element. Every boundary between the loop and the data, that is, a connection, a process or a transaction, raises the fixed cost of an iteration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is a join over &lt;code&gt;information_schema&lt;/code&gt; views sometimes so slow?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The views in &lt;code&gt;information_schema&lt;/code&gt; are themselves queries over several system tables. If you join two of them, it can happen that the Postgres query planner cannot push the join condition into the views and evaluates one view completely for every row of the other. In the execution plan you recognize this by a Nested Loop whose inner subplan reads an entire system table and carries a &lt;code&gt;loops=&lt;/code&gt; value equal to the number of outer rows, here the number of tables. Whether it happens depends on the query and the version: the key query was affected on Postgres 17 and not on Postgres 18, the join of columns and tables was affected on both. The remedy is a query directly on &lt;code&gt;pg_catalog&lt;/code&gt; or a single view without a join.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does a read-only role see no keys in &lt;code&gt;information_schema.table_constraints&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This view shows only tables that the role owns or on which it has some privilege other than &lt;code&gt;SELECT&lt;/code&gt;. &lt;code&gt;information_schema.columns&lt;/code&gt;, by contrast, shows the columns already with &lt;code&gt;SELECT&lt;/code&gt;. A role that may only &lt;code&gt;SELECT&lt;/code&gt; on a table therefore sees all of its columns but none of its primary or unique keys. Whoever needs the keys for such a role reads them from &lt;code&gt;pg_constraint&lt;/code&gt; and checks visibility themselves. The query in the case study reproduces the rule of &lt;code&gt;information_schema.columns&lt;/code&gt; and not that of &lt;code&gt;table_constraints&lt;/code&gt;: it shows a role the keys if the role owns the table, checked with &lt;code&gt;pg_has_role&lt;/code&gt;, or has a privilege on the column, checked with &lt;code&gt;has_column_privilege&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does a loop keep running after an error instead of aborting?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;COMMIT&lt;/code&gt; alone is not enough for that. It secures what has already been written, but it still ends the call at the first unhandled error. The loop can only keep running if every iteration gets its own &lt;code&gt;BEGIN … EXCEPTION&lt;/code&gt; block that catches the error and increments a counter. When the block catches an error, Postgres has already rolled back the changes of that iteration, because a block with error handling runs as a subtransaction. The &lt;code&gt;COMMIT&lt;/code&gt; after it then has nothing left to commit for that table, and no half snapshot can come about. There is a trap here: the &lt;code&gt;COMMIT&lt;/code&gt; has to go after this block, not inside it. Inside a block with error handling it fails with &lt;code&gt;cannot commit while a subtransaction is active&lt;/code&gt;, and a &lt;code&gt;WHEN OTHERS&lt;/code&gt; handler swallows exactly this error silently. The procedure then runs through without error and writes nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you measure the duration of a loop when the browser gives up first?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The duration can be read from the written data. If every iteration writes its rows in a transaction of its own and stamps them with &lt;code&gt;now()&lt;/code&gt;, the span between the smallest and the largest timestamp after an undisturbed run gives the duration of the loop up to the last iteration. If everything runs in a single transaction, &lt;code&gt;now()&lt;/code&gt; returns the same value everywhere, and the column then needs &lt;code&gt;clock_timestamp()&lt;/code&gt; as its default. In the long run this is no substitute for instrumentation, but it works after the fact and without any code change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Downstream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/design-pattern-the-architecture-of-an-etl-process-how-to-isolate-bad-data-cleanly/" rel="noopener noreferrer"&gt;Design Pattern // The Architecture of an ETL Process&lt;/a&gt; — at which stage of an ETL pipeline catalog reads like this one have their place.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/derive-data-quality-rules-from-schema/" rel="noopener noreferrer"&gt;Deriving Data Quality Rules from the Schema&lt;/a&gt; — how mandatory fields, keys and type limits from &lt;code&gt;information_schema&lt;/code&gt; turn into validation rules.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/shred-xml-sql-server/" rel="noopener noreferrer"&gt;Reading Complex XML in SQL Server — Shredding a Thousand Attributes Generically Instead of Looping for Hours&lt;/a&gt; — the same idea applied to an XML document: shred generically instead of looping per object type and attribute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;From the same project:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click&lt;/a&gt; — four debugging sessions in which a measurement replaced the guess.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner&lt;/a&gt; — the shared machine on which these measurements ran as well.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/agentic-coding-experience/" rel="noopener noreferrer"&gt;Agentic Coding from a User's Perspective&lt;/a&gt; — how working with a coding agent changes the daily routine.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>Two Agents, One Working Tree — What Broke When I Ran Parallel Claude Code Sessions in One Git Repository</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sat, 12 Sep 2026 06:46:26 +0000</pubDate>
      <link>https://dev.to/marcus1968/two-agents-one-working-tree-what-broke-when-i-ran-parallel-claude-code-sessions-in-one-git-3kce</link>
      <guid>https://dev.to/marcus1968/two-agents-one-working-tree-what-broke-when-i-ran-parallel-claude-code-sessions-in-one-git-3kce</guid>
      <description>&lt;p&gt;A Claude Code session opens a Git repository with no pending changes, works for half an hour and commits whatever has changed at the end. Run alone, that is correct. As soon as a second session or a human works in the same repository, the very same routine can pull someone else’s work into the commit, and Git does not warn about it.&lt;/p&gt;

&lt;p&gt;This article tells five real cases from a little over a week in which parallel Claude Code sessions shared the same repository, sometimes with each other, sometimes with the maintainer. Three of the five cases were the agent’s mistakes. The agent noticed and reported two of them itself. The third was only discovered by the neighbouring session whose work had vanished. The cases yield countermeasures, and the most obvious one is precisely the one that does not hold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The common root:&lt;/strong&gt; A session sees a repository with no pending changes when it starts. If a new or modified file appears during its run because a parallel session or the human has just created or changed it, that looks to the session exactly like a file its own tooling produced. So it treats the foreign file like its own and either commits it or cleans it away. The assumption „everything new comes from me“ is never contradicted, because Git records no author for changes in the working directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The index is shared state:&lt;/strong&gt; Git collects changes for the next commit in a staging area called the index, and &lt;code&gt;git add&lt;/code&gt; puts them there. When several sessions work in the same working directory, they share its index too. Adding only your own file with &lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt; instead of &lt;code&gt;git add -A&lt;/code&gt; does not protect you, because &lt;code&gt;git commit&lt;/code&gt; without a path commits the entire index, including whatever another session has already put there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destructive operations are never cleanup in parallel operation:&lt;/strong&gt; Whoever resets a file to its previous version, discards a state or deletes a file hits everything under the given path, not just their own work. The judgement „that was me“ is an assumption that can be checked, not an observation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared counters collide:&lt;/strong&gt; If two sessions assign the next sequential number at the same time, say for a new bug entry, both read the same highest number and both assign the same new one. If both then politely step aside to the next number, they collide again. Checking beforehand alone does not prevent this, because the window lies between reading and writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The machine is shared too:&lt;/strong&gt; Parallel sessions share not only the repository but the computer the human is working on at the same time. An agent that starts 220 endless loops for a load test sees only exit codes afterwards, not the desktop that stutters for twelve minutes on the human’s side. These costs have no feedback loop to the agent, and with several sessions running the human cannot even tell which one is causing them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What holds:&lt;/strong&gt; Against foreign index entries, &lt;code&gt;git commit --only &amp;lt;paths&amp;gt;&lt;/code&gt;, a look at the index as a separate step before the commit and a &lt;code&gt;git show --stat&lt;/code&gt; afterwards hold up. Shared counters need a tie-break rule agreed in advance, a right-of-way rule for the tie that decides without negotiation which side steps aside, plus a check after writing that compares the number list against the files on disk instead of just looking for duplicate numbers. None of these commands protects against simultaneous changes to the same file. Where the workflow allows it, each session therefore gets its own Git worktree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rule came after the damage:&lt;/strong&gt; The countermeasures have been in a rule file of the blog repository since the second case. They are a consequence of the cases, not a precondition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Basic Git knowledge is assumed, prior Claude Code experience is not. The two Git terms that carry the article, working tree and index, are briefly explained in the first section. The cases come from two repositories, the private app project DI² and the repository of this blog, all between 28 August and 4 September 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Parallel Claude Code Sessions Produce Errors That Don’t Exist Alone
&lt;/h2&gt;

&lt;p&gt;A coding agent session has a clear view of its task and a blind spot for its surroundings. Two Git terms come up again and again in this article and deserve a short introduction. Git calls the files that sit on disk and that it compares against the last commit the working tree. The index is the staging area for the commit: with &lt;code&gt;git add&lt;/code&gt; you put the changes there that the next commit is supposed to contain. The session sees the working tree when it starts, it sees the output of its commands, and at the end it sees which files have changed. What it does not see is whether someone else has used the same working tree in the meantime. Git does not record which process or which session caused a change in the working tree. In normal operation every working tree has its own index, and whoever works in the same working tree shares that index with everyone else there. A second session or a human with an open editor leaves traces in it that cannot be told apart from your own.&lt;/p&gt;

&lt;p&gt;From this follows an assumption that is correct when a session runs alone and wrong when it does not: „The working tree was clean when I started, so everything new comes from me.“ Whether it turns into a problem depends on timing, not on how careful the session is, and none of it feels any different to the session than the normal case.&lt;/p&gt;

&lt;p&gt;All cases in this article go back to the same cause. With every command, a session only thinks about the files it touched itself. Git and the shell do not know that boundary: &lt;code&gt;git commit&lt;/code&gt; takes the whole index, a &lt;code&gt;checkout&lt;/code&gt; or a delete command acts on everything under the given path no matter who changed it, and a load test occupies the whole machine. As long as a session works alone, this goes unnoticed, because everything that changed came from it anyway. As soon as a second session or a human works in the same repository, that is no longer true, and the session does to foreign work exactly what it would do to its own. That is where the errors come from that could not occur at all without a second actor in the same working tree or on the same machine.&lt;/p&gt;

&lt;p&gt;The five cases below occurred within a little over a week in the everyday work of two repositories, without anyone looking for them. Sometimes two sessions worked side by side on different topics, sometimes one session worked while the maintainer touched files in the same repository. Both are perfectly normal modes of operation once you work with more than one agent. The parent article &lt;a href="https://sql.marcus-belz.de/en/agentic-coding-experience/" rel="noopener noreferrer"&gt;Agentic Coding from a User’s Perspective&lt;/a&gt; places parallel operation within the overall experience. This article deals with it alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: Foreign Work Swept Into a Commit
&lt;/h2&gt;

&lt;p&gt;In the DI² project, the maintainer and an agent session were working on the same repository, at times simultaneously. The session routinely committed with &lt;code&gt;git add -A&lt;/code&gt; and wrote a detailed commit message that stayed close to its topic. Between two of its commits, the maintainer had been working somewhere else entirely, on a test case convention, a skill and a CI guard. The session’s next &lt;code&gt;git add -A&lt;/code&gt; commit swept up those four files. The commit message is about a scroll bugfix. Of the roughly one hundred lines of someone else’s convention work, it says not a word.&lt;/p&gt;

&lt;p&gt;This was noticed not at commit time but hours later, and only by chance. While material was being incorporated into an article collection, it contained quotations from repo files with the note that they were not yet committed and that the follow-up commit should be located during verification. The quoted passages were found, but in a commit whose message is about a completely different topic. Without that cross-check of the evidence, the case would have gone undetected.&lt;/p&gt;

&lt;p&gt;This is not data loss, the work is fully in the repository. The damage lies in traceability. Anyone looking for the origin of the convention change no longer finds it via the commit message, only via a content search with &lt;code&gt;git log -S&lt;/code&gt;. On top of that, the commit silently claims authorship of someone else’s work, complete with the agent’s &lt;code&gt;Co-Authored-By&lt;/code&gt; line. The obvious lesson is to stop staging wholesale and to name your own files individually. The second case shows why that lesson is incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: The Obvious Countermeasure Doesn’t Hold
&lt;/h2&gt;

&lt;p&gt;One day later, in the repository of this blog, two parallel Claude Code sessions were working in the same working tree, this time with no human involved. Session A had staged five renames of code files with &lt;code&gt;git mv&lt;/code&gt; but had not committed them yet. At that moment, session B created its own commit, and did so exactly according to the lesson from case 1, with &lt;code&gt;git add&lt;/code&gt; on a single, explicitly named file. The commit swept up the five foreign renames anyway. Its message is exclusively about the material collection session B had been working on.&lt;/p&gt;

&lt;p&gt;The reason is not carelessness but the way Git works. &lt;code&gt;git add &amp;lt;path&amp;gt;&lt;/code&gt; only controls what additionally goes into the index. &lt;code&gt;git commit&lt;/code&gt; without a path then commits the entire index, including everything someone else has already put there. In a working tree used by several actors at once, the index is thus shared state. Whoever took „name files individually“ as the lesson from case 1 is not protected against this case and feels safe anyway.&lt;/p&gt;

&lt;p&gt;Session B had even built in a check, a &lt;code&gt;git status&lt;/code&gt; in the same command. But it hung behind the commit via &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add material.md &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"…"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git status &lt;span class="nt"&gt;--short&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the output appeared only after the fact had been created. A check that runs after the action is no longer a check, it is a log entry. The reliable order separates the look at the index from the commit and names the paths explicitly on the commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-status&lt;/span&gt;     &lt;span class="c"&gt;# separate step before the commit: What is in the index?&lt;/span&gt;
git commit &lt;span class="nt"&gt;--only&lt;/span&gt; material.md &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"…"&lt;/span&gt;
git show &lt;span class="nt"&gt;--stat&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; HEAD      &lt;span class="c"&gt;# afterwards: Does the commit contain exactly the expected files?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What matters here is not the &lt;code&gt;--only&lt;/code&gt; keyword but the path argument on the commit. As soon as paths appear on the command line, Git, according to its &lt;a href="https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---only" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, commits only their current content and leaves changes already staged for other paths untouched. &lt;code&gt;--only&lt;/code&gt; is the default mode for this and merely makes the intent explicit. The rule file of this blog spells it out anyway, because then the command shows its intent. The behaviour was reproduced for this article in a throwaway repository. With a foreign rename in the index, &lt;code&gt;git add b &amp;amp;&amp;amp; git commit&lt;/code&gt; produces a commit with two files, &lt;code&gt;git commit --only b&lt;/code&gt; produces a commit with a single file, and the foreign rename stays in the index unchanged.&lt;/p&gt;

&lt;p&gt;The path argument does have a limit, though, and it matters for this topic. It protects against foreign entries at other paths, not against foreign changes to the same file. Git commits the current content of the named paths from the working tree, not the state the session put there with &lt;code&gt;git add&lt;/code&gt;. If another session has edited the same file shortly before, its change lands in the commit as well, and no command warns about it. This too was reproduced: the index held one line of the session’s own, the working tree an additional foreign one, and &lt;code&gt;git commit --only&lt;/code&gt; committed both. &lt;code&gt;git show --stat&lt;/code&gt; then showed two insertions instead of one, a hint only noticed by someone who expects the number. So &lt;code&gt;git commit --only&lt;/code&gt; solves the problem of the shared index, not the problem of the shared file.&lt;/p&gt;

&lt;p&gt;In this case too, the damage was traceability, not loss. Not a single line of the renames themselves changed, but the commit message does not describe them. What went well in this case is at the same time the contrast to the next one. Session B noticed the mistake itself, did not repair it on its own authority and informed the other session. A repair would have damaged the history further, because session A’s follow-up commit was already sitting on top of the affected commit. A &lt;code&gt;git reset --soft&lt;/code&gt; to the state before would have taken that commit out of the branch. Its changes would have remained in the working tree, but the next commit would have absorbed them under session B’s message, and the original commit would no longer have been reachable via the branch, typically only via the reflog. This too was reproduced for this article in the throwaway repository. The decision to leave the history as it was rested with the maintainer. Session A did not simply take the report on trust but cross-checked it with &lt;code&gt;git show --stat&lt;/code&gt; and then left a pointer to the actual location in the revision log of the affected article. That pointer closes the gap the commit message left behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 3: Foreign Work Deleted
&lt;/h2&gt;

&lt;p&gt;Case 3 is the mirror image of case 1. There, a session swept up foreign work, here it deleted foreign work. The cause is the same, the damage is the more expensive of the two.&lt;/p&gt;

&lt;p&gt;In the DI² project, two sessions were working on different topics at the same time. Session A was fixing a bug, session B was drafting a UI concept for another feature in parallel. Session A had seen a working tree with no pending changes at the start. During a longer tool run, two foreign files appeared there, a modified concept file and a new mockup. Session A attributed both to its own run and justified this by claiming its tooling had worked beyond the assignment. Then it discarded both files. It reverted the modified file to its previous version and deleted the new one.&lt;/p&gt;

&lt;p&gt;In the summary to the maintainer, the mistake got worse. Session A not only reported the rollback but also claimed the discarded text had contained a fabricated user approval. That approval had in fact been given, just in the other session. A deletion thus turned into a false accusation against the session’s own tool chain as well.&lt;/p&gt;

&lt;p&gt;It was not session A that found the case. Session B restored its work and sent a message into the other session. It is rendered here in substance, not verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Two files from my working state just disappeared: the modified
concept file and the new mockup. Both are mine, not products of
your tooling. I have restored them.
From now on, please run no restore or cleanup operations
(checkout, restore, delete) on paths you have not touched
yourself in this session. If you notice files you cannot
attribute: report them, do not remove them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The message did not undo the deletion, session B had already done that itself. But it formulated the rule that later went into the rule file, and it gave the session that caused the damage a piece of information it could never have read from the working tree: that it was not alone.&lt;/p&gt;

&lt;p&gt;Why the misattribution is understandable was described above. Why it is dangerous nonetheless is shown by the difference between a commit and a change Git does not yet know about at all. A commit can usually be reconstructed or taken back with a &lt;code&gt;git revert&lt;/code&gt;. A discarded change that was neither committed nor put into the index with &lt;code&gt;git add&lt;/code&gt;, by contrast, cannot be recovered by Git. Here it only came back because the second session still had its state in memory. Two lessons follow. Destructive operations such as reverting, resetting and deleting act on everything under the given path, not just on your own work. That is why, in parallel operation, they are never mere cleanup. And your own attribution is an assumption, not an observation. „That was me“ can be checked, via timestamps, content and change history. Whoever does not check it should not claim it either, least of all in a summary to the human.&lt;/p&gt;

&lt;p&gt;Self-control did not work in this case. Without the second session, the work would have vanished silently, and the false claim would have stood as a finding. That makes it the most uncomfortable and therefore the most valuable of the five cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 4: Duplicate Numbers, Because Politeness Is Symmetric
&lt;/h2&gt;

&lt;p&gt;The fourth case has its point not in the conflict itself but in the cooperative reaction to it. Both sides behaved decently, and precisely because of that the conflict arose a second time.&lt;/p&gt;

&lt;p&gt;Two sessions in the DI² project each created a new bug independently of one another. Number assignment follows a documented rule: the highest number assigned in the index file plus one gives the next. Both sessions read the same highest number, and both assigned the same new one. Both noticed the collision, and both stepped aside to the next number. The second collision was identical to the first.&lt;/p&gt;

&lt;p&gt;Checking beforehand would not have helped, because both sides had calculated correctly. The critical window lies between reading and writing. A check run before would have given both sides a green light and lulled them into false security. The first reflex, „just check first“, treats the problem like a slip. In fact it is a race: whoever gets overtaken by the other side between their read and their write has lost despite a correct check. Such collisions on sequential numbers therefore cannot be ruled out in parallel operation, only detected and resolved.&lt;/p&gt;

&lt;p&gt;In the end, two things helped. The first was detection after writing, in both directions. The project has an automatic check for this, called a guard there, which compares the index file against the files on disk. It reports not only „number assigned twice“ but also „file without index row“ and „index row without file“, three different intermediate states of the same process. A check that only knew duplicates would have let the later stages through. The origin of this guard is remarkable. It had been built two days earlier for an entirely different symptom, counters in the header of an index file that had drifted away from the content below. What it then caught was a duplicate assignment by parallel sessions, which nobody had thought of when building it. The reason is transferable. The guard does not check the symptom but the condition behind it that must always hold: index and files on disk must match in both directions. A check that had only looked at whether the counter was right would have stayed silent at the collision.&lt;/p&gt;

&lt;p&gt;The second was an asymmetric resolution rule. The other session proposed sorting alphabetically by identifier, and the smaller identifier gets the smaller number. What matters is not that the rule is particularly clever but that both sides can compute it to the same result without negotiation. On the first collision, stepping aside is the decent thing to do. But if both step aside, the same conflict arises again, just like two people in a corridor each making way for the other. Two cooperative parties cannot get past each other without an asymmetric rule. Whoever runs parallel agents therefore needs not only detection but a tie-break rule that is fixed in advance, a right-of-way rule for the tie. Otherwise the agents negotiate, and negotiating costs rounds.&lt;/p&gt;

&lt;p&gt;Two side findings from the resolution belong here as well. The first concerns the countermeasure from case 2. &lt;code&gt;git commit --only&lt;/code&gt; keeps its promise, but it refuses to work on a new file as long as Git does not know the file yet. The error message then reads &lt;code&gt;pathspec did not match any file(s) known to git&lt;/code&gt;. So the file first has to be added with &lt;code&gt;git add&lt;/code&gt;, and exactly that step puts your own entry next to the foreign entries already sitting in the index. The safeguard held, because the commit took only the one file. But the procedure forces you through precisely the state you wanted to avoid. The second side finding is an ownership criterion for shared files. A file that cannot be split and carries entries from both sides belongs in the commit of the side whose substantive change it carries, not in the commit of the side that happens to finish first. One of the two sessions had initially proposed the wrong direction and was corrected by the other. Its commit would otherwise have contained half a process without the file move that belonged to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 5: The Machine Is Shared Too
&lt;/h2&gt;

&lt;p&gt;The first four cases are about collateral damage in the repository. The fifth hits the human’s machine. Parallel sessions share not only the repository but also the computer the human is working on at the same time. What is shared here is no longer Git state but computing power, and different tools apply, not &lt;code&gt;git&lt;/code&gt; but &lt;code&gt;taskset&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt; and &lt;code&gt;trap&lt;/code&gt;. The machine’s load is the one resource whose consumption the agent causes and cannot perceive itself.&lt;/p&gt;

&lt;p&gt;The assignment in the DI² project was a test that failed only sporadically. The diagnosis found not a slow test but two time budgets that did not fit together. The test library gives each individual wait 1000 ms, the test runner gives the whole test body 5000 ms, and the densest tests chain seven waits in a row. Without load, the file runs through in just over a second. So the defect can only be observed under load at all. That makes the verification expensive for the machine, and it makes it legitimate at the same time.&lt;/p&gt;

&lt;p&gt;The agent generated the load itself, with a single line. It started 220 CPU endless loops in the background, then three to five full runs of the test suite followed, and the whole thing was repeated three times, over twelve minutes in total. On the maintainer’s 28-core workstation, that produced a load average of 234, roughly eight times the core count, and every core sat at one hundred percent. The agent did not notice. The maintainer noticed his machine no longer responding, opened the process monitor and asked in a third session, with a screenshot: „Are you still doing something? When you run, CPU usage goes up considerably.“&lt;/p&gt;

&lt;p&gt;The actual finding is not a wrong idea but a lost one. An earlier subagent on the same bug track had answered the same question with six endless loops, pinned to two cores via &lt;code&gt;taskset&lt;/code&gt;. That produces the same contention for the test suite, but the rest of the machine stays free. The later subagent went unpinned to 56 loops, then to 200 and finally to 220. The frugal method had already been tried on the same track and was not picked up again when scaling up. Knowledge one session has is not automatically available to the next.&lt;/p&gt;

&lt;p&gt;Cleanup did happen, but in the wrong place. Every command ended in &lt;code&gt;kill $(jobs -p)&lt;/code&gt;, and that worked: no processes were left behind, and the load average dropped back into the single digits. But the cleanup hung on the last line of the command instead of on a &lt;code&gt;trap&lt;/code&gt;. A timeout, a crash or an abort by the human would have left 220 endless loops running until the next reboot, and that precisely at the moment the human steps in because the machine has frozen. Transparency was not lacking either, it just came in the wrong tense. The final report named the method correctly and unprompted, with load average and core count. That disclosure was complete and ineffective at the same time, because it came only once the damage was over. An announcement beforehand would have cost two sentences.&lt;/p&gt;

&lt;p&gt;Why does the agent not notice? It could well measure the load, &lt;code&gt;uptime&lt;/code&gt; or &lt;code&gt;top&lt;/code&gt; would put the load average in its output. What it lacks is the feedback about what that number means for the human. It sees exit codes and no stuttering desktop, and no tool tells it which load the owner of the machine is willing to accept right now. The costs fall entirely on a human it cannot observe. Unlike with tokens or run time, there is no feedback loop here. Resource-intensive verification remains the right thing to do, because load and repetition are often the only way to demonstrate a sporadic problem at all. Three conditions make it tolerable. The load runs pinned instead of on the whole machine, provided the test does not require system-wide contention. The cleanup hangs on a &lt;code&gt;trap&lt;/code&gt; instead of on the last line. And the agent announces beforehand what it is about to do instead of reporting afterwards, because whether a machine may be unusable for twelve minutes is the owner’s decision. The following minimal pattern shows the first two conditions in code. It is not a runbook, and not a cleanup guarantee either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'kill $(jobs -p) 2&amp;gt;/dev/null'&lt;/span&gt; EXIT INT TERM
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 6&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;taskset &lt;span class="nt"&gt;-c&lt;/span&gt; 0,1 &lt;span class="nb"&gt;timeout &lt;/span&gt;900 sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'while :; do :; done'&lt;/span&gt; &amp;amp; &lt;span class="k"&gt;done
&lt;/span&gt;taskset &lt;span class="nt"&gt;-c&lt;/span&gt; 0,1 &amp;lt;&lt;span class="nb"&gt;test &lt;/span&gt;run&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line 2 creates the contention on exactly the two cores on which the test also runs in line 3, and gives each loop its own &lt;code&gt;timeout&lt;/code&gt; after which it terminates itself. The rest of the machine stays with the human. Line 1 cleans up the started loops when the shell exits. This &lt;code&gt;trap&lt;/code&gt; has two limits, and both were reproduced for this article. If an abort signal arrives while the test run is still going, Bash executes the &lt;code&gt;trap&lt;/code&gt; only once the test run returns. And against a &lt;code&gt;kill -9&lt;/code&gt; of the shell itself, it does not help at all. That is exactly what the &lt;code&gt;timeout&lt;/code&gt; in line 2 is for. The third condition belongs in the assignment, and there it costs two sentences. The following assignment is a pattern, not a quotation from the session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reproduce the sporadic failure under load. Conditions:
load only on two pinned cores (taskset), cleanup via trap,
and tell me beforehand how long the machine will be loaded.
If you need more than two cores, ask first.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One side finding concerns attribution. Three sessions were running in parallel, and the maintainer could not tell which of them was causing the load. In the end the evidence came from the session transcripts, where the command stood with a timestamp, and from the decay of the load average, which matched the time of the last logged run. Whoever runs several agents in parallel needs a method to attribute load to a session, and needs it before it becomes necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Holds and What Doesn’t
&lt;/h2&gt;

&lt;p&gt;Countermeasures emerged from the five cases, and they emerged after the fact. After case 2 they went into the Git rule file in the repository of this blog, as a section of its own called „Parallel sessions“, and since case 4 they also carry the side finding about new files. How such a rule file is built and why the countermeasure lives there and not in the maintainer’s head is described in &lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;I Gave Claude Code 27 Rule Files Instead of One CLAUDE.md&lt;/a&gt;. The balance after a little over a week of parallel Claude Code sessions looks like this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What holds:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git commit --only &amp;lt;paths&amp;gt;&lt;/code&gt;:&lt;/strong&gt; The path argument on the commit limits it to the given paths, which can also be directories or patterns, and leaves staged changes outside them alone, &lt;code&gt;--only&lt;/code&gt; makes that explicit. Against foreign index entries this is the simplest and most robust variant, because it does not depend on the session’s attention. Against foreign changes to the same file it does not protect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The look at the index as a separate step before the commit:&lt;/strong&gt; A &lt;code&gt;git diff --cached --name-status&lt;/code&gt; or a &lt;code&gt;git status --short&lt;/code&gt; runs as its own command before the commit and not behind it via &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;. Whatever sits there and does not belong to your own change gets named or excluded. This check is a plausibility check and not a lock, because between the look and the commit another session can change the index again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git show --stat --oneline HEAD&lt;/code&gt; after the commit:&lt;/strong&gt; This look checks whether the commit contains the expected files and only those. It finds the mistake the two steps before let through. Whether the content of the files is right as well is only shown by &lt;code&gt;git show HEAD -- &amp;lt;path&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report instead of repair:&lt;/strong&gt; If foreign work sits in your own commit on a branch someone else is still working on, the session informs the affected side and leaves the decision about the history to the maintainer. If the history stays as it is, the session leaves a pointer to the location where someone would look for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One worktree per session, where the workflow allows it:&lt;/strong&gt; With &lt;code&gt;git worktree add&lt;/code&gt;, every session gets its own working tree with its own index, and cases 1 to 3 disappear in the form described here. Shared remain the repository objects and the branches, among other things &lt;code&gt;HEAD&lt;/code&gt; and the index are specific to each worktree. Everything outside the working tree stays shared as well, the machine and the shared counters included, more on that in the FAQ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the tie-break rule in advance:&lt;/strong&gt; For every shared counter there is an asymmetric rule that both sides can compute to the same result without negotiation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the condition, not the symptom:&lt;/strong&gt; The guard compares index and files on disk in both directions. A check like that also catches errors nobody thought of when it was built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-check instead of believing:&lt;/strong&gt; Both sessions in case 4 re-measured each other’s status reports instead of taking them over. That found an omission in the report that the reporting side could not have noticed itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What doesn’t hold:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;„Just check first“:&lt;/strong&gt; In a race between two writers, the window lies between reading and writing. The check beforehand gives a green light and lulls into security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The lesson from case 1 as protection against case 2:&lt;/strong&gt; &lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt; instead of &lt;code&gt;git add -A&lt;/code&gt; closes one gap and leaves the neighbouring one open. A lesson that closes one gap creates a false sense of security for the next.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repair via &lt;code&gt;git reset &amp;lt;commit&amp;gt;&lt;/code&gt;:&lt;/strong&gt; Resetting the branch acts on everything that has been added since that commit. If a commit by another session is already sitting on top, it disappears from the branch. The three modes differ only in what gets reset besides the branch: &lt;code&gt;--soft&lt;/code&gt; leaves index and working tree alone, the default mode resets the index, and &lt;code&gt;--hard&lt;/code&gt; additionally discards unsaved changes to tracked files in the working tree, including another session’s, and overwrites untracked files that stand in the way of a tracked file of the target state. The damage of the repair is then greater than that of the mistake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup on the last line:&lt;/strong&gt; It does not survive precisely the abort that needs it most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparency after the fact:&lt;/strong&gt; A complete report after the damage is honest, but it no longer changes anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One side finding belongs here because it concerns the figures with which agents demonstrate their diligence. A shared working tree distorts not only files but measurements. A full test run, started at the very moment the other session was renaming a file, reported three failed test files with an error that had nothing to do with them in substance. On the next run, everything was green. Whoever writes such numbers into a report documents a state that never existed. In parallel operation, a single red run is therefore not a finding but a hypothesis, and it only counts once it can be repeated on a quiet tree. How measurements can be separated from assumptions when the agent takes over the diagnosis will be the subject of a separate article on this blog about debugging with a coding agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;To a session, its own and foreign changes in the working tree look the same, and Git never contradicts the assumption „everything new comes from me“.&lt;/li&gt;
&lt;li&gt;In a shared working tree, the Git index is shared state. &lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt; does not protect, because &lt;code&gt;git commit&lt;/code&gt; without a path commits the entire index. Against foreign entries at other paths, &lt;code&gt;git commit --only &amp;lt;paths&amp;gt;&lt;/code&gt; is the most robust of the variants considered here, supplemented by a look at the index before the commit and a &lt;code&gt;git show --stat&lt;/code&gt; afterwards. Against simultaneous changes to the same file, a separate worktree helps most reliably.&lt;/li&gt;
&lt;li&gt;Destructive operations are never mere cleanup in parallel operation. Your own attribution is an assumption, and whoever cannot check it reports instead of deleting.&lt;/li&gt;
&lt;li&gt;Shared counters can collide at any time in parallel operation. What is needed is a check after writing that verifies the intended state, and an asymmetric tie-break rule fixed in advance.&lt;/li&gt;
&lt;li&gt;Parallel sessions also share the human’s machine, and its load is a resource with no feedback loop to the agent. Resource-intensive verification therefore runs pinned, time-limited, with cleanup via &lt;code&gt;trap&lt;/code&gt; and with an announcement beforehand.&lt;/li&gt;
&lt;li&gt;The rule came after the damage. Three of the five cases were the agent’s mistakes. The agent reported two of them itself, the third was found by the neighbouring session whose work had been lost.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is it enough to stage files individually instead of using &lt;code&gt;git add -A&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. &lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt; only controls what additionally goes into the index. If something from another session or from the human is already sitting there, &lt;code&gt;git commit&lt;/code&gt; without a path takes it along. Protection only comes from &lt;code&gt;git commit --only &amp;lt;paths&amp;gt;&lt;/code&gt;, which ignores the rest of the index, or from a look at the index as a separate step before the commit. A &lt;code&gt;git status&lt;/code&gt; hanging behind the commit via &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; shows the mistake only once it has already happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can &lt;code&gt;git commit --only&lt;/code&gt; prevent foreign changes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only one kind of them. The path argument keeps staged changes at other paths out of the commit, that is the shared-index problem from case 2. It does not keep out what another session has changed in the same file, because Git commits the current content of the named paths from the working tree. Whoever wants to be sure what is in the commit checks the content afterwards with &lt;code&gt;git show HEAD -- &amp;lt;path&amp;gt;&lt;/code&gt; or gives every session its own worktree from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when two sessions edit the same file?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a shared working tree the file exists only once, and Git reports nothing. If both change individual spots, both changes end up in the file afterwards, and whoever commits first takes the other’s along. If one session instead rewrites the whole file from an older read, the other session’s change has silently vanished. If it was staged, its state still sits as a blob in the index. After another &lt;code&gt;git add&lt;/code&gt; it is only a dangling object that &lt;code&gt;git fsck --lost-found&lt;/code&gt; may still find, but that is no guarantee. If it was never staged, it is gone. The situation can only be recognised by clues: an &lt;code&gt;MM&lt;/code&gt; in &lt;code&gt;git status&lt;/code&gt; on a file you did not stage yourself, or an editing tool that refuses to write because the file has changed since it was last read. Claude Code’s Edit tool behaved exactly like that in the sessions behind this article, as of September 2026, a &lt;code&gt;sed&lt;/code&gt; or a Python script does not. Git only detects the situation reliably once both sessions work in their own worktrees on their own branches. Then there are two versions, and merging them produces a merge conflict as soon as both have changed the same lines. Different lines Git merges automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do you do when a commit has swept up foreign work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On a branch someone else is still working on, the first reflex, repairing the commit on your own authority, is the wrong one. Whoever works alone on an unpublished branch may reset it. A &lt;code&gt;git reset&lt;/code&gt; to an earlier commit and other interventions in the history act on everything that has been added since that commit. If a commit by another session is already sitting on top, the correction takes it out of the branch, and with &lt;code&gt;--hard&lt;/code&gt; it also discards that session’s unsaved changes to tracked files. Better to inform the affected side and let the maintainer decide whether the history gets touched. If it stays as it is, a pointer to the actual location belongs where someone would look for it, for instance in the revision log of the affected article or in the spec of the affected task. The content is not lost. Only its origin can no longer be found via the commit message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does a session recognise that it is not alone in the repository?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With Git alone, not reliably at all, and that is precisely the core of the problem. There are clues: an index that is not empty at the start, files that newly appear or change during a tool run although your own tooling did not touch them, or a commit in the log that did not come from your own session. None of them is proof. That is why a session treats its attribution as an assumption and checks it via timestamps and content before discarding anything. The most reliable information comes from the human. Whoever starts two sessions tells both that they are not alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are Git worktrees the solution for parallel Claude Code sessions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For cases 1 to 3 they largely solve the problem, because they remove its common cause, the shared working tree. A &lt;a href="https://git-scm.com/docs/git-worktree" rel="noopener noreferrer"&gt;worktree&lt;/a&gt; gives every session its own working tree with its own index, only the repository, its objects and its branches stay shared. In practice a worktree also means a branch of its own per session or a detached &lt;code&gt;HEAD&lt;/code&gt;, because a branch cannot be checked out in two worktrees at the same time. So the merge back is part of the workflow, and that is where Git then reports the conflicts on the same file as well. The difference to the shared working tree lies exactly there: own files on disk, own index, own &lt;code&gt;HEAD&lt;/code&gt;, but the same commits and branches underneath. Foreign files in the working tree and foreign entries in the index thereby disappear. The remaining cases persist, though. The machine is still shared, and shared counters then collide when the branches are merged instead of at write time. Besides, in practice the human often works in the main working tree, and a session running there has all five problems again. Worktrees separate the working trees, not the actors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you commission load tests without freezing your own machine?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Best with three conditions in the assignment. The load runs pinned to a few cores, via &lt;code&gt;taskset&lt;/code&gt; for instance, provided the test does not require system-wide contention, and the test runs on the same cores so that the contention arises where it is needed. The cleanup hangs on a &lt;code&gt;trap&lt;/code&gt; so that it takes effect when the script ends, and every load loop gets its own &lt;code&gt;timeout&lt;/code&gt; so that it terminates even if the shell gets killed the hard way. And the agent announces beforehand how long the machine will be loaded, so that the human can decide whether to allow that right now. An agent cannot derive these conditions on its own, because it can measure the load but cannot see its cost to the human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Parent article:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/agentic-coding-experience/" rel="noopener noreferrer"&gt;Agentic Coding from a User’s Perspective — Experience: The Work Doesn’t Disappear, It Shifts&lt;/a&gt; — the overall experience of which parallel operation is one section.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hub:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code — Rules, Skills and Agents That Enforce Conventions&lt;/a&gt; — the enforcement system the rule file from this article belongs to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sibling article:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click — Debugging Postgres, Docker and a Swapping Server with a Coding Agent&lt;/a&gt; — why a finding only holds once a measurement stands behind it, and why a red test run in a shared working tree is only a hypothesis at first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule files:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;I Gave Claude Code 27 Rule Files Instead of One CLAUDE.md&lt;/a&gt; — how a rule file is built into which a countermeasure like &lt;code&gt;git commit --only&lt;/code&gt; goes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next door:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ssis-vs-transact-sql-source-code-management/" rel="noopener noreferrer"&gt;SSIS vs. SQL: Source Code Management — Why SP Diffs Are Readable and &lt;code&gt;.dtsx&lt;/code&gt; Diffs Are Not&lt;/a&gt; — source code management from the era before agents, with the same core concern: a diff has to stay readable.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>git</category>
      <category>productivity</category>
    </item>
    <item>
      <title>An EU Region Is Not an EU Provider — Vercel, Supabase and the GDPR Third-Country Question</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:25:48 +0000</pubDate>
      <link>https://dev.to/marcus1968/an-eu-region-is-not-an-eu-provider-vercel-supabase-and-the-gdpr-third-country-question-3he4</link>
      <guid>https://dev.to/marcus1968/an-eu-region-is-not-an-eu-provider-vercel-supabase-and-the-gdpr-third-country-question-3he4</guid>
      <description>&lt;p&gt;"We host in Frankfurt, so we're GDPR-compliant." The sentence comes up in almost every conversation about Vercel and Supabase, and it is half the answer to a question that has two halves. The region answers where the data lives. It does not answer whose law applies to the companies that process that data.&lt;/p&gt;

&lt;p&gt;This article takes the second half apart. It explains what an EU region delivers legally and what it does not, why the European Commission has twice declared transfers to the United States permissible by decision and the Court of Justice of the European Union has struck down both decisions while the third is under review right now, and what the CLOUD Act, a US law from 2018, has to do with a data center in Frankfurt. At the end there is a checklist for the third-country question that lets you examine and document your own use of &lt;strong&gt;Vercel or Supabase&lt;/strong&gt;, plus a decision aid for the alternative. The occasion is a concrete one: In the DI² project this question was asked before the first deploy, and the answer shaped the &lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;infrastructure&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not compliant by default:&lt;/strong&gt; Vercel and Supabase offer EU regions and a data processing agreement, the contract that lays down what the provider may do with its customers' data. The conditions for a compliant deployment still have to be established and documented by the user. The default setting is not the compliant one, and at Vercel functions run in the United States unless you choose otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The chain counts, not the seat:&lt;/strong&gt; Vercel is a US company. Supabase's contracting party sits in Singapore, the US company Supabase, Inc. appears as a subcontractor for support in its own list, and hosting runs on Amazon Web Services (AWS). Whose law applies is not stated by the provider's legal notice but by its list of subcontractors that touch the data. The GDPR calls them sub-processors, and the providers publish them as a "Subprocessor List".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three attempts at a transfer mechanism:&lt;/strong&gt; The European Commission has three times found by decision that the United States offers an adequate level of protection, each time on the basis of an arrangement with the US government. The first two, Safe Harbor of 2000 and Privacy Shield of 2016, were struck down by the Court of Justice in 2015 and 2020, both times for the same reason. Against the third, the Data Privacy Framework of 2023, the General Court of the European Union dismissed the first action in September 2025, and the appeal is pending before the Court of Justice. As of 9 September 2026 it stands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CLOUD Act does not ask where the data is stored:&lt;/strong&gt; A provider under US jurisdiction must in principle hand over data in its possession or under its control, no matter which country it is stored in. There is no agreement with the EU that would open the law's special objection procedure. Microsoft France confirmed as much before the French Senate in June 2025.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A contract does not cure that:&lt;/strong&gt; The data processing agreement governs the relationship between user and provider. It does not override a US law. Both risks land on the user in the end, because the user is responsible for the processing, even though the provider has obligations of its own. The checklist at the end makes them visible instead of removing them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The alternative is a provider question:&lt;/strong&gt; Anyone who wants to rule the two risks out rather than carry them has only one route, no provider under US jurisdiction in the chain. That means an EU provider or a server of your own in the EU, and both come at a price that the sibling article on infrastructure puts into numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; The article is aimed at developers and small teams in the EU who run a web application with a login on Vercel, Supabase or comparable services, or plan to. No legal background is required, and terms such as adequacy decision, standard contractual clauses and data processing agreement are explained where they first appear. All provider statements are taken from the providers' own documentation, quotations verbatim, retrieved on 9 September 2026. The article is not legal advice, and it does not replace an assessment of your own case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Answer: Not by Default
&lt;/h2&gt;

&lt;p&gt;Can you use Vercel and Supabase in a GDPR-compliant way? Yes, but not by creating an account and picking Frankfurt as the region. Both providers say so themselves, just not on the landing page. Vercel writes in its compliance documentation that it supports its customers' GDPR compliance and lists five commitments to that end, from the security level to the standard contractual clauses. Supabase writes that it supports GDPR-compliant deployments. In both cases the subject of the sentence is the customer. The provider supplies the tools, and the user has to apply them, document them and answer for them.&lt;/p&gt;

&lt;p&gt;What that involves fits in one paragraph. The user signs the data processing agreement, which the GDPR calls a contract under Article 28 and practice usually calls a DPA. The user chooses the region instead of accepting the default. The user documents the basis on which data may leave the territory of the EU, because with both providers it can do so even with an EU region, at Vercel by the provider's own reservation, at Supabase through support and subcontractors. The user extends the privacy policy to name the recipient, the third country and the basis. And the user consciously decides whether to carry two risks that no contract takes away. Those two risks are the core of this article. The rest is the groundwork needed to understand them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an EU Region Delivers — and Who Is in the Chain
&lt;/h2&gt;

&lt;p&gt;An EU region is a promise about storage location. At Supabase that means: Whoever creates a project in an AWS region gets the database, the auth service and storage hosted in that region, as the &lt;a href="https://supabase.com/security" rel="noopener noreferrer"&gt;security page&lt;/a&gt; states. At Vercel the region is a choice for the part of the application that computes on the server, the so-called serverless functions, and the default is not Europe. The &lt;a href="https://vercel.com/docs/security/compliance" rel="noopener noreferrer"&gt;compliance documentation&lt;/a&gt; says it in one sentence: "The default location for Vercel functions is the U.S." In front of those functions sits Vercel's worldwide delivery network, a CDN (content delivery network), which accepts visitors' requests at the nearest of twenty locations and serves the pages from there. The chosen region applies to the functions, not to that network.&lt;/p&gt;

&lt;p&gt;The question of whether a service can be used in a GDPR-compliant way has two parts. The first is: Where does the data live? The region answers it, and for Supabase and Vercel alike that part is settled. The second is: Which companies process the data, and whose law are those companies subject to? No region answers that. The answer lies in the list of subcontractors the provider brings in to run the service and who touch the data in the process. Both providers publish that list, and at both it reads differently from what the marketing suggests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vercel:&lt;/strong&gt; Vercel, Inc. is a US company. It is certified under the EU-US Data Privacy Framework, and it additionally relies on standard contractual clauses for transfers. Both statements are in the compliance documentation. Directly below them stands the reservation that puts the region into perspective: "Vercel may transfer data to and in the United States and anywhere else in the world where Vercel or its service providers maintain data processing operations." Vercel may therefore move data to the United States and to any other location where it or its service providers process data. Backups, according to the same page, are replicated globally and are not accessible to customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supabase:&lt;/strong&gt; Here the research for this article produced a different picture from the expected one. According to the &lt;a href="https://supabase.com/terms" rel="noopener noreferrer"&gt;terms of service&lt;/a&gt; and the &lt;a href="https://supabase.com/legal/dpa" rel="noopener noreferrer"&gt;data processing agreement&lt;/a&gt;, the contracting party is not a US company but Supabase Pte. Ltd., based in Singapore. The terms place the contract under California law, and the data processing agreement relies on standard contractual clauses for transfers, a contract text prescribed by the European Commission in which the recipient commits to European data protection rules, and does not mention the Data Privacy Framework. Supabase's own &lt;a href="https://supabase.com/legal/customer-resources/subprocessor-list" rel="noopener noreferrer"&gt;subprocessor list&lt;/a&gt;, in the version dated 1 June 2026 and retrieved on 9 September 2026, then names the companies that actually touch the data: Amazon Web Services for hosting, Cloudflare, Google and Fly.io for further hosting services, Vercel for hosting, and at the top of the list Supabase, Inc. for support. Supabase's &lt;a href="https://supabase.com/privacy" rel="noopener noreferrer"&gt;privacy policy&lt;/a&gt; names the United States and Singapore as transfer destinations, each on the basis of standard contractual clauses, and describes its own services as "primarily hosted in and provided from the United States". The contracting party sits in Singapore, the data sits with a US host, and the US company has access as a subcontractor.&lt;/p&gt;

&lt;p&gt;For the GDPR the seat in Singapore changes nothing about the classification. An adequacy decision is the European Commission's finding that a country outside the EU protects personal data as well as the EU itself does. Where one exists, data may go there as if it stayed in the EU. For Singapore no such decision existed at the time of writing, and for the United States it applies only to companies with an active certification under the Data Privacy Framework, and only to the data types that certification covers. Every transfer to Singapore is therefore a third-country transfer that needs a mechanism of its own, in Supabase's case the standard contractual clauses. What the case shows is something else: The provider's legal notice is the wrong place to answer the provider question. The right place is the list of subcontractors. As soon as a service provider under US jurisdiction appears there that processes personal data or can access it, and with services on AWS, Google Cloud or Azure one regularly does, two things have to be examined separately for that data. First, whether a third-country transfer takes place and on what basis, which is the next section. Second, what state access risk that link brings with it, which is the section after that. How large the second risk actually is depends on which data the link really has in its possession or under its control. The EU region has settled the storage location. The chain has settled which legal systems have a say.&lt;/p&gt;

&lt;p&gt;Three things come on top with every region, because they do not hang on the storage location. Support access, which the provider needs to operate the service, happens from wherever its staff sit. Metadata, meaning account data, invoices and access logs, runs in the provider's systems and not in the chosen region. And backups sit wherever the provider replicates them, at Vercel explicitly worldwide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Transfer Mechanism: Three Attempts
&lt;/h2&gt;

&lt;p&gt;The first of the two risks concerns the basis on which data may leave the EU at all. The GDPR allows transfers to a country outside the EU only under the conditions of its Chapter V. The yardstick behind it is that the data keeps a level of protection there that is essentially equivalent to the European one, and there are three routes to that. The first is an adequacy decision by the European Commission under Article 45, with which the Commission finds across the board that a country offers that level. The second is appropriate safeguards under Article 46, in practice almost always the Commission's standard contractual clauses, with which the recipient contractually commits to European rules, more rarely binding corporate rules within a group of companies. The third is a set of narrowly drawn exceptions under Article 49, such as explicit consent for a single transfer, which are no use for a running service.&lt;/p&gt;

&lt;p&gt;For the United States the Commission has taken the first route three times, and twice the Court of Justice has struck the decision down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe Harbor, the 2000 decision:&lt;/strong&gt; US companies could self-certify that they observed certain principles. After the revelations about US surveillance programs in 2013, Max Schrems, then a law student in Vienna, lodged a complaint against Facebook with the Irish data protection authority because his data was going to the United States. The complaint reached the Court of Justice via the Irish High Court, and the Court declared the decision invalid on &lt;a href="https://curia.europa.eu/jcms/upload/docs/application/pdf/2015-10/cp150117en.pdf" rel="noopener noreferrer"&gt;6 October 2015&lt;/a&gt;. The reasoning was that US authorities had general access to the content of communications and EU citizens had no legal remedy against it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy Shield, the 2016 decision:&lt;/strong&gt; The successor came with an ombudsperson in the US State Department and further commitments. Again it was a complaint by Schrems, who by then was running data protection cases full time with the organization &lt;a href="https://noyb.eu/en" rel="noopener noreferrer"&gt;noyb&lt;/a&gt;, and both judgments therefore carry his name, Schrems I and Schrems II. The Court of Justice struck the decision down on &lt;a href="https://curia.europa.eu/jcms/upload/docs/application/pdf/2020-07/cp200091en.pdf" rel="noopener noreferrer"&gt;16 July 2020&lt;/a&gt;, on the same grounds in updated form. The surveillance powers under Section 702 of the Foreign Intelligence Surveillance Act (FISA), the US law on foreign intelligence, and Executive Order 12333, a presidential order issued without Congress, were not limited to what is necessary, EU citizens could not challenge them before any US court, and the ombudsperson was neither independent nor able to bind the intelligence services. In the same judgment the Court made clear that the standard contractual clauses remain valid, but that whoever exports data must assess for the specific transfer whether the law and practice of the receiving country guarantee the level of protection, and must provide additional measures where needed. That assessment is usually called a transfer impact assessment, and it applies then as now. Between 2020 and 2023 it affected almost every transfer to the United States, and since the third attempt only those that do not rely on it, as in the Supabase case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Privacy Framework, the 2023 decision:&lt;/strong&gt; The third attempt rests on an executive order of the US President from October 2022 that lays down proportionality principles for the intelligence services and sets up a review court, the Data Protection Review Court, before which EU citizens can bring complaints against surveillance measures. On that basis the Commission adopted the &lt;a href="https://eur-lex.europa.eu/eli/dec_impl/2023/1795/oj" rel="noopener noreferrer"&gt;adequacy decision&lt;/a&gt; on 10 July 2023. US companies self-certify again, and the US Department of Commerce keeps the list. Vercel says it is on it. Supabase does not mention the framework in its contract, its security page or its privacy policy and relies on standard contractual clauses.&lt;/p&gt;

&lt;p&gt;The decision is under attack. The French member of parliament Philippe Latombe challenged it directly before the General Court of the European Union, and the General Court dismissed the action on &lt;a href="https://curia.europa.eu/jcms/upload/docs/application/pdf/2025-09/cp250106en.pdf" rel="noopener noreferrer"&gt;3 September 2025&lt;/a&gt;. It found that the United States ensured an adequate level of protection at the time of the decision, and it expressly tied that finding to that point in time. On 31 October 2025 Latombe lodged an &lt;a href="https://www.wilmerhale.com/en/insights/blogs/wilmerhale-privacy-and-cybersecurity-law/20251201-european-court-of-justice-to-review-challenge-to-eu-us-data-privacy-framework" rel="noopener noreferrer"&gt;appeal&lt;/a&gt; with the Court of Justice, case number C-703/25 P, on four grounds. On 4 June 2026 the Court admitted Microsoft as an intervener in support of the Commission. As of 9 September 2026 no hearing date is known, and the decision stands.&lt;/p&gt;

&lt;p&gt;Two things are worth keeping in view. First, the framework rests on an executive order, not on a statute, and an executive order can be changed by the next president. Second, the US oversight body that is supposed to monitor compliance with the commitments, the Privacy and Civil Liberties Oversight Board, has been without a quorum since three of its members were dismissed in January 2025. Its own &lt;a href="https://www.pclob.gov/Board" rel="noopener noreferrer"&gt;members page&lt;/a&gt; lists a single serving member on 9 September 2026. The dismissals are themselves the subject of litigation: A federal court in Washington declared them unlawful in May 2025, and the appeals court stayed that ruling in &lt;a href="https://media.cadc.uscourts.gov/orders/docs/2025/07/25-5197LDSN2.pdf" rel="noopener noreferrer"&gt;July 2025&lt;/a&gt; for the duration of the proceedings. Whether and how that enters the judicial review of the framework is open. For the user both mean that the basis of their transfer depends on political decisions in another country.&lt;/p&gt;

&lt;p&gt;If this decision is struck down too, what happened in 2020 happens again. Whoever relied on the framework alone is left without a basis and has to switch to standard contractual clauses, including their own assessment of US law. Whoever documented both from the start, the provider's list entry and the standard contractual clauses in the data processing agreement together with the assessment of US law, has less to do on that day. That is exactly why the checklist below carries them as two separate items.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CLOUD Act: Possession or Control, Not Storage Location
&lt;/h2&gt;

&lt;p&gt;The second risk, the CLOUD Act, is independent of the first, and it survives every adequacy decision. Its origin is a dispute over emails. In 2013 US investigators demanded from Microsoft, by warrant, a mailbox that sat in a data center in Dublin. Microsoft refused, arguing that a US warrant did not reach as far as Ireland. An appeals court sided with Microsoft in 2016, the Supreme Court took the case, and before it could decide, Congress passed a law that answered the question. The Clarifying Lawful Overseas Use of Data Act, CLOUD Act for short, was signed on 23 March 2018.&lt;/p&gt;

&lt;p&gt;The core rule is in &lt;a href="https://www.law.cornell.edu/uscode/text/18/2713" rel="noopener noreferrer"&gt;18 U.S.C. § 2713&lt;/a&gt;. A provider must in principle hand over content and subscriber records that are "within such provider's possession, custody, or control", and that "regardless of whether such communication, record, or other information is located within or outside of the United States". In one sentence: What counts is whether the provider possesses or controls the data, not where it is stored. A data center in Frankfurt is no argument against this rule.&lt;/p&gt;

&lt;p&gt;The law provides a special objection procedure for the case that disclosure would violate the law of another state, but only under two conditions at once. The customer concerned must not be a US person, meaning neither a US citizen nor resident in the United States, and the United States must have concluded an agreement under this law with the country where the data is stored. Such agreements exist so far with the United Kingdom, in force since October 2022, and with &lt;a href="https://www.homeaffairs.gov.au/about-us/our-portfolios/national-security/lawful-access-telecommunications/australia-united-states-cloud-act-agreement" rel="noopener noreferrer"&gt;Australia&lt;/a&gt;, in force since January 2024. With the EU or any of its member states there is none. For data in Frankfurt the second condition is missing, and with it this route. What remains is the general possibility of challenging a demand as unlawful, as Microsoft did in the Ireland case. That presupposes a provider that does so.&lt;/p&gt;

&lt;p&gt;On the European side stands Article 48 of the GDPR. It recognizes decisions by authorities of a third country only if they rest on an international agreement, such as a mutual legal assistance treaty in which states promise each other support in investigations. A US provider with EU customers thus stands between two legal systems that demand opposite things. The data processing agreement with the customer does not resolve that conflict, because a contract between two companies does not override a US law. What the contract can deliver is a commitment to inform the customer and to contest demands for disclosure as far as the law allows.&lt;/p&gt;

&lt;p&gt;What that sounds like in practice was shown by Microsoft France before a committee of inquiry of the French Senate in June 2025. Anton Carniaux, in charge of legal and public affairs there, was asked whether he could guarantee that data of French citizens would never be handed to US authorities without France's consent. His answer is in the &lt;a href="https://www.senat.fr/compte-rendu-commissions/20250609/ce_commande_publique.html" rel="noopener noreferrer"&gt;minutes&lt;/a&gt; of the session of 10 June 2025: "Non, je ne peux pas le garantir, mais, encore une fois, cela ne s'est encore jamais produit." No, he could not guarantee it, but it had never happened so far either. And on the question of the obligation: "Lorsque nous sommes obligés de les donner, nous les donnons." When Microsoft is obliged to hand data over, it hands it over. The two sentences belong together. The first names the risk and how often it has materialized so far, the second the rule behind it: disclosure after the provider's own check of whether the demand is lawful, not before. That is not a statement about other providers, but it is one about the limit of what any provider can promise.&lt;/p&gt;

&lt;p&gt;That leads to proportionality. The probability that US authorities take an interest in the user table of an ETL tool with a handful of users is low. The risk is structural, not practical. It consists in the user writing a promise into their privacy policy that they cannot keep, because their provider cannot keep it. Whoever understands that and accepts it consciously acts differently from someone who does not know it. That difference is what the last item of the checklist is about.&lt;/p&gt;

&lt;p&gt;A question that comes up regularly here concerns the European subsidiaries of US groups. If the contracting party is called AWS Europe in Luxembourg or Microsoft Ireland, does that help? The seat of the contracting party alone does not answer the question. The rule turns on possession, custody or control, and whether a company within a group controls the data in that sense is a matter of actual access, not of the org chart. The Microsoft Ireland case was the trigger for the law because it concerned data held by a European subsidiary, but no blanket rule follows from it that every EU subsidiary automatically holds data under the control of its US parent. What does follow is the duty to check which company actually gets to the data. That is exactly what makes the Supabase case above so instructive. There it is the other way around: The contracting party sits outside the United States, and the US company is in the chain regardless, with support access.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Checklist
&lt;/h2&gt;

&lt;p&gt;What a user has to examine and record for the third-country question at Vercel, Supabase or a comparable service can be worked through in seven items. The list does not create compliance and does not replace an assessment of the processing as a whole. It does not remove the two risks, it makes them visible and documents the decision to carry them. For an operator who gets asked, that is the difference between an answer and a shrug.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the subprocessor list:&lt;/strong&gt; Mark every company subject to US jurisdiction (host, parent company, support), and record for each link which data it processes or can view. Only for that data does the CLOUD Act question arise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign the data processing agreement:&lt;/strong&gt; Conclude the contract under Art. 28 GDPR (DPA) and file it. Check whether it contains standard contractual clauses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose the region:&lt;/strong&gt; Do not accept the default. At Vercel set the function region, at Supabase the project region. Record what the region covers and what it does not (CDN, backups, support, metadata).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document the transfer mechanism, both separately:&lt;/strong&gt; The provider's active entry under the Data Privacy Framework (look it up yourself on dataprivacyframework.gov, including its scope) and the standard contractual clauses from the contract together with your own assessment of US law (transfer impact assessment). If the framework is struck down, the second basis carries on, but only with that assessment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extend the privacy policy:&lt;/strong&gt; Recipient with company name and seat, third country, transfer mechanism, and a note on possible government access under the law of the third country.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscribe to changes of the subprocessor list:&lt;/strong&gt; Both providers announce changes, and the list changes several times a year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide the residual risk consciously and record it with a date:&lt;/strong&gt; Which data sits with the provider, how serious would access be, and why is the operator prepared to carry that.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The provider information in this article is current as of 9 September 2026. The lists change, and what counts is always the current version at the provider. Item 1 comes first because it determines the others. Whoever reads the list knows for whom to document item 4 and what to write in item 5. Item 7 is the one most people leave out. It is the only one that demands a decision rather than a document, and it records the actual risk decision that stands behind the other six items.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Alternative: EU Provider or Self-Hosting
&lt;/h2&gt;

&lt;p&gt;Anyone who wants to rule the two risks out rather than carry them has only one route, no provider under US jurisdiction in the chain. Anyone who can carry them has the checklist. In practice the one route means either an EU provider that runs its own data center, or a server of your own with such a provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What an EU provider solves:&lt;/strong&gt; With a provider based in the EU, with its own infrastructure and with no third-country subcontractors in the chain, examples for the first two conditions being Hetzner in Germany, IONOS in Germany and OVHcloud in France, without rating the products, the third-country transfer can drop away for the infrastructure layer, provided support access and the remaining data flows also stay in the EU. Then there is no adequacy decision to check, no standard contractual clauses for the host and no CLOUD Act for the data center. The data processing agreement remains mandatory, and so does the look at the subprocessor list, because an EU provider can use US service providers. Reading the list is item 1 here too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What self-hosting costs:&lt;/strong&gt; The price is not primarily money but operations. Whoever runs an application with a login on their own server takes on the reverse proxy, which passes requests from the internet on to the application, the certificates, the identity provider, which manages login and users, the database, the backups and the monthly updates across every layer. In the DI² project that was the decision, and the sibling article &lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner&lt;/a&gt; describes what it cost, from the RAM budget through secrets in two places to the monthly bill. On your own server in the EU the third-country question does not arise for the infrastructure. That is the real difference between the two routes, not the price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision aid:&lt;/strong&gt; Three questions suffice for most cases. First, which data sits with the provider, only account data or also what users enter into the application? The closer to health, financial or employee data, the heavier item 7 of the checklist weighs. Second, who will ask, a customer with a data protection department of their own, a supervisory authority, nobody? Whoever has business customers in the EU will get the question. Third, who carries operations, and what happens when that person is out for two weeks? A managed service answers the third question better than a server of your own, and that is a legitimate reason for Vercel or Supabase. It should just be taken together with the answers to the first two questions and written down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An EU region answers where the data lives. It does not answer whose law applies to the companies that process it. That answer is in the subprocessor list.&lt;/li&gt;
&lt;li&gt;Vercel is a US company with a certification under the Data Privacy Framework, a US default for functions and an explicit reservation to transfer data to the United States and to any other processing location. Supabase's contracting party sits in Singapore, the US company Supabase, Inc. is a subcontractor, and hosting sits with AWS. In both chains there are companies under US jurisdiction.&lt;/li&gt;
&lt;li&gt;The transfer mechanism for the United States, a decision of the European Commission, has been struck down twice by the Court of Justice, and the third is under review right now. As of 9 September 2026 the Data Privacy Framework stands, and appeal C-703/25 P is pending. Whoever documents the list entry and the standard contractual clauses separately has less to do on the day of a judgment.&lt;/li&gt;
&lt;li&gt;The CLOUD Act in principle obliges providers under US jurisdiction to hand over data in their possession or under their control, regardless of storage location. The law's special objection procedure presupposes an agreement that does not exist with the EU. A data processing agreement changes none of that.&lt;/li&gt;
&lt;li&gt;The checklist makes both risks visible and documents the decision to carry them. It does not remove them. Whoever does not want to carry them needs an EU provider or a server of their own in the EU, and both cost operations rather than money.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Vercel or Supabase GDPR-compliant if I pick the Frankfurt region?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not by default, the region only settles the storage location. At Vercel the documented reservation remains, to transfer data to the United States and to any other processing location, and at Supabase the US companies remain in the subprocessor list, above all the host AWS and Supabase, Inc. for support. Both are third-country transfers that need a mechanism, a data processing agreement and a note in the privacy policy. The region is item 3 of the checklist, not the checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the Data Privacy Framework still valid in September 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The European Commission's adequacy decision of 10 July 2023 stands, and the General Court of the European Union dismissed the first action against it on 3 September 2025. Appeal C-703/25 P has been pending before the Court of Justice since 31 October 2025, Microsoft has been admitted as an intervener in support of the Commission since 4 June 2026, and as of 9 September 2026 no hearing date is known. To check the current state, search for the case number on curia.europa.eu.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the Court of Justice strikes down the Data Privacy Framework too?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same as in July 2020 with Privacy Shield. Transfers that rest on the framework alone lose their basis from one day to the next. Whoever has additionally agreed standard contractual clauses in the data processing agreement relies on them from that day on and has to carry out the assessment of US law themselves, as every company did between 2020 and 2023. That is why the checklist documents both mechanisms separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the CLOUD Act apply to AWS, Azure or Google Cloud data in Germany?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Storage in Germany changes nothing if the provider is subject to US jurisdiction. The rule turns on possession, custody or control, so the question is which company within the group actually gets to the data. A contracting party based in Luxembourg or Dublin does not answer that by itself, but a US parent in the org chart does not automatically answer it the other way either. Whether and how a group contests a disclosure in a given case is a matter of its contract and its practice, not of the subsidiary's seat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to care about this for a hobby project with five users?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The obligations apply regardless of size as soon as personal data is processed, and a login table with email addresses is enough for that. The risk of government access is structural, not practical, with five users. What remains is the duty not to promise anything in the privacy policy that the provider cannot keep. The checklist takes an afternoon for a small project, and item 7 is then an honest line instead of an empty promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sibling article:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner — How I Host a Next.js App with Self-Hosted Keycloak&lt;/a&gt; — the route without a US provider in the chain, with the setup, the decisions and the cost side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hub of this branch:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/agentic-coding-experience/" rel="noopener noreferrer"&gt;Agentic Coding from a User's Perspective — Experience: The Work Doesn't Disappear, It Shifts&lt;/a&gt; — the experience level that the infrastructure decisions belong to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cluster hub:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code — Rules, Skills and Agents That Enforce Conventions&lt;/a&gt; — the enforcement system behind the project.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Agentic Coding from a User's Perspective — Experience: The Work Doesn't Disappear, It Shifts</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Wed, 09 Sep 2026 15:46:16 +0000</pubDate>
      <link>https://dev.to/marcus1968/agentic-coding-from-a-users-perspective-experience-the-work-doesnt-disappear-it-shifts-4l09</link>
      <guid>https://dev.to/marcus1968/agentic-coding-from-a-users-perspective-experience-the-work-doesnt-disappear-it-shifts-4l09</guid>
      <description>&lt;p&gt;Since spring 2026, two projects have been taking shape with the same coding agent: an ETL generator on Next.js and PostgreSQL, and this blog. The agent does not complete lines, it works through assignments in the repository. This article collects my agentic coding experience from a user's perspective. I build databases, ETL processes and the framework behind them, and I use the agent as a tool without doing AI research or building AI tools myself.&lt;/p&gt;

&lt;p&gt;Is it worth it? Yes. I spent years building client-server applications in VB.NET and C#, and I am now building a TypeScript application without knowing TypeScript. That is the visible gain. The less visible part is that the work has shifted: from programming to structuring. Rules and skills for the agent, feature specs, bug files, test cases, documentation. In my working life, other people used to deliver that work, a project lead for example, and I consumed it as a developer. I underestimated how much of it there is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is it worth it? Yes:&lt;/strong&gt; A TypeScript application with frontend, tests and CI, built by someone who does not know TypeScript but knows databases, ETL and .NET.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The work is not gone, it has shifted:&lt;/strong&gt; Rules, skills, specs, bug files, test cases, documentation. Project leads and teams used to deliver that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The guidance lives in the repo, not in the prompt:&lt;/strong&gt; The prompts are short because rule files and specs carry the work. That includes intermediate states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The infrastructure was the biggest hurdle:&lt;/strong&gt; one server with four environments, self-hosted identity, containers, TLS, and no cookie banner because the architecture does not need one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control is a loop:&lt;/strong&gt; An agent extends the radius of what I can build, not the radius of what I can judge. Whatever I cannot judge therefore goes through recurring checks that the agent runs. A green signal is not proof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This article is itself an example:&lt;/strong&gt; The draft comes from the agent. Selection, corrections and publication are my work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; none. Anyone who wants to go deeper will find the way into this blog's technical articles at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Agentic Coding Means Here
&lt;/h2&gt;

&lt;p&gt;A coding agent is a language model that, with tool access, executes several steps in a row on its own inside a repository: reading files, running commands, changing code, running tests, committing. The difference to autocompletion is not the quality of individual lines but the mode of work. I formulate an assignment, the agent works through it and reports a result.&lt;/p&gt;

&lt;p&gt;In both projects the tool is Claude Code, in the terminal and in VS Code. The observations do not depend on the product, though. They depend on an agent having access to repository, shell and database, and on a human deciding what happens with it. The mechanics behind rule files, skills and sub-agents are covered by the hub article &lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code&lt;/a&gt;. This article is about what they changed in my daily work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study 1: A TypeScript Application Without TypeScript Skills
&lt;/h2&gt;

&lt;p&gt;The app project is called DI² and is an ETL generator on Next.js and PostgreSQL. The database layer consists of more than 150 DDL files, the repository is private, and I work on it alone. On the ETL side, the app's tools build on a framework of my own that I am publishing on GitHub step by step.&lt;/p&gt;

&lt;h3&gt;
  
  
  From .NET to TypeScript
&lt;/h3&gt;

&lt;p&gt;What I bring is data models, SQL, ETL processes and years of client-server applications in VB.NET and C#. I know how an application with database, business logic and user interface is cut, where state lives and where errors originate. What I do not bring is the language. TypeScript, the React ecosystem and the test tooling of the web world were new to me, and to this day I cannot read a file of this application the way I read a stored procedure.&lt;/p&gt;

&lt;p&gt;The frontend, the CI pipeline and the test infrastructure came into being anyway. The clearest example is the 241-line Node script that checks the project's SQL conventions and that I could not have written. My role was a different one: I knew the conventions, I could judge a finding as right or wrong, and I made the framing decisions. Acceptance ran through runs, not through reading the code, as the &lt;a href="https://sql.marcus-belz.de/en/sqlfluff-cant-lint-sql-conventions/" rel="noopener noreferrer"&gt;article about the guard&lt;/a&gt; shows. An agent extends the radius of what I can build. It does not extend the radius of what I can judge. Where the two diverge, acceptance needs a criterion that can be checked without understanding the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conventions in the Repo Instead of in My Head
&lt;/h3&gt;

&lt;p&gt;In a classic solo project, the conventions live in the developer's head. With an agent that starts every session without the history of the previous one, that does not work. In the DI² project this turned into &lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;27 rule files&lt;/a&gt;, 28 by now, one per convention, each with its reasoning. Features and bugs get their own files with a running ID before a single line of code exists. How such a rule set is derived from an existing codebase is described in the &lt;a href="https://sql.marcus-belz.de/en/deriving-sql-conventions-with-claude-code/" rel="noopener noreferrer"&gt;generate-refine-derive loop&lt;/a&gt;. What it costs when rules alone are not enough is shown by &lt;a href="https://sql.marcus-belz.de/en/ai-code-drift-799-font-sizes/" rel="noopener noreferrer"&gt;799 hardcoded font sizes&lt;/a&gt; despite an unambiguous design rule. What these rule files cost at the start of every session, and how that can be measured and brought down, is described in &lt;a href="https://sql.marcus-belz.de/en/context-window-full-before-first-prompt/" rel="noopener noreferrer"&gt;The Context Window Is Full Before the First Prompt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The second half of this rule I only noticed when it failed. I accidentally ended a session in the middle of a bug fix and asked a new one to pick up where the old one had left off. The agent found no remains, neither in the working tree nor in the stash. That getting back in still took only minutes was down to the bug file in the repository: error description, database evidence, reproduction steps and three named hypotheses with a checking order. The intermediate state lives in the repo, or it does not live at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Infrastructure Was the Biggest Hurdle
&lt;/h3&gt;

&lt;p&gt;One part of the project that cost me a lot of time is not a line of application code but the infrastructure underneath: a single VPS at Hetzner with four environments, a self-hosted Keycloak as identity provider, an nginx in front of it, a good dozen containers, and a VPS provider that blocks outbound mail ports. I did not write a single line of it. What I decided was that there would be one server instead of four, a self-hosted identity instead of a cloud service, and a location in the EU, and behind these decisions is a design goal I cared about in particular: the application should get by without a cookie banner. What remains are four cookies that are permitted without consent: the login's session token and CSRF token, a language cookie for the public pages, and the state of the navigation. The setup, the decisions behind it and the cost side are described in &lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading Is No Longer a Control
&lt;/h3&gt;

&lt;p&gt;Two things together changed the way I work. The first is the language: Whoever cannot read the language of their code cannot control it by reading. The second is the volume. The agent writes so many lines in so little time that reading and understanding all of it would not be possible even if I knew the language. Reading my own code in full, the way I knew it from my .NET projects, therefore drops out as a control. That is not an admission of failure but an unavoidable change in the way of working. If reading drops out as a control, something else has to take its place.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Control Loop
&lt;/h3&gt;

&lt;p&gt;The control I cannot provide myself has to happen somewhere else: in a loop of checks that the agent runs, but whose yardstick is not its own judgment but rules, tests and audit reports. It does not run once but repeatedly. After every piece of feedback, a rule is sharpened. Recurring checks have skills, that is, versioned prompts in the repository, among them one for security audits and one for update checks. I had unit tests pushed under existing code after the fact. The foundation for that only came in July 2026, and by now there are more than 700 tests. Since the end of August there is a catalog of manual test cases with acceptance criteria. All in all, 14 skills and 28 rule files carry this loop. How that loop runs in detail, what evidence there is for it and where it has failed is described in &lt;a href="https://sql.marcus-belz.de/en/verify-ai-generated-code/" rel="noopener noreferrer"&gt;The Control Loop&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It has found things I would not have found by reading. A rate-limiting rule in the web server had been pointing at an endpoint that no longer existed since the identity provider was switched, and for months it looked like protection. Another case started with a vague sentence from me that the page was barely responding. The agent compared all four environments, found production affected as well although it did not even have the new version, and discovered 2.9 GB in swap. The identity service was sitting on disk, hence 9.6 seconds for the first login and 0.19 seconds for every subsequent one. No application code was involved. Both cases are told in detail in &lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One lesson matters more to me than any number: A success signal is not proof. A test suite was green for four weeks while three fix attempts went right past a scroll bug, because the test had stubbed exactly the value whose wrongness was the bug. An audit report said "passed", and only my follow-up question revealed that the decisive click path had never been executed. Both signals attest that a process ran, not that an effect occurred. Since then, verifying means finding a second, independent route to the same question. How to write test cases that deliver that is covered in the article &lt;a href="https://sql.marcus-belz.de/en/how-to-write-manual-test-cases/" rel="noopener noreferrer"&gt;How to Write Manual Test Cases&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study 2: Writing a Blog with an Agent
&lt;/h2&gt;

&lt;p&gt;This blog has been run editorially with the same tool since May 2026. Back then the articles were moved from WordPress into a repository, one folder per topic with a German and an English version. Today there are 65 topics, 55 of them live in both languages. What the agent does here differs less from the app project than I would have expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spec, Rounds, Revision Log
&lt;/h3&gt;

&lt;p&gt;Before every article comes a spec: role in the topic cluster, target audience, search intent, outline, boundary to neighboring articles. Then the article runs through fixed rounds, from the critique of the existing text through structure and style to the English version. Every round gets an entry in a revision log and its own commit. Nine rule files carry the conventions, among them style heuristics against typical markers of AI-written text: no semicolon in the prose, at most one dash per paragraph, no first-person plural. Every one of these heuristics came out of a piece of feedback from me. The agent writes drafts, I correct, and the correction becomes the rule for the next article.&lt;/p&gt;

&lt;p&gt;The way onto the live site remains manual. I copy the text into the WordPress block editor, set images and SEO fields, and publish. Afterwards the agent verifies remotely via live HTML and REST API. For the article on the &lt;a href="https://sql.marcus-belz.de/en/functional-design-aesthetics-of-sql/" rel="noopener noreferrer"&gt;functional aesthetics of SQL&lt;/a&gt; it found four defects across the two language versions that way, defects invisible in the repository because they only arise in the editor: an image that had been pointing at another article's file for months, an SEO title that lost its last letter on paste, a jump link that led nowhere because of an old HTML anchor, and, in the English version, a link to the article on SSMS editor options that pointed at the German instead of the English version. I corrected that link twice in the block editor, and twice it stayed wrong. The agent read the post's modification timestamp via the REST API: It had not moved on either attempt, so my save clicks had not been saves at all. The cause was the editor's link dialog. Whoever types in the new URL and then clicks outside the dialog instead of confirming it with Enter or "Apply" discards the change, even if they press "Update" afterwards. On the third attempt the link held. The finding has been a note in the rule file for live verification ever since.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Review Comes from a Fresh Session
&lt;/h3&gt;

&lt;p&gt;A language model can also evaluate a text, and I have used that from the start. The methodology, however, I had to develop myself. While reviewing an article I noticed that a model anchors on its earlier verdicts within the same chat session: first rating about 8 out of 10, after several revisions in the same session 9.7, in a fresh session clearly more critical again. The high scores were not quality but history. Since then every review runs in a fresh session of a model from a different vendor, with a frozen standard prompt. The score is a diagnosis, not a goal.&lt;/p&gt;

&lt;p&gt;I hand the result back to the agent as a raw paste, and it sorts it into three categories: adopt, matter of taste, already addressed. Experience from more than twenty reviews: 30 to 50 percent of the points are valid. The review is deliberately context-free, and precisely for that reason it needs as its counterpart the agent with repository context, which can look up in the revision log that a suggestion was already rejected months ago. A review is a finder, not proof. When one flagged a statement as "too absolute", the agent's cross-check showed it was not too absolute but wrong. What settled it in the end was a test against running database containers.&lt;/p&gt;

&lt;p&gt;Fresh, though, means more than having no history. A new session of the same model that wrote the text would have no prior context, but it would bring the same blind spots: the patterns I read as markers of AI-written prose are more likely caught by a model from a different vendor than by one from the same family. And a session inside my development environment sees the repository, loads the rules automatically and can look up in the log what the template deliberately leaves out. So the counted review stays with the outside model and the same template for every article, otherwise the scores cannot be compared. A fresh session of my own model has a different place: as a rule check that holds the text against the blog’s style heuristics and conventions. That is a different question from “is the text good”, and it needs exactly the context the reviewer must not have. These review experiences will get an article of their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Generalizes
&lt;/h2&gt;

&lt;p&gt;The agentic coding experience in both projects produced the same patterns. Four of them carry my daily work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The guidance lives in the repo, not in the prompt.&lt;/strong&gt; Rule files, specs, bug files and revision logs are the actual work. The prompts only name the entry point and the assignment. The intermediate state belongs there too: What an agent has worked out is recorded promptly as a commit or a note in the feature or bug file. Language models live on text. What is not written down does not exist for the next session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The structuring work is the price of the gain in radius.&lt;/strong&gt; I get an application I would not have built alone and a blog I would not sustain alone. In return I take on roles that are spread across a team: writing requirements, formulating acceptance criteria, declaring test cases, maintaining documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The repo learns, not the agent.&lt;/strong&gt; Every session starts without the history of the previous one. What was learned from a mistake exists for the next session only if it is written down: as a rule or a checkpoint in the repository. That also works backwards: Many defects I correct in old articles today were not violations when they were written, because the rule only came afterwards. The existing body ages relative to the rules that were learned from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning remains mandatory.&lt;/strong&gt; This blog holds the position that &lt;a href="https://sql.marcus-belz.de/en/functional-design-aesthetics-of-sql/#formatting-is-learning" rel="noopener noreferrer"&gt;formatting is learning&lt;/a&gt;: Whoever indents a statement by hand builds the mental model of the data. For agentic coding that applies with more force. Whoever does nothing but sign off loses, over time, the model they need for signing off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Gets Rough: Limits from a User's Perspective
&lt;/h2&gt;

&lt;p&gt;The limits are the part of my agentic coding experience that has occupied me the most.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Drift Despite Rules
&lt;/h3&gt;

&lt;p&gt;A rule with a stated reason improves the hit rate but does not guarantee compliance, and at high volume every residual rate turns into measurable drift. The frontend case with 799 hardcoded font sizes is the evidence, and the answer was not a better rule but a check that does not get tired. The blog's editorial work knows the same effect: Despite the style heuristics, every first draft from the agent contains semicolons or compressed notation, and I recognize AI-written text by exactly these patterns. What is in the rules still has to be checked, only the checking moves from reading to counting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collateral Damage in the Working Tree and on My Machine
&lt;/h3&gt;

&lt;p&gt;As soon as more than one session works in the same repository, or one session works alongside me, errors arise that do not exist in single operation: someone else's work in my own commit, deleted files of a parallel session, numbers assigned twice, and once 220 infinite loops that froze my computer for twelve minutes. Against the shared index, only &lt;code&gt;git commit --only&lt;/code&gt; with explicit paths turned out to be reliable, and the lesson from the first case would not have prevented the second. The five cases and what holds against them are in &lt;a href="https://sql.marcus-belz.de/en/parallel-claude-code-sessions-same-repo/" rel="noopener noreferrer"&gt;Two Agents, One Working Tree&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  You Can Only Sign Off on What You Can Judge
&lt;/h3&gt;

&lt;p&gt;Review effort is the new bottleneck. The answer is not to read faster but to shift acceptance: to runs instead of reading, to criteria instead of impression, to external reviews instead of my own goodwill. Each of these shifts presupposes that I can formulate the criteria and recognize a result as right or wrong. With the Node guard I could, because I knew the SQL conventions. With a script whose domain logic I could not judge, the same division of labor would be a risk.&lt;/p&gt;

&lt;p&gt;Added to that is a weakness that belongs to the tool's strength. An agent usually tests hypotheses systematically with measurements, and precisely because of that it hardly shows on the occasions when it builds a coherent story from incomplete clues instead. During one troubleshooting session the agent formulated a plausible cause as if it had been measured. It had not been. Since then I ask of every finding which part of it was measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working Prompts as Examples
&lt;/h2&gt;

&lt;p&gt;The prompts in both projects are short because the guidance lives in the repository. Three examples show what a prompt still has to do then.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting a revision series.&lt;/strong&gt; Verbatim apart from the translation from German, only the internal identifiers are replaced by their meaning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let's start the triage of the legacy articles. all relevant changes in the
articles and the lessons from revising them go into the article about agentic
coding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines are enough because the spec, the work list and the rule files exist in versioned form. The agent read them, ran the first quality pass and established the material collection for this article as a side duty of the series.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Having an article reviewed externally.&lt;/strong&gt; This prompt goes to ChatGPT in a fresh session, that is, to a model from a different vendor. It is versioned and has been frozen since August 2026. The original is German, shown here in translation and shortened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a critical, independent technical editor for a technical blog about
SQL Server, Postgres, data quality and ETL. You know neither the author nor
earlier versions of the text, and you have no reason for leniency.

Rate the article in four dimensions with a score from 1 to 10 each:
technical correctness, structure and readability, SEO fitness, language and style.

Duties: Every point of criticism quotes its location verbatim. For every point
a concrete suggestion for improvement. No courtesy points. At the end, an overall
score and the five most important points, prioritized by impact.

--- ARTICLE ---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important sentence is the second one. Without it, the model rates the history instead of the text. The duty to quote the location verbatim makes the result sortable, because a point without a location can neither be implemented nor rejected with reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A recurring check as a skill.&lt;/strong&gt; A skill is a versioned prompt that I call with a slash command. This is how the description of the skill that checks for updates begins, verbatim apart from the translation and the replaced file paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checks updates for dev dependencies (npm, Docker image declarations, GitHub
Actions) AND for the components that actually run on the VPS (OS packages,
Docker Engine, nginx, running container tags, Certbot). Writes findings to the
update documentation and to a dated security report. Keeps a history of
applied/deferred updates. Modifies nothing on its own.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill defines what is checked, where the result goes and what the agent is not allowed to do. For the server side it produces a checklist of read-only commands that I run myself. The acceptance criterion is the history, which shows me at the next run what was left undone.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Article Came About
&lt;/h2&gt;

&lt;p&gt;This article is about transparency, so it has to deliver it itself. The text is a draft by the agent, produced from a spec and a material file that agent sessions kept over six weeks. Into this file, whichever session was working wrote down, on my instruction, what happened during the work, its own mistakes included. The first version was twice as long and written in the third person. That it became shorter, stands in the first person and mentions the infrastructure at all is the result of a discussion in which I asked the agent to listen first before rewriting.&lt;/p&gt;

&lt;p&gt;My role is the same as in both case studies. I decided that this article exists and what it carries. I read the draft, discard, cut, correct and approve. I have it rated in a fresh session of another model, transfer it into WordPress by hand and check the links. What I do not do is write the text myself. And what I cannot do is in the text: write a Node script, see an effect in swap, retrieve a data point from a lost session. I had no prior experience of what this way of working would bring. What I underestimated was not the tool but the share of rules, specs, test cases and documentation that I had previously received from others.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can you tell that a text was produced with AI assistance?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Often, yes. Such texts give themselves away through recurring patterns. In German, in my experience, these are above all semicolons in the prose, verbless afterthoughts behind a dash, parentheses into which a whole piece of information is packed, and abstract terms that appear as the subject of a sentence and act there like a person. I have recorded these patterns in a rule file, and every draft is read against it before publication. Whether you can tell how a text came about therefore depends less on the model than on the correction pass that a human makes with a concrete list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you build a TypeScript application without TypeScript skills?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, under conditions. You need a model of how an application is cut, where state lives and where errors originate. In my case that came from years with .NET and databases. You need acceptance criteria that can be checked without reading the code: runs, tests, audits. And the infrastructure underneath remains a hurdle of its own that no programming language makes smaller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you control what you cannot judge yourself?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not by reading, but through a loop of checks that work without understanding the code: rules that are sharpened after every piece of feedback, skills for recurring checks, tests that have to be red on the old version, and audits with a report. The loop does not replace understanding, but it replaces reading. Every verdict has to state its open points as a condition, otherwise a "passed" travels further than its caveat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do you start as a user?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With a single convention that lives in the repository and one assignment that has an acceptance criterion. Not with a large rule set, because that grows out of feedback. Anyone who wants to set up a database project with Claude Code will find a concrete entry point in the article on the &lt;a href="https://sql.marcus-belz.de/en/claude-code-project-with-database-setup/" rel="noopener noreferrer"&gt;open starter kit&lt;/a&gt;, and the mechanics behind it are explained by the &lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;cluster's hub article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the effort worth it for a solo project?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Going by my agentic coding experience in both projects, yes. The time saved on programming is largely eaten up by the structuring work. What remains is a gain in radius: a TypeScript application including infrastructure that I would not have built alone, and a blog with spec, review protocol and revision log that a single person would not sustain without an agent. The condition is that you take on the roles that project leads and teams otherwise carry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hub:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code — Rules, Skills and Agents That Enforce Conventions&lt;/a&gt; — the enforcement system whose user experience this article describes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Spokes of this article:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner — How I Host a Next.js App with Self-Hosted Keycloak&lt;/a&gt; — the setup, the decisions behind it and the cost side that this article only touches on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/parallel-claude-code-sessions-same-repo/" rel="noopener noreferrer"&gt;Two Agents, One Working Tree — What Broke When I Ran Parallel Claude Code Sessions in One Git Repository&lt;/a&gt; — five cases from parallel operation and the countermeasures that hold.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click — Debugging Postgres, Docker and a Swapping Server with a Coding Agent&lt;/a&gt; — four debugging sessions in which measuring replaced guessing, along with the limit of that way of working.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/verify-ai-generated-code/" rel="noopener noreferrer"&gt;The Control Loop — How I Get AI-Generated Code Checked That I Can't Read Myself&lt;/a&gt; — the five stations of the loop, their evidence and their blind spots.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/eu-region-is-not-eu-provider/" rel="noopener noreferrer"&gt;An EU Region Is Not an EU Provider — Vercel, Supabase and the GDPR Third-Country Question&lt;/a&gt; — the second infrastructure spoke: why an EU region settles the storage location but not the provider question, with a checklist.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/context-window-full-before-first-prompt/" rel="noopener noreferrer"&gt;The Context Window Is Full Before the First Prompt — When Project Rules in Claude Code Become a Fixed Cost of Every Session&lt;/a&gt; — the tooling spoke: what the rule files cost at the start of every session, measured before and after, and why the dividing line is rule versus record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure and deploy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/postgres-database-ci-cd/" rel="noopener noreferrer"&gt;Database CI/CD with PostgreSQL&lt;/a&gt; — the lifecycle from object file to deploy.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/github-actions-postgres-deployment/" rel="noopener noreferrer"&gt;GitHub Actions for Postgres Deploys — a Throwaway Database as Quality Gate&lt;/a&gt; — the deploy gate behind the infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/deploy-sql-schema-without-migration-tool/" rel="noopener noreferrer"&gt;Deploying a SQL Schema Without a Migration Tool&lt;/a&gt; — the directory convention of the database layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rules, drift, guard:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;I Gave Claude Code 27 Rule Files Instead of One CLAUDE.md&lt;/a&gt; — structure and upkeep of the rule files.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-code-drift-799-font-sizes/" rel="noopener noreferrer"&gt;AI-Assisted Coding Gave Me 799 Hardcoded Font Sizes&lt;/a&gt; — the measured drift finding.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/sqlfluff-cant-lint-sql-conventions/" rel="noopener noreferrer"&gt;Why sqlfluff Can't Lint Our SQL Conventions — and a 240-Line Script Can&lt;/a&gt; — the guard I could not have written myself.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/deriving-sql-conventions-with-claude-code/" rel="noopener noreferrer"&gt;Deriving SQL Conventions with Claude Code — the Generate-Refine-Derive Loop&lt;/a&gt; — how the rule files come about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Getting started:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-project-with-database-setup/" rel="noopener noreferrer"&gt;Setting Up a Claude Code Project with a Development Workflow and Database — the Open Starter Kit Explained&lt;/a&gt; — the practical beginning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next door:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/functional-design-aesthetics-of-sql/" rel="noopener noreferrer"&gt;The Functional Aesthetics of SQL — Why Structured Code Is Faster to Edit&lt;/a&gt; — the thesis "formatting is learning".&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/is-claude-male-or-female/" rel="noopener noreferrer"&gt;Is Claude a Woman or a Man? — and Why We Ask in the First Place&lt;/a&gt; — the other non-technical question about the tool.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Design Pattern // Logging an ETL Process with T-SQL — How to Capture Run, Component and Action in Evaluable Log Tables</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sat, 05 Sep 2026 10:10:41 +0000</pubDate>
      <link>https://dev.to/marcus1968/design-pattern-logging-an-etl-process-with-t-sql-how-to-capture-run-component-and-action-in-38ei</link>
      <guid>https://dev.to/marcus1968/design-pattern-logging-an-etl-process-with-t-sql-how-to-capture-run-component-and-action-in-38ei</guid>
      <description>&lt;p&gt;An ETL process finishes without an exception — but was everything really loaded that should have been? The mere fact that a process did not abort says nothing about whether it actually did what was expected of it. A readable, evaluable log is what turns a gut feeling into a defensible statement.&lt;/p&gt;

&lt;p&gt;This design pattern logs an ETL run on three levels and answers the questions that success or failure hinge on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How long does the ETL process take overall?&lt;/li&gt;
&lt;li&gt;How long does a single component take — a stored procedure, an SSIS package or another building block?&lt;/li&gt;
&lt;li&gt;How long does a specific SQL statement take?&lt;/li&gt;
&lt;li&gt;How many rows did a SQL statement actually affect?&lt;/li&gt;
&lt;li&gt;And above all: did the process as a whole, a component or a single statement complete successfully?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — what this article covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three-tier logging&lt;/strong&gt; — the tables &lt;code&gt;[LL].[Execution]&lt;/code&gt;, &lt;code&gt;[LL].[Component]&lt;/code&gt; and &lt;code&gt;[LL].[Trace]&lt;/code&gt; capture run, component and action at increasing granularity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stored-procedure toolkit&lt;/strong&gt; — procedures such as &lt;code&gt;[LL].[spInsertTrace]&lt;/code&gt; and &lt;code&gt;[LL].[spUpdateTrace]&lt;/code&gt; write and update the log records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exception handling with &lt;code&gt;[LL].[Error]&lt;/code&gt;&lt;/strong&gt; — a TRY/CATCH pattern ends the run in an orderly way and records every error in an evaluable form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuation of the ETL architecture&lt;/strong&gt; — the example uses schema &lt;code&gt;T2&lt;/code&gt; from the architecture article and adds the logging layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites.&lt;/strong&gt; SQL Server and a basic understanding of stored procedures and TRY/CATCH. This article is part of the ETL design-pattern cluster. As a lead-in, see &lt;a href="https://sql.marcus-belz.de/en/data-quality-in-an-etl-process/" rel="noopener noreferrer"&gt;Data Quality in an ETL Process&lt;/a&gt; and &lt;a href="https://sql.marcus-belz.de/en/design-pattern-the-architecture-of-an-etl-process-how-to-isolate-bad-data-cleanly/" rel="noopener noreferrer"&gt;The Architecture of an ETL Process&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;At its core the approach is straightforward: at the start of every action a log record is written, and once the action finishes it is updated with the outcome &lt;em&gt;success&lt;/em&gt; or &lt;em&gt;failure&lt;/em&gt; — and, where useful, with further information. In principle, that is all there is to it. In practice, a little more is needed after all.&lt;/p&gt;

&lt;p&gt;Even though creating a log should not be a big deal, there is quite a bit to say about it. This article starts with the result and works backwards from there. The process logging presented here is three-tier and uses the following log tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[LL].[Execution]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[LL].[Component]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[LL].[Trace]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tables log an ETL process and its associated components and steps at increasing granularity — read from top to bottom. The ETL process, the components and the individual steps are each logged with exactly one record.&lt;/p&gt;

&lt;p&gt;Closely tied to logging the process is logging errors. The approach presented here uses the following table for that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[LL].[Error]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following figures show the result of three-tier logging for a simple, compact, but complete ETL process.&lt;/p&gt;

&lt;p&gt;In the table &lt;code&gt;[LL].[Trace]&lt;/code&gt; every action is logged with, among other things, the name of the procedure, the target entity the procedure processes, a short description of what was actually done, and further information such as the number of rows affected and the execution time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftrpqn5zsdiu2nhsqmley.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftrpqn5zsdiu2nhsqmley.png" alt="Result set of the [LL].[Trace] table: several trace records, one per action, with columns Component, Entity, Action, AffectedRows, State and Success from an example run." width="798" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Table [LL].[Trace]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the table &lt;code&gt;[LL].[Component]&lt;/code&gt; the calls of procedures and SSIS packages — or, more generally, components — are logged. Here, too, a short description of the component’s task, the target entity and the execution time are recorded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm1aj8xdrvzt4p20seug.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm1aj8xdrvzt4p20seug.png" alt="Result set of the [LL].[Component] table: one record per called component, with Component, Entity, State and Success." width="800" height="73"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Table [LL].[Component]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At the top level, every execution of the ETL process is logged with exactly one record in the table &lt;code&gt;[LL].[Execution]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygp1jx79vvzl7bpzofiu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygp1jx79vvzl7bpzofiu.png" alt="Result set of the [LL].[Execution] table: a single record for the entire ETL run, with Process, Start, End, State and Success." width="797" height="27"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Table [LL].[Execution]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The three process-logging tables — &lt;code&gt;[LL].[Execution]&lt;/code&gt;, &lt;code&gt;[LL].[Component]&lt;/code&gt; and &lt;code&gt;[LL].[Trace]&lt;/code&gt; — contain two columns, &lt;code&gt;[State]&lt;/code&gt; and &lt;code&gt;[Success]&lt;/code&gt;, in which the success or failure of an action, a component or the ETL process is stored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the Derivation…
&lt;/h2&gt;

&lt;p&gt;After this brief preview of the result, the following aspects of the approach need to be clarified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three-Tier Logging&lt;/li&gt;
&lt;li&gt;Table Description, Declaration and Data Model&lt;/li&gt;
&lt;li&gt;ETL Process Execution Status&lt;/li&gt;
&lt;li&gt;Logging Procedures&lt;/li&gt;
&lt;li&gt;Exception Handling&lt;/li&gt;
&lt;li&gt;Example: Logging and Exception Handling in an ETL Process&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Three-Tier Logging
&lt;/h2&gt;

&lt;p&gt;The approach logs an ETL process in the three tables &lt;code&gt;[LL].[Execution]&lt;/code&gt;, &lt;code&gt;[LL].[Component]&lt;/code&gt; and &lt;code&gt;[LL].[Trace]&lt;/code&gt;. Each table is intended for logging specific artifacts. This section introduces how each table is used, its columns and the code to create it.&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[Execution]
&lt;/h3&gt;

&lt;p&gt;This table logs the execution of an ETL process with exactly one record. An ETL process needs a clearly identifiable entry point. That can be a stored procedure, an SSIS package, a Talend job or a &lt;em&gt;SQL Server Agent&lt;/em&gt; job. At the start of the entry point’s execution, a log record is inserted into this table. After all tasks have been processed successfully, this record is updated to status &lt;em&gt;success&lt;/em&gt;, or to &lt;em&gt;error&lt;/em&gt; in the failure case. The table thus provides an overview of all executions of the ETL process, along with information such as the status and the duration of the run.&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[Component]
&lt;/h3&gt;

&lt;p&gt;This table logs the execution of a component with exactly one record. A component can be a stored procedure, an SSIS package or a Talend job. A component is characterized by the fact that it controls and performs one or more data manipulations. As with the &lt;code&gt;[LL].[Execution]&lt;/code&gt; table, a log record is inserted at the start of execution and updated to status &lt;em&gt;success&lt;/em&gt; or &lt;em&gt;error&lt;/em&gt; on completion. The table therefore holds, per ETL run, a list of the executed components, along with information such as the status and the duration of the run.&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[Trace]
&lt;/h3&gt;

&lt;p&gt;The name of this table already hints that it is intended for detailed logging of the ETL process’s actions — it creates a trace. Which actions are logged is a design decision for the developer. It is advisable, however, to log at least every INSERT, UPDATE and DELETE statement with its own record. In production systems, this granularity should be weighed deliberately against log volume and write load — a single &lt;code&gt;INSERT … SELECT&lt;/code&gt; can move millions of rows and still remains exactly one trace record. Besides the status fields already mentioned, the table also stores the number of rows affected, which lets the developer judge whether the statements did exactly what was expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[Error]
&lt;/h3&gt;

&lt;p&gt;Errors detected in an ETL process belong in the log. A distinction has to be made between logging exceptions and logging data errors. The structure of this table is designed for logging data errors and contains columns in which every data error can be recorded completely and in an evaluable form. This article focuses on logging an ETL process rather than logging data errors. Logging the process is closely tied to explicit exception handling, and a separate section is dedicated to exception handling and the logging of exceptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The LL Schema Name
&lt;/h3&gt;

&lt;p&gt;The schema name &lt;code&gt;LL&lt;/code&gt; stands for &lt;em&gt;Logging Layer&lt;/em&gt;. This schema holds all log tables and the stored procedures used for logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table Description, Declaration and Data Model
&lt;/h2&gt;

&lt;p&gt;The following sections describe the tables for logging the process as well as the table for logging errors. Finally, a diagram shows the data model of these tables.&lt;/p&gt;

&lt;p&gt;Three notes up front: The duration of an execution derives, at the execution level, from the columns &lt;code&gt;[Start]&lt;/code&gt; and &lt;code&gt;[End]&lt;/code&gt;, and at the component and trace levels from &lt;code&gt;[CreatedOn]&lt;/code&gt; (written when the action starts) and &lt;code&gt;[ModifiedOn]&lt;/code&gt; (set by the closing update via trigger). The timestamps use &lt;code&gt;datetime&lt;/code&gt; with &lt;code&gt;GETUTCDATE()&lt;/code&gt; for historical reasons — for a new design, &lt;code&gt;datetime2&lt;/code&gt; with &lt;code&gt;SYSUTCDATETIME()&lt;/code&gt; would be the natural choice today, and its higher precision pays off precisely for trace records written in quick succession. And the audit columns &lt;code&gt;[ModifiedOn]&lt;/code&gt;/&lt;code&gt;[ModifiedBy]&lt;/code&gt; are maintained by an AFTER UPDATE trigger per table: that keeps the logging procedures lean, but costs an additional UPDATE on every status change and is a deliberate trade-off at very high log volumes. With the default database setting &lt;code&gt;RECURSIVE_TRIGGERS OFF&lt;/code&gt;, the trigger's self-update does not recurse (measured on SQL Server 2022).&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[Execution]
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Columns
&lt;/h4&gt;

&lt;p&gt;The table has the following columns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Null?&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bigint&lt;/code&gt; (IDENTITY)&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Primary key, sequential run id.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Process]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Name of the ETL process.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Start]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Start time of the run (UTC).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[End]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;End time of the run.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[DeltaStart]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Start of the delta window (for incremental loads).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[DeltaEnd]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;End of the delta window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[User]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Executing DB login (&lt;code&gt;SUSER_SNAME()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Machine]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Host the run was started from.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Version]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Version number of the ETL process.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[State]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Status: &lt;em&gt;processing&lt;/em&gt; / &lt;em&gt;warning&lt;/em&gt; / &lt;em&gt;success&lt;/em&gt; / &lt;em&gt;error&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Success]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;0 = not (successfully) completed, 1 = successful.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creation time, default &lt;code&gt;GETUTCDATE()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creating login, default &lt;code&gt;SUSER_SNAME()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Login of the last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Declaration
&lt;/h4&gt;

&lt;p&gt;The table is created with the following statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[Execution] - logging table (top level): exactly one record per ETL&lt;/span&gt;
&lt;span class="c1"&gt;-- run. Inserted at the start of the entry procedure with [State] = 'processing'&lt;/span&gt;
&lt;span class="c1"&gt;-- and updated to 'success' or 'error' at the end.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="nb"&gt;bigint&lt;/span&gt;        &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="nb"&gt;datetime&lt;/span&gt;                      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;End&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;DeltaStart&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;DeltaEnd&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;User&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Machine&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="nb"&gt;int&lt;/span&gt;                               &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="nb"&gt;bit&lt;/span&gt;                           &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="nb"&gt;datetime&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Execution_CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Execution_CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PK_LL_Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;CLUSTERED&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CK_LL_Execution_StateSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'warning'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;TR_LL_Execution_Update&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;SET&lt;/span&gt;
           &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="k"&gt;FROM&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;
            &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[Component]
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Columns
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Null?&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bigint&lt;/code&gt; (IDENTITY)&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Primary key of the component.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ExecutionId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Execution]&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Source]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(5)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Type of source (e.g. &lt;em&gt;SSIS&lt;/em&gt;, &lt;em&gt;T-SQL&lt;/em&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Component]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Name of the component (procedure, package, job).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Version]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Version number of the component.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Entity]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Target entity the component processes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Step]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Description of the step.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Description]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Additional description.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[FileId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Reference to a processed file.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[State]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Status: &lt;em&gt;processing&lt;/em&gt; / &lt;em&gt;warning&lt;/em&gt; / &lt;em&gt;success&lt;/em&gt; / &lt;em&gt;error&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Success]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;0 = not (successfully) completed, 1 = successful.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creation time, default &lt;code&gt;GETUTCDATE()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creating login, default &lt;code&gt;SUSER_SNAME()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Login of the last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Declaration
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[Component] - logging table (middle level): exactly one record per&lt;/span&gt;
&lt;span class="c1"&gt;-- component call (stored procedure, SSIS package, Talend job).&lt;/span&gt;
&lt;span class="c1"&gt;-- Foreign key to [LL].[Execution].&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="nb"&gt;bigint&lt;/span&gt;        &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt;                        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="nb"&gt;int&lt;/span&gt;                               &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FileId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="nb"&gt;bit&lt;/span&gt;                           &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="nb"&gt;datetime&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Component_CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Component_CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PK_LL_Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;CLUSTERED&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Component_ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CK_LL_Component_StateSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'warning'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;TR_LL_Component_Update&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;SET&lt;/span&gt;
           &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="k"&gt;FROM&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;
            &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[Trace]
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Columns
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Null?&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bigint&lt;/code&gt; (IDENTITY)&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Primary key of the trace record.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ExecutionId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Execution]&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ComponentId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Component]&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Source]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(5)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Type of source (e.g. &lt;em&gt;SSIS&lt;/em&gt;, &lt;em&gt;T-SQL&lt;/em&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Component]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Name of the calling component.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Task]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Task name (e.g. SSIS task).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Entity]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Target entity of the action.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Step]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Description of the step.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Description]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Additional description.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[FileId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Reference to a processed file.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Action]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Type of action (&lt;em&gt;insert&lt;/em&gt; / &lt;em&gt;update&lt;/em&gt; / &lt;em&gt;delete&lt;/em&gt; / …).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[AffectedRows]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Number of rows affected by the action.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[State]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Status: &lt;em&gt;processing&lt;/em&gt; / &lt;em&gt;warning&lt;/em&gt; / &lt;em&gt;success&lt;/em&gt; / &lt;em&gt;error&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Success]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;0 = not (successfully) completed, 1 = successful.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creation time, default &lt;code&gt;GETUTCDATE()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creating login, default &lt;code&gt;SUSER_SNAME()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Login of the last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Declaration
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[Trace] - logging table (finest level): exactly one record per single&lt;/span&gt;
&lt;span class="c1"&gt;-- action (INSERT/UPDATE/DELETE, single SQL step, single task). Foreign keys&lt;/span&gt;
&lt;span class="c1"&gt;-- to [LL].[Execution] and [LL].[Component].&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="nb"&gt;bigint&lt;/span&gt;        &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt;                        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ComponentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt;                        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FileId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;AffectedRows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="nb"&gt;bit&lt;/span&gt;                           &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="nb"&gt;datetime&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Trace_CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Trace_CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PK_LL_Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;CLUSTERED&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Trace_ComponentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ComponentId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Trace_ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CK_LL_Trace_StateSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'warning'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;TR_LL_Trace_Update&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;SET&lt;/span&gt;
           &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="k"&gt;FROM&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;
            &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[Error]
&lt;/h3&gt;

&lt;p&gt;As mentioned above, the structure of this table is designed for logging data errors, which are not the subject of this article. The table is, however, also used for logging exceptions. The following description covers only the columns required for logging an exception.&lt;/p&gt;

&lt;h4&gt;
  
  
  Columns
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Null?&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bigint&lt;/code&gt; (IDENTITY)&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Primary key of the error record.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ExecutionId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Execution]&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ComponentId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Component]&lt;/code&gt; (if known).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[TraceId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Trace]&lt;/code&gt; (if known).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ErrorType]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;char(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Type of error (exception vs. data error).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Source]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(5)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Type of source (e.g. &lt;em&gt;SSIS&lt;/em&gt;, &lt;em&gt;T-SQL&lt;/em&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Component]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Component in which the error occurred.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[TaskName]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Task name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Entity]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Affected target entity.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Step]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Step in which the error occurred.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Description]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Error text (&lt;code&gt;ERROR_MESSAGE()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Number]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Error number (&lt;code&gt;ERROR_NUMBER()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Line]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Error line (&lt;code&gt;ERROR_LINE()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[State]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Error state (&lt;code&gt;ERROR_STATE()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creation time, default &lt;code&gt;GETUTCDATE()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creating login, default &lt;code&gt;SUSER_SNAME()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Declaration
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[Error] - logging table for exceptions and data errors.&lt;/span&gt;
&lt;span class="c1"&gt;-- In the context of this article we only use the exception columns;&lt;/span&gt;
&lt;span class="c1"&gt;-- the data-error columns (ID1Value/ID2Value/ID3Value/ErrorValue,&lt;/span&gt;
&lt;span class="c1"&gt;-- ID*ColumnName, FileName, FileId) are relevant for a follow-up article.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;              &lt;span class="nb"&gt;bigint&lt;/span&gt;        &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;                        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ComponentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;TraceId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ErrorType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="nb"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;TaskName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;            &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;SchemaName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FileId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID1Value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID1ColumnName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID2Value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID2ColumnName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID3Value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID3ColumnName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ErrorValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ErrorColumnName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FileName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="nb"&gt;int&lt;/span&gt;                               &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;            &lt;span class="nb"&gt;int&lt;/span&gt;                               &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="nb"&gt;datetime&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Error_CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                     &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Error_CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PK_LL_Error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;CLUSTERED&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Error_ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Error_ComponentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ComponentId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Error_TraceId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;TraceId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Data Model
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkn7zot0z5i9ps5uj0hq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkn7zot0z5i9ps5uj0hq.png" alt="Data model of the LL schema: [LL].[Execution], [LL].[Component] and [LL].[Trace] as a 1:n chain from top to bottom, plus [LL].[Error] with foreign keys to all three tables." width="799" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One deliberate redundancy in &lt;code&gt;[LL].[Trace]&lt;/code&gt; stands out: &lt;code&gt;[ExecutionId]&lt;/code&gt; sits there in addition to &lt;code&gt;[ComponentId]&lt;/code&gt;, so that run-level evaluations can skip the join through &lt;code&gt;[LL].[Component]&lt;/code&gt;. The database does not enforce the consistency of that pair — a trace record could in theory point to a component of a different run. Anyone who wants to enforce it adds a composite foreign key on &lt;code&gt;[Component]([Id], [ExecutionId])&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ETL Process Execution Status
&lt;/h2&gt;

&lt;p&gt;The three process-log tables have two columns, &lt;code&gt;[State]&lt;/code&gt; and &lt;code&gt;[Success]&lt;/code&gt;, which store the current status of the ETL process, of a component’s execution or of a specific action — for example an INSERT, UPDATE or DELETE. The current status is held in the &lt;code&gt;[State]&lt;/code&gt; column with the values &lt;em&gt;processing&lt;/em&gt;, &lt;em&gt;warning&lt;/em&gt;, &lt;em&gt;success&lt;/em&gt; and &lt;em&gt;error&lt;/em&gt;. Success is stored in the &lt;code&gt;[Success]&lt;/code&gt; column as &lt;em&gt;1&lt;/em&gt;. Only the combination of both values reveals the current status reliably. The following combinations are valid:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;[State]&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;[Success]&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;processing&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Action, component or run has started and is still running.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;warning&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Finished, but with a warning — not counted as success.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;warning&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Finished with a warning, yet counted as success.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;success&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Completed successfully.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;error&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Aborted with an error.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Valid combinations of the [State] and [Success] columns&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Other combinations of status values are not allowed. This is enforced twice: the process-logging procedures raise an exception if an invalid combination is passed in, and a CHECK constraint per table additionally rejects invalid combinations at the database level (error 547, measured on SQL Server 2022).&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging Procedures
&lt;/h2&gt;

&lt;p&gt;The following procedures, among others, are available for inserting and updating log records in the tables above:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Procedure&lt;/th&gt;
&lt;th&gt;Schema&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spInsertExecution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens the execution log (top level) at the start of the run.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateExecution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Closes the execution log with status &lt;em&gt;success&lt;/em&gt; or &lt;em&gt;error&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spInsertComponent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens a component log for a component.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateComponentSuccess&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Closes a component log successfully.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateComponentError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Closes a component log in the error case.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spInsertTrace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes a trace record for a single action.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateTrace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updates a trace record (general).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateTraceSuccess&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updates a trace record to &lt;em&gt;success&lt;/em&gt; / 1.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateTraceError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updates a trace record to &lt;em&gt;error&lt;/em&gt; / 0.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spInsertErrorException&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes a caught exception to &lt;code&gt;[LL].[Error]&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Procedures for process logging&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In essence these procedures perform an INSERT or UPDATE on the log tables. The values to be logged are passed as parameters. The procedures validate the parameters passed in and raise an exception in case of invalid parameters. For parameter validation and for raising errors consistently, they use the two helpers &lt;code&gt;[dbo].[spRaiseError]&lt;/code&gt; and &lt;code&gt;[dbo].[fnIsNullOrEmpty]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The following code examples show the procedures &lt;code&gt;[LL].[spInsertTrace]&lt;/code&gt;, &lt;code&gt;[LL].[spUpdateTrace]&lt;/code&gt; and &lt;code&gt;[LL].[spUpdateTraceSuccess]&lt;/code&gt;. In addition, &lt;code&gt;[LL].[spInsertExecution]&lt;/code&gt; stands in for all the remaining procedures, which follow the same pattern: validate the parameters, run the INSERT or UPDATE, return the generated id.&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[spInsertTrace]
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[spInsertTrace] - inserts a trace record into [LL].[Trace]&lt;/span&gt;
&lt;span class="c1"&gt;-- and returns the generated id via @p_traceId.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- Parameters:&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_executionId    bigint        Execution id of the current ETL run&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_componentId    bigint        Component id of the calling component log&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_traceId        bigint OUTPUT Id of the newly inserted trace record&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_source          nvarchar(5)   Source system (SSIS, T-SQL, ...)&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_component       nvarchar(128) Name of the calling component&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_task            nvarchar(128) Task name (e.g. SSIS task), optional&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_entity          nvarchar(128) Target entity, optional&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_step            nvarchar(max) Description of the step&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_description     nvarchar(max) Additional description, optional&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_fileId         bigint        Reference to [LL].[FileList].[Id], optional&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_action          nvarchar(100) Action label (Insert/Update/Delete/...)&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_affectedRows   bigint        Number of rows affected&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_state           nvarchar(100) processing / warning / success / error&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_success         bit           0 = processing/warning/error, 1 = success&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_componentId&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_source&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_component&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_task&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_entity&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_step&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_fileId&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bit&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OBJECT_SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
        &lt;span class="c1"&gt;-- Parameter checks&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_executionId&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_componentId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_componentId&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_source&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_component&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_step&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_state&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_success&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Validate the State/Success combination (whitelist)&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'warning'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Invalid state &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; for p_success = &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Write the trace record&lt;/span&gt;
        &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ComponentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FileId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;AffectedRows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OUTPUT&lt;/span&gt; &lt;span class="n"&gt;Inserted&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;
        &lt;span class="k"&gt;VALUES&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_componentId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_source&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_component&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_task&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_entity&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_step&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;DATALENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
                  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_fileId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
        &lt;span class="n"&gt;THROW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[spUpdateTrace]
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[spUpdateTrace] - updates an existing trace record&lt;/span&gt;
&lt;span class="c1"&gt;-- with Description, Action, AffectedRows, State and Success.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- Parameters:&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_traceId        bigint        Id of the trace record to update&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_description     nvarchar(max) Description of the result&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_action          nvarchar(100) Action label, optional&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_affectedRows   bigint        Number of rows affected&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_state           nvarchar(100) processing / warning / success / error&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_success         bit           0 = processing/warning/error, 1 = success&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bit&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;tempId&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OBJECT_SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
        &lt;span class="c1"&gt;-- Parameter checks&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_traceId&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_success&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_state&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Validate the State/Success combination (whitelist)&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'warning'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Invalid state &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; for p_success = &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Check that the trace record exists&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;tempId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;tempId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'A record with [Id] = &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; could not be found.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Update the trace record&lt;/span&gt;
        &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="k"&gt;SET&lt;/span&gt;
               &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt;
                                  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;DATALENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;DATALENGTH&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
              &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;
              &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;AffectedRows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;
              &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;
              &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt;
         &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
        &lt;span class="n"&gt;THROW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[spUpdateTraceSuccess]
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[spUpdateTraceSuccess] - convenience wrapper around spUpdateTrace for&lt;/span&gt;
&lt;span class="c1"&gt;-- the common case "action finished successfully" (State = 'success',&lt;/span&gt;
&lt;span class="c1"&gt;-- Success = 1). Saves setting both fields on every call.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
         &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[spInsertExecution]
&lt;/h3&gt;

&lt;p&gt;The procedure below was not shown in detail in the original example, but it stands in for all the remaining logging procedures: once you understand how it is built, you can derive &lt;code&gt;spInsertComponent&lt;/code&gt;, &lt;code&gt;spUpdateComponent*&lt;/code&gt;, &lt;code&gt;spUpdateTraceError&lt;/code&gt; and &lt;code&gt;spInsertErrorException&lt;/code&gt; analogously — the only difference is the column assignment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[spInsertExecution] - reference for the remaining logging procedures.&lt;/span&gt;
&lt;span class="c1"&gt;--&lt;/span&gt;
&lt;span class="c1"&gt;-- Structurally identical to [LL].[spInsertTrace]: parameter checks -&amp;gt; INSERT&lt;/span&gt;
&lt;span class="c1"&gt;-- -&amp;gt; id returned via OUTPUT variable. Once you understand the pattern, you can&lt;/span&gt;
&lt;span class="c1"&gt;-- spInsertComponent, spUpdateComponent*, spUpdateTraceError,&lt;/span&gt;
&lt;span class="c1"&gt;-- derive spInsertErrorException, spUpdateExecution analogously - only the&lt;/span&gt;
&lt;span class="c1"&gt;-- column assignment differs.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_process&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_version&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OBJECT_SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_process&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;User&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Machine&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OUTPUT&lt;/span&gt; &lt;span class="n"&gt;Inserted&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;
        &lt;span class="k"&gt;VALUES&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_process&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;HOST_NAME&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_version&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
        &lt;span class="n"&gt;THROW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exception Handling
&lt;/h2&gt;

&lt;p&gt;To ensure that an ETL process ends in an &lt;em&gt;orderly&lt;/em&gt; way rather than aborting hard without proper logging, explicit exception handling is required. &lt;em&gt;Orderly&lt;/em&gt; means in this case that the process catches a raised exception, performs an UPDATE on the relevant log record in the tables &lt;code&gt;[LL].[Execution]&lt;/code&gt;, &lt;code&gt;[LL].[Component]&lt;/code&gt; and &lt;code&gt;[LL].[Trace]&lt;/code&gt; — wherever applicable — setting &lt;code&gt;[State]&lt;/code&gt; = &lt;em&gt;error&lt;/em&gt; and &lt;code&gt;[Success]&lt;/code&gt; = &lt;em&gt;0&lt;/em&gt;, and logs the exception in the table &lt;code&gt;[LL].[Error]&lt;/code&gt;. The following diagram shows the basic mechanics of exception handling:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4v0utkhqis8f1wergo8e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4v0utkhqis8f1wergo8e.png" alt="Flowchart of the exception handling: Start, insert log with State processing, workload, branch on exception — the success path updates to success, the error path writes to [LL].[Error], sets State error and optionally re-raises the exception via THROW." width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Exception handling&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Only after logging has completed may the process re-raise the exception to the caller, which would then abort the calling process hard. Re-raising the exception to the caller is not strictly necessary, however, once the error has been logged and the relevant log records have been updated to &lt;code&gt;[State]&lt;/code&gt; = &lt;em&gt;error&lt;/em&gt; and &lt;code&gt;[Success]&lt;/code&gt; = &lt;em&gt;0&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Exception handling is required at least at the top level of an ETL process’s execution, which is logged in the table &lt;code&gt;[LL].[Execution]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One point deserves particular attention here: the interplay of logging and transactions. If the ETL work runs inside an open transaction and the logging procedures write in the same session, a &lt;code&gt;ROLLBACK&lt;/code&gt; in the CATCH block also rolls back the log records already written — of all cases, it is the failure case that loses its log. If an error additionally puts the transaction into the uncommittable state (&lt;code&gt;XACT_STATE()&lt;/code&gt; = -1, the usual case for runtime errors in a TRY block under &lt;code&gt;SET XACT_ABORT ON&lt;/code&gt;), an INSERT into the log tables fails in the CATCH block with error 3930 until the &lt;code&gt;ROLLBACK&lt;/code&gt; has been executed (measured on SQL Server 2022). The order in the CATCH block is therefore: roll back the transaction first, then log. The example in this article works without an explicit transaction and deliberately sidesteps the problem.&lt;/p&gt;

&lt;p&gt;And the error logging itself is not guaranteed to succeed. If one of the logging calls in the CATCH block fails — say, on missing permissions or a full log —, its exception masks the original error. Anyone who wants to guard against that wraps the logging calls in the CATCH block in a TRY/CATCH of their own: the final &lt;code&gt;THROW&lt;/code&gt; then re-raises the original exception in every case.&lt;/p&gt;

&lt;p&gt;The code example below shows exception handling including the logging into the tables &lt;code&gt;[LL].[Execution]&lt;/code&gt; and &lt;code&gt;[LL].[Error]&lt;/code&gt; in line with the diagram above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- Exception-handling skeleton: shows the complete pattern of&lt;/span&gt;
&lt;span class="c1"&gt;-- TRY/CATCH + insert error + update execution + THROW. The order in the&lt;/span&gt;
&lt;span class="c1"&gt;-- CATCH block is crucial - log first, then THROW.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- Execution logging variables&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;processName&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Error logging variables&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Exception variables (lowercase by convention)&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="c1"&gt;-- Initialize the execution logging variables&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;processName&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Name of ETL-Process'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Initialize the error logging variables&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'sql'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Procedure Name'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Any Entity'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Do something'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;processName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Workload (the actual ETL work happens here)&lt;/span&gt;
    &lt;span class="n"&gt;THROW&lt;/span&gt; &lt;span class="mi"&gt;50001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Any Exception'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_NUMBER&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_LINE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_STATE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertErrorException&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;THROW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example: Logging and Exception Handling in an ETL Process
&lt;/h2&gt;

&lt;p&gt;After these explanations of the basics of exception handling, the following diagram shows the exception handling of a simple, compact, but complete ETL process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwcrlnuvgpabafgr14vs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwcrlnuvgpabafgr14vs.png" alt="Diagram of the exception handling in the example ETL process: the entry procedure calls several worker procedures, in the DELETE step of the deepest procedure an exception occurs that is propagated upward and logged at every level." width="800" height="1192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Example of exception handling and logging for an example ETL process&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This example shows the logging of an ETL process built entirely from stored procedures, consisting of five stored procedures. The entry procedure &lt;code&gt;[T2].[spETLProcess]&lt;/code&gt; calls the two procedures &lt;code&gt;[T2].[spDoSomething_1]&lt;/code&gt; and &lt;code&gt;[T2].[spDoSomething_2]&lt;/code&gt;. The first one logs an INSERT, an UPDATE and a DELETE step each, while the second procedure executes two further procedures, &lt;code&gt;[T2].[spDoSomething_2_1]&lt;/code&gt; and &lt;code&gt;[T2].[spDoSomething_2_2]&lt;/code&gt;. These procedures, too, log an INSERT, an UPDATE and a DELETE step each — the DML statements themselves are deliberately only sketched as placeholder comments in the demo procedures. During the execution of the DELETE statement in the procedure &lt;code&gt;[T2].[spDoSomething_2_2]&lt;/code&gt;, an exception is raised that leads to an orderly termination of the ETL process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4dtg136frvu1zkheqyfq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4dtg136frvu1zkheqyfq.png" alt="Call hierarchy of the example ETL process: spETLProcess calls spDoSomething_1 and spDoSomething_2, spDoSomething_2 in turn spDoSomething_2_1 and spDoSomething_2_2. Each worker procedure runs INSERT, UPDATE and DELETE, with the failing DELETE step highlighted." width="664" height="1224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Procedure calls of the example ETL process&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Since showing all procedures would lead to repetition, only the entry procedure &lt;code&gt;[T2].[spETLProcess]&lt;/code&gt; and the procedure &lt;code&gt;[T2].[spDoSomething_2_2]&lt;/code&gt; are shown here. All remaining worker procedures use the same TRY/CATCH pattern as &lt;code&gt;[T2].[spDoSomething_2_2]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two decisions in the example are deliberate. The worker procedures re-raise a caught exception via &lt;code&gt;THROW&lt;/code&gt;, so that every level can close its own log record. The entry procedure &lt;code&gt;[T2].[spETLProcess]&lt;/code&gt;, by contrast, consumes the exception after logging is complete — the run ends in an orderly way with &lt;code&gt;[State]&lt;/code&gt; = &lt;em&gt;error&lt;/em&gt;, without aborting the caller hard. If an orchestrator such as SQL Server Agent is supposed to see the failure, an additional &lt;code&gt;THROW&lt;/code&gt; belongs at the end of its CATCH block, otherwise the job ends as spuriously successful from the scheduler's point of view.&lt;/p&gt;

&lt;p&gt;And when passing on the row count, order matters: &lt;code&gt;@@ROWCOUNT&lt;/code&gt; is captured into the variable &lt;code&gt;@affectedRows&lt;/code&gt; immediately after the respective DML statement, and only that variable is passed to &lt;code&gt;[LL].[spUpdateTraceSuccess]&lt;/code&gt;. After an &lt;code&gt;EXEC&lt;/code&gt;, &lt;code&gt;@@ROWCOUNT&lt;/code&gt; does not carry the business row count of the called component but the value of its last executed statement, and even a simple variable assignment sets the value to 1 (measured on SQL Server 2022). The orchestration steps in &lt;code&gt;[T2].[spETLProcess]&lt;/code&gt; therefore log no row count of their own — the workers deliver it in their trace records. In the demo workers, &lt;code&gt;SET @affectedRows = @@ROWCOUNT;&lt;/code&gt; deliberately remains part of the placeholder comment, and the placeholder steps pass &lt;code&gt;NULL&lt;/code&gt;, because without real DML there would be no business row count to capture. For statements beyond 2 billion rows, &lt;code&gt;ROWCOUNT_BIG()&lt;/code&gt; takes the place of &lt;code&gt;@@ROWCOUNT&lt;/code&gt;. The column &lt;code&gt;[AffectedRows]&lt;/code&gt; is sized as &lt;code&gt;bigint&lt;/code&gt; for that.&lt;/p&gt;

&lt;h3&gt;
  
  
  [T2].[spETLProcess]
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [T2].[spETLProcess] - example entry procedure of the ETL process.&lt;/span&gt;
&lt;span class="c1"&gt;-- Calls two worker procedures spDoSomething_1 and spDoSomething_2&lt;/span&gt;
&lt;span class="c1"&gt;-- and logs both calls in [LL].[Trace].&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spETLProcess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Error variables&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;-- Logging variables&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;affectedRows&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;affectedRows&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OBJECT_SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'T-SQL'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'[].[]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
        &lt;span class="c1"&gt;-- Open the execution log&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'ETL process'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Open the component log&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Orchestrate ETL process'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertComponent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="c1"&gt;-- Call [T2].[spDoSomething_1]&lt;/span&gt;
        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Execute [T2].[spDoSomething_1]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'execute'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spDoSomething_1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Orchestration step without a row count of its own - the DML rows&lt;/span&gt;
        &lt;span class="c1"&gt;-- are logged by the workers in their own trace records&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="c1"&gt;-- Call [T2].[spDoSomething_2]&lt;/span&gt;
        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Execute [T2].[spDoSomething_2]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'execute'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spDoSomething_2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Close the component log&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateComponentSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Close the execution log&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_NUMBER&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_LINE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_STATE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertErrorException&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                 &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;     &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateComponentError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [T2].[spDoSomething_2_2]
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [T2].[spDoSomething_2_2] - deepest worker procedure in the example tree.&lt;/span&gt;
&lt;span class="c1"&gt;-- Called by [T2].[spDoSomething_2] (see diagram 041007.png).&lt;/span&gt;
&lt;span class="c1"&gt;-- Simulates an INSERT, an UPDATE and a DELETE - the DELETE raises an&lt;/span&gt;
&lt;span class="c1"&gt;-- exception to demonstrate the exception-handling pattern.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spDoSomething_2_2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Error variables&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;-- Logging variables&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;affectedRows&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;affectedRows&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OBJECT_SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'T-SQL'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'[].[]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_executionId&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Open the component log&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Do something'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertComponent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="c1"&gt;-- INSERT (succeeds)&lt;/span&gt;
        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Insert data'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'insert'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- An INSERT statement would go here, immediately followed by:&lt;/span&gt;
        &lt;span class="c1"&gt;-- SET @affectedRows = @@ROWCOUNT;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="c1"&gt;-- UPDATE (succeeds)&lt;/span&gt;
        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Update data'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'update'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- An UPDATE statement would go here, immediately followed by:&lt;/span&gt;
        &lt;span class="c1"&gt;-- SET @affectedRows = @@ROWCOUNT;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="c1"&gt;-- DELETE (deliberately raises an exception)&lt;/span&gt;
        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Delete data'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'delete'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- A DELETE statement would go here - artificially raised exception:&lt;/span&gt;
        &lt;span class="n"&gt;THROW&lt;/span&gt; &lt;span class="mi"&gt;50001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Exception in [T2].[spDoSomething_2_2]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Close the component log&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateComponentSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_NUMBER&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_LINE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_STATE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertErrorException&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                 &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;     &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateComponentError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;THROW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following script contains the statement to execute the ETL process. To obtain a compact process log, the log tables are cleared beforehand. This clearing, including &lt;code&gt;DBCC CHECKIDENT&lt;/code&gt;, is a pure demo reset for reproducible ids — in live operation, log tables are not emptied. The closing SELECT statements produce the result shown at the beginning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- Example run: execute the ETL process and evaluate the log.&lt;/span&gt;
&lt;span class="c1"&gt;--&lt;/span&gt;
&lt;span class="c1"&gt;-- Block 1 clears the log tables (TRUNCATE / DELETE + RESEED),&lt;/span&gt;
&lt;span class="c1"&gt;-- block 2 starts the example ETL process, block 3 shows the result.&lt;/span&gt;
&lt;span class="c1"&gt;-- The SELECT statements return exactly the three result sets shown at the&lt;/span&gt;
&lt;span class="c1"&gt;-- start of the article (tables 041001, 041002, 041003).&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;

&lt;span class="c1"&gt;-- 01: Clear the log tables&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;DBCC&lt;/span&gt; &lt;span class="n"&gt;CHECKIDENT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'[LL].[Component]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RESEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;DBCC&lt;/span&gt; &lt;span class="n"&gt;CHECKIDENT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'[LL].[Execution]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RESEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 02: Start the ETL process&lt;/span&gt;
&lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spETLProcess&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;-- 03: Evaluate the log&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;ETL processes are data-driven processes. If data arrives that was not expected, there is a high probability that either not all data — or even wrong data — ends up in the target system. Something this article mentions only in passing: at the lowest logging level, the number of rows affected can also be logged. Knowing the number of expected versus actually processed rows is a good indicator of the success or failure of an ETL process.&lt;/p&gt;

&lt;p&gt;This approach thus supports the development of robust ETL processes and at the same time makes the data quality of a run assessable.&lt;/p&gt;

&lt;p&gt;What goes without saying in software development is often neglected when building ETL processes: explicit exception handling. The exception handling presented here first ensures that a process ends in an &lt;em&gt;orderly&lt;/em&gt; way in the event of an error. But when is there actually an error? And does an error always have to abort the process? Combined with the knowledge of the expected versus actually processed number of rows, genuine error handling can be implemented.&lt;/p&gt;

&lt;p&gt;The approach presented here is, in a way, an invitation to engage more with the data and the expected result. The log — and in particular the knowledge of the rows processed — helps the developer judge the correctness of the work already during the development of the ETL process.&lt;/p&gt;

&lt;p&gt;The procedures presented here merely provide a toolkit for logging. Used correctly, they produce a readable, evaluable log.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between &lt;code&gt;[LL].[Component]&lt;/code&gt; and &lt;code&gt;[LL].[Trace]&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both log steps of a run, but at different levels of granularity. &lt;code&gt;[LL].[Component]&lt;/code&gt; holds exactly one record per called component (stored procedure, SSIS package, Talend job) — that is, "which building block ran when and with what status?". &lt;code&gt;[LL].[Trace]&lt;/code&gt; goes one level deeper and logs the individual actions within a component (typically every INSERT, UPDATE and DELETE) together with the number of rows affected. A component therefore usually has several trace entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a dedicated &lt;code&gt;[LL]&lt;/code&gt; schema instead of putting the logging tables in the default schema?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;LL&lt;/code&gt; schema ("Logging Layer") cleanly separates the logging infrastructure from the business data. This has practical advantages: permissions can be granted precisely (for example, write access to &lt;code&gt;LL&lt;/code&gt; only for the ETL procedures), retention and backup of the logs can be controlled separately, and in scripts it is immediately clear that an object belongs to logging. It also prevents the logging tables from colliding with business tables of the same name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the pattern be implemented with Postgres instead of SQL Server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — the concept is database-neutral: the logging tables and the three-tier structure stay the same. The concrete implementation, however, has to be adapted to PL/pgSQL semantics. Instead of &lt;code&gt;THROW&lt;/code&gt;, you re-raise the exception in PL/pgSQL with &lt;code&gt;RAISE&lt;/code&gt;, &lt;code&gt;TRY/CATCH&lt;/code&gt; becomes a &lt;code&gt;BEGIN … EXCEPTION WHEN OTHERS THEN … END&lt;/code&gt; block, and &lt;code&gt;@@ROWCOUNT&lt;/code&gt; corresponds to &lt;code&gt;GET DIAGNOSTICS &amp;lt;var&amp;gt; = ROW_COUNT&lt;/code&gt;. One important difference remains: an &lt;code&gt;EXCEPTION&lt;/code&gt; block forms an implicit subtransaction in PL/pgSQL. If it catches an error, all changes of the block are rolled back — including log records already written (measured on PostgreSQL 16). The error logging therefore belongs in the &lt;code&gt;EXCEPTION&lt;/code&gt; branch — what the handler itself writes is kept. If you want to measure the execution time of individual statements without your own trace table, you can additionally draw on &lt;code&gt;pg_stat_statements&lt;/code&gt;, as an aggregated statement statistic without the run-level correlation via execution, component and trace ids.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does this pattern differ from SQL Server Audit or pgAudit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SQL Server Audit&lt;/code&gt; and &lt;code&gt;pgAudit&lt;/code&gt; are database-side auditing mechanisms: they log database and server events — logins, DDL, also object access such as SELECT or INSERT — at the infrastructure level, without the business correlation of an ETL run via execution, component and trace ids. The pattern presented here, by contrast, is &lt;strong&gt;application logging&lt;/strong&gt; — it records the business view of the ETL process: which run, which component, which action, how many rows, success or failure. The two complement each other. For the question "did the ETL run do the right thing functionally?" application logging is the appropriate tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why are log entries missing after an error with a rollback?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the log INSERTs ran in the same transaction as the ETL work. A &lt;code&gt;ROLLBACK&lt;/code&gt; in the CATCH block then also rolls back the log records already written. If the error puts the transaction into the uncommittable state (&lt;code&gt;XACT_STATE()&lt;/code&gt; = -1, the usual case for runtime errors in a TRY block under &lt;code&gt;SET XACT_ABORT ON&lt;/code&gt;), an INSERT in the CATCH block even fails with error 3930 as long as the &lt;code&gt;ROLLBACK&lt;/code&gt; is outstanding. In the CATCH block, the rule is therefore: roll back the transaction first, then log — or keep the logging outside the surrounding transaction in the first place (see the Exception Handling section).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you log the number of rows affected with @@ROWCOUNT?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Capture the value of &lt;code&gt;@@ROWCOUNT&lt;/code&gt; into a variable immediately after the INSERT, UPDATE or DELETE statement and pass that variable to the logging procedure — every further statement in between overwrites the value. In this pattern, the number ends up in the &lt;code&gt;[AffectedRows]&lt;/code&gt; column of the trace record via &lt;code&gt;[LL].[spUpdateTraceSuccess]&lt;/code&gt;. For statements with more than 2 billion rows, &lt;code&gt;ROWCOUNT_BIG()&lt;/code&gt; takes the place of &lt;code&gt;@@ROWCOUNT&lt;/code&gt;. The column &lt;code&gt;[AffectedRows]&lt;/code&gt; is sized as &lt;code&gt;bigint&lt;/code&gt; accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I integrate an SSIS package into the pattern?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An SSIS package is treated like any other component: at the start it calls &lt;code&gt;[LL].[spInsertComponent]&lt;/code&gt; (for example from an Execute SQL Task or a Script Task), performs its tasks and finishes with &lt;code&gt;[LL].[spUpdateComponentSuccess]&lt;/code&gt;, or with &lt;code&gt;[LL].[spUpdateComponentError]&lt;/code&gt; in the error case. The returned &lt;code&gt;ComponentId&lt;/code&gt; is passed along through an SSIS variable so that downstream trace calls can reference it. This way the package appears seamlessly alongside the T-SQL components in the same log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;This article is part of the &lt;strong&gt;ETL design-pattern cluster&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-in-an-etl-process/" rel="noopener noreferrer"&gt;Data Quality in an ETL Process&lt;/a&gt; — the root of the article series.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/design-pattern-the-architecture-of-an-etl-process-how-to-isolate-bad-data-cleanly/" rel="noopener noreferrer"&gt;Design Pattern // The Architecture of an ETL Process&lt;/a&gt; — provides the schema &lt;code&gt;T2&lt;/code&gt; used in the example of this article.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/etl-vs-elt-explained/" rel="noopener noreferrer"&gt;ETL vs. ELT — How to Tell Which Pattern You Actually Built&lt;/a&gt; — classifies the pattern logged here as ETL vs. ELT.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Checking Data Quality with SQL — a Configurable Framework&lt;/a&gt; — uses the same error-table idea for generic data quality checks.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/safe-type-conversion-t-sql/" rel="noopener noreferrer"&gt;Design Pattern // Safe Type Conversion with T-SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/type-conversion-basics-t-sql/" rel="noopener noreferrer"&gt;Data Quality // Fundamentals of Type Conversion with T-SQL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>sqlserver</category>
      <category>etl</category>
      <category>logging</category>
    </item>
    <item>
      <title>Deriving Data Quality Rules from the Schema — What the Metadata Already Knows</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Wed, 02 Sep 2026 11:13:56 +0000</pubDate>
      <link>https://dev.to/marcus1968/deriving-data-quality-rules-from-the-schema-what-the-metadata-already-knows-23p2</link>
      <guid>https://dev.to/marcus1968/deriving-data-quality-rules-from-the-schema-what-the-metadata-already-knows-23p2</guid>
      <description>&lt;p&gt;The rule "&lt;code&gt;country_code&lt;/code&gt; is mandatory" lives in your database twice: once as &lt;code&gt;NOT NULL&lt;/code&gt; in the target table's schema, and once as a hand-typed row in the check configuration. On the next &lt;code&gt;ALTER TABLE&lt;/code&gt;, only one of the two places changes, and the check silently goes wrong. With &lt;strong&gt;derived data quality rules&lt;/strong&gt; you no longer type that repetition: the metadata already knows which columns are mandatory, which keys must be unique and which value ranges the types allow. And by pinning the derived state at deployment time, you detect schema drift instead of suffering it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NOT NULL, key uniqueness and type bounds&lt;/strong&gt; can be projected mechanically from &lt;code&gt;information_schema&lt;/code&gt; — including the message text and the business key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projected at deployment time, not on every run:&lt;/strong&gt; an ETL process is a contract between the source and the target system. The derived rules are generated and persisted at deployment, schema changes are not adopted automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The live projection stays on as a drift detector:&lt;/strong&gt; a diff against the deployed state shows when the contract has to be renegotiated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only is not a convenience:&lt;/strong&gt; derived rules are neither edited nor deactivated, the &lt;code&gt;is_active&lt;/code&gt; flag belongs to the manual rules. Whoever wants a change changes the schema and redeploys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handwritten rules remain for the business logic&lt;/strong&gt; the schema does not contain. Exactly those deserve the review time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Postgres as the example engine and the framework from the sub-hub &lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Data Quality Checks with SQL&lt;/a&gt; with its shared error table and the rule configuration &lt;code&gt;dq.check_rule&lt;/code&gt;. The metadata principle needs only &lt;code&gt;information_schema&lt;/code&gt; and thus carries over to any engine that offers it. The code shown uses Postgres syntax and has to be adapted to the dialect of any other engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that already exists
&lt;/h2&gt;

&lt;p&gt;The framework from the sub-hub works off a configuration table: one row is one rule. That is deliberate, because a new check is a data row instead of a code change, and the business side can read along. Such a row is still rolled out through a deployment — why it has to be that way is shown further down in this article. The price of the configuration only shows over time. A large share of the rows you type into such a configuration merely repeats what the target table enforces anyway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;customer_id&lt;/span&gt;    &lt;span class="nb"&gt;int&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;source_system&lt;/span&gt;  &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt;      &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;   &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;          &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;            &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;pk_customer&lt;/span&gt;         &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;uq_customer_source&lt;/span&gt;  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one table already dictates seven check rules for the staging layer: four mandatory fields (lines 5 through 8), two unique keys (lines 11 and 12), plus the length and value-range bounds of the types. Whoever enters them into &lt;code&gt;dq.check_rule&lt;/code&gt; by hand creates seven copies of a truth that already lives in the schema.&lt;/p&gt;

&lt;p&gt;Copies drift. When a column &lt;code&gt;phone varchar(30) NOT NULL&lt;/code&gt; is added later, someone has to remember to extend the configuration as well. If they forget, the check stays silent and the load fails at the target constraint — exactly the scenario the pre-filter was supposed to prevent. If, the other way around, a &lt;code&gt;NOT NULL&lt;/code&gt; is dropped, the old rule keeps checking against a requirement that no longer exists and produces findings without a basis. Both failures are quiet. Nobody gets a message that configuration and schema have drifted apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which data quality rules can be derived
&lt;/h2&gt;

&lt;p&gt;There is a simple way out of the drift problem: derive these rules instead of typing them. Everything the target schema enforces as a constraint or as a declared type bound translates mechanically into a rule row. The mapping onto the framework's rule types is one to one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metadata source&lt;/th&gt;
&lt;th&gt;Derived check&lt;/th&gt;
&lt;th&gt;Rule type in the framework&lt;/th&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_nullable = 'NO'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;mandatory-field check&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;constraint&lt;/code&gt; (WHERE clause)&lt;/td&gt;
&lt;td&gt;Completeness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PRIMARY KEY&lt;/code&gt; / &lt;code&gt;UNIQUE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;uniqueness, cardinality 1 (all key columns &lt;code&gt;NOT NULL&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unique&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uniqueness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FOREIGN KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reference check against the master table (see the outlook below)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lookup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Consistency / Integrity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;character_maximum_length&lt;/code&gt;, &lt;code&gt;numeric_precision&lt;/code&gt;/&lt;code&gt;_scale&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;length and value-range bound&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;constraint&lt;/code&gt; (WHERE clause)&lt;/td&gt;
&lt;td&gt;Validity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Which quality dimensions these checks pay into is laid out by the concept article &lt;a href="https://sql.marcus-belz.de/en/data-quality-dimensions-error-classes/" rel="noopener noreferrer"&gt;Data Quality: Dimensions and Error Classes&lt;/a&gt;. The pattern itself comes from a metadata-driven ETL project: there, the projection ran over a dedicated metadata model with business-side extras such as business and alternate keys and a per-column null handling. For this article it has been transferred to &lt;code&gt;information_schema&lt;/code&gt; so that it can be followed without a model of your own. The principle stays the same: the rules live in the metadata, and the projection merely reads them out.&lt;/p&gt;

&lt;h3&gt;
  
  
  NOT NULL from is_nullable
&lt;/h3&gt;

&lt;p&gt;The simplest projection. Every mandatory column of the target table becomes a mandatory-field check on the staging layer — as a finished row in the configuration table's format, including &lt;code&gt;where_clause&lt;/code&gt; and message text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'constraint'&lt;/span&gt;                            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;                               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%I IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;                                     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s is mandatory in the target schema'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NO'&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the demo table this yields four rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;check_column&lt;/th&gt;
&lt;th&gt;where_clause&lt;/th&gt;
&lt;th&gt;severity&lt;/th&gt;
&lt;th&gt;message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customer_id IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;customer_id is mandatory in the target schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_system IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;source_system is mandatory in the target schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_id IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;source_id is mandatory in the target schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;country_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;country_code IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;country_code is mandatory in the target schema&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details are worth a look. The &lt;code&gt;format()&lt;/code&gt; with &lt;code&gt;%I&lt;/code&gt; on line 6 quotes the column name as an identifier — a column name with special characters yields a correct &lt;code&gt;where_clause&lt;/code&gt;, not a broken one. And the projection targets &lt;code&gt;staging&lt;/code&gt; (line 3) although it reads from &lt;code&gt;core&lt;/code&gt;: checking happens at the source, enforcing happens at the target. That is the division of labour of the whole framework. What would fail as a constraint at the target, the pre-filter finds beforehand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uniqueness from PRIMARY KEY and UNIQUE
&lt;/h3&gt;

&lt;p&gt;Keys live in two catalog views: &lt;code&gt;information_schema.table_constraints&lt;/code&gt; knows the constraints, &lt;code&gt;information_schema.key_column_usage&lt;/code&gt; their columns. With multi-column keys, the columns must be sorted &lt;strong&gt;by position&lt;/strong&gt;, that is by &lt;code&gt;ordinal_position&lt;/code&gt; within the key. That field does not count the column's position in the table but its position in the key, so it reproduces exactly the order from the DDL. Without an &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;code&gt;array_agg&lt;/code&gt; emits the columns in whatever order the join happens to deliver them, and that order is neither guaranteed nor stable over time in Postgres. A different execution plan is enough to turn &lt;code&gt;source_system,source_id&lt;/code&gt; into &lt;code&gt;source_id,source_system&lt;/code&gt; on the next run.&lt;/p&gt;

&lt;p&gt;For the check result this would be harmless, because a &lt;code&gt;GROUP BY&lt;/code&gt; over both columns finds the same duplicates in any order. The damage happens one level up, because &lt;code&gt;check_column&lt;/code&gt; carries the key columns as a comma-separated list, and that list identifies the rule. At runtime the runner splits it back up, builds the multi-column &lt;code&gt;GROUP BY&lt;/code&gt; from it, and writes the value belonging to each listed column into &lt;code&gt;error_value&lt;/code&gt; in the error log. If the order flips, the same rule looks like a different one on the next run, the string comparison against the target constraint fails, and the column-value pairs in the log come out in a different order. The message suffers too, because anyone reading "Key (source_id, source_system) not unique" has to re-sort the columns against the DDL in their head. The projection therefore sorts exactly once, when collecting into the array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CTE_key_column&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;key_column&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_constraints&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column_usage&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PRIMARY KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UNIQUE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;bool_and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NO'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'unique'&lt;/span&gt;                                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;                                 &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;                                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;                                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Key (%s) not unique'&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;CTE_key_column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result: one rule per key, not one per column.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;check_column&lt;/th&gt;
&lt;th&gt;max_occurrence&lt;/th&gt;
&lt;th&gt;message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Key (customer_id) not unique&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_system,source_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Key (source_system, source_id) not unique&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The composite key from line 12 of the table DDL lands position-correct as the comma list &lt;code&gt;source_system,source_id&lt;/code&gt; in &lt;code&gt;check_column&lt;/code&gt;, which is exactly the form the runner expects and splits back up.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;HAVING&lt;/code&gt; on line 27 additionally draws a semantic line. Only keys whose columns are all &lt;code&gt;NOT NULL&lt;/code&gt; get projected. The additional join on &lt;code&gt;information_schema.columns&lt;/code&gt; takes care of that. The reason lies in the &lt;code&gt;NULL&lt;/code&gt; semantics of &lt;code&gt;UNIQUE&lt;/code&gt;: under the default &lt;code&gt;NULLS DISTINCT&lt;/code&gt;, Postgres allows any number of missing values in a &lt;code&gt;UNIQUE&lt;/code&gt; column, while the check's &lt;code&gt;GROUP BY&lt;/code&gt; collapses several &lt;code&gt;NULL&lt;/code&gt; into one group and would report them as duplicates. A rule projected without this filter onto a nullable key would be stricter than the constraint it claims to mirror — it would flag rows the target accepts without complaint. For a &lt;code&gt;PRIMARY KEY&lt;/code&gt; the condition always holds. A &lt;code&gt;UNIQUE&lt;/code&gt; over nullable columns deliberately stays out and belongs in a user-defined rule instead, with whatever &lt;code&gt;NULL&lt;/code&gt; treatment the business actually means. How the uniqueness check deals with composite keys and the &lt;code&gt;NULL&lt;/code&gt; semantics of &lt;code&gt;UNIQUE&lt;/code&gt; in detail is covered by the spoke &lt;a href="https://sql.marcus-belz.de/en/find-duplicates-with-sql/" rel="noopener noreferrer"&gt;Finding Duplicates with SQL&lt;/a&gt;. The cardinality of derived key rules is always 1: a &lt;code&gt;UNIQUE&lt;/code&gt; constraint knows no "at most three times".&lt;/p&gt;

&lt;p&gt;The target table's foreign keys can be projected into &lt;code&gt;lookup&lt;/code&gt; rules as well. This is deliberately an outlook rather than a recipe, because that projection needs more metadata than the two above. &lt;code&gt;information_schema.referential_constraints&lt;/code&gt; names only the two constraints involved, that is the foreign key and the referenced primary or unique key. The columns themselves live in &lt;code&gt;key_column_usage&lt;/code&gt; again, on both sides: the referencing columns come from a lookup by the foreign key's name in their &lt;code&gt;ordinal_position&lt;/code&gt;, the referenced columns from a second lookup into the same view by the name of the target constraint. The two sides are joined through &lt;code&gt;position_in_unique_constraint&lt;/code&gt;, which states for each foreign key column where its counterpart sits within the referenced key. Only that mapping yields the column pairs the check builds its &lt;code&gt;LEFT JOIN … IS NULL&lt;/code&gt; from. For this, the lookup routine accepts the child and master columns each as a comma-separated list, and both lists have to be position-aligned: the first child field belongs to the first master field, pair by pair. The projection therefore has to produce both lists in exactly the same key order, or the generated check joins the wrong pairs. On &lt;code&gt;NULL&lt;/code&gt; semantics, catalog and routine do agree: under &lt;code&gt;MATCH SIMPLE&lt;/code&gt;, the Postgres default, a composite foreign key accepts every row in which even one of the participating columns is &lt;code&gt;NULL&lt;/code&gt;, and the lookup check skips exactly those rows too, because a missing value is a completeness finding and not an integrity one. Anyone looking that match type up in the catalog finds it there as &lt;code&gt;match_option = 'NONE'&lt;/code&gt;, not as &lt;code&gt;SIMPLE&lt;/code&gt;. A foreign key declared as &lt;code&gt;MATCH FULL&lt;/code&gt; would show up as &lt;code&gt;FULL&lt;/code&gt; and would need a stricter check, because it rejects partially filled keys — one more reason this projection stays an outlook. How the check for &lt;a href="https://sql.marcus-belz.de/en/find-orphaned-records-sql/" rel="noopener noreferrer"&gt;orphaned records&lt;/a&gt; itself works is covered by the corresponding spoke.&lt;/p&gt;

&lt;h3&gt;
  
  
  Type bounds from numeric_precision and character_maximum_length
&lt;/h3&gt;

&lt;p&gt;The data types themselves are check rules too. A &lt;code&gt;varchar(2)&lt;/code&gt; says "at most two characters", a &lt;code&gt;numeric(3,0)&lt;/code&gt; says "absolute value below 1000". In the staging layer, which still holds such values as unchecked text or generous types, this becomes a value-range rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'constraint'&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_maximum_length&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'length(%I) &amp;gt; %s'&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_maximum_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'abs(%I) &amp;gt;= %s'&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;trim_scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_precision&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_scale&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt;               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_maximum_length&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s longer than %s characters'&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_maximum_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s outside numeric(%s,%s)'&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_precision&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt;               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_maximum_length&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_precision_radix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;check_column&lt;/th&gt;
&lt;th&gt;where_clause&lt;/th&gt;
&lt;th&gt;message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length(source_system) &amp;gt; 10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;source_system longer than 10 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length(source_id) &amp;gt; 20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;source_id longer than 20 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;country_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length(country_code) &amp;gt; 2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;country_code longer than 2 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length(email) &amp;gt; 100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;email longer than 100 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;age&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;abs(age) &amp;gt;= 1000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;age outside numeric(3,0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The filter on line 32 is this projection's fine print. For &lt;code&gt;int&lt;/code&gt; and &lt;code&gt;bigint&lt;/code&gt; columns, &lt;code&gt;numeric_precision&lt;/code&gt; counts &lt;strong&gt;bits&lt;/strong&gt;, not decimal digits — &lt;code&gt;information_schema.columns&lt;/code&gt; reveals this via &lt;code&gt;numeric_precision_radix = 2&lt;/code&gt;. Without the radix filter, the projection would claim a bound of 10 to the power of 32 for &lt;code&gt;customer_id&lt;/code&gt;, which neither matches the type semantics nor makes for a meaningful check. Only for explicitly declared &lt;code&gt;numeric(p,s)&lt;/code&gt; types (radix 10) does the precision carry a business statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project at deployment time, not on every run
&lt;/h2&gt;

&lt;p&gt;What do you do with the three queries? Two obvious answers each come with a catch. The first: take the results into the configuration by hand via &lt;code&gt;INSERT INTO dq.check_rule&lt;/code&gt;. That only automates the typing effort. The inserted rows would be editable copies, stale again after the next &lt;code&gt;ALTER TABLE&lt;/code&gt;. The second: turn the projection into the runtime rule set directly, as a view, and every rule is fresh at every moment. Exactly that freshness is the second answer's catch.&lt;/p&gt;

&lt;p&gt;An ETL process is a &lt;strong&gt;contract between the source and the target system&lt;/strong&gt;, and that contract is valid at development time. When either side changes, the contract is renegotiated, not adjusted automatically. A live view would do exactly that, in both directions. If the target gets stricter, say through a new &lt;code&gt;NOT NULL&lt;/code&gt; column, the run that was green yesterday fails today without any deployment on the ETL side. If the target gets laxer because a constraint is dropped, the view silently drops the corresponding check, and the pre-filter softens without anyone having decided it. Add the audit angle: which rules were in force for a given run is a question a view can, by principle, not answer.&lt;/p&gt;

&lt;p&gt;For perspective: within a single application that validates its own schema, live derivation is the right model. There is no second contracting party there, the schema is the only truth. The contract logic begins as soon as two systems are involved.&lt;/p&gt;

&lt;p&gt;The conclusion that holds therefore separates two points in time. The projection itself stays a view — the tool that can compute the derived rules fresh from the metadata at any moment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;derived_rule&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CTE_pk_column&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;-- primary key of the target table = business key of the error table&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pk_column&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_constraints&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column_usage&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PRIMARY KEY'&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;CTE_key_column&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;-- every key as an ordered column array; only keys whose columns are&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;-- all NOT NULL (otherwise the rule would check more strictly than&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;-- the constraint guarantees - Postgres default NULLS DISTINCT)&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;key_column&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_constraints&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column_usage&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PRIMARY KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UNIQUE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;bool_and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NO'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- NOT NULL from is_nullable&lt;/span&gt;
 &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'constraint'&lt;/span&gt;                            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;                               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;
 &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id2_column&lt;/span&gt;
 &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id3_column&lt;/span&gt;
 &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%I IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;                                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;
 &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;                                     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s is mandatory in the target schema'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;CTE_pk_column&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;69&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NO'&lt;/span&gt;
 &lt;span class="mi"&gt;73&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- uniqueness from PRIMARY KEY / UNIQUE&lt;/span&gt;
 &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'unique'&lt;/span&gt;
 &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;
 &lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;79&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;83&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;84&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;
 &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Key (%s) not unique'&lt;/span&gt;
 &lt;span class="mi"&gt;87&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
 &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;89&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;CTE_key_column&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;CTE_pk_column&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;91&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CTE on lines 3 through 22 pulls one more thing out of the metadata that the configuration table would otherwise demand by hand: the business key, through which the error table later maps a finding back to the source record. That, too, lives in the schema — it is the target table's primary key. As an ordered array it spreads across &lt;code&gt;id1_column&lt;/code&gt; through &lt;code&gt;id3_column&lt;/code&gt;, and the &lt;code&gt;HAVING count(*) &amp;lt;= 3&lt;/code&gt; on line 21 draws the line of the error table's contract: it carries no more than three mapping columns.&lt;/p&gt;

&lt;p&gt;The projection becomes the rule set only through the &lt;strong&gt;deployment step&lt;/strong&gt;: it reads the view exactly once and writes the result into a table of its own. That table is the signed contract. It records which derived rules were valid at deployment time, and the runner reads it exclusively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;check_type&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;schema_name&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id1_column&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id2_column&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id3_column&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;where_clause&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;max_occurrence&lt;/span&gt;  &lt;span class="nb"&gt;int&lt;/span&gt;         &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;        &lt;span class="nb"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;deployed_on&lt;/span&gt;     &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- deployment step: sign the contract. Runs at deployment time against&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- the target system, not on every run of the runner.&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id1_column&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id2_column&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id3_column&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;max_occurrence&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id1_column&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id2_column&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id3_column&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;max_occurrence&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;derived_rule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table deliberately carries no &lt;code&gt;active&lt;/code&gt; flag. The configuration table's &lt;code&gt;is_active&lt;/code&gt; belongs exclusively to the manual rules, because a derived rule is neither edited nor deactivated. The &lt;code&gt;deployed_on&lt;/code&gt; field documents, in passing, when the contract was signed.&lt;/p&gt;

&lt;p&gt;The contract idea does not stop at the derived rules. The manual rules in the configuration table change only as part of a deployment as well — inserted, edited or toggled via &lt;code&gt;is_active&lt;/code&gt; in a versioned way, not live. The reason is the same as with the schema: a rule list that can be rearranged by hand between two deployments no longer tells you, at a glance, what was in force during the last run. The execution log does record that, but the list is supposed to predict what runs, not merely document what ran. &lt;code&gt;is_active&lt;/code&gt; is thus a versioned switch, not a live dial.&lt;/p&gt;

&lt;p&gt;The check scope of a table is now — within the limits the projection itself draws — the union of both worlds: the deployed derived rules plus the active handwritten ones from the configuration table. Add a single user-defined rule as an example — an age limit that no constraint contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_rule&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'age'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'age &amp;lt; 18'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'W'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Customer under 18 - review with business'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'derived'&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'user-defined'&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_rule&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;active&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;origin&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;origin&lt;/th&gt;
&lt;th&gt;check_type&lt;/th&gt;
&lt;th&gt;check_column&lt;/th&gt;
&lt;th&gt;where_clause&lt;/th&gt;
&lt;th&gt;severity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;country_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;country_code IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customer_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customer_id IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_id IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_system IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;unique&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customer_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;unique&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;user-defined&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;age&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;age &amp;lt; 18&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;W&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six out of this table's seven rules come from the schema. Exactly one was typed — the one that actually contains business knowledge. How large that share turns out does depend on the table, though: a target table without mandatory fields and without key constraints gives the projection simply nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The projection as drift detector
&lt;/h2&gt;

&lt;p&gt;What happens when the target schema changes after the deployment? To the running process, at first: nothing. The runner reads the deployed state, and that is exactly the point of the model. The change does not stay invisible, though, because the view keeps computing the live state — and the difference between the two can be queried mechanically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;uq_customer_source&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'added'&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;id3_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;derived_rule&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;EXCEPT&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;id3_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'removed'&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;id3_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;EXCEPT&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;id3_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;derived_rule&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;drift&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;drift&lt;/th&gt;
&lt;th&gt;check_type&lt;/th&gt;
&lt;th&gt;check_column&lt;/th&gt;
&lt;th&gt;message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;added&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;email is mandatory in the target schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;removed&lt;/td&gt;
&lt;td&gt;unique&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_system,source_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key (source_system, source_id) not unique&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two directions of the diff tell different stories. An &lt;strong&gt;added&lt;/strong&gt; row means: the target has become stricter. Without a redeploy, the pre-filter does not know the new guarantee, and the load would fail at the target constraint instead of at the pre-filter — the detector shows this before it happens. A &lt;strong&gt;removed&lt;/strong&gt; row means: the target has become laxer. The pre-filter checks more strictly than necessary, which endangers no run but is worth a negotiation. In both cases the answer is the same: review the contract and sign it again deliberately, that is, run the deployment step once more. The diff works as a check in the deployment gate or as a warning before each run. It is a signal, never an automatic adoption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why read-only is not a convenience
&lt;/h2&gt;

&lt;p&gt;For the deployed derived rules, a hard convention applies: no &lt;code&gt;UPDATE&lt;/code&gt;, no deactivating, no deleting. That is why the table carries no &lt;code&gt;is_active&lt;/code&gt; either — this flag belongs exclusively to the manual rules in the configuration table. What looks like a limitation is the core of the pattern, not its side effect.&lt;/p&gt;

&lt;p&gt;If the derived rule were editable, it would be a copy with an expiry date. Any change to it would decouple it from its source, and from that moment on there would again be two truths: the one in the schema and the one in the rule. Worse still: a well-meant "let's switch this one check off for a bit" would bypass a guarantee that the target schema keeps enforcing regardless. The load would then fail at a constraint for which there apparently was no active rule. The check would no longer be the target's honest pre-filter but its own drifting opinion of it.&lt;/p&gt;

&lt;p&gt;For the same reason, the criticality of derived rules follows the source semantics rather than an editor's judgement. A &lt;code&gt;NOT NULL&lt;/code&gt; and a unique key are hard constraints at the target. Their derived rules therefore carry &lt;code&gt;E&lt;/code&gt; as in error, because a violation would make the load fail there. That is not an opinion to be debated per rule but a property of the schema.&lt;/p&gt;

&lt;p&gt;Whoever really wants to get rid of a derived rule has exactly one path: change the schema and redeploy. That sounds inconvenient, and it is intended. The discussion "does this column really have to be mandatory?" belongs at the table, not at a configuration row that merely mirrors the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary you don't build
&lt;/h2&gt;

&lt;p&gt;As soon as two rule sources exist, one question suggests itself: what happens when both check the same thing? If someone manually creates a uniqueness rule on &lt;code&gt;customer_id&lt;/code&gt; that already exists as a derived one — do you need conflict detection, a precedence scheme, a merge?&lt;/p&gt;

&lt;p&gt;The better answer is: you never let the situation arise. Instead of detecting and resolving conflicts between derived and user-defined rules, the user-defined territory is cut so that it never enters the derived one. Concretely: the keys the schema already secures are not offered again in the rule editor — no "check primary key" quick pick, no pre-filled uniqueness form for the constraint columns. Those checks already exist, automatically and non-negotiably. The free uniqueness type is there for the cases the schema does &lt;strong&gt;not&lt;/strong&gt; know: a composite business key plus a validity date, or an "at most three times per region", in other words different column combinations and different cardinalities.&lt;/p&gt;

&lt;p&gt;The conflict you don't build does not have to be resolved. There is no merge logic, no precedence table and no special case in the runner — not because the problem was solved elegantly, but because by construction it does not exist. That is a design decision, not an algorithm, and it is cheaper and more robust than any conflict detection you would have to write instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplicates among your own rules
&lt;/h2&gt;

&lt;p&gt;Entirely without checks, though, the user-defined territory does not get by. Within the handwritten rules, the same check can accidentally be created twice, and then two identical findings for the same violation would sit in the error table. What gets blocked is therefore the &lt;strong&gt;exact content duplicate&lt;/strong&gt;: same rule type, same columns, same condition.&lt;/p&gt;

&lt;p&gt;What deliberately does &lt;strong&gt;not&lt;/strong&gt; get blocked is worth noting. Several rules of the same type per column are explicitly allowed: two different value-range checks on the same column are two separate, legitimate checks with their own messages. A "one rule per column and type" constraint would forbid exactly the cases a freely configurable rule table is built for.&lt;/p&gt;

&lt;p&gt;The second subtlety: the criticality is &lt;strong&gt;not&lt;/strong&gt; part of a rule's identity. Two rules with an identical condition but different severity count as the same duplicate. Otherwise the same check could exist once as an error and once as a warning, and the same violation would be reported twice — once blocking, once not. Whoever wants to change a rule's severity edits the existing rule instead of placing a second one next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What remains handwritten
&lt;/h2&gt;

&lt;p&gt;After all the deriving, a remainder is left, and it is the most valuable part of the configuration. The schema only holds what the database can enforce. Everything else is business knowledge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-field conditions&lt;/strong&gt; — a discount requires an active status, an end date lies after the start date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plausibility and temporal logic&lt;/strong&gt; — an order date does not lie in the future, an age under 18 is possible but worth reviewing (the example rule from above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value lists owned by the business&lt;/strong&gt; — allowed status values or product codes deliberately not cemented as constraints, because the business side maintains them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The derived rules need no review time, because they cannot be wrong — they claim nothing the target schema does not enforce anyway. Every minute a review spends nodding off "&lt;code&gt;customer_id&lt;/code&gt; is mandatory" is missing from the question of whether the age limit is right. The projection shifts the attention to where mistakes are actually possible: into the handwritten rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limit
&lt;/h2&gt;

&lt;p&gt;The projection is exactly as good as the schema it reads from. That is its strength and its limit at once, and the limit deserves the same clear look as the pattern itself.&lt;/p&gt;

&lt;p&gt;Where mandatory fields exist only in the application, because the column allows &lt;code&gt;NULL&lt;/code&gt; and only the input form enforces a value, &lt;code&gt;information_schema&lt;/code&gt; sees nothing, and the projection yields nothing. The same goes for business keys that were never created as constraints, and for length bounds on &lt;code&gt;text&lt;/code&gt; columns without a declared length. The projection must not silently paper over these gaps: it delivers exactly the guarantees the schema states, and not a single one more. If you want more derived, you have to make the schema more honest — which keys and constraints a target table should carry in the first place is covered by the article on &lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres table conventions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This pattern deserves to be called by its name: it is a deficit of application development, and not a rare one. Many applications treat the database as mere storage. Mandatory fields, value ranges and keys are checked by application code, while the schema allows almost anything. As long as only the application itself writes, this goes unnoticed. But anyone who wants to load data into such an application past the input form, say during a migration or through an interface, stands there without any of the guarantees this article projects. The consequence is uncomfortable but clear: exactly the rules that an honest schema would make derivable have to be created and maintained manually as user-defined rules for safe loading — with the full drift risk this article started with.&lt;/p&gt;

&lt;p&gt;On top of that comes a Postgres-specific subtlety: &lt;code&gt;information_schema&lt;/code&gt; is portable but not complete. A &lt;code&gt;CREATE UNIQUE INDEX&lt;/code&gt; without an accompanying constraint does not appear in &lt;code&gt;table_constraints&lt;/code&gt;, and neither do partial unique indexes or the &lt;code&gt;NULLS NOT DISTINCT&lt;/code&gt; behaviour of newer Postgres versions. If you use such constructs, project from the system catalogs &lt;code&gt;pg_constraint&lt;/code&gt; and &lt;code&gt;pg_index&lt;/code&gt; instead — with more detail, but without portability. A partial unique index translated into a full uniqueness rule would simply be wrong, so its index predicate belongs in the rule, or the index deliberately stays out of the projection.&lt;/p&gt;

&lt;p&gt;Deriving does not replace thinking about data quality. It moves the thinking to where it belongs: into the schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which data quality rules can be derived from the schema?&lt;/strong&gt;&lt;br&gt;
Everything the target table declares as a constraint or type: mandatory-field checks from &lt;code&gt;is_nullable&lt;/code&gt;, uniqueness checks from &lt;code&gt;PRIMARY KEY&lt;/code&gt; and &lt;code&gt;UNIQUE&lt;/code&gt; (provided the key columns are &lt;code&gt;NOT NULL&lt;/code&gt;), reference checks from foreign keys, plus length and value-range bounds from &lt;code&gt;character_maximum_length&lt;/code&gt; and &lt;code&gt;numeric_precision&lt;/code&gt;. Not derivable is business logic without a schema trace — cross-field conditions, temporal plausibility, value lists owned by the business.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why should derived data quality rules not be editable?&lt;/strong&gt;&lt;br&gt;
Because an editable derived rule would be a copy with an expiry date. After the first change there would be two truths: the one in the schema and the one in the rule. A switched-off check would additionally hide a guarantee the target still enforces. That is why the deployed state deliberately carries no &lt;code&gt;is_active&lt;/code&gt; — the flag belongs to the manual rules. Whoever wants to change a derived rule changes the schema and redeploys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;information_schema&lt;/code&gt; enough, or do I need my own metadata model?&lt;/strong&gt;&lt;br&gt;
For NOT NULL, keys and type bounds, &lt;code&gt;information_schema&lt;/code&gt; is enough, and it is portable — the same views exist in SQL Server. The Postgres system catalogs (&lt;code&gt;pg_constraint&lt;/code&gt;, &lt;code&gt;pg_index&lt;/code&gt;) additionally see unique indexes without constraints and partial indexes. A metadata model of your own pays off only once business metadata should join in that the database does not know — say, business keys without a database constraint or a per-column null-handling semantic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if schema and business requirement contradict each other?&lt;/strong&gt;&lt;br&gt;
Then one of the two sides is wrong, and that belongs resolved at the schema rather than overridden in the configuration. If the business says "email is mandatory" while the column allows &lt;code&gt;NULL&lt;/code&gt;, the schema is too lax: add the constraint, and the derived rule follows automatically. Until then, a user-defined rule with severity warning can make the gap visible without faking a hard guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I keep derived and user-defined rules apart?&lt;/strong&gt;&lt;br&gt;
Through the origin of the data itself: derived rules live only in the deployment table, which only the deployment step fills, user-defined ones only in the configuration table. A combined view with an &lt;code&gt;origin&lt;/code&gt; column (via &lt;code&gt;UNION ALL&lt;/code&gt;) shows the complete check scope. Only one rule of discipline matters: never copy projected rules into the configuration table by hand — there they would be editable copies, and the drift starts all over again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Framework and routines:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Data Quality Checks with SQL&lt;/a&gt; — the sub-hub: the configurable framework with error table, rule configuration and runner, whose configuration this article fills automatically.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/validate-data-with-sql/" rel="noopener noreferrer"&gt;Validating Data with SQL&lt;/a&gt; — the routine behind the projected WHERE rules: value ranges, mandatory fields and the NULL trap.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/find-duplicates-with-sql/" rel="noopener noreferrer"&gt;Finding Duplicates with SQL&lt;/a&gt; — the uniqueness routine: cardinality, composite keys and the NULL semantics of UNIQUE.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/find-orphaned-records-sql/" rel="noopener noreferrer"&gt;Finding Orphaned Records with SQL&lt;/a&gt; — the lookup routine that derived foreign-key rules build on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-severity-levels/" rel="noopener noreferrer"&gt;Three Severity Levels, Not Pass/Fail&lt;/a&gt; — the severity spoke of the series: why E/W/I can do more than passed/failed, and how severity decides whether a rule becomes a constraint or stays in the pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Theory:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-dimensions-error-classes/" rel="noopener noreferrer"&gt;Data Quality: Dimensions and Error Classes&lt;/a&gt; — the concept frame: which dimensions the derived rules pay into.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Target schema:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres Table Conventions&lt;/a&gt; — which keys and constraints a target table should carry so the projection has something to read.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataquality</category>
      <category>sql</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>Formatting SQL Statements (Part 2) — Statement Structure: SELECT, WHERE, FROM, JOIN</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Mon, 31 Aug 2026 09:04:54 +0000</pubDate>
      <link>https://dev.to/marcus1968/formatting-sql-statements-part-2-statement-structure-select-where-from-join-4i4i</link>
      <guid>https://dev.to/marcus1968/formatting-sql-statements-part-2-statement-structure-select-where-from-join-4i4i</guid>
      <description>&lt;p&gt;If you can't tell at a glance where the &lt;code&gt;WHERE&lt;/code&gt; clause of a 200-line &lt;code&gt;SELECT&lt;/code&gt; statement starts and ends, you have a structure problem — not a content problem. This article shows how to format SQL statements so that &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt; and &lt;code&gt;JOIN&lt;/code&gt; stay immediately recognizable even in long queries.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;→ Part of a series.&lt;/strong&gt; This is part 2 and covers &lt;strong&gt;statement structure&lt;/strong&gt; (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;). The basics for identifiers, delimiters, commas, and aliases are in &lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-1/" rel="noopener noreferrer"&gt;Part 1 — Identifiers, Delimiters, Commas, Aliases&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;TL;DR — what this article delivers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Main elements&lt;/strong&gt; (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;) belong on &lt;strong&gt;separate lines&lt;/strong&gt; with consistent indentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHERE&lt;/code&gt; clause&lt;/strong&gt; — align operands and operators column-style, indent equivalent conditions equally. The parenthesis structure becomes visually readable this way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FROM&lt;/code&gt; clause&lt;/strong&gt; — table directly after the &lt;code&gt;JOIN&lt;/code&gt; operator, &lt;code&gt;ON&lt;/code&gt; keyword on its own line, &lt;code&gt;JOIN&lt;/code&gt; conditions formatted like a mini &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres bridge + auto-formatters&lt;/strong&gt; at the end — &lt;code&gt;[brackets]&lt;/code&gt; vs. &lt;code&gt;"quotes"&lt;/code&gt;, &lt;code&gt;DISTINCT ON&lt;/code&gt;, &lt;code&gt;LATERAL&lt;/code&gt;. sqlfluff and pgFormatter complement manual discipline, they don't replace it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; SSMS or any SQL editor with configurable tab width is enough. A live AdventureWorks database is not required — the examples illustrate patterns, not runnable pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  General Thoughts on Indentation
&lt;/h2&gt;

&lt;p&gt;A useful analogy for SQL structure is the outline of a table of contents. The indented version below is much faster to scan than the flat variant further down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Chapter Level 1
   1.1. Chapter Level 2
      1.1.1. Chapter Level 3
      1.1.2. Chapter Level 3
   1.2. Chapter Level 2
2. Chapter Level 1
   2.1. Chapter Level 2
      2.1.1. Chapter Level 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For comparison, the same TOC flat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Chapter Level 1
1.1. Chapter Level 2
1.1.1. Chapter Level 3
1.1.2. Chapter Level 3
1.2. Chapter Level 2
2. Chapter Level 1
2.1. Chapter Level 2
2.1.1. Chapter Level 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flat TOCs work too, but only with additional formatting options like upper/lower case, bold, or italic to distinguish levels. In a SQL editor, those options are typically not available (SSMS renders plain text with syntax highlighting, no bold for identifiers). So indentation remains the structural tool.&lt;/p&gt;

&lt;p&gt;One thing up front: All layout rules in this article are formatting conventions. SQL enforces neither the line breaks nor the indentation or the position of commas and keywords. The conventions aim to make the structure of a statement visible before you read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main Language Elements
&lt;/h2&gt;

&lt;p&gt;This article looks at the six central clauses of a &lt;code&gt;SELECT&lt;/code&gt; statement:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;br&gt;
&lt;code&gt;FROM&lt;/code&gt;&lt;br&gt;
&lt;code&gt;WHERE&lt;/code&gt;&lt;br&gt;
&lt;code&gt;GROUP BY&lt;/code&gt;&lt;br&gt;
&lt;code&gt;HAVING&lt;/code&gt;&lt;br&gt;
&lt;code&gt;ORDER BY&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A complete &lt;code&gt;SELECT&lt;/code&gt; statement can contain further clauses (&lt;code&gt;WITH&lt;/code&gt;, &lt;code&gt;TOP&lt;/code&gt; or &lt;code&gt;LIMIT&lt;/code&gt;, &lt;code&gt;OFFSET&lt;/code&gt;/&lt;code&gt;FETCH&lt;/code&gt;, window definitions) — the same indentation principles apply to them. Treating the six clauses as the first level, their contents sit one indentation level deeper. The resulting basic structure of a SQL statement looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;grouping&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;aggregations&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ground rule of this style guide: The main elements of a SQL statement sit on separate lines.&lt;/p&gt;

&lt;p&gt;As counterexamples, here are two commonly seen formatting styles that ignore this rule. In both, you have to read at least parts of the statement to recognize where a main element starts and ends.&lt;/p&gt;

&lt;h3&gt;
  
  
  Left-Aligned Main and Sub-Elements
&lt;/h3&gt;

&lt;p&gt;You occasionally find both top-level elements and the next-level elements left-aligned in the same column. You see this especially often in the &lt;code&gt;FROM&lt;/code&gt; clause: Data sources (tables, views, CTEs) are indented the same as the introducing keyword &lt;code&gt;FROM&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;table1&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table3&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;grouping&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;aggregations&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Right-Aligned Keywords
&lt;/h3&gt;

&lt;p&gt;In this example, the main clauses of the &lt;code&gt;SELECT&lt;/code&gt; statement (without regard for the &lt;code&gt;BY&lt;/code&gt; keyword) are right-aligned. This indentation style makes alignment extra work because you have to deal with varying indent widths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;field1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field3&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table1&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table3&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;condition1&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;condition2&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;condition3&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;grouping&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="n"&gt;condition1&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;condition2&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;condition3&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For simple statements, however, right-aligned keywords can read quite well — &lt;code&gt;CREATE INDEX&lt;/code&gt; is a typical example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="n"&gt;NONCLUSTERED&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IndexName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;FactInternetSalesReason&lt;/span&gt;&lt;span class="p"&gt;]([&lt;/span&gt;&lt;span class="n"&gt;SalesOrderNumber&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For full &lt;code&gt;SELECT&lt;/code&gt; statements with multiple JOINs, group-by columns and complex conditions, this style breaks down.&lt;/p&gt;

&lt;h2&gt;
  
  
  SELECT Field List
&lt;/h2&gt;

&lt;p&gt;The natural reading direction of a SQL statement is left to right and top to bottom. With keyboard and mouse, vertical navigation is easier than horizontal navigation. The scroll wheel and the &lt;code&gt;Page Up&lt;/code&gt;/&lt;code&gt;Page Down&lt;/code&gt; keys make fast vertical navigation possible even within long complex statements — provided that field lists are written vertically. The more important effect is editor-independent: One line per expression makes long field lists scannable and easier to compare, move, or extend.&lt;/p&gt;

&lt;p&gt;Field names should be written as a vertical list with leading commas — the detailed reasoning (comma readability, box-selection pattern) is in &lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-1/" rel="noopener noreferrer"&gt;Part 1, section “The Comma”&lt;/a&gt;. One field per line. Because the field list is logically subordinate to the &lt;code&gt;SELECT&lt;/code&gt; keyword, field names are indented by the agreed indent width:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;field1&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field2&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field3&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;field1&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field2&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field3&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;field1&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field2&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  WHERE Clause
&lt;/h2&gt;

&lt;p&gt;The order here deliberately departs from SQL syntax: The &lt;code&gt;WHERE&lt;/code&gt; clause introduces the central condition pattern first, which is then reused for the &lt;code&gt;FROM&lt;/code&gt; and &lt;code&gt;HAVING&lt;/code&gt; clauses. A &lt;code&gt;WHERE&lt;/code&gt; clause contains one or more conditions (predicates) connected by logical operators. Two points deserve attention when formatting these conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;alignment of operands&lt;/li&gt;
&lt;li&gt;indentation of equivalent conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Alignment of Operands
&lt;/h3&gt;

&lt;p&gt;A simple condition consists of two operands and an operator (&lt;code&gt;=&lt;/code&gt;, &lt;code&gt;!=&lt;/code&gt;, &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;, &lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;NOT IN&lt;/code&gt;, etc.). In a compound expression built from multiple single conditions, operands and operators should be aligned column-style. In the following example, field names have different lengths and different operators are applied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field___1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="s1"&gt;'something'&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field__2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field_____3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces a table-like notation that allows fast visual navigation within the condition components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Indentation of Equivalent Conditions
&lt;/h3&gt;

&lt;p&gt;If the &lt;code&gt;WHERE&lt;/code&gt; clause contains more than one condition, they are connected by logical operators like &lt;code&gt;AND&lt;/code&gt; or &lt;code&gt;OR&lt;/code&gt;. Parentheses are required when the intended logical grouping differs from operator precedence (&lt;code&gt;AND&lt;/code&gt; binds more tightly than &lt;code&gt;OR&lt;/code&gt;). Beyond that, they can improve readability in complex expressions. Depending on complexity, you quickly end up with deeply nested structures.&lt;/p&gt;

&lt;p&gt;To keep complex nested expressions readable, give particular attention to structure and indentation of the &lt;code&gt;WHERE&lt;/code&gt; clause: equivalent conditions are written underneath each other with the same indent, and a logical connection of equivalent conditions gets an indent that matches the parenthesis hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand01&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand02&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand03&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand05&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand05&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand06&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand07&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand08&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand09&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand11&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand13&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand14&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logical connections become visually readable through this indentation. A screenshot of the same &lt;code&gt;WHERE&lt;/code&gt; clause in Notepad++ makes the effect even more obvious, because the editor's vertical guide lines at the tab stops emphasize the parenthesis hierarchy:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr17hf6ws207nrrm3tori.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr17hf6ws207nrrm3tori.png" alt="WHERE clause in Notepad++ with vertical indent guide lines at the tab stops, making the nesting of the parenthesis constructs visible."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FROM Clause
&lt;/h2&gt;

&lt;p&gt;As with the other main elements, the subordinate elements of the &lt;code&gt;FROM&lt;/code&gt; clause are written indented. In most cases these are data sources — tables, views, and Common Table Expressions (CTEs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sub-SELECTs in the &lt;code&gt;FROM&lt;/code&gt; clause can be replaced by CTEs&lt;/strong&gt; when the subquery forms a self-contained logical step, is needed more than once, or makes the statement hard to follow: A CTE structures it top-down. For small subqueries used only once, a derived table is often the more compact choice. Converting is a structuring decision — how the CTE is executed is up to the engine's optimizer. What Postgres does differently (&lt;code&gt;MATERIALIZED&lt;/code&gt; / &lt;code&gt;NOT MATERIALIZED&lt;/code&gt;) is covered in the &lt;strong&gt;FAQ&lt;/strong&gt; at the end of the article.&lt;/p&gt;

&lt;p&gt;For formatting a &lt;code&gt;JOIN&lt;/code&gt; clause there are four building blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;table (or view / CTE)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JOIN&lt;/code&gt; operator&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ON&lt;/code&gt; keyword&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JOIN&lt;/code&gt; conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the following code example, there is no visual anchor to identify the elements of the &lt;code&gt;FROM&lt;/code&gt; clause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;table1&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table2&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table3&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table4&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The elements of the &lt;code&gt;FROM&lt;/code&gt; clause belong on separate lines for readability. Exception: the joined table sits directly after the &lt;code&gt;JOIN&lt;/code&gt; operator. In this style guide, the &lt;code&gt;ON&lt;/code&gt; keyword sits left-aligned with the &lt;code&gt;JOIN&lt;/code&gt; operator on the line below — that keeps &lt;code&gt;JOIN&lt;/code&gt; and join condition visible as two separate structural levels. The same rules as for the &lt;code&gt;WHERE&lt;/code&gt; clause apply to &lt;code&gt;JOIN&lt;/code&gt; conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;table1&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table2&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table3&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table4&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  GROUP BY, HAVING, ORDER BY
&lt;/h2&gt;

&lt;p&gt;The remaining main elements follow the same principles as the &lt;code&gt;SELECT&lt;/code&gt; field list and the &lt;code&gt;WHERE&lt;/code&gt; clause — in short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GROUP BY&lt;/code&gt;&lt;/strong&gt; contains a list of grouping expressions — for simple queries formatted like a scaled-down &lt;code&gt;SELECT&lt;/code&gt;: one expression per line with a leading comma, indented by convention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HAVING&lt;/code&gt;&lt;/strong&gt; is a condition list like &lt;code&gt;WHERE&lt;/code&gt; — operands aligned column-style, equivalent conditions indented equally. The difference is semantic (post-&lt;code&gt;GROUP BY&lt;/code&gt; filter), not typographic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ORDER BY&lt;/code&gt;&lt;/strong&gt; contains a list of sort expressions — formatted analogous to the &lt;code&gt;GROUP BY&lt;/code&gt; clause, with optional &lt;code&gt;ASC&lt;/code&gt; / &lt;code&gt;DESC&lt;/code&gt; per expression (held in a separate column when both sort directions appear in the same statement).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="nb"&gt;Year&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Sales&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;FactSales&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="nb"&gt;Year&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Sales&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="nb"&gt;Year&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Auto-Formatters and “Formatting Is Learning”
&lt;/h2&gt;

&lt;p&gt;Tools like &lt;strong&gt;sqlfluff&lt;/strong&gt; (multi-dialect — T-SQL, Postgres, MySQL, BigQuery, …) and &lt;strong&gt;pgFormatter&lt;/strong&gt; (Postgres-focused) generate the layouts shown here automatically and are useful as a pre-commit hook or CI step. They are &lt;strong&gt;not&lt;/strong&gt; a replacement for manual formatting.&lt;/p&gt;

&lt;p&gt;The act of indenting, aligning aliases and placing parentheses &lt;strong&gt;forces&lt;/strong&gt; the writer to read the statement fully and mentally model the table relationships. Auto-formatters produce the layout — they don't produce the mental model that emerges while writing. In the age of Copilot and Cursor, this matters twice: Generated SQL without understanding is a risk, because it produces technically correct queries that still don't answer the business question.&lt;/p&gt;

&lt;p&gt;Pragmatic recommendation: format manually first, then let the formatter run as a final consistency pass (e.g. &lt;code&gt;sqlfluff fix&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres Bridge
&lt;/h2&gt;

&lt;p&gt;The examples in this article use T-SQL notation (&lt;code&gt;[brackets]&lt;/code&gt;, positional &lt;code&gt;T01&lt;/code&gt; aliases). The layout rules themselves are &lt;strong&gt;engine-neutral&lt;/strong&gt; — they apply 1:1 to Postgres as well. There are only a handful of places where Postgres behaves differently, and none of them change the format pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identifier quoting:&lt;/strong&gt; The SQL standard uses double quotes for delimited identifiers (&lt;code&gt;"name"&lt;/code&gt;) — Postgres follows it. T-SQL typically uses &lt;code&gt;[brackets]&lt;/code&gt; but also understands double quotes when &lt;code&gt;QUOTED_IDENTIFIER&lt;/code&gt; is active. With case-sensitive identifiers, quoting becomes semantically relevant in Postgres (a separate article on case sensitivity in SQL Server vs. Postgres is planned).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DISTINCT ON&lt;/code&gt;:&lt;/strong&gt; the Postgres idiom for “first row per group”. Which row comes first is determined only by an &lt;code&gt;ORDER BY&lt;/code&gt; that fixes the order within each group unambiguously — its leftmost expressions must be the &lt;code&gt;DISTINCT ON&lt;/code&gt; expressions. In T-SQL, emulate this with &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY … ORDER BY …)&lt;/code&gt; and a filter on &lt;code&gt;rn = 1&lt;/code&gt;. The formatting follows the &lt;code&gt;SELECT&lt;/code&gt; field list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LATERAL&lt;/code&gt;:&lt;/strong&gt; lets a subquery in the &lt;code&gt;FROM&lt;/code&gt; clause reference columns of preceding &lt;code&gt;FROM&lt;/code&gt; items. T-SQL uses &lt;code&gt;CROSS APPLY&lt;/code&gt; / &lt;code&gt;OUTER APPLY&lt;/code&gt; for this. A &lt;code&gt;LATERAL&lt;/code&gt; join is formatted like any other &lt;code&gt;JOIN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RETURNING&lt;/code&gt;:&lt;/strong&gt; Postgres returns the affected rows on &lt;code&gt;INSERT&lt;/code&gt;/&lt;code&gt;UPDATE&lt;/code&gt;/&lt;code&gt;DELETE&lt;/code&gt; and, since version 17, on &lt;code&gt;MERGE&lt;/code&gt; as well. SQL Server offers &lt;code&gt;OUTPUT&lt;/code&gt;, a functionally related clause with its own syntax and forms (&lt;code&gt;OUTPUT INTO&lt;/code&gt;). Both are formatted as their own clause line with a field list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CTEs:&lt;/strong&gt; &lt;code&gt;WITH …&lt;/code&gt; is standardized and structured essentially the same in both engines, the extensions differ in detail. Postgres has the options &lt;code&gt;MATERIALIZED&lt;/code&gt; / &lt;code&gt;NOT MATERIALIZED&lt;/code&gt; (see FAQ).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;JOIN&lt;/code&gt; indentation, &lt;code&gt;WHERE&lt;/code&gt; parenthesis pattern, &lt;code&gt;ORDER BY&lt;/code&gt; lists:&lt;/strong&gt; There is no engine difference for these layout patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the format discipline carries on both engines. A separate follow-up article on the Postgres identifier specifics is planned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The main elements of a &lt;code&gt;SELECT&lt;/code&gt; statement belong on separate lines, their subordinate elements one indentation level deeper. Every pattern in this article derives from that ground rule: the field list as a vertical list with leading commas, the &lt;code&gt;WHERE&lt;/code&gt; clause with column-aligned operands and equally indented equivalent conditions, the &lt;code&gt;FROM&lt;/code&gt; clause with the table directly after the &lt;code&gt;JOIN&lt;/code&gt; operator and the &lt;code&gt;ON&lt;/code&gt; keyword on its own line.&lt;/p&gt;

&lt;p&gt;Applied consistently, these patterns make the structure of a statement visible before its content is read: The parenthesis hierarchy of a nested &lt;code&gt;WHERE&lt;/code&gt; clause and the relationships between the tables are already in the layout. The pattern is engine-neutral and carries in SQL Server and Postgres alike. The vocabulary level (identifiers, delimiters, commas, aliases) is covered in &lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-1/" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;. More important than any single rule is that the same hierarchy stays consistently visible across the whole project — consistency beats purity.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do you format complex SQL queries with multiple JOINs and nested WHERE conditions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Following the rules in this article: each clause on its own line, subordinate elements one level deeper. Each &lt;code&gt;JOIN&lt;/code&gt; gets its table right next to the operator and its &lt;code&gt;ON&lt;/code&gt; on the following line. In the &lt;code&gt;WHERE&lt;/code&gt; clause, equivalent conditions sit underneath each other and the parenthesis hierarchy sets the indent depth. That keeps even a statement with five joins navigable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should a sub-SELECT be replaced by a CTE?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As soon as the subquery makes the statement hard to follow, is needed more than once, or forms a self-contained logical step. CTEs read top-down instead of nested and can be referenced multiple times in the same query. SQL Server, however, may execute the defining query again for each reference, because a CTE is not a materialized intermediate result there. For testing, temporarily replace the outer query with a &lt;code&gt;SELECT * FROM cte_name&lt;/code&gt; — a CTE definition cannot be run standalone. Small subqueries used only once may stay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should the &lt;code&gt;JOIN&lt;/code&gt; operator always be explicit (&lt;code&gt;INNER&lt;/code&gt;, &lt;code&gt;LEFT&lt;/code&gt;, &lt;code&gt;RIGHT&lt;/code&gt;, &lt;code&gt;FULL&lt;/code&gt;)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Syntactically, the &lt;code&gt;INNER&lt;/code&gt; is optional — a bare &lt;code&gt;JOIN&lt;/code&gt; means &lt;code&gt;INNER JOIN&lt;/code&gt;. This style guide still always writes out the &lt;code&gt;JOIN&lt;/code&gt; type (&lt;code&gt;INNER JOIN&lt;/code&gt;, &lt;code&gt;LEFT JOIN&lt;/code&gt;, &lt;code&gt;RIGHT JOIN&lt;/code&gt;, &lt;code&gt;FULL JOIN&lt;/code&gt;): the default is not obvious to every reader, and when skimming, the difference between &lt;code&gt;JOIN&lt;/code&gt; and &lt;code&gt;LEFT JOIN&lt;/code&gt; is easy to miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the format pattern apply to Postgres as well?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — the layout rules transfer unchanged, see the &lt;strong&gt;Postgres Bridge&lt;/strong&gt; section above. The only practical adjustment is identifier quoting (&lt;code&gt;"…"&lt;/code&gt; instead of &lt;code&gt;[…]&lt;/code&gt;). The layout rules for &lt;code&gt;SELECT&lt;/code&gt; field lists, &lt;code&gt;WHERE&lt;/code&gt; clauses, &lt;code&gt;FROM&lt;/code&gt; clauses and &lt;code&gt;JOIN&lt;/code&gt; indentation are engine-neutral.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is an auto-formatter like sqlfluff or pgFormatter enough — or do you still need to format manually?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Auto-formatters deliver layout, but not the learning effect. Anyone who only pipes a 200-line statement through a formatter hasn't read the statement. Anyone who structures it manually builds the mental models of table relationships — and often spots logical errors along the way. Pragmatic workflow: manual first, then the formatter as a finishing pass for consistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about &lt;code&gt;MATERIALIZED&lt;/code&gt; / &lt;code&gt;NOT MATERIALIZED&lt;/code&gt; for CTEs in Postgres?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since version 12, Postgres folds a non-recursive, side-effect-free CTE into the parent query by default when it is referenced exactly once — predicate pushdown is then possible. With multiple references, Postgres does not fold such a CTE by default and treats it as a separate computation instead. &lt;code&gt;MATERIALIZED&lt;/code&gt; forces this separate computation, &lt;code&gt;NOT MATERIALIZED&lt;/code&gt; allows folding even with multiple references. For performance-critical queries with expensive CTEs, the &lt;a href="https://www.postgresql.org/docs/current/queries-with.html" rel="noopener noreferrer"&gt;Postgres docs on &lt;code&gt;WITH&lt;/code&gt; queries&lt;/a&gt; are worth a read. SQL Server has no corresponding syntax: a CTE there is not an independently materialized object, it is optimized as part of the overall statement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do I find Part 1 (identifiers, delimiters, commas, aliases)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-1/" rel="noopener noreferrer"&gt;Part 1 — Formatting SQL Statements&lt;/a&gt;. It covers the smaller building blocks: regular vs. delimited identifiers, leading vs. trailing comma, systematic &lt;code&gt;T01&lt;/code&gt;/&lt;code&gt;T02&lt;/code&gt; aliases, qualified column names. Part 1 + Part 2 together form a complete style guide for &lt;code&gt;SELECT&lt;/code&gt; statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part of the series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-1/" rel="noopener noreferrer"&gt;Formatting SQL Statements (Part 1) — Identifiers, Delimiters, Commas, Aliases&lt;/a&gt;&lt;/strong&gt; — the vocabulary layer: regular vs. delimited identifiers, comma position, systematic aliases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/structuring-and-formatting-sql-statements/" rel="noopener noreferrer"&gt;Structuring and Formatting SQL Statements — The Cluster Path&lt;/a&gt;&lt;/strong&gt; — the hub article with the reading path through all formatting topics on this blog.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Upstream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/functional-design-aesthetics-of-sql/" rel="noopener noreferrer"&gt;The Functional Aesthetics of SQL&lt;/a&gt;&lt;/strong&gt; — the “why” of SQL formatting: Structured code is faster to read, review, and change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Downstream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/inline-comments-in-complex-sql-statements/" rel="noopener noreferrer"&gt;Inline Comments in Complex SQL Statements&lt;/a&gt;&lt;/strong&gt; — the meta layer: inline and block comments that document the structure shown here without destroying it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/editor-options-in-ssms/" rel="noopener noreferrer"&gt;Editor Options in SSMS&lt;/a&gt;&lt;/strong&gt; — the editor basis for this layout: tab width, spaces instead of tabs, block selection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/plpgsql-procedure-conventions/" rel="noopener noreferrer"&gt;SQL Conventions // PL/pgSQL Procedures You Can Still Read in Two Years&lt;/a&gt;&lt;/strong&gt; — the continuation of the statement layout for Postgres procedures.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>sqlserver</category>
      <category>postgres</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Formatting SQL Statements (Part 1) — Identifiers, Delimiters, Commas, Aliases</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Thu, 27 Aug 2026 12:10:05 +0000</pubDate>
      <link>https://dev.to/marcus1968/formatting-sql-statements-part-1-identifiers-delimiters-commas-aliases-4mi0</link>
      <guid>https://dev.to/marcus1968/formatting-sql-statements-part-1-identifiers-delimiters-commas-aliases-4mi0</guid>
      <description>&lt;p&gt;Anyone who has ever had to debug a badly or completely unformatted SELECT with 30 columns and half a dozen joins knows the feeling: it isn't the SQL that eats up your day — it's hunting down what the statement is actually trying to do. SQL formatting isn't a matter of taste; it's a maintenance tool — and it starts with a naming convention.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;→ Part of a series.&lt;/strong&gt; This is part 1 and covers identifiers, delimiters, commas, and aliases. For the layout of longer statements, continue with &lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-2/" rel="noopener noreferrer"&gt;Part 2 — Structure and Formatting&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What this article covers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identifiers and delimiters&lt;/strong&gt; — when to use square brackets, when quotation marks, and what counts as a regular identifier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comma, semicolon, spacer&lt;/strong&gt; — the small separators with a big impact on readability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table aliases and qualified column names&lt;/strong&gt; — why systematic &lt;code&gt;T01&lt;/code&gt;-style aliases scale better in wide statements than mnemonic abbreviations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Box selection&lt;/strong&gt; as the killer argument for leading commas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SSMS serves as the example editor. The principles apply equally to DataGrip, VS Code, and DBeaver. The SQL examples reference &lt;code&gt;AdventureWorksDW2017&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Naming Convention at All?
&lt;/h2&gt;

&lt;p&gt;Capital letters are an established device in most written languages to emphasise individual words. In programming, that translates into notations such as &lt;strong&gt;CamelCase&lt;/strong&gt; (each compound word starts with a capital), &lt;strong&gt;camelCase&lt;/strong&gt; (the same, except the first word is lower case), or &lt;strong&gt;snake_case&lt;/strong&gt; (everything lower case with underscores between words). Conventions like these are bundled into a &lt;strong&gt;naming convention&lt;/strong&gt; — a deliberate decision about how identifiers, function names, and data types should be written. In practice, however, you'll often find that developers don't even hold to their &lt;em&gt;own&lt;/em&gt; preferred convention.&lt;/p&gt;

&lt;p&gt;Sticking to a naming convention improves readability of any text, and of code in particular. tHE SAME Sentence Written Once Again With DEVIATIONS from the generally KNOWN convention that nouns are CAPITALISED and verbs and adjectives are NOT — and the text Turns Unreadable: "adherence to a NAMING convention IMPROVES The readability of text In General And of code In Particular."&lt;/p&gt;

&lt;p&gt;The following statement comes from the view &lt;code&gt;vTimeSeries&lt;/code&gt; in the database &lt;code&gt;AdventureWorksDW2017&lt;/code&gt;, slightly reworked. It does not follow any naming convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;Right&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModelRegion&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TimeIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calendaryear&lt;/span&gt;&lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;udfbuildiso8601date&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;CALENDARYEAR&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;reportingdate&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;vDMPrep&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-250'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-750'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;Left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;Right&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;],[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;udfBuildISO8601Date&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the version below, function names are upper case, every field name uses delimiters, and data types are lower case. Every element is laid out consistently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; 
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; 
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; 
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModelRegion&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TimeIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;udfBuildISO8601Date&lt;/span&gt;&lt;span class="p"&gt;]([&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReportingDate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;vDMPrep&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; 
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-250'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-750'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; 
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; 
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; 
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; 
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;udfBuildISO8601Date&lt;/span&gt;&lt;span class="p"&gt;]([&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether you like the formatting or not is a personal call. Either way, the second statement is, at a glance, tidier and easier to grasp.&lt;/p&gt;

&lt;p&gt;The sections that follow walk through the most important parts of a naming convention. The list is not exhaustive; treat it as a starting point. One thing is worth saying up front: some of it is technical fact (how &lt;code&gt;QUOTED_IDENTIFIER&lt;/code&gt; behaves, say, or how Postgres folds case), but most of it is reasoned convention — a choice a team can just as well make differently, as long as it makes that choice consistently. A &lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-2/" rel="noopener noreferrer"&gt;second part&lt;/a&gt; picks up with best practices for the structure of SQL statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regular Identifiers
&lt;/h2&gt;

&lt;p&gt;Every database object has a name — its &lt;strong&gt;identifier&lt;/strong&gt;. Each database vendor defines rules for what a valid identifier looks like. In SQL Server, identifiers are typically capped at 128 characters and must not contain spaces. The exact definition of a regular identifier is in the online documentation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver17" rel="noopener noreferrer"&gt;learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For practical purposes, though, that definition is framed too generously. A convention worth having draws tighter boundaries than the database system demands.&lt;/p&gt;

&lt;p&gt;Before settling on a spelling style, take a look at how your database actually treats identifiers. &lt;strong&gt;SQL Server&lt;/strong&gt; with a case-insensitive collation does not distinguish between upper and lower case: &lt;code&gt;FactInternetSales&lt;/code&gt; and &lt;code&gt;factinternetsales&lt;/code&gt; point to the same table. That is the usual situation, since the installation defaults are case-insensitive, but the collation is in fact selectable per instance, per database, and even per column. Case-insensitivity gives you the freedom to choose an emphatic notation such as &lt;strong&gt;CamelCase&lt;/strong&gt; without breaking your statement. &lt;strong&gt;Postgres&lt;/strong&gt;, on the other hand, folds unquoted identifiers to lower case: &lt;code&gt;FactInternetSales&lt;/code&gt; becomes &lt;code&gt;factinternetsales&lt;/code&gt; internally. You can still write CamelCase without quotes, but it loses its capitalisation along the way. The only way to keep it is &lt;code&gt;"double quotes"&lt;/code&gt;, which becomes a burden on every statement you write. That is why &lt;strong&gt;snake_case&lt;/strong&gt; is the common convention in the Postgres world.&lt;/p&gt;

&lt;p&gt;Behind this blog are ten years of SQL Server practice with CamelCase as a settled convention. Since the switch to Postgres three years ago, snake_case has taken over there. The full depth of case-sensitivity differences between the two engines deserves its own article, and one is on the way. The examples here stay with the CamelCase notation that is typical for SQL Server and comes with &lt;code&gt;AdventureWorksDW2017&lt;/code&gt; anyway.&lt;/p&gt;

&lt;p&gt;The underscore &lt;code&gt;_&lt;/code&gt; is a widely used word separator and a legal part of a regular identifier. In the SQL Server world it can nonetheless be avoided for the most part: CamelCase serves the same purpose and keeps identifiers more compact.&lt;/p&gt;

&lt;p&gt;Special characters such as &lt;code&gt;@&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, and &lt;code&gt;$&lt;/code&gt; are permitted by the definition, but under this blog's convention they have no place in an identifier. Technically they are allowed even without delimiters inside a regular identifier; they only carry a special meaning in leading position, where &lt;code&gt;@&lt;/code&gt; marks a variable and &lt;code&gt;#&lt;/code&gt; a temporary table. That double role is precisely the problem. Using the characters in column or table identifiers creates visual noise and invites confusion with those special meanings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;English&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;DayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;SpanishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Spanish&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="n"&gt;DayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FrenchDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;French&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;DayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimDate&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the definition, regular identifiers may even contain Unicode letters from any language. This blog's recommendation nonetheless limits the choice to the letters of the Latin alphabet &lt;code&gt;[a-zA-Z]&lt;/code&gt; plus the digits &lt;code&gt;[0-9]&lt;/code&gt; where digits are unavoidable. That reduces the rules for a good identifier to just two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Letters of the Latin alphabet only&lt;/li&gt;
&lt;li&gt;Digits as a fallback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A disciplined approach to regular identifiers always goes hand in hand with the development of a naming convention for the objects themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delimiters
&lt;/h2&gt;

&lt;p&gt;Once an identifier contains a space, it stops being a regular identifier. Using non-regular identifiers is bad style. They are nonetheless permitted as long as they are wrapped either in quotation marks or in square brackets.&lt;/p&gt;

&lt;p&gt;Double quotation marks are the form the SQL standard provides for delimiting identifiers. Microsoft additionally allows square brackets, diverging from the standard. This blog prefers the brackets as the proprietary form of delimiter.&lt;/p&gt;

&lt;p&gt;Delimiters set identifiers apart clearly from the other language elements of a SQL statement and so contribute a great deal to readability. This blog's convention therefore applies delimiters throughout, regardless of whether an identifier is regular or not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schemas&lt;/li&gt;
&lt;li&gt;Tables&lt;/li&gt;
&lt;li&gt;Views&lt;/li&gt;
&lt;li&gt;Column names&lt;/li&gt;
&lt;li&gt;Aliases&lt;/li&gt;
&lt;li&gt;All programmable objects (functions, stored procedures etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a deliberate house convention, not a general best practice. For regular identifiers the delimiters are optional according to the Microsoft documentation, and plenty of teams quote only where they have to. Whoever opts for quoting throughout gains the visual separation, at the price of more characters per identifier.&lt;/p&gt;

&lt;p&gt;The statement below is identical to the second one in the overview, but written without any delimiters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; 
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; 
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; 
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; 
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ModelRegion&lt;/span&gt; 
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;TimeIndex&lt;/span&gt; 
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt; 
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;udfBuildISO8601Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ReportingDate&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vDMPrep&lt;/span&gt; 
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; 
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-250'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-750'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; 
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; 
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; 
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; 
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; 
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Region&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;udfBuildISO8601Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plenty of auto-generated scripts from Microsoft tooling use square brackets as delimiters. That is not a documented recommendation, but it is an observable pattern: SQL Server Management Studio (SSMS) emits brackets in the SELECT and DDL statements generated through the context menu on a table. Microsoft is not entirely consistent here, though: when you create a view through the wizard, or look at the SQL panel of the Edit feature, delimiters are dropped wherever they can be. The two screenshots below — both showing auto-generated SQL — make a fine case study in unmaintainable code:&lt;/p&gt;

&lt;h3&gt;
  
  
  View
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrovljrx9eifisy44nni.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrovljrx9eifisy44nni.png" alt="The SSMS view designer: a relationship diagram of four tables (f, p, d, c) with column lists and join lines at the top, a column grid in the middle with entries such as " width="500" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Edit Feature
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpyq8hbfxgw4ommibwi86.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpyq8hbfxgw4ommibwi86.png" alt="The SSMS Edit feature: at the top an auto-generated SELECT TOP (200) against DimDate without delimiters, below it a data grid with five sample rows (DateKey 20050101–20050105) showing the English, Spanish, and French day names." width="500" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One restriction applies to quotation marks as delimiters: they only work when the SQL Server setting &lt;code&gt;QUOTED_IDENTIFIER&lt;/code&gt; is set to &lt;code&gt;ON&lt;/code&gt; (the default in SSMS and in the common client libraries):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;QUOTED_IDENTIFIER&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the setting &lt;code&gt;OFF&lt;/code&gt;, SQL Server reads double quotes as string literals, and non-regular identifiers can only be written with square brackets. The brackets work regardless of this setting — a practical advantage of the proprietary form. &lt;code&gt;ON&lt;/code&gt; is more than a matter of style, too: several SQL Server features require it, among them indexed views, indexes on computed columns, and filtered indexes.&lt;/p&gt;

&lt;p&gt;Details in the online documentation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/set-quoted-identifier-transact-sql?view=sql-server-ver17" rel="noopener noreferrer"&gt;learn.microsoft.com/en-us/sql/t-sql/statements/set-quoted-identifier-transact-sql&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spacer
&lt;/h2&gt;

&lt;p&gt;What is meant here is the vertical spacer, that is, the blank line. The natural reading direction of a SQL statement is left to right and top to bottom.&lt;/p&gt;

&lt;p&gt;While general-purpose code consists of many short statements, SQL is designed to do a lot of work in a single statement. An SQL statement can easily span a hundred lines or more. That makes writing a good SQL statement a particular challenge. A key criterion for grasping the structure and intent of a statement quickly isn't only a clear layout and consistent formatting, but also compactness. On a typical monitor at a sensible resolution, SSMS shows around forty lines of SQL when only a query window is open and no result pane. In day-to-day use, twenty-five to thirty lines is the realistic maximum.&lt;/p&gt;

&lt;p&gt;There are colleagues in our trade who insert a blank line after every line&lt;/p&gt;

&lt;p&gt;of code. Excessive blank lines force the reader to lean on the navigation&lt;/p&gt;

&lt;p&gt;keys or the mouse wheel just to get from one end of the statement to the&lt;/p&gt;

&lt;p&gt;other. Worse, the overall context of the statement becomes much harder&lt;/p&gt;

&lt;p&gt;to take in.&lt;/p&gt;

&lt;p&gt;Blank lines can be a useful stylistic device to separate logical blocks. Overusing them, however, makes the statement harder to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Semicolon
&lt;/h2&gt;

&lt;p&gt;The SQL standard provides for the semicolon as the terminator of a statement. SQL Server, at least, is forgiving here and doesn't force you to use one. There are only a handful of cases where the semicolon is mandatory.&lt;/p&gt;

&lt;p&gt;One example: when using a &lt;em&gt;Common Table Expression&lt;/em&gt;, the statement preceding the keyword &lt;code&gt;WITH&lt;/code&gt; must end with a semicolon. To sidestep the problem, many developers write the leading &lt;code&gt;WITH&lt;/code&gt; as &lt;code&gt;;WITH&lt;/code&gt;. Formally, the semicolon belongs to the preceding statement — &lt;code&gt;;WITH&lt;/code&gt; is a compatibility idiom, not a syntax of its own.&lt;/p&gt;

&lt;p&gt;Other engines are stricter, or more precisely their tooling and procedural languages are. A single statement handed to &lt;strong&gt;Postgres&lt;/strong&gt; directly by a driver works without a trailing semicolon. In &lt;code&gt;psql&lt;/code&gt; and in multi-statement scripts such as &lt;code&gt;pg_dump&lt;/code&gt; output, the semicolon marks the end of a statement, and inside PL/pgSQL function bodies it is part of the syntax as a statement terminator. &lt;strong&gt;Oracle&lt;/strong&gt; follows the same pattern: in PL/SQL blocks (&lt;code&gt;BEGIN … END;&lt;/code&gt;) the semicolon is required, while in clients such as SQL*Plus its role depends on the execution context.&lt;/p&gt;

&lt;p&gt;Using the semicolon consistently is a sign of care either way, and it helps readability. What it does not do is make a statement portable — SQL dialects differ in entirely different places, from data types through functions to the procedural language. What it does remove is an unnecessary syntactic dependency on the client and the execution context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Comma
&lt;/h2&gt;

&lt;p&gt;Field lists in SQL are separated by commas. Some developers put the comma before the field name, others put it after. The pro/con discussion usually centres on how easy it is to add or remove a field. The real reason the comma belongs at the front, though, is readability and the option to format the statement with &lt;strong&gt;box selection&lt;/strong&gt; (called "column editor" or "Spaltenauswahl" in SSMS, "Box Selection" in VS Code and DataGrip). There is more on box selection in the article &lt;a href="https://sql.marcus-belz.de/en/functional-design-aesthetics-of-sql/" rel="noopener noreferrer"&gt;The Functional Aesthetics of SQL&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leading Comma
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;SpanishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimDate&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to add another field, say &lt;code&gt;FrenchDayNameOfWeek&lt;/code&gt;, it's easier to add it at the end — you only insert the text &lt;code&gt;,[FrenchDayNameOfWeek]&lt;/code&gt; after &lt;code&gt;[SpanishDayNameOfWeek]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;SpanishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FrenchDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimDate&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want the new field at position one, you need two edits: insert the line &lt;code&gt;[FrenchDayNameOfWeek]&lt;/code&gt; before &lt;code&gt;[EnglishDayNameOfWeek]&lt;/code&gt;, and prepend a comma to &lt;code&gt;[EnglishDayNameOfWeek]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FrenchDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;EnglishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;SpanishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimDate&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Trailing Comma
&lt;/h3&gt;

&lt;p&gt;When the comma is written after each field, the situation reverses: it's easier to add a new field at position one than at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Readability
&lt;/h3&gt;

&lt;p&gt;The comma is a separator. It marks the transition from one field to the next. When it sits at the end of each field, it loses its separating character because field names have different lengths.&lt;/p&gt;

&lt;p&gt;In the statement below, it isn't immediately obvious whether the identifier &lt;code&gt;[ProductName]&lt;/code&gt; is a column name or has some other role. The reader has to look at the line above to realise that &lt;code&gt;[ProductName]&lt;/code&gt; is the alias for &lt;code&gt;[EnglishProductName]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ListPrice&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DealerPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the comma sits in front of each field, the ambiguity goes away. It is immediately clear that &lt;code&gt;[ProductName]&lt;/code&gt; is not a separate field — there is no comma in front of it, so it must belong to the line above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ListPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;DealerPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Comma and Box Selection
&lt;/h3&gt;

&lt;p&gt;Adding aliases to every field of the following statement is a little awkward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ListPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;DealerPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every comma has to move at least one position to the right, and the keyword &lt;code&gt;AS&lt;/code&gt; and the alias have to be inserted in front of it. That applies to every row of the field list, one at a time, by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AliasEnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AliasSize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AliasColor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ListPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AliasListPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;DealerPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AliasDealerPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same end result can be achieved with far less work if the SQL statement is laid out so that box selection works on it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx63qjwoclb3c8pone8ib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx63qjwoclb3c8pone8ib.png" alt="SSMS editor showing the DimProduct SELECT in box-selection-ready form: to the right of each column are the aligned aliases AS [AliasEnglishProductName], AS [AliasSize], AS [AliasColor] and others. The prefix Alias is highlighted as a blue box-selection band spanning all five rows — a single keystroke applies to every row simultaneously." width="415" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No commas need to move. With box selection, &lt;code&gt;AS&lt;/code&gt; is typed once, the field names are used as the basis for the alias, copied as a block after &lt;code&gt;AS&lt;/code&gt;, and the prefix &lt;code&gt;Alias&lt;/code&gt; is prepended — again with box selection. The best part: the effort is essentially independent of the number of rows you have to process.&lt;/p&gt;

&lt;p&gt;Box selection becomes truly effective only with leading commas. That is the killer argument for putting the comma at the front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on the SSMS bias.&lt;/strong&gt; This argument is largely about SSMS and its classic box selection. &lt;strong&gt;DataGrip&lt;/strong&gt; and other modern editors offer more powerful refactoring tools — multi-cursor at arbitrary positions, automatic alias generation via refactor commands, semantic search-and-replace across the whole codebase. In that world you save the same effort even without leading commas. The box-selection argument carries less weight there. The &lt;strong&gt;readability arguments&lt;/strong&gt; (see the "Readability" sub-section) hold regardless of the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Function Names
&lt;/h2&gt;

&lt;p&gt;SSMS highlights function names in pink, which makes them easy enough to spot. Since not every editor does syntax highlighting, function names should also be visually distinguishable by consistent upper- or lower-case spelling.&lt;/p&gt;

&lt;p&gt;In other words: function names should be either fully upper case or fully lower case.&lt;/p&gt;

&lt;p&gt;Microsoft itself, like most database vendors, writes function names in upper case in the online documentation. That documentation spelling is not a binding standard, but it is a convenient orientation — this blog's convention follows the upper-case form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table Aliases
&lt;/h2&gt;

&lt;p&gt;A widely used practice is to derive a table alias as a "speaking" abbreviation from the table name. For the table &lt;code&gt;FactInternetSales&lt;/code&gt; one might use the alias &lt;code&gt;IS&lt;/code&gt;, taking the starting letters of the compound words in the table name (ignoring the &lt;code&gt;Fact&lt;/code&gt; prefix). Since &lt;code&gt;IS&lt;/code&gt; is a reserved word, that alias must be wrapped in delimiters. The example also shows a weakness of mnemonic abbreviations: they can collide with reserved words and then force additional quoting. For other tables, you might end up with aliases like these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Alias&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DimCustomer&lt;/td&gt;
&lt;td&gt;CUST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DimProduct&lt;/td&gt;
&lt;td&gt;P&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DimProductCategory&lt;/td&gt;
&lt;td&gt;PC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DimProductSubcategory&lt;/td&gt;
&lt;td&gt;PSC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A SELECT statement built on those tables might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;CUST&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;CUST&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;PC&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductCategoryName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;PSC&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductSubCategoryName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;OrderDate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;FactInternetSales&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimCustomer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CUST&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;CustomerKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CUST&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;CustomerKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;ProductKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProductSubcategory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;PSC&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductSubcategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PSC&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductSubcategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProductCategory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;PC&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;PC&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductCategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PSC&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductCategoryKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The uneven indentation of the field names is a side-effect of the different lengths of the aliases — somewhere between one and four characters. The field list looks "restless", and as soon as more language elements join the party (functions, CASE expressions etc.) it can become hard to read quickly.&lt;/p&gt;

&lt;p&gt;Now imagine a SELECT over twenty tables or more. At some point it becomes hard to come up with a meaningful alias for every table. From roughly the fifth alias onward — that is a rule of thumb, not a hard boundary — the derivation from table names rarely improves readability any further, because the aliases are simply too cryptic.&lt;/p&gt;

&lt;p&gt;Wouldn't systematic — possibly even indexed — aliases be easier to identify in a complex statement?&lt;/p&gt;

&lt;p&gt;A systematic alias scheme could be defined like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Aliases must be a fixed number of characters&lt;/li&gt;
&lt;li&gt;Aliases are indexed (with one or more leading letters)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If, for instance, you use the letter &lt;code&gt;T&lt;/code&gt; for &lt;em&gt;Table&lt;/em&gt; (or &lt;code&gt;F&lt;/code&gt; for &lt;em&gt;Fact table&lt;/em&gt;, &lt;code&gt;D&lt;/code&gt; for &lt;em&gt;Dimension&lt;/em&gt;, etc.) followed by a two-digit 1-based index, you end up with aliases such as &lt;code&gt;T01&lt;/code&gt;, &lt;code&gt;T02&lt;/code&gt;, &lt;code&gt;D01&lt;/code&gt;, &lt;code&gt;F01&lt;/code&gt;, ….&lt;/p&gt;

&lt;p&gt;Using those aliases, the statement above reads more cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T05&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductCategoryName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductSubCategoryName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;OrderDate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;FactInternetSales&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimCustomer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;CustomerKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;CustomerKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProductSubcategory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductSubcategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductSubcategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProductCategory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T05&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T05&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductCategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductCategoryKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field names in the SELECT list line up cleanly.&lt;/p&gt;

&lt;p&gt;The real killer argument is the same here, too: aliases of equal length are what makes box selection useful in the first place.&lt;/p&gt;

&lt;p&gt;There's another important reason to use aliases at all, and it applies to mnemonic and systematic ones alike: &lt;strong&gt;Intellisense&lt;/strong&gt; (in SSMS) only really works once aliases are in place. When the developer types an alias followed by a dot, a context menu opens listing only the columns of the corresponding table.&lt;/p&gt;

&lt;p&gt;When the cursor sits after the dot, the context menu can also be invoked manually with the shortcut &lt;code&gt;Ctrl+Space&lt;/code&gt;. It works without a preceding alias as well. In that case, however, SSMS effectively offers the entire T-SQL vocabulary at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuxirpk0l903uhhdhrxox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuxirpk0l903uhhdhrxox.png" alt="The SSMS Intellisense context menu appears after typing T01. — it lists only the columns of the table bound to the alias, FactInternetSales (CarrierTrackingNumber highlighted, CurrencyKey, CustomerKey, CustomerPONumber, DiscountAmount, DueDate, DueDateKey, ExtendedAmount, Freight, …). A tooltip on the right shows the data type of the highlighted column: " width="480" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Aliases make SQL code easier to read and therefore more maintainable. In statements spanning several tables they are practically indispensable. This blog's convention applies them to simple single-table statements as well, for the sake of uniformity and Intellisense comfort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qualified Column Names
&lt;/h2&gt;

&lt;p&gt;As soon as a statement references multiple tables, sooner or later the same column name appears in more than one of them and stops being unambiguous. In the example below we want to return name and phone number for both the employee and the reseller of a &lt;code&gt;FactResellerSales&lt;/code&gt; fact:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmr25r7oln125j57oh3m2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmr25r7oln125j57oh3m2.png" alt="SSMS editor showing the FactResellerSales SELECT: the field list contains [Phone] AS [Employee_Phone] and [Phone] AS [Reseller_Phone] twice without any table alias prefix. The FROM clause uses the T01/T02/T03 aliases but the SELECT never references them — the statement is ambiguous and not executable." width="357" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The statement is not executable because the column &lt;code&gt;Phone&lt;/code&gt; exists in both dimensions &lt;code&gt;DimEmployee&lt;/code&gt; and &lt;code&gt;DimReseller&lt;/code&gt;. SQL Server aborts with two errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  1: Msg 209, Level 16, State 1, Line 4
  2: Ambiguous column name 'Phone'.
  3: Msg 209, Level 16, State 1, Line 6
  4: Ambiguous column name 'Phone'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specifying a table alias in front of the column name is mandatory here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Phone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Employee_Phone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or — if no table aliases are used — the table name itself has to precede the column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DimEmployee&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Phone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Employee_Phone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The latter, however, does nothing for readability.&lt;/p&gt;

&lt;p&gt;A fully qualified object name in SQL Server consists of up to four parts: server, database, schema, and object. A column reference adds the column as a fifth element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="k"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="k"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="k"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An example with fully qualified table names that include the database name:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favzm2ya138b2wouf2n7s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favzm2ya138b2wouf2n7s.png" alt="SSMS editor showing the same FactResellerSales SELECT as before, this time with fully qualified table names [AdventureWorksDW2017].[dbo].[FactResellerSales] T01, [AdventureWorksDW2017].[dbo].[DimEmployee] T02, and [AdventureWorksDW2017].[dbo].[DimReseller] T03 in the FROM/JOIN clause — the database name is baked into the table reference and binds the statement to this specific database." width="462" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Any qualification beyond the schema prevents the application from being deployable in a database that doesn't share the name &lt;code&gt;AdventureWorksDW2017&lt;/code&gt;. Cases like that do occur in practice: ETL pipelines whose tables were fully qualified down to the database name (&lt;code&gt;[Database].[Schema].[Table].[Column]&lt;/code&gt;) could not be deployed to production once they were finished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engine note: Postgres.&lt;/strong&gt; The five-part scheme &lt;code&gt;Server.Database.Schema.Table.Column&lt;/code&gt; only applies in SQL Server. &lt;strong&gt;Postgres&lt;/strong&gt; by default does &lt;strong&gt;not&lt;/strong&gt; allow cross-database queries: a cluster does hold several databases, but a connection is bound to exactly one of them, so the &lt;code&gt;Database.&lt;/code&gt; element doesn't exist in practice. If you really need to read or write across database boundaries, you'll need the extensions &lt;code&gt;postgres_fdw&lt;/code&gt; (Foreign Data Wrapper) or &lt;code&gt;dblink&lt;/code&gt;. Both are extra setup, not a default. In practical terms this means that within a Postgres database, &lt;code&gt;Schema.Table.Column&lt;/code&gt; (three parts) is the relevant qualification, while in SQL Server it is one option among several. The recommendation stays the same in both worlds: &lt;strong&gt;never qualify beyond the schema level&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A naming convention is the entry point to maintainable SQL code: regular identifiers built from letters and, where needed, digits; delimiters applied consistently; function names in upper case. The small separators have the largest effect of all. The leading comma keeps field lists readable and ready for box selection, the semicolon makes statements unambiguous towards the client and the execution context, and sparing blank lines keep the whole picture on a single screen.&lt;/p&gt;

&lt;p&gt;Systematic aliases such as &lt;code&gt;T01&lt;/code&gt; play to their strength in wide statements and line the field list up cleanly. Qualified column names belong up to the schema level and no further. Which individual rule you pick matters less than it seems: SQL becomes maintainable through a consistent convention that makes structure visible and changes easy. &lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-2/" rel="noopener noreferrer"&gt;Part 2 — Structure and Formatting&lt;/a&gt; builds on this and covers the layout of longer statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Leading comma — isn't that unusual?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When reading, the comma loses its separating role if it sits at the end of a line (see the section "Readability"). When writing, the leading comma saves the most effort the moment box selection comes into play. Modern auto-formatters make the comma position configurable. &lt;code&gt;sqlfluff&lt;/code&gt; (T-SQL and Postgres) places commas at the end of the line by default and only switches to leading commas through its layout configuration (&lt;code&gt;line_position = leading&lt;/code&gt;). &lt;code&gt;pgFormatter&lt;/code&gt; (Postgres) likewise defaults to the trailing comma and offers &lt;code&gt;--comma-start&lt;/code&gt; for leading ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need square brackets in Postgres too?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Postgres does &lt;strong&gt;not&lt;/strong&gt; support &lt;code&gt;[brackets]&lt;/code&gt;. Its delimiters are double quotes: &lt;code&gt;"EnglishProductName"&lt;/code&gt;. There is no &lt;code&gt;QUOTED_IDENTIFIER&lt;/code&gt; setting either. Identifier quoting in Postgres is always on. Postgres folds identifiers without quotes to lower case (&lt;code&gt;MyColumn&lt;/code&gt; becomes &lt;code&gt;mycolumn&lt;/code&gt;). Identifiers in quotes keep their spelling and have to be quoted exactly that way on every reference. If your codebase is multi-engine in the long run, &lt;code&gt;"double quotes"&lt;/code&gt; are ANSI-SQL-compliant and work in both worlds — the &lt;code&gt;[brackets]&lt;/code&gt; are SQL Server-specific.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Systematic &lt;code&gt;T01&lt;/code&gt;-style aliases vs. speaking aliases — which is better?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a trade-off. Speaking aliases (&lt;code&gt;CUST&lt;/code&gt;, &lt;code&gt;P&lt;/code&gt;, &lt;code&gt;PC&lt;/code&gt;) are mnemonic and work fine as long as the statement only has three or four tables. As soon as five or more tables come into play — typical for ETL pipelines or wide reporting queries — the mnemonic edge fades and the alignment suffers from the varying lengths. Systematic &lt;code&gt;T01&lt;/code&gt;-style aliases scale, stay the same length, and make box selection effortless. The price is semantic: &lt;code&gt;T05.[CustomerKey]&lt;/code&gt; doesn't tell you the table. Anyone who wants to know jumps to the FROM clause. Systematic aliases optimise for visual structure and mechanical editing, then, while speaking aliases optimise for orientation in the content. As a rule of thumb, this blog uses speaking aliases up to four tables and systematic ones from five onward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if my team uses a different convention?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consistency beats purity. If the team has settled on trailing commas or speaking aliases, applying that convention consistently is more important than picking the "better" variant.&lt;/p&gt;

&lt;p&gt;In practice this is one of the hardest points overall. In every larger team, each developer has a very personal sense of what "looks good" and what doesn't. That individual matter of taste is exactly what stands in the way of team-wide uniformity, and experience across many projects bears this out again and again. All the more reason for the convention that the team has agreed on to be applied consistently across every team member. Only then do the SQL statements stay readable and maintainable for everyone involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-cursor — does it replace box selection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Modern editors like VS Code, DataGrip, and DBeaver provide true multi-cursor support (&lt;code&gt;Ctrl+Alt+Down Arrow&lt;/code&gt; or &lt;code&gt;Alt+Click&lt;/code&gt;) that goes beyond the rectangular box selection: cursors can sit at arbitrary positions rather than only along a rectangular column. SSMS only has classic box selection (&lt;code&gt;Alt+Shift+Arrow&lt;/code&gt;). Azure Data Studio, once part of this line-up, was retired by Microsoft on 28 February 2026; the recommended successor is VS Code with the mssql extension. Conceptually, multi-cursor covers the same need and goes beyond it. Box-selection-friendly formatting remains the prerequisite either way. If you work across several editors, format once cleanly rather than once per tool.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>sqlserver</category>
      <category>database</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Three Severity Levels, Not Pass/Fail — and Why Severity Decides Where the Rule Lives</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:39:01 +0000</pubDate>
      <link>https://dev.to/marcus1968/three-severity-levels-not-passfail-and-why-severity-decides-where-the-rule-lives-ecn</link>
      <guid>https://dev.to/marcus1968/three-severity-levels-not-passfail-and-why-severity-decides-where-the-rule-lives-ecn</guid>
      <description>&lt;p&gt;Most data quality systems don't die of bad checks. They die of a missing field in the rule model: the severity. A missing country code in three out of 80,000 rows blocks the nightly load, someone switches the check off "temporarily", and from that moment everything runs unchecked. Steering data quality with &lt;strong&gt;severity levels&lt;/strong&gt; instead of binary pass/fail escapes that trap. And severity can do more: it decides whether a rule belongs in the target schema as a constraint or in the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binary pass/fail ends in one of two dead ends:&lt;/strong&gt; either a triviality blocks the whole load, or everything passes and nobody reads the findings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three levels are enough:&lt;/strong&gt; an error blocks, a warning is visible without blocking, an information merely documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Severity is a routing decision:&lt;/strong&gt; only an error rule is a candidate for a &lt;code&gt;CHECK&lt;/code&gt; or &lt;code&gt;UNIQUE&lt;/code&gt; constraint in the target schema — warnings and information stay in the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promoting has a price:&lt;/strong&gt; when a warning becomes an error, the existing data suddenly has to be clean. &lt;code&gt;NOT VALID&lt;/code&gt; and &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; make the transition controllable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The limits can be named precisely:&lt;/strong&gt; cross-row, cross-table and time-based rules are beyond what a constraint can do. Those belong in the pipeline, permanently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Postgres as the example engine and the framework from the sub-hub &lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Data Quality Checks with SQL&lt;/a&gt; with the shared error table, the rule configuration &lt;code&gt;dq.check_rule&lt;/code&gt; and the quality gate. The principle itself is engine-neutral and applies just as well to SQL Server or any other system with constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why binary fails
&lt;/h2&gt;

&lt;p&gt;A binary check system knows exactly one question per record: pass or fail. That sounds clean and leads, in practice, into one of two dead ends.&lt;/p&gt;

&lt;p&gt;Dead end one: &lt;strong&gt;everything that fails blocks.&lt;/strong&gt; Then a missing email address stops the same load as a duplicate customer number. The process stands still, the business day waits, and the person on call makes a decision at three in the morning that actually belongs in a rule review: they switch the check off. Not out of carelessness, but because the system offers them no other gradation.&lt;/p&gt;

&lt;p&gt;Dead end two: &lt;strong&gt;nothing blocks, everything is merely logged.&lt;/strong&gt; Then a findings table fills up that somebody still reads in the beginning. A few weeks later thousands of entries sit there, important ones next to trivial ones, and because the system itself doesn't say which of them demand action, at some point nobody looks anymore. The check keeps running and is dead anyway.&lt;/p&gt;

&lt;p&gt;Both reflexes are understandable. The problem is not that people handle the system badly. The problem is that a binary model withholds the one piece of information that should steer the handling: how bad is this finding?&lt;/p&gt;

&lt;h2&gt;
  
  
  The three levels
&lt;/h2&gt;

&lt;p&gt;Three severity levels answer exactly that question, and you rarely need more than three. The pattern is not new: syslog levels and linters have known it for decades. What makes it interesting is what each level concretely triggers in the ETL process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error (&lt;code&gt;E&lt;/code&gt;)&lt;/strong&gt; blocks. A record with an error finding does not pass the quality gate and does not reach the target layer. Error is the right level for everything the load would fail on at the target anyway: missing mandatory values, duplicate keys, unknown references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning (&lt;code&gt;W&lt;/code&gt;)&lt;/strong&gt; is visible but does not block. The record flows on, the finding stays logged. That covers the largest share of real-world data problems: things that are odd, unclean or worth a look, but must not hold up the process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Information (&lt;code&gt;I&lt;/code&gt;)&lt;/strong&gt; merely documents. No pressure to act, no blocking, pure awareness, for instance a missing phone number whose frequency you want to observe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the framework's rule model, the severity (some rule models call the field "criticality") is a column of the configuration table, with a strict default and a &lt;code&gt;CHECK&lt;/code&gt; on itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;   &lt;span class="nb"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'E'&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ck_check_rule_severity&lt;/span&gt;  &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'W'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'I'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default &lt;code&gt;E&lt;/code&gt; is a deliberate decision: whoever writes a rule means it seriously at first. Downgrading to a warning is an active, documented step. The opposite default would silently turn new rules into mere observers. Five rules show the three levels in the configuration table's format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_rule&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'country_code'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'country_code IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'E'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'country code is mandatory in the target schema'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'age'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="s1"&gt;'age IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="s1"&gt;'E'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'age is mandatory in the target schema'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'age'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="s1"&gt;'age &amp;lt; 18'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="s1"&gt;'W'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer under 18 - needs business review'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'email IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'W'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email missing - needs business review'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'phone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'phone IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'I'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'phone number not provided - for information only'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The information rule deliberately targets a field that is optional from a business point of view: a missing phone number is not a defect, but a value whose rate you want to know. On &lt;code&gt;age&lt;/code&gt;, by contrast, two rules with different levels sit side by side: if the age is missing entirely, that is an error, because the target treats the column as mandatory. If it is merely conspicuously low, that is a warning. The choice of level is a business statement, not a technical one.&lt;/p&gt;

&lt;p&gt;What the levels mean in practice is shown by a run against seven demo rows (one clean, one per severity, and three combinations). The framework's staging table is extended by the optional column &lt;code&gt;phone&lt;/code&gt; for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'a@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'+49 30 1234567'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- clean&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'b@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'+49 40 2345678'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- E: country code missing&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'AT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'c@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'+43 1 3456789'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;-- W: under 18&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CH'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'d@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;-- I: phone number missing&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'e@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'+41 44 5678901'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- E+E: country code and age missing&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'f@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'+49 89 6789012'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- E+W: country code missing, under 18&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;-- E+E+W+I: everything at once&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_run_checks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_findings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 11&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner writes the severity counters back into the source table, one per record and level:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;country_code&lt;/th&gt;
&lt;th&gt;email&lt;/th&gt;
&lt;th&gt;age&lt;/th&gt;
&lt;th&gt;phone&lt;/th&gt;
&lt;th&gt;sys_error&lt;/th&gt;
&lt;th&gt;sys_warning&lt;/th&gt;
&lt;th&gt;sys_info&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;DE&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:a@example.com"&gt;a@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;+49 30 1234567&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:b@example.com"&gt;b@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;+49 40 2345678&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:c@example.com"&gt;c@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;+43 1 3456789&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;CH&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:d@example.com"&gt;d@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:e@example.com"&gt;e@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;+41 44 5678901&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:f@example.com"&gt;f@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;+49 89 6789012&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The quality gate queries only one of the three columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
   &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
   &lt;span class="n"&gt;sys_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of the seven rows pass the gate. The warning row and the information row flow along, the four rows with at least one error stay behind — logged, with a plain-text message, findable for follow-up. The combination rows show in passing that &lt;code&gt;sys_error&lt;/code&gt; is a counter, not a flag, and that the three counters run independently of each other: row 5 carries two errors, row 6 one error and one warning, row 7 fills all three counters at once. The gate still asks only for &lt;code&gt;sys_error = 0&lt;/code&gt;. No on-call engineer has to decide at night whether a missing phone number matters more than the daily close. That decision was made long ago, in the rule review, when someone wrote &lt;code&gt;W&lt;/code&gt; into the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Severity is a routing decision
&lt;/h2&gt;

&lt;p&gt;Up to here, severity looks like a label on the finding. It can do more: it answers the question of &lt;strong&gt;where a rule may be enforced&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reasoning behind it is simple. A constraint in the target schema is the hardest form a rule can take: on its own, it knows no exception, no log line and no "flows on anyway". Only the calling application can soften that, the constraint itself knows no gradation. Only a rule whose violation must truly never reach the target deserves that hardness. That is exactly the definition of the error level. A warning, on the other hand, is &lt;em&gt;supposed&lt;/em&gt; to be allowed through, otherwise it would be an error. A constraint that rejects warning violations would contradict the rule's own definition. The routing follows from that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;E&lt;/code&gt; is a candidate for a constraint:&lt;/strong&gt; &lt;code&gt;NOT NULL&lt;/code&gt;, &lt;code&gt;CHECK&lt;/code&gt;, &lt;code&gt;UNIQUE&lt;/code&gt;, foreign keys. The rule is enforced twice: up front as a check in the source, hard as a guarantee at the target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;W&lt;/code&gt; and &lt;code&gt;I&lt;/code&gt; stay in the pipeline:&lt;/strong&gt; they exist only as check rules, produce findings and never block. A target constraint for them is not merely unnecessary, it is wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Severity is therefore not a report field but the switch that decides about constraint derivation. How the error rules of the configuration can conversely be &lt;a href="https://sql.marcus-belz.de/en/derive-data-quality-rules-from-schema/" rel="noopener noreferrer"&gt;derived from the target schema&lt;/a&gt; is shown by this series' sibling article. The coupling works in both directions, and derived rules always carry &lt;code&gt;E&lt;/code&gt; there, because their source is a hard constraint.&lt;/p&gt;

&lt;p&gt;Concretely, the routing looks like this. The target layer materializes the two error rules as &lt;code&gt;NOT NULL&lt;/code&gt; on &lt;code&gt;country_code&lt;/code&gt; and &lt;code&gt;age&lt;/code&gt;, while the warning rules and the information rule deliberately have no counterpart there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;   &lt;span class="nb"&gt;int&lt;/span&gt;   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;           &lt;span class="nb"&gt;int&lt;/span&gt;   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;pk_customer&lt;/span&gt;  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
   &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
   &lt;span class="n"&gt;sys_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- INSERT 0 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 17-year-old row and the row without a phone number arrive as well. That is exactly the intent: the warning has served its purpose (the finding sits in the log), and the target accepts the record. What would happen if someone materialized the warning rule as a constraint after all is shown by the direct attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
   &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ck_customer_adult&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: check constraint ck_customer_adult of relation customer&lt;/span&gt;
&lt;span class="c1"&gt;--        is violated by some row&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ALTER TABLE&lt;/code&gt; fails on its own existing data, on precisely the row the warning was explicitly supposed to let pass. The error is not an accident but the database pointing out the contradiction: this rule was classified as &lt;code&gt;W&lt;/code&gt;, and a &lt;code&gt;CHECK&lt;/code&gt; is the enforcement form of &lt;code&gt;E&lt;/code&gt;. The same holds for the information rule with the opposite sign: its hard counterpart would be a &lt;code&gt;NOT NULL&lt;/code&gt; on &lt;code&gt;phone&lt;/code&gt; — technically perfectly expressible, but wrong, because an information must never block. The target allows the missing phone number deliberately, the pipeline merely keeps count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four places, one decision
&lt;/h2&gt;

&lt;p&gt;Zooming out of the framework, there are four places where a data quality rule can live. Each can do something the others cannot, and each has a price:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Place&lt;/th&gt;
&lt;th&gt;What it can do&lt;/th&gt;
&lt;th&gt;What it costs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Source&lt;/strong&gt; (pre-filter before the load)&lt;/td&gt;
&lt;td&gt;finds &lt;em&gt;all&lt;/em&gt; bad records up front, classifies by severity, the load carries on with the good ones&lt;/td&gt;
&lt;td&gt;its own infrastructure: error table, rule configuration, runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Pipeline&lt;/strong&gt; (check steps in the process)&lt;/td&gt;
&lt;td&gt;the most expressive option — every rule type, every severity, trends and reports&lt;/td&gt;
&lt;td&gt;the rule lives next to the data, and every path around the pipeline bypasses it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Target schema&lt;/strong&gt; (constraints)&lt;/td&gt;
&lt;td&gt;the only guarantee that &lt;em&gt;every&lt;/em&gt; write path respects, including the manual hotfix&lt;/td&gt;
&lt;td&gt;knows only two outcomes, can only express error semantics, blocks the whole run of a set-based load when in doubt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Reporting&lt;/strong&gt; (dashboards, analyses)&lt;/td&gt;
&lt;td&gt;makes warnings and information visible over time, shows trends&lt;/td&gt;
&lt;td&gt;enforces nothing, depends on the findings of the other places&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Today's industry default clearly sits on the second place: tools like Great Expectations, Soda or dbt tests formulate checks in the pipeline, and most of them know severity gradations as well. dbt, for instance, distinguishes &lt;code&gt;error&lt;/code&gt; and &lt;code&gt;warn&lt;/code&gt; per test. That is a workable model, not a mistake. You should just be able to name what you are buying into: a rule that lives exclusively in the pipeline is valid only there. The colleague with the direct &lt;code&gt;INSERT&lt;/code&gt;, the second load script, the weekend migration — none of them run through the pipeline, and none of them hit its checks. A constraint in the target schema has no such gap, because it sits in the data itself.&lt;/p&gt;

&lt;p&gt;The four places are therefore not competitors you pick one of. The decision is made per rule, and severity is its first criterion: error rules deserve the double floor of pre-filter plus constraint, warnings and information belong in pipeline and reporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the source still checks first
&lt;/h2&gt;

&lt;p&gt;If only error rules become constraints, why check in the source at all? You could simply let the &lt;code&gt;CHECK&lt;/code&gt; do what it is there for.&lt;/p&gt;

&lt;p&gt;The answer is already in the &lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;framework article&lt;/a&gt;, and it remains fully valid under the routing view: a constraint knows only two outcomes. The row fits, or the whole load breaks. When loading thousands of rows, "breaks" is the worst of all options, because a single bad row stops the complete process, and the error message names at best that one row, not the other nine that were still waiting behind it.&lt;/p&gt;

&lt;p&gt;One qualification belongs here: this all-or-nothing applies to the set-based load, that is, a single &lt;code&gt;INSERT … SELECT&lt;/code&gt; moving all rows in one transaction. That is exactly how an ETL process developed in SQL works, and dbt belongs on this side as well, because its models compile to set-based SQL statements. Row-based ETL tools like SSIS or Talend process the rows individually instead: a failing row can be routed out through an error output there, the remaining rows carry on, and the constraint stops only that one row rather than the whole load. You pay for that convenience with the row-by-row processing itself, which is considerably slower than a set-based load, and with error handling that lives inside the tool instead of in a queryable table. The pre-filter is the set-based answer to the same need: it does what a row-based tool's error output does — just up front, in set logic and with severity levels.&lt;/p&gt;

&lt;p&gt;The error output also has a second, less obvious weakness: the checks in the data flow run sequentially. The first hit routes the row out, and the remaining checks never see it — unless you explicitly wire the error path through all further check steps, which quickly clutters the data flow. In practice that means: you fix the first error found, run the load again, find the second one, fix it, find the third. The pre-filter knows no such iterating, because every rule runs set-based across all rows. That is why row 7 of the demo sits in the log completely after a single run, with two errors, one warning and one information.&lt;/p&gt;

&lt;p&gt;The pre-filter in the source and the constraint at the target are therefore not alternatives but two halves of the same error rule. The check in the source finds &lt;em&gt;all&lt;/em&gt; records that would fail at the target, classifies them and lets the load carry on with the clean ones. The constraint at the target guarantees the rule even for everything that goes past the pre-filter: the manual hotfix, the forgotten second script. If the pre-filter fails, the load breaks loudly instead of silently accepting bad data. If the constraint is dropped, the pre-filter still checks. Only together do the two produce the property neither half has alone: complete findings &lt;em&gt;and&lt;/em&gt; a hard guarantee.&lt;/p&gt;

&lt;p&gt;So the question is not &lt;em&gt;whether&lt;/em&gt; schema or pipeline. The question is which rule deserves both — and that is answered by the severity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you promote a rule
&lt;/h2&gt;

&lt;p&gt;Rules are not static. The business decides that customers under 18 must no longer be created: the warning from above is to become an error. In the configuration that is one statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_rule&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
   &lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'E'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
       &lt;span class="k"&gt;schema_name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'age &amp;lt; 18'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_run_checks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_findings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 11&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the next run on, the 17-year-old row blocks at the gate (&lt;code&gt;sys_error = 1&lt;/code&gt;). That is the easy part. The demanding part follows from the routing: an error rule is a constraint candidate, so the target schema should guarantee the new hardness as well. And right here waits the price of promotion — &lt;strong&gt;the existing data suddenly has to be clean&lt;/strong&gt;. The 17-year-old row arrived at the target long ago, perfectly legitimately, under the old rule. The direct &lt;code&gt;ADD CONSTRAINT&lt;/code&gt; fails on it, as seen above.&lt;/p&gt;

&lt;p&gt;Postgres offers a controlled path for this transition. &lt;code&gt;NOT VALID&lt;/code&gt; accepts the constraint immediately without checking the existing data. For new rows it applies from the first second anyway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
   &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ck_customer_adult&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;VALID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: new row violates check constraint ck_customer_adult&lt;/span&gt;

&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="n"&gt;VALIDATE&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ck_customer_adult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order is the point. First &lt;code&gt;NOT VALID&lt;/code&gt; seals the future, then the existing data is cleaned up calmly (in the demo via &lt;code&gt;DELETE&lt;/code&gt;, in practice more likely: routed out for business review), and only the final &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; re-checks the existing rows and completes the guarantee. Between the two steps the constraint sits in a documented intermediate state: it applies to new rows while the existing data is still unchecked. Postgres shows this state in the catalog as &lt;code&gt;convalidated = false&lt;/code&gt;. The &lt;code&gt;VALIDATE&lt;/code&gt; does not lock the table against writes, it runs with a weak lock alongside normal operation. One subtlety in passing: a &lt;code&gt;CHECK&lt;/code&gt; does not fire on &lt;code&gt;NULL&lt;/code&gt;, so a missing age would pass &lt;code&gt;ck_customer_adult&lt;/code&gt; without complaint. Here that case is already caught by the &lt;code&gt;NOT NULL&lt;/code&gt; that the error rule &lt;code&gt;age IS NULL&lt;/code&gt; materializes at the target anyway. The same pattern carries a retrofitted &lt;code&gt;NOT NULL&lt;/code&gt; on an existing table. How to roll that out without downtime is shown in &lt;a href="https://sql.marcus-belz.de/en/add-not-null-column-existing-table-postgres/" rel="noopener noreferrer"&gt;a dedicated article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For SQL Server the principle holds with different mechanics: &lt;code&gt;WITH NOCHECK&lt;/code&gt; likewise adds a constraint without checking the existing data. The difference sits in the aftermath. The constraint stays permanently marked as not trusted until a &lt;code&gt;WITH CHECK CHECK CONSTRAINT&lt;/code&gt; re-validates the existing rows, and an untrusted constraint is ignored by the optimizer in plan decisions. Whoever does only the first step has the guarantee but gives away performance.&lt;/p&gt;

&lt;p&gt;Demoting, the reverse path, is just as much a routing decision, by the way: &lt;code&gt;E&lt;/code&gt; becomes &lt;code&gt;W&lt;/code&gt;, so the corresponding constraint at the target has to go, otherwise it keeps blocking a rule that is only supposed to observe. Promoting pulls constraints in, demoting clears them away — configuration and schema move together.&lt;/p&gt;

&lt;p&gt;Taken together, that is the real payoff of the three levels: a rule has a controlled lifecycle. It starts as an observation, proves itself, gets promoted and moves into the schema as a guarantee via &lt;code&gt;NOT VALID&lt;/code&gt; and &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; — and, when needed, walks the same path back. Severity is the control knob of that lifecycle, not just a label on the finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;p&gt;The routing "&lt;code&gt;E&lt;/code&gt; becomes a constraint" has one restriction that hides in the word &lt;em&gt;candidate&lt;/em&gt;: not every error rule &lt;em&gt;can&lt;/em&gt; become a constraint. A &lt;code&gt;CHECK&lt;/code&gt; sees exactly one row. Within that row it may compare several columns, a &lt;code&gt;CHECK (end_date &amp;gt; start_date)&lt;/code&gt; is perfectly legitimate. But three rule families lie beyond its reach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-row rules.&lt;/strong&gt; "A key may occur at most three times" is one row in the configuration as a check rule (&lt;code&gt;max_occurrence = 3&lt;/code&gt;), but not expressible with the declarative constraints (&lt;code&gt;UNIQUE&lt;/code&gt;, &lt;code&gt;CHECK&lt;/code&gt;, foreign keys), because &lt;code&gt;UNIQUE&lt;/code&gt; knows only cardinality 1. Sum, share and distribution rules belong here as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-table rules.&lt;/strong&gt; A &lt;code&gt;CHECK&lt;/code&gt; may not contain a subquery, Postgres rejects that with a clear error message. The only cross-table guarantee the schema knows is the foreign key. Everything beyond it stays pipeline work, say "the discount code must match the customer group".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-based rules.&lt;/strong&gt; Postgres silently assumes that a &lt;code&gt;CHECK&lt;/code&gt; expression always yields the same result, but does not enforce that immutability: a &lt;code&gt;CHECK (order_date &amp;lt;= current_date)&lt;/code&gt; is accepted, and that is precisely what makes it a trap. What is valid today is no longer valid tomorrow, and at the latest when restoring a dump or running a &lt;code&gt;VALIDATE&lt;/code&gt;, existing rows fail that were correct when they were created. Rules with a time reference belong in the pipeline, where every run checks against the current reference date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The direction of view matters: these rules do not land in the pipeline &lt;em&gt;because sadly nothing better exists&lt;/em&gt;. The pipeline is the right place for them: it checks set-based, knows the run context and can still rate a violation as an error and block it at the gate. A cross-row error rule is an error without a constraint counterpart, and that is not a contradiction to the routing but its honest limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix
&lt;/h2&gt;

&lt;p&gt;The per-rule decision, condensed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule type&lt;/th&gt;
&lt;th&gt;Typical severity&lt;/th&gt;
&lt;th&gt;Place of enforcement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mandatory field the target enforces&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-filter in the source &lt;strong&gt;+&lt;/strong&gt; &lt;code&gt;NOT NULL&lt;/code&gt; at the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard value range (type bound, country-code format)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-filter &lt;strong&gt;+&lt;/strong&gt; &lt;code&gt;CHECK&lt;/code&gt; at the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key uniqueness&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-filter &lt;strong&gt;+&lt;/strong&gt; &lt;code&gt;PRIMARY KEY&lt;/code&gt;/&lt;code&gt;UNIQUE&lt;/code&gt; at the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference to a master table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-filter &lt;strong&gt;+&lt;/strong&gt; &lt;code&gt;FOREIGN KEY&lt;/code&gt; at the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business anomaly (worth a look, not blocking)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pipeline, finding in the log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pure observation (frequencies, missing optional values)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;I&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pipeline &lt;strong&gt;+&lt;/strong&gt; reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-row (cardinality &amp;gt; 1, sums, shares)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;E&lt;/code&gt; or &lt;code&gt;W&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pipeline only — &lt;code&gt;UNIQUE&lt;/code&gt; cannot do cardinality &amp;gt; 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-table beyond the foreign key&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;E&lt;/code&gt; or &lt;code&gt;W&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pipeline only — &lt;code&gt;CHECK&lt;/code&gt; cannot hold a subquery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-based (date against a reference day)&lt;/td&gt;
&lt;td&gt;mostly &lt;code&gt;W&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pipeline only — a constraint ages badly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two readings sit in this table. Top to bottom: the harder the guarantee, the higher the rule stands, and only the &lt;code&gt;E&lt;/code&gt; rows reach the schema. And across: the place is never "either source or target". Every constraint row carries both, because the pre-filter delivers the findings and the constraint the guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why is binary pass/fail not enough?&lt;/strong&gt;&lt;br&gt;
Because it withholds the urgency of a finding. Either every triviality then blocks the load, or nothing blocks and the findings list goes stale. The three severity levels error, warning and information separate "must not proceed" from "worth a look" and "for information only", and they make the reaction configurable per rule instead of negotiable per night shift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which check rules belong in a CHECK constraint?&lt;/strong&gt;&lt;br&gt;
Only error rules whose violation must never reach the target, and of those only the ones a constraint can express: checks on a single row without a time reference and without looking into other tables. Warnings never belong in a constraint, because a constraint that rejects non-blocking findings contradicts the rule's own definition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when I promote a warning to an error?&lt;/strong&gt;&lt;br&gt;
Two things. From the next run on, affected records block at the quality gate. And the target schema should follow, because an error rule deserves a constraint counterpart. For that, the existing data has to be clean: in Postgres, &lt;code&gt;ADD CONSTRAINT … NOT VALID&lt;/code&gt; takes the guarantee on immediately for new rows, then the existing data is cleaned up and re-checked via &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a warning block the load?&lt;/strong&gt;&lt;br&gt;
No, and that is its purpose. A record with warning findings passes the quality gate, and the finding stays in the log with a plain-text message. Blocking is reserved for the severity error alone. A warning that would need to hold up the process is misclassified and should be promoted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does data quality belong in dbt and Great Expectations or in the database?&lt;/strong&gt;&lt;br&gt;
Both have their place. Pipeline tools like dbt tests, Great Expectations or Soda are expressive, well maintained and know severity gradations themselves. Their checks, however, only apply to paths through the pipeline. A direct &lt;code&gt;INSERT&lt;/code&gt; past it hits none of them. Hard error rules therefore deserve an additional constraint in the target schema that every write path respects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Framework and routines:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Data Quality Checks with SQL&lt;/a&gt; — the series' sub-hub: error table, rule configuration, runner and the quality gate whose severity mechanics this article deepens.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/validate-data-with-sql/" rel="noopener noreferrer"&gt;Validate Data with SQL&lt;/a&gt; — the WHERE routine: value ranges, mandatory fields and the NULL trap of three-valued logic.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/find-duplicates-with-sql/" rel="noopener noreferrer"&gt;Find Duplicates with SQL&lt;/a&gt; — the uniqueness routine: cardinality, composite keys and the NULL semantics of UNIQUE.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/find-orphaned-records-sql/" rel="noopener noreferrer"&gt;Find Orphaned Records with SQL&lt;/a&gt; — the lookup routine: checking referential integrity without foreign keys.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/derive-data-quality-rules-from-schema/" rel="noopener noreferrer"&gt;Derive Data Quality Rules from the Schema&lt;/a&gt; — the routing's opposite direction: target constraints become error rules of the configuration, projected mechanically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Theory:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-dimensions-error-classes/" rel="noopener noreferrer"&gt;Data Quality: Dimensions and Error Classes&lt;/a&gt; — the conceptual frame: which dimensions the checks cover and which they don't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Target schema and deployment:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres Table Conventions&lt;/a&gt; — which keys and constraints a target table should carry so hard counterparts exist at all.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/add-not-null-column-existing-table-postgres/" rel="noopener noreferrer"&gt;Adding a NOT NULL Column to an Existing Table&lt;/a&gt; — the same promotion pattern for mandatory fields: expand/contract without downtime.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/design-pattern-the-architecture-of-an-etl-process-how-to-isolate-bad-data-cleanly/" rel="noopener noreferrer"&gt;Design Pattern // The Architecture of an ETL Process&lt;/a&gt; — the staging architecture in which the pre-filter and the gate have their place.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataquality</category>
      <category>sql</category>
      <category>postgres</category>
      <category>database</category>
    </item>
  </channel>
</rss>
