<?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>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>
    <item>
      <title>Database CI/CD with PostgreSQL — the Complete Lifecycle from Object File to Automated Deploy</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Mon, 03 Aug 2026 19:30:30 +0000</pubDate>
      <link>https://dev.to/marcus1968/database-cicd-with-postgresql-the-complete-lifecycle-from-object-file-to-automated-deploy-5gi0</link>
      <guid>https://dev.to/marcus1968/database-cicd-with-postgresql-the-complete-lifecycle-from-object-file-to-automated-deploy-5gi0</guid>
      <description>&lt;p&gt;In many projects the database schema lives in the database instead of the repository — grown out of years of hand-run &lt;code&gt;ALTER&lt;/code&gt;s, fully documented nowhere. It only becomes visible when a second environment is needed or a deploy breaks. &lt;strong&gt;Postgres database CI/CD&lt;/strong&gt; flips that relationship: the repository describes the desired state, and every environment — from the throwaway database in CI to production — is built from the same versioned scripts. This article walks the complete lifecycle in six stations, deliberately without a migration framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key points up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The lifecycle in six stations: bootstrap, object deploy, tests, evolution with data, CI gate, teardown.&lt;/li&gt;
&lt;li&gt;Each station solves a concrete problem — and has its own deep-dive article in this cluster.&lt;/li&gt;
&lt;li&gt;The tools: Bash, &lt;code&gt;psql&lt;/code&gt;, and GitHub Actions — deliberately without Flyway or Liquibase.&lt;/li&gt;
&lt;li&gt;The honest limit: when a migration tool is the better choice — and which stations it still will not take off your hands.&lt;/li&gt;
&lt;li&gt;Everything runs: the command sequence comes from the open starter kit and is verified end-to-end against Postgres 17.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Postgres, a bash, and a GitHub repository; Docker if you want to replay the lifecycle locally. All building blocks come from the open &lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;DI² Starter Kit on GitHub&lt;/a&gt; — the scripts, the example schema tree, and the CI pipeline live there in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lifecycle in Six Stations
&lt;/h2&gt;

&lt;p&gt;"Introducing database CI/CD" often gets translated as "installing a migration tool". That falls short — a tool answers only part of the question. The complete lifecycle of a database environment has six stations, and each of them needs an answer, framework or not:&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%2Fph13z0o7mka4xv46gk1u.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%2Fph13z0o7mka4xv46gk1u.png" alt="Pipeline diagram of the database deployment lifecycle: six stations from Bootstrap (create.sh) through Deploy (deploy.sh), Test (db/tests), Evolve (transitions), and CI Gate (ci.yml, highlighted in blue) to Teardown (drop.sh); a dashed arrow leads from Teardown back to Bootstrap." width="800" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a command sequence, the whole cycle is unspectacular — and that is precisely the ambition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash db/scripts/create.sh &lt;span class="nb"&gt;local&lt;/span&gt;        &lt;span class="c"&gt;# Station 1: bootstrap — database, roles, schema&lt;/span&gt;
bash db/scripts/deploy.sh all &lt;span class="nb"&gt;local&lt;/span&gt;    &lt;span class="c"&gt;# Station 2: object deploy — all schema objects&lt;/span&gt;
bash db/tests/run.sh                   &lt;span class="c"&gt;# Station 3: object tests against a throwaway DB&lt;/span&gt;
bash db/scripts/deploy.sh all &lt;span class="nb"&gt;local&lt;/span&gt;    &lt;span class="c"&gt;# second run: idempotency check + run-once skip&lt;/span&gt;
bash db/scripts/drop.sh &lt;span class="nb"&gt;local&lt;/span&gt;          &lt;span class="c"&gt;# Station 6: teardown — database and roles gone for good&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These five lines are verified end-to-end against Postgres 17 in a throwaway Docker container: bootstrap, full deploy, and all object tests run green, the second deploy acknowledges the run-once scripts with &lt;code&gt;skipped (already applied)&lt;/code&gt;, and after the teardown, database and roles are gone without a trace — &lt;code&gt;pg_database&lt;/code&gt; no longer contains an entry for the created database, &lt;code&gt;pg_roles&lt;/code&gt; none of the created roles. Station 4 (evolution with data) and station 5 (CI gate) have no commands of their own: the evolution lives in the deployed files themselves, and the gate runs the same sequence automatically on every pull request.&lt;/p&gt;

&lt;p&gt;The following sections walk through the stations — each with the problem the station solves and a pointer to the article that provides the depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 1: Bootstrap — Creating Database, Roles, and Schema Reproducibly
&lt;/h2&gt;

&lt;p&gt;Before any schema object can be deployed, the environment has to exist: the database itself, the extensions, the application schema, and the roles. The bootstrap creates all of that with one script call — &lt;code&gt;create.sh &amp;lt;env&amp;gt;&lt;/code&gt; — and it does so for every environment with the same scripts. &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;dev&lt;/code&gt;, and &lt;code&gt;prod&lt;/code&gt; differ only in a small configuration file with connection coordinates, not in the procedure.&lt;/p&gt;

&lt;p&gt;Two decisions shape this station. First, the &lt;strong&gt;role separation&lt;/strong&gt;: four roles emerge, each with a clear job — the database owner, the schema owner (under which every later deploy runs), a read-write group role, and the service account the application connects with. Passwords never end up in files: the script only needs the existing superuser password; for the three new roles you choose your own passwords and, ideally, put them straight into the password manager.&lt;/p&gt;

&lt;p&gt;Second, the &lt;strong&gt;drop-and-recreate semantics&lt;/strong&gt;: the bootstrap is deliberately not idempotent. If the database or roles already exist, a preflight aborts and points to the teardown — a half-overwritten setup would be worse than an aborted one. Reproducibility here means: an environment is built entirely from the versioned scripts — not adjusted by hand after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 2: Object Deploy — the Directory Is the Migration Plan
&lt;/h2&gt;

&lt;p&gt;The object deploy is the centerpiece of the model. Every schema object — table, function, procedure, trigger, view — lives as its own file in the repository and describes its &lt;strong&gt;desired state&lt;/strong&gt;, phrased idempotently: &lt;code&gt;CREATE TABLE IF NOT EXISTS&lt;/code&gt; and &lt;code&gt;CREATE OR REPLACE&lt;/code&gt; where the engine offers them; where it does not — for triggers or constraints — &lt;code&gt;DROP … IF EXISTS&lt;/code&gt; plus re-creation does the same job, at the price that the object briefly disappears and is recreated during the deploy. The deploy runner loads these files in a fixed section order straight from the directory structure — tables before functions, functions before triggers — and within each section by file-name number. There is no separate deploy plan to maintain (and forget): &lt;strong&gt;the directory is the migration plan.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The essential advantage over a classic migration chain: the DDL core of every schema object sits in one place, readable as one file — not spread across forty change scripts building on each other. And every deploy is the same call, whether against an empty or a current database: &lt;code&gt;deploy.sh all &amp;lt;env&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why this model works without a migration chain, what the directory convention looks like in detail, and where its limits are, is laid out in &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 foundation all further stations build on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 3: Apply-Smoke and Object Tests
&lt;/h2&gt;

&lt;p&gt;A merged DDL change that does not run on an empty database is broken by definition — it just does not fail where it was written, but on the &lt;strong&gt;next&lt;/strong&gt; environment that gets built from zero. What helps against this error class is a discipline, not a tool: before every merge, the complete deploy runs once against an empty throwaway database — the apply-smoke.&lt;/p&gt;

&lt;p&gt;On top of that come the &lt;strong&gt;object tests&lt;/strong&gt;: one SQL file per schema object with &lt;code&gt;DO $$ … ASSERT&lt;/code&gt; blocks checking the happy path, expected error guards, and constraints. Both together run locally in a self-cleaning Docker container (&lt;code&gt;db/tests/run.sh&lt;/code&gt;) — and identically once more in the CI gate of station 5. Whoever forgets the smoke locally is reminded in the pull request at the latest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 4: Evolution with Data
&lt;/h2&gt;

&lt;p&gt;Before go-live, schema evolution is trivial: teardown, bootstrap, deploy — the database is disposable. As soon as environments hold data nobody may lose, this station becomes the most demanding part of the lifecycle. The framework-free model answers it with two building blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Convergent object files:&lt;/strong&gt; an existing table is never dropped and recreated. New columns join the object file as an idempotent &lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html" rel="noopener noreferrer"&gt;&lt;code&gt;ALTER TABLE … ADD COLUMN IF NOT EXISTS&lt;/code&gt;&lt;/a&gt; — the file keeps describing the desired state, and every environment converges to it on the next deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run-once transitions:&lt;/strong&gt; everything that cannot be phrased idempotently — backfills, conversions, data rescues — becomes a timestamped change script that runs exactly once per database. A tracker inside the database records what has already run, by filename and checksum; editing an applied script aborts the deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The harder cases — renames, type changes, dropped columns — follow the same two-building-block pattern, but almost always need the transition part plus an intermediate step in which the old and the new form exist side by side.&lt;/p&gt;

&lt;p&gt;The two deep dives: &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 a Populated Table&lt;/a&gt; plays through the most common single case — the expand/contract pattern that makes structural change and backfill work together on a populated table. And &lt;a href="https://sql.marcus-belz.de/en/track-database-schema-changes/" rel="noopener noreferrer"&gt;Tracking Schema Changes Without a Framework&lt;/a&gt; builds the run-once tracker from scratch — including the realization that Flyway and Liquibase make the same four design decisions in their tracking core, for all the tools bring along beyond it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 5: CI as the Gate — GitHub Actions
&lt;/h2&gt;

&lt;p&gt;All previous stations run on demand — station 5 makes them binding. On every pull request, GitHub Actions spins up a Postgres &lt;a href="https://docs.github.com/en/actions/using-containerized-services/creating-postgresql-service-containers" rel="noopener noreferrer"&gt;service container&lt;/a&gt; as a throwaway database and runs half the lifecycle on it automatically: bootstrap, full deploy, object tests — and then a &lt;strong&gt;second deploy in the same run&lt;/strong&gt;, which checks the idempotency of all object files and the skip path of the run-once scripts. Wired up as a required status check, a red run blocks the merge.&lt;/p&gt;

&lt;p&gt;The gate catches the error classes that can be checked mechanically — syntax and structural consistency, load order, idempotency — and needs not a single secret to do so. What the workflow looks like in detail, what exactly the double-deploy trick checks, and how it continues from the gate to deploys on real environments: &lt;a href="https://sql.marcus-belz.de/en/github-actions-postgres-deployment/" rel="noopener noreferrer"&gt;GitHub Actions for Postgres Deploys&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 6: Teardown
&lt;/h2&gt;

&lt;p&gt;The teardown is the underrated station of the lifecycle. &lt;code&gt;drop.sh &amp;lt;env&amp;gt;&lt;/code&gt; terminates active connections, drops the database, and removes all four roles — each with &lt;code&gt;IF EXISTS&lt;/code&gt;, so the script can safely run again after a partial failure. In the verified run, nothing was left behind: no entry for the database left in &lt;code&gt;pg_database&lt;/code&gt;, none of the four roles left in &lt;code&gt;pg_roles&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why a station of its own for throwing things away? Because teardown is the &lt;strong&gt;litmus test for stations 1 and 2&lt;/strong&gt;: an environment may only disappear without hesitation if bootstrap and object deploy can rebuild it completely. If the thought of &lt;code&gt;drop.sh&lt;/code&gt; makes you nervous because knowledge lives in the database that exists nowhere in the repository, the lifecycle is not closed yet. Before go-live, drop-and-rebuild is the normal reset path; after that, the hard rule applies: never on an environment with data worth protecting without a verified backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Does a Migration Tool Make Sense?
&lt;/h2&gt;

&lt;p&gt;The framework-free lifecycle carries well as long as three things come together: &lt;strong&gt;one&lt;/strong&gt; database engine, a manageable team, and a deploy that is shell-based anyway. Then Bash and &lt;code&gt;psql&lt;/code&gt; deliver the core — desired-state files, run-once tracking, CI gate — without a new dependency in the stack. A tacit prerequisite here: the schema can be described fully declaratively in the repository. Where externally managed objects, replication setups, or extensive partitioning are involved, the claim of a "complete lifecycle" only holds with reservations.&lt;/p&gt;

&lt;p&gt;A migration tool like &lt;a href="https://flywaydb.org/" rel="noopener noreferrer"&gt;Flyway&lt;/a&gt; or &lt;a href="https://www.liquibase.com/" rel="noopener noreferrer"&gt;Liquibase&lt;/a&gt; becomes legitimate as soon as its extras are genuinely needed: support for multiple database engines, rollback or undo workflows, a lock table against concurrently running deploys, dry-run and report tooling for audit requirements, programmatic migrations. Add to that what cannot be pinned to individual stations: an established ecosystem with documentation, IDE integration, and support — and a standard several teams can agree on without having to learn a home-grown build first. From multi-team or multi-database operation onward, these are no longer comfort features but necessities — then the framework is the right choice, without anything being wrong with the model shown here. Beyond the scope of this article remain the operational topics outside the deploy model: zero-downtime strategies such as blue/green or rolling deployments, and the locking questions of very large tables — the latter is shown on a concrete case in the &lt;a href="https://sql.marcus-belz.de/en/add-not-null-column-existing-table-postgres/" rel="noopener noreferrer"&gt;expand/contract article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The honest assessment cuts the other way too, though: &lt;strong&gt;at its core, a migration tool replaces two of the six stations&lt;/strong&gt; — the object deploy and the evolution mechanics. Bootstrap, object tests, the CI gate, and the teardown remain your own work, framework or not. Having built the lifecycle once yourself, you make the tooling decision from the inside: you know which stations the tool takes over, which ones it leaves open — and you read error messages like "Migration checksum mismatch" for what they are: the same design decisions, just behind a different surface.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do you need Flyway or Liquibase for database CI/CD with Postgres?&lt;/strong&gt;&lt;br&gt;
No, not necessarily. The core — versioned desired-state files, run-once tracking for data changes, and a CI gate against a throwaway database — fits into Bash and &lt;code&gt;psql&lt;/code&gt;. A framework pays off when its extras are needed: multi-engine support, rollback workflows, a lock against concurrent deploys, or audit reporting. It takes two of the six lifecycle stations off your hands; four remain your own work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you deploy a Postgres schema automatically?&lt;/strong&gt;&lt;br&gt;
With a script runner that applies the object files from the repository in a fixed section and number order, and a CI workflow that calls exactly this runner — on every pull request against a throwaway database, on release against the real environment. Since every file describes its desired state idempotently, the deploy is always the same call, whether the database is empty or current.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between an object deploy and migrations?&lt;/strong&gt;&lt;br&gt;
A migration chain describes the path: numbered change scripts that build on each other and run in sequence. An object deploy describes the destination: one file per schema object with its desired state, applicable any number of times. The object approach keeps the current state readable in one place; for data-dependent steps it additionally needs run-once scripts — exactly the point where the two models meet again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you test DDL changes before the merge?&lt;/strong&gt;&lt;br&gt;
With a full dress rehearsal against an empty throwaway database: complete deploy, then object tests as &lt;code&gt;DO $$ … ASSERT&lt;/code&gt; blocks, then a second deploy as the idempotency check. Locally a Docker container takes care of it; in the pull request, the same procedure runs as a GitHub Actions gate. What this test bench does not see — effects on existing data, locks on large tables — remains the job of the evolution discipline from station 4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What roles does a Postgres environment need at minimum?&lt;/strong&gt;&lt;br&gt;
Four have proven themselves, with a clear division of labor: a database owner, a schema owner under which all deploys run (deployed objects automatically belong to it), a read-write group role for data access, and a service account for the application that is a member of that group role. The &lt;code&gt;postgres&lt;/code&gt; superuser stays reserved for bootstrap and teardown and appears in no application connection.&lt;/p&gt;

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

&lt;ul&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 — Directory Convention Instead of Flyway or Liquibase&lt;/a&gt; — station 2 in depth: the apply model the whole lifecycle builds on.&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 a Populated Table — the Expand/Contract Pattern&lt;/a&gt; — station 4 played through on its most common single case.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/track-database-schema-changes/" rel="noopener noreferrer"&gt;Tracking Schema Changes Without a Framework — Run-Once Scripts, Checksums and Immutability&lt;/a&gt; — station 4: the tracker built from scratch, with the Flyway comparison.&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; — station 5 in depth: service container, double deploy, environments.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/verify-migration-data-quality/" rel="noopener noreferrer"&gt;Verifying the Migration — Data Quality and Row Reconciliation After the Move&lt;/a&gt; — if the reason for the rebuild was a database migration: the acceptance check afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;The open DI² Starter Kit on GitHub&lt;/a&gt; — all six stations runnable in context.
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Setting Up a Claude Code Project with a Development Workflow and Database — the Open Starter Kit Explained</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Wed, 29 Jul 2026 14:05:30 +0000</pubDate>
      <link>https://dev.to/marcus1968/setting-up-a-claude-code-project-with-a-development-workflow-and-database-the-open-starter-kit-19k4</link>
      <guid>https://dev.to/marcus1968/setting-up-a-claude-code-project-with-a-development-workflow-and-database-the-open-starter-kit-19k4</guid>
      <description>&lt;p&gt;An empty repo and Claude Code, Anthropic's coding agent in the terminal — that's all it takes to get going. And that is exactly the problem: the model writes code immediately, but by default nothing ensures that a specification exists first, that a review happens afterwards, or that the database schema deploys reproducibly. A Claude Code project setup is therefore less a tooling decision than a process decision — the question is not whether the AI can code, but what sequence it works through.&lt;/p&gt;

&lt;p&gt;This article shows a complete setup for that, in overview: the open DI² starter kit — a fixed, numbered development workflow from requirements to deploy, a deployable database part, and a template that prunes itself during setup. It was not designed on a drawing board; it is a distillate of a running project.&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 scaffold:&lt;/strong&gt; conventions live as rules, tools as skills in &lt;code&gt;.claude/&lt;/code&gt; — and the difference between the two decides what every session costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The workflow:&lt;/strong&gt; eleven numbered phases from &lt;code&gt;/init&lt;/code&gt; to &lt;code&gt;/deploy&lt;/code&gt;, with the bug loop running across them; each feature spec is the contract the phases work against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The database part:&lt;/strong&gt; plain SQL with a directory convention, idempotently deployable, tested in CI against a throwaway database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The design decision:&lt;/strong&gt; start maximal and prune via the &lt;code&gt;/init&lt;/code&gt; interview, instead of starting empty and building up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The deliberate omissions:&lt;/strong&gt; no hooks, no subagents — simplicity is part of the design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open on GitHub:&lt;/strong&gt; the kit is forkable as a starter kit (MIT license).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Claude Code and an editor; for the database part, optionally a PostgreSQL (local or in a container). The examples come from the open &lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;DI² starter kit on GitHub&lt;/a&gt;. Mechanics as of July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scaffold: What Lives in .claude/
&lt;/h2&gt;

&lt;p&gt;Setting up a Claude Code project starts at the top level with four directories — &lt;code&gt;.claude/&lt;/code&gt;, &lt;code&gt;docs/&lt;/code&gt;, &lt;code&gt;features/&lt;/code&gt; and &lt;code&gt;db/&lt;/code&gt;:&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/      loads automatically: conventions, security rules, the SQL rule tree — and stack.md
└── skills/     loads on demand: one folder per workflow phase, invoked as /requirements, /qa, …
docs/           PRD, bug tracking, knowledge base
features/       INDEX.md + one spec per feature — the project's working state
db/             the database part: schemas, deploy scripts, tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The split under &lt;code&gt;.claude/&lt;/code&gt; is the scaffold's most important mechanism. &lt;strong&gt;Rules&lt;/strong&gt; — the &lt;code&gt;CLAUDE.md&lt;/code&gt; and every file under &lt;code&gt;.claude/rules/&lt;/code&gt; — are fully in context at every session start, in the model's working memory: the constitution of the project, applying without being asked. &lt;strong&gt;Skills&lt;/strong&gt; are tools on demand: initially only their description line sits in context; the body loads on invocation. One rule holds a special role: &lt;code&gt;stack.md&lt;/code&gt; is the authoritative source for the tech stack — runtime, UI, database, deploy, CI including the build and test commands; the &lt;code&gt;CLAUDE.md&lt;/code&gt; carries only a short summary of it. That keeps the skills stack-neutral: they read the stack at runtime instead of hard-coding it in their own text.&lt;/p&gt;

&lt;p&gt;How the two loading paths differ in detail — and what an overstuffed rules folder costs every single session in context — is taken apart in &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow: From Requirements to Deploy
&lt;/h2&gt;

&lt;p&gt;The heart of the setup is a numbered sequence: eleven phases, 0 through 10, each one its own skill.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 /init → 1 /requirements → 2 /architecture → [3 /ux] → [4 /backend] → [5 /frontend]
        → 6 /qa → 7 /review → 8 /check-updates → 9 /security → 10 /deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fr8cwhj55h1j6pekhr38g.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%2Fr8cwhj55h1j6pekhr38g.png" alt="The starter kit's numbered workflow: top row /init (0), /requirements (1), /architecture (2), /ux (3), /backend (4), /frontend (5); bottom row /qa (6), /review (7), /check-updates (8), /security (9), /deploy (10). /ux, /backend and /frontend are dashed as optional, the arrow between /security and /deploy is labeled " width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Specs drive the sequence: &lt;code&gt;/requirements&lt;/code&gt; writes one specification per feature, with user stories and acceptance criteria, and &lt;code&gt;features/INDEX.md&lt;/code&gt; tracks the status of all features — the single overview every later phase orients itself by. The rest of the chain works against that spec: &lt;code&gt;/architecture&lt;/code&gt; drafts the technical design, &lt;code&gt;/qa&lt;/code&gt; tests against the acceptance criteria, &lt;code&gt;/review&lt;/code&gt; checks the diff against spec and conventions. Played through on a small feature, it 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;/requirements   spec feat-0001-csv-import.md — user stories + acceptance criteria
/architecture   design: staging table plus COPY import; no UI → /ux is skipped
/backend        implementation against the spec
/qa             tests against the spec's acceptance criteria
/review         diff check against spec and conventions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The square brackets in the chain mark phases not every project needs: an API service without a user interface knows no &lt;code&gt;/ux&lt;/code&gt; and no &lt;code&gt;/frontend&lt;/code&gt;; the pruning at setup time removes them entirely. And phase 0 falls out of line: &lt;code&gt;/init&lt;/code&gt; runs exactly once, at setup — more on that in a moment.&lt;/p&gt;

&lt;p&gt;Two properties keep the sequence in check. First, no phase jumps to the next on its own: every transition is triggered by a human; the kit calls this human-in-the-loop. Second, &lt;code&gt;/security&lt;/code&gt; is set as a gate: the project-wide security audit comes before a production deploy, not after it. Running across all phases are &lt;code&gt;/bug&lt;/code&gt; (records and closes bugs under &lt;code&gt;docs/bugs/&lt;/code&gt;, at any time), &lt;code&gt;/auth&lt;/code&gt; (for auth projects) and &lt;code&gt;/help&lt;/code&gt; — which answers the question "where am I right now, and what comes next".&lt;/p&gt;

&lt;h2&gt;
  
  
  The Database Part: Plain SQL, Deployable and Tested
&lt;/h2&gt;

&lt;p&gt;What sets this setup apart from typical Claude Code templates: it has a real database part. Under &lt;code&gt;db/&lt;/code&gt; sits a complete, runnable PostgreSQL example — tables with audit columns, procedures, triggers, a view, seed data, plus deploy scripts and tests for the database objects. None of this is carried by a migration tool but by a directory convention: object files in fixed numbering, written idempotently, an apply runner plays them in in a defined order; one-off, data-dependent steps run alongside as run-once transitions. In CI, every schema state is brought up against a throwaway database and tested before it reaches a real environment. Doing without a migration tool is a deliberate decision of this setup, not a universal recommendation: where a team already has an established migration tool in place, there is no reason to switch.&lt;/p&gt;

&lt;p&gt;The conventions these SQL objects are written to — naming, alignment, object skeletons — live as a rule tree under &lt;code&gt;.claude/rules/sql/&lt;/code&gt; and are the subject of the sister overview &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;, with its deep dives on &lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;tables&lt;/a&gt;, &lt;a href="https://sql.marcus-belz.de/en/plpgsql-procedure-conventions/" rel="noopener noreferrer"&gt;procedures&lt;/a&gt; and &lt;a href="https://sql.marcus-belz.de/en/plpgsql-function-conventions/" rel="noopener noreferrer"&gt;functions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How the deploy works without Flyway or Liquibase in detail — idempotency, run-once transitions, the &lt;code&gt;schema_apply_log&lt;/code&gt; table, the runner and the CI safety net — is described in &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;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Design Decision: Start Maximal, Then Prune
&lt;/h2&gt;

&lt;p&gt;The template ships all fourteen skills and every rule tree — including the parts a given project will never need. Pruning happens at setup: &lt;code&gt;/init&lt;/code&gt; is a one-time bootstrap interview. It first captures the product vision and the stack's parameters — runtime, UI, backend, database, auth, deploy, CI — and writes the vision into the &lt;code&gt;CLAUDE.md&lt;/code&gt;, the stack profile into &lt;code&gt;stack.md&lt;/code&gt;. It then removes what the answers rule out: no UI means &lt;code&gt;/ux&lt;/code&gt; and &lt;code&gt;/frontend&lt;/code&gt; go, UI rule tree included; no database means the entire &lt;code&gt;db/&lt;/code&gt; tree disappears. At the end, &lt;code&gt;/init&lt;/code&gt; deletes itself — a bootstrap runs only once.&lt;/p&gt;

&lt;p&gt;Why delete instead of build up? Because deleting is the easier operation: whoever deletes decides against the complete picture: every file is a yes/no question. Whoever builds up has to remember what is missing, and forgotten things don't speak up. On top of that comes the context math: rules that don't apply would otherwise load into every single session.&lt;/p&gt;

&lt;p&gt;The interview, the role of &lt;code&gt;stack.md&lt;/code&gt; as single source of truth, and the full pruning matrix — which answer removes which files — are described in &lt;a href="https://sql.marcus-belz.de/en/claude-code-project-template-self-pruning/" rel="noopener noreferrer"&gt;Maximal Template Over Empty Repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Kit Deliberately Leaves Out
&lt;/h2&gt;

&lt;p&gt;An overview is only complete with what's missing — and the starter kit leaves out a few things on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No hooks.&lt;/strong&gt; Claude Code can bind shell commands to events, to be executed automatically — before a tool call, after a change, at session start. The kit passes on that: what happens, happens visibly in the dialog and is written in the rules, where the human reads it and the model follows it. That costs a degree of automation but saves the hard-to-trace layer between intent and execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No subagents.&lt;/strong&gt; Claude Code can hand subtasks to separate agents with their own context. Here too the kit keeps the work in the main context, where the human sees and can steer it. It is the same decision that keeps the phase transitions user-initiated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No app skeleton.&lt;/strong&gt; There is no &lt;code&gt;src/&lt;/code&gt;: the kit is not a Next.js, Python or whatever template. The application scaffold emerges through the workflow, against the stack the project chose in the interview.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exactly one option per matter of taste.&lt;/strong&gt; One UI ruleset (React/Tailwind/shadcn), two SQL dialects (PostgreSQL and SQL Server) — no further variants sit side by side. If a project needs a different one, the rule is: port, don't stack — copy the shipped rule tree, adapt it, remove the original.&lt;/p&gt;

&lt;p&gt;These omissions are part of the picture: the kit is not a framework claiming completeness but an opinionated setup from a real project. Where an opinion doesn't fit, it is replaceable in Markdown files — that is exactly why everything lies open.&lt;/p&gt;

&lt;p&gt;Honest framing also includes the counter-calculation: the workflow costs real extra effort per feature: spec, QA and review are steps of their own, not side effects. For a throwaway script or a quick prototype the setup is oversized; there, the empty repo remains the better choice. And the structure only carries with discipline: skip phases regularly and you end up with the folders, but not the process. What that effort buys, the comparison shows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Empty repo&lt;/th&gt;
&lt;th&gt;Starter kit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requirements&lt;/td&gt;
&lt;td&gt;in the chat history&lt;/td&gt;
&lt;td&gt;one spec per feature, versioned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order&lt;/td&gt;
&lt;td&gt;ad hoc&lt;/td&gt;
&lt;td&gt;eleven numbered phases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conventions&lt;/td&gt;
&lt;td&gt;re-explained every session&lt;/td&gt;
&lt;td&gt;rules, loaded automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review and QA&lt;/td&gt;
&lt;td&gt;on request&lt;/td&gt;
&lt;td&gt;phases of their own, security as a gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;manual work&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;db/&lt;/code&gt; tree, idempotent and CI-tested&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Setting Up a New Claude Code Project with the Starter Kit
&lt;/h2&gt;

&lt;p&gt;The kit is public on &lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, MIT-licensed. For a new project there are two equivalent paths: use the repository as a GitHub template ("Use this template") or run the bundled copy script (&lt;code&gt;new-project.ps1&lt;/code&gt; or &lt;code&gt;new-project.sh&lt;/code&gt;). Both paths produce a repository with a fresh Git history. A direct &lt;code&gt;git clone&lt;/code&gt; of the template is off-limits as a project start: it drags along the template's history and upstream linkage, neither of which belongs in the new project.&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="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;my-project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--template&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;marcusbelz/di2-starter-kit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--clone&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open the new project in Claude Code, invoke &lt;code&gt;/init&lt;/code&gt;, answer the interview — and specify the first feature with &lt;code&gt;/requirements&lt;/code&gt;. From there, the numbered workflow carries.&lt;/p&gt;

&lt;p&gt;Two transparency notes belong to the provenance. First, the lineage: the kit is the generalized, product-free &lt;code&gt;.claude/&lt;/code&gt; toolbox of the DI² project. That project in turn descends from the AI Coding Starter Kit by AlexPEClub (MIT); the attribution is documented in the repository. Second, the making: the texts in the kit are written by Claude, under human direction — every file commissioned, checked and signed off; the README states this working mode openly. And an honest maturity note: the conventions grew out of daily use in the DI² project and are proven there; the packaging as a self-pruning template is young. Whoever adopts the kit takes on a well-thought-out setup, not a framework matured over many years.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How do I set up a Claude Code project in a structured way?&lt;/strong&gt;&lt;br&gt;
With a template that brings process and conventions along: create a new repository from the template (GitHub "Use this template" or the copy script — no direct &lt;code&gt;git clone&lt;/code&gt;), invoke &lt;code&gt;/init&lt;/code&gt; in Claude Code and answer the interview about vision and tech stack. After that the project prunes itself, and &lt;code&gt;/requirements&lt;/code&gt; specifies the first feature — from there the numbered workflow leads all the way to deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a Claude Code project need a fixed folder structure?&lt;/strong&gt;&lt;br&gt;
Only &lt;code&gt;.claude/&lt;/code&gt; with rules and skills is a convention of Claude Code itself. Everything else — &lt;code&gt;docs/&lt;/code&gt;, &lt;code&gt;features/&lt;/code&gt;, &lt;code&gt;db/&lt;/code&gt; — is a decision of the setup. But that is exactly where the difference between "the model writes code" and "the project has a process" lives: specs, bugs and schema states need a fixed place, or they exist only in the chat history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between rules and skills?&lt;/strong&gt;&lt;br&gt;
Rules load fully into context at every session start and apply without being asked; skills load on demand and run on command. The decision rule: what must always apply becomes a rule — what executes as a procedure becomes a skill. The loading mechanics in detail are in &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I integrate a database into the Claude Code workflow?&lt;/strong&gt;&lt;br&gt;
As a dedicated &lt;code&gt;db/&lt;/code&gt; tree in the repository: object files in a directory convention, written idempotently, a runner plays them in, and CI tests every state against a throwaway database. No migration tool is needed for that — how it works is shown in &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;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the starter kit tied to a tech stack?&lt;/strong&gt;&lt;br&gt;
No. The workflow skills name no language and no framework — they read the stack at runtime from &lt;code&gt;stack.md&lt;/code&gt;, which &lt;code&gt;/init&lt;/code&gt; fills at setup. Stack-specific are only the rule trees for UI and SQL dialect; the pruning keeps exactly the variant the project chose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the setup work with Cursor or GitHub Copilot — why Claude Code?&lt;/strong&gt;&lt;br&gt;
The building blocks are tied to Claude Code mechanics: rules and skills under &lt;code&gt;.claude/&lt;/code&gt; with their distinct loading paths exist natively there, as do the slash invocations of the phases. The ideas behind them — specs as a contract, numbered phases, security before deploy — are tool-neutral: anyone working with Cursor or Copilot can adopt the process but would have to port the template themselves.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-project-template-self-pruning/" rel="noopener noreferrer"&gt;Maximal Template Over Empty Repo — a Claude Code Setup That Prunes Itself via /init&lt;/a&gt; — the design decision "start maximal, then prune" in detail.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt; — the loading mechanics behind the scaffold.&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 database part in depth.&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 sister overview: the SQL conventions that live in the kit as a rule tree.&lt;/li&gt;
&lt;li&gt;SQL depth behind the rule tree: &lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres Table Conventions&lt;/a&gt;, &lt;a href="https://sql.marcus-belz.de/en/plpgsql-procedure-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Procedure Conventions&lt;/a&gt;, &lt;a href="https://sql.marcus-belz.de/en/plpgsql-function-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Function Conventions&lt;/a&gt;.&lt;/li&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; — the experience report on this setup's rule structure: anatomy, don'ts, inventories.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;The open DI² starter kit on GitHub&lt;/a&gt; — template, workflow skills and database part, ready to fork.

&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Gave Claude Code 27 Rule Files Instead of One CLAUDE.md</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sat, 25 Jul 2026 16:49:51 +0000</pubDate>
      <link>https://dev.to/marcus1968/i-gave-claude-code-27-rule-files-instead-of-one-claudemd-4nl0</link>
      <guid>https://dev.to/marcus1968/i-gave-claude-code-27-rule-files-instead-of-one-claudemd-4nl0</guid>
      <description>&lt;p&gt;There is a moment when a CLAUDE.md tips over. That moment does not sit on any calendar, it shows in behavior: the file keeps getting longer, and the rules inside it keep getting followed less. Every new convention you write in dilutes the ones already there. That is exactly where the project behind this article stood — and the answer was not a better CLAUDE.md, but its dissolution into individual rule files. Today there are 27 of them.&lt;/p&gt;

&lt;p&gt;This article is the experience report: how Claude Code rules need to be structured so they hold up over months, what a single rule file must carry, and why the most important parts are not the rules themselves. They are the don'ts and the inventories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key points up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One file per convention&lt;/strong&gt; — 27 rule files instead of one CLAUDE.md, each with a single topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anatomy of a rule that holds:&lt;/strong&gt; rule, reasoning, don'ts, inventory, cross-references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don'ts beat prescriptions&lt;/strong&gt; — at least in the DI² project: a negative example is concrete and recognizable, a prescription competes against the training prior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inventories are the drift radar:&lt;/strong&gt; the part of a rule that claims an actual state of the code — and therefore the first to stand out when code and documentation diverge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;paths:&lt;/code&gt; scoping&lt;/strong&gt; lets large rules load only where they apply — 3 of the 27 files use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The honest downside:&lt;/strong&gt; maintenance effort, rule conflicts, and a state in which the documentation deliberately runs ahead of the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites.&lt;/strong&gt; A project with Claude Code and a &lt;code&gt;.claude/rules/&lt;/code&gt; directory. The pattern transfers to any coding agent that loads convention files into its context — Cursor rules or comparable mechanisms in other tools work on the same principle. What loads into context when, and what that costs, is covered by the sibling article &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt; — this article starts one level earlier: at the question of what the rule files themselves have to look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Starting Point: One File That Kept Growing
&lt;/h2&gt;

&lt;p&gt;The project behind the numbers is DI², an ETL generator built on Next.js and PostgreSQL, its code written almost entirely AI-assisted with Claude Code. In the beginning, all conventions lived where every Claude Code project collects them first: in the CLAUDE.md. That works as long as the file is short. It stops working once the file becomes a container in which database conventions, color tokens and commit rules all sit side by side.&lt;/p&gt;

&lt;p&gt;This effect is hard to measure, but it is easy to feel. It also matches what research shows about long contexts: language models make measurably worse use of information sitting in the middle of long inputs (&lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;"Lost in the Middle", Liu et al. 2023&lt;/a&gt;). In my experience, a rule sitting in line 40 of a long file gets followed less than the same rule in its own, topically named file. A second effect has less to do with the model than with the humans: in a 400-line file, even the author cannot find a rule again when they want to check whether it still holds. Why a single file does not scale structurally, and which loading mechanics sit behind that, belongs to the &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;skills-vs-rules article&lt;/a&gt; — this one is about what comes after.&lt;/p&gt;

&lt;p&gt;The consequence in the project: a &lt;code&gt;.claude/rules/&lt;/code&gt; directory holding 27 rule files, one per convention (as of July 22, 2026). The same count holds for the public edition of this rule structure, the &lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;di2-starter-kit on GitHub&lt;/a&gt; — you can verify it there, not counting the subfolder READMEs. There is a file for the table conventions, one for dialogs, one for loading states, one for the security model. The directory keeps growing with the project. In the very week this article was written, three new files arrived, for views, triggers and database policies. A rule system is finished when the project is finished, which is to say never.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a Rule That Holds
&lt;/h2&gt;

&lt;p&gt;After several months with this system, a fixed structure has emerged. A rule file that holds consists of five parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# &amp;lt;Convention&amp;gt; (&amp;lt;project&amp;gt;)&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; One-sentence summary: What does this rule enforce, and where does it apply?&lt;/span&gt;

&lt;span class="gu"&gt;## The Rule&lt;/span&gt;

Every &lt;span class="nt"&gt;&amp;lt;structure&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;procedure&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;does&lt;/span&gt; &lt;span class="na"&gt;exactly&lt;/span&gt; &lt;span class="na"&gt;one&lt;/span&gt; &lt;span class="na"&gt;thing&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stated&lt;/span&gt; &lt;span class="na"&gt;imperatively&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;.
One convention per file — do not mix topics.

&lt;span class="gu"&gt;## Reasoning&lt;/span&gt;

Why this rule exists: the concrete incident, bug or review finding
that triggered it. A rule without a reason gets weighed away in
trade-offs — the reason is part of the rule, not decoration.

&lt;span class="gu"&gt;## Don'ts&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; ❌ &lt;span class="sb"&gt;`&amp;lt;concrete negative example from your own code&amp;gt;`&lt;/span&gt; — why it drifts.
&lt;span class="p"&gt;-&lt;/span&gt; ❌ &lt;span class="sb"&gt;`&amp;lt;second negative example&amp;gt;`&lt;/span&gt; — what applies instead (with the target spelling).

&lt;span class="gu"&gt;## Inventory&lt;/span&gt;

| Usage site | File | Status |
|---|---|---|
| &lt;span class="nt"&gt;&amp;lt;Site&lt;/span&gt; &lt;span class="na"&gt;A&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; | &lt;span class="sb"&gt;`components/site-a.tsx`&lt;/span&gt; | ✅ compliant |
| &lt;span class="nt"&gt;&amp;lt;Site&lt;/span&gt; &lt;span class="na"&gt;B&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; | &lt;span class="sb"&gt;`components/site-b.tsx`&lt;/span&gt; | ⏳ retrofit open |

&lt;span class="gu"&gt;## Cross-References&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;neighbor-rule.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;neighbor-rule.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; — boundary: what is governed there, not here.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule itself (the first two or three sentences under &lt;em&gt;The Rule&lt;/em&gt;) is the smallest part, and that is no accident. It states imperatively what applies. Everything beyond that belongs to the other four parts.&lt;/p&gt;

&lt;p&gt;The reasoning is not a courtesy to the reader. An agent weighs trade-offs, and as observed in the DI² project, a rule without a reason loses that weighing more easily against a plausible counterargument from the concrete case. A rule with a reason anchors what the concrete case has to argue against. The difference shows in exactly the moments that matter: namely, when the model considers an exception justified. How a convention and its reasoning come into being in the first place is described in the methodology article &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;.&lt;/p&gt;

&lt;p&gt;The remaining three parts are the actual substance of this article. The don'ts and the inventory each get their own section below, and the cross-references almost explain themselves: they draw the boundary to the neighboring rule so that two files do not creep into governing the same topic. Every boundary violation that surfaces later gets recorded there as an explicit reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Don'ts Beat Prescriptions
&lt;/h2&gt;

&lt;p&gt;This is the central observation from several months of rule maintenance in the DI² project. Whether it transfers to other projects has not been examined — it is project experience, not a study. A prescription says what should be. A negative example shows what must not be — with a concrete, recognizable spelling. The difference looks small at first glance. In practice it is not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Before: a prescription without an anchor --&amp;gt;&lt;/span&gt;

Use only the project tokens for font sizes.

&lt;span class="c"&gt;&amp;lt;!-- After: negative examples with recognition value --&amp;gt;&lt;/span&gt;

&lt;span class="gs"&gt;**Don'ts:**&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; ❌ &lt;span class="sb"&gt;`text-[12px]`&lt;/span&gt; — raw pixel value, drifts; snap target is &lt;span class="sb"&gt;`text-token-meta`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; ❌ &lt;span class="sb"&gt;`text-sm`&lt;/span&gt; — framework default instead of the project scale; in the app
  scope the linter guard flags it as an error.
&lt;span class="p"&gt;-&lt;/span&gt; ❌ Inline &lt;span class="sb"&gt;`line-height`&lt;/span&gt; override via &lt;span class="sb"&gt;`[line-height:Xpx]`&lt;/span&gt; — the scale ships
  its own line height; a deliberate override needs a code comment with
  a justification.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prescription "use only the project tokens" is factually correct, and yet it accomplishes little. At every generation it competes against the model's training prior, in which &lt;code&gt;text-sm&lt;/code&gt; is the statistically most common way to write small text. "Only project tokens" first has to be translated onto the concrete case, and the rule gets lost in that translation.&lt;/p&gt;

&lt;p&gt;The negative example skips the translation. &lt;code&gt;text-[12px]&lt;/code&gt; is precisely the string the model is about to write — it sits verbatim in the rule, marked with a ❌ and the reason. A don't leaves no room for interpretation. It additionally names the snap target, the spelling that applies instead. Whoever reads the don't, whether human or model, afterwards knows both things: what is wrong and what belongs in its place. There is no don't-specific magic behind this, but a familiar effect from prompt research: concrete examples are easier for language models to act on than abstract prescriptions. Negative examples are simply the form in which a rule file can harness that effect.&lt;/p&gt;

&lt;p&gt;The best don'ts do not come from the rule author's imagination but from real finds. Every time a review or a bug surfaces a new bypass variant, exactly that variant goes into the rule file as a don't. The rule system thereby learns the same mistakes the code has already made once. There was no shortage of material: the sibling article &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; documents the drift finding from which the font-size don'ts emerged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inventories as a Drift Radar
&lt;/h2&gt;

&lt;p&gt;A rule states a target state, and target states have an inconvenient property: they cannot become wrong. "Every dialog carries a leading icon" stays correct as a sentence even when six dialogs without icons have long been sitting in the code. The rule notices none of it.&lt;/p&gt;

&lt;p&gt;An inventory changes that. It lists the concrete usage sites of the convention together with their actual state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Inventory + Retrofit Backlog&lt;/span&gt;

As of now, this rule is the documented truth. Existing sites without
&lt;span class="nt"&gt;&amp;lt;convention&amp;gt;&lt;/span&gt; are brought in line as a tracked follow-up step (retrofit) —
until then the code deliberately lags behind the documentation.

| Dialog | File | Status |
|---|---|---|
| Editor (domain object A) | &lt;span class="sb"&gt;`components/object-a-editor-dialog.tsx`&lt;/span&gt; | ✅ icon leads left |
| Inspector (domain object B) | &lt;span class="sb"&gt;`components/object-b-inspector.tsx`&lt;/span&gt; | ✅ icon leads left |
| Invite user | &lt;span class="sb"&gt;`components/user-invite-dialog.tsx`&lt;/span&gt; | ⏳ retrofit open |
| Bulk delete | &lt;span class="sb"&gt;`components/bulk-delete-dialogs.tsx`&lt;/span&gt; | ⏳ retrofit open (destructive tone) |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table is the part of a rule file that can fail against the actual state of the code. A reasoning section or a cross-reference can go stale too, but only the inventory makes a checkable claim about what the code looks like right now. If a new dialog arrives and is missing from the table, the inventory is incomplete. If a listed dialog gets rebuilt and its status is not updated, the inventory is stale. Exactly this vulnerability is what makes it valuable: a rule without an inventory can exist past the code unnoticed for years, while the lag of a rule with an inventory becomes visible at the next reconciliation, at the latest. The inventory is the rule's drift radar.&lt;/p&gt;

&lt;p&gt;In the project, 9 of the 27 rule files carry such sections. They come in two flavors. The &lt;strong&gt;caller inventory&lt;/strong&gt; lists who uses a component or convention — it answers the question "if I change this, what is affected?" before anyone has to search. The &lt;strong&gt;retrofit backlog&lt;/strong&gt; lists which existing sites do not yet satisfy the convention. Both forms share the mechanics but differ in the direction of view: one looks at the rule's users, the other at its open debts.&lt;/p&gt;

&lt;p&gt;The inventory is not maintained in a separate documentation session but in the same commit as the code change. Whoever adds a dialog adds it to the table. Whoever completes a retrofit sets the status to ✅. That sounds like a high demand on discipline. In an agent workflow, though, it is the cheapest possible moment, because the agent usually has the rule file in context anyway when it works inside the rule's scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules That Load Only Where They Apply
&lt;/h2&gt;

&lt;p&gt;As the file count grows, the context-cost question returns. 27 files that all load all the time would just be a partitioned CLAUDE.md at the same cost. The lever against that is a &lt;code&gt;paths:&lt;/code&gt; front matter that binds a rule to its path scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/app/api/**"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/lib/db*"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Backend conventions&lt;/span&gt;

This rule loads only when the task touches files in its path scope —
API routes and the database access layer. A frontend task does not
pay its context costs.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the project, 3 of the 27 files carry this front matter: the backend rules, the frontend rules and the security model. The selection follows a simple criterion. These three files are large, and their scope is a clearly bounded subtree of the project. A backend rule inside a pure frontend task is dead context. The remaining 24 files load unscoped because they are either small or apply across the whole project, like the commit conventions.&lt;/p&gt;

&lt;p&gt;This article deliberately goes no deeper here. The mechanics behind it — what Claude Code loads into context when, what separates rules from skills, and how the costs add up — are the subject of &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt;. For the structural question of this article, the finding suffices: &lt;code&gt;paths:&lt;/code&gt; scoping is the main reason 27 files do not mean 27-fold costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Downside
&lt;/h2&gt;

&lt;p&gt;A rule system of this size is not free, and an experience report that hides that would be advertising.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance is real work.&lt;/strong&gt; Every rule file wants attention at every convention change, inventories want updating in the same commit, and the cross-references between files go stale when a rule moves. The effort is not a one-time investment but a running cost. In the project it is the kind of work that pays off, but it does not disappear just because you approve of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules end up in conflict.&lt;/strong&gt; With 27 files it happens that two rules govern the same case from different angles — the dialog rule wants an icon, the confirmation-dialog rule forbids one for its special case. The resolution is the same every time: the conflict gets written into both files as an explicit carve-out, with references to each other. Undecided conflicts are the worst thing that can happen to a rule system, because then the prioritization stays ambiguous — which rule prevails depends on the particular context and the task at hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The documentation runs ahead of the code — deliberately.&lt;/strong&gt; When a new convention is decided, it applies to new code immediately. The existing sites are not rebuilt within the hour but tracked as a retrofit backlog in the inventory and brought in line step by step. That state is not a failure as long as it is documented. The rule file says honestly: this is the truth, and the code deliberately lags behind at these listed sites. Untracked, the same state would be a lie, because then the documentation claims an actual state that does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Rule Needs a Linter
&lt;/h2&gt;

&lt;p&gt;Prose has a limit, and the sibling article &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; has measured it out in detail: a documented rule raises the generation hit rate but does not guarantee it. At high volume, any residual rate turns into visible drift.&lt;/p&gt;

&lt;p&gt;From that follows a division of labor that has proven itself in the project. Whatever is machine-checkable gets a guard in addition to the rule — in the font-size case a custom ESLint rule at error level that flags exactly the spellings from the don'ts. The rule file remains the source all the same: it explains the why, defines the mapping and lists the deliberate exceptions, while the linter enforces only the checkable subset.&lt;/p&gt;

&lt;p&gt;Whatever is not machine-checkable stays a pure prose rule and needs the inventory as its substitute radar. No linter can decide whether a dialog icon is the right one for the domain. Whether all dialogs have one is written in the inventory table. The rule of thumb from the project: a rule whose violation can be expressed as a search pattern is a linter candidate. A rule whose violation a human has to recognize needs an inventory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently Today
&lt;/h2&gt;

&lt;p&gt;Looking back, I would approach three things differently, and all three can be named concretely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Too many files too early.&lt;/strong&gt; The first weeks produced rule files for topics that did not even have a second usage site yet. A convention for a single case is not a convention but a note. Today a new rule file comes into being only when the same decision comes up for the second time. And cleanup is part of the deal: a rule whose scope has disappeared, or whose content a neighboring rule meanwhile carries, gets deleted or merged into that neighbor. A rule set that only ever grows becomes the very unwieldy file it was meant to replace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules without inventories that went stale silently.&lt;/strong&gt; The early files consisted of rule and reasoning, without a state-of-the-code part. After two months, some of them described a state the code had long left behind, and nobody had noticed. Only the inventory sections made the aging visible. In hindsight, every rule with usage sites should have carried one from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovered &lt;code&gt;paths:&lt;/code&gt; too late.&lt;/strong&gt; The scoping arrived only when the context costs were already noticeable, and converting existing files to clean path scopes was more tedious than an early cut would have been. Anyone starting today should ask one short question at every new rule file: does this apply everywhere, or in one subtree? Answering that question costs ten seconds at creation time. Not asking it costs a refactor later.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;When should I split my CLAUDE.md?&lt;/strong&gt;&lt;br&gt;
At the latest when, while writing a new convention into it, the question comes up where it actually belongs. That is the signal that the file carries multiple topics. A second signal is the repeated ignoring of a rule that verifiably sits in the file — the rule is drowning in its surroundings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many rule files are too many?&lt;/strong&gt;&lt;br&gt;
The number itself is not the limit, the context costs are. 27 files work because the large ones load path-scoped and the unscoped ones are small. How those costs add up and where the line runs is covered by &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do negative examples work better than prescriptions?&lt;/strong&gt;&lt;br&gt;
Because they close the translation gap. A prescription first has to be applied by the model onto the concrete case, while a don't already contains the wrong spelling verbatim — precisely the string the training prior would suggest, marked with the reason and the target spelling. Recognizing is more reliable than deriving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do I do when two rules contradict each other?&lt;/strong&gt;&lt;br&gt;
Write the conflict into both files, as an explicit carve-out with references to each other. Which rule wins in the overlap case must be stated in the files, not in the author's head. An undecided conflict otherwise gets decided by chance, depending on which rule happens to sit more prominently in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the code always have to match the documentation?&lt;/strong&gt;&lt;br&gt;
No — and that may be the most counterintuitive lesson. A new convention applies to new code immediately, while the existing code is brought in line step by step as a tracked retrofit backlog. The tracking is what matters: a documented lag is a deliberate decision, an undocumented one is documentation that lies.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Going deeper:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code — What Auto-Loads, What Loads on Demand&lt;/a&gt; — the loading mechanics and context costs behind this article.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-project-template-self-pruning/" rel="noopener noreferrer"&gt;Maximal Template Over Empty Repo — a Claude Code Setup That Prunes Itself via /init&lt;/a&gt; — how a rule inventory gets tailored at project start.&lt;/li&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&lt;/a&gt; — the sub-pillar: the setup this rule system lives in.&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;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 drift finding that makes rules and guards necessary in the first place.&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 a convention comes into being before you pour it into a rule file.&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 — Rules, Skills and Agents&lt;/a&gt; — the pillar: the enforcement system as a whole.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Starter kit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;The open DI² starter kit on GitHub&lt;/a&gt; — a project template built on this article's one-file-per-convention principle, ready to tailor.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>AI-Assisted Coding Gave Me 799 Hardcoded Font Sizes</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Tue, 21 Jul 2026 23:42:38 +0000</pubDate>
      <link>https://dev.to/marcus1968/ai-assisted-coding-gave-me-799-hardcoded-font-sizes-27fn</link>
      <guid>https://dev.to/marcus1968/ai-assisted-coding-gave-me-799-hardcoded-font-sizes-27fn</guid>
      <description>&lt;p&gt;It didn't start with an audit. It started with a nagging feeling: the interface looked restless. You don't notice it at first glance, but on the second and third look it is there — a timestamp slightly larger here than there, a dialog title a touch smaller than in the neighboring dialog. Everyone knows the discipline from letters and résumés: same typeface, same font size, same alignment. The same holds for an application interface, except the violation doesn't show up in any one spot. It shows up as a diffuse restlessness across many screens.&lt;/p&gt;

&lt;p&gt;Only that feeling led to a counting command across the frontend, and the count delivered the explanation: 799 hits for raw pixel font sizes like &lt;code&gt;text-[13px]&lt;/code&gt;, spread across 25 distinct pixel values in 74 files. And that in a project which had a documented font-size scale with six tokens all along. The scale was used 263 times and bypassed roughly 1,180 times. This is not a sloppiness finding from some legacy codebase grown over a decade — it is the state of a codebase built AI-assisted with Claude Code from day one. This is what &lt;strong&gt;AI code drift&lt;/strong&gt; looks like: every single suggestion is locally plausible, and what adds up is the restlessness you can see before you can measure it.&lt;/p&gt;

&lt;p&gt;This article is the experience report — with the real numbers, the rule that ended the problem, and the honest admission that a convention living only in prose loses against a language model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key points up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The finding:&lt;/strong&gt; 799 raw pixel font sizes against 263 token usages, even though the scale was documented. The ratio is the message, not the single number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The biggest source of drift lies between the tokens:&lt;/strong&gt; values like &lt;code&gt;11px&lt;/code&gt; and &lt;code&gt;12px&lt;/code&gt;, for which no token existed at all, form the majority at roughly 400 occurrences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No code review catches this&lt;/strong&gt;, because every diff is harmless on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better prompts raise the hit rate but don't eliminate the drift&lt;/strong&gt; — at high generation volume, any residual rate becomes visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What holds:&lt;/strong&gt; a 6-step scale, a fixed element-to-token mapping, an unambiguous snap rule for all in-between values, and a linter at &lt;code&gt;error&lt;/code&gt; level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; The example uses Tailwind CSS and ESLint in a Next.js project. The pattern applies to any codebase with design tokens, regardless of framework — and, as the end of the article shows, just as much to SQL conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Finding: 799 Against 263
&lt;/h2&gt;

&lt;p&gt;The project behind the numbers is DI², an ETL generator built on Next.js and PostgreSQL whose code was written almost entirely AI-assisted. For font sizes, a clear convention existed: six named tokens from &lt;code&gt;text-di-h1&lt;/code&gt; (18px) down to &lt;code&gt;text-di-label&lt;/code&gt; (10px), defined in the Tailwind configuration and described in a brand rule file that the agent loads for every frontend task. Everything that follows is therefore the measurement of a single project — one data point, not proof. What makes the case interesting beyond the project is the mechanism behind it, and that mechanism, as we will see, is not project-specific.&lt;/p&gt;

&lt;p&gt;The inventory on June 25, 2026 across all &lt;code&gt;src/**/*.tsx&lt;/code&gt; files produced three categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Declaration style&lt;/th&gt;
&lt;th&gt;Occurrences&lt;/th&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;th&gt;Assessment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw pixels &lt;code&gt;text-[Xpx]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;799&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;drift, 25 distinct pixel values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind defaults &lt;code&gt;text-xs&lt;/code&gt;/&lt;code&gt;text-sm&lt;/code&gt;/&lt;code&gt;text-base&lt;/code&gt; …&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;383&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;drift within the app scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical tokens &lt;code&gt;text-di-*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;263&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;the target pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three rows distinguish three ways of declaring the same font size. &lt;code&gt;text-[13px]&lt;/code&gt; is Tailwind's arbitrary-value syntax: the pixel value sits literally inside square brackets and acts like an inline &lt;code&gt;font-size&lt;/code&gt; — any value is possible, and that is how 25 different ones come into being. &lt;code&gt;text-xs&lt;/code&gt;, &lt;code&gt;text-sm&lt;/code&gt; and &lt;code&gt;text-base&lt;/code&gt;, on the other hand, are named size steps, but they belong to the framework's bundled default scale (12, 14 and 16 pixels). That looks disciplined, because it follows a scale. It is just the wrong one: Tailwind's generic scale instead of the project's own, whose six tokens don't even contain those three values. The third row, finally, is the project's own scale — the target pattern. Read top to bottom, the table is a ladder: freehand value, foreign scale, own scale.&lt;/p&gt;

&lt;p&gt;That two scales coexist is not an accident, by the way — it is the framework's default. Tailwind ships its complete size ladder with every project, and the project's own tokens were added via &lt;code&gt;theme.extend&lt;/code&gt; — and &lt;code&gt;extend&lt;/code&gt; means exactly that: extend, not replace. From that point on, every &lt;code&gt;text-sm&lt;/code&gt; compiles just as happily as every &lt;code&gt;text-di-body&lt;/code&gt;. There is no moment at which the project consents to the foreign scale, and none at which it announces itself. It is simply there, from day one, as the statistically closest choice for anyone who needs small text — human or model.&lt;/p&gt;

&lt;p&gt;A second term needs an explanation, because it recurs throughout the article. The &lt;strong&gt;app scope&lt;/strong&gt; is the application behind the login: the dashboard and administration pages plus the components they render. The token scale applies only there. Outside it lie two zones with rights of their own — the public pages (landing, legal, login), which deliberately carry larger formats, and the bundled UI base components, which internally work with Tailwind defaults. A &lt;code&gt;text-sm&lt;/code&gt; is therefore not wrong per se; on a marketing page it is legitimate. That is why the second row says drift &lt;strong&gt;within the app scope&lt;/strong&gt;: of the 383 occurrences, only the share inside the application counts as drift.&lt;/p&gt;

&lt;p&gt;The convention, then, was ignored at a ratio of roughly 1:4.5. To rerun the inventory in your own project, all you need is a search tool like &lt;code&gt;ripgrep&lt;/code&gt; (the examples are written in PowerShell):&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="c"&gt;# Raw pixel font sizes: total number of occurrences&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-filename&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'text-\[[0-9.]+px\]'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Measure-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Line&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Distribution: which pixel value occurs how often?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-filename&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'text-\[[0-9.]+px\]'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Group-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Descending&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# For comparison: the canonical tokens&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-filename&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'text-di-(h1|h2|body|meta|micro|label)'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Measure-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Line&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timeline delivers a punchline of its own. Between the first count and the recount three weeks later, shortly before the cleanup migration started, the total grew from 799 to 813 occurrences and from 25 to 26 distinct values. The drift kept growing while its removal was already being planned. A convention that is not enforced doesn't lose once — it loses a little more every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Nobody Caught It
&lt;/h2&gt;

&lt;p&gt;The first reflex at a number like this: how did that slip through? The answer is uncomfortable because it describes no negligence, but a structural gap.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;text-[12px]&lt;/code&gt; is not wrong in any single diff. It renders correctly, it looks fine in the preview, it breaks no test. A reviewer reading the diff of a new dialog checks the logic, the states, the accessibility. They do not compare whether the font size in this file is consistent with the one in 73 other files. The drift doesn't live in any one file. It lives &lt;strong&gt;between&lt;/strong&gt; the files.&lt;/p&gt;

&lt;p&gt;It was visible all the same — just not as a defect, but as the nagging feeling from the beginning. A restless interface shows the symptom, not the location: which of the 74 files do you point at when no single line is wrong on its own? So the impression stayed without consequence for a long time. It couldn't be pinned to any diff, and what can't be pinned to a diff ends up in no review comment and no ticket. Visual regression tests usually don't catch this form of inconsistency either, because they compare each view against its own baseline. Two views that are both brand new share no baseline against which the difference could stand out. Only the inventory turned the feeling into a finding with numbers — and thereby into something fixable.&lt;/p&gt;

&lt;p&gt;Then there is the speed. A person who builds one dialog a day makes a handful of font-size decisions per week, and muscle memory keeps them reasonably stable. An agent that creates twenty components in the same week makes the same decision a hundred times over — and each one is optimized locally, not for consistency with all the previous ones. The inconsistency doesn't come from carelessness; it comes from the sheer volume of independent single decisions. The densest single file in the inventory accounted for 70 raw pixel sizes on its own.&lt;/p&gt;

&lt;p&gt;Humans produce the same drift — honesty demands saying so. The difference is not the kind of mistake but the pace and the volume. What a team accumulates in two years of wild growth, AI-assisted development manages in a quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Drift Lives Between the Tokens
&lt;/h2&gt;

&lt;p&gt;The most revealing part of the inventory is the distribution of the 25 pixel values. It splits into three classes, and the middle one is the interesting one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact token matches:&lt;/strong&gt; values that correspond to a token, just written raw — &lt;code&gt;13px&lt;/code&gt; instead of &lt;code&gt;text-di-body&lt;/code&gt; (166 occurrences), &lt;code&gt;11.5px&lt;/code&gt; instead of &lt;code&gt;text-di-meta&lt;/code&gt; (46), plus the remaining token values. This class is mechanically repairable and visually a no-op.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Off-token values:&lt;/strong&gt; roughly 400 occurrences on values for which &lt;strong&gt;no token exists at all&lt;/strong&gt; — led by &lt;code&gt;11px&lt;/code&gt; with 196 and &lt;code&gt;12px&lt;/code&gt; with 143 occurrences, plus &lt;code&gt;12.5px&lt;/code&gt; (42) and &lt;code&gt;14px&lt;/code&gt; (19).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliberate exceptions:&lt;/strong&gt; marketing and legal pages with large formats of their own, such as &lt;code&gt;22px&lt;/code&gt; or &lt;code&gt;44px&lt;/code&gt;, which intentionally sit outside the app scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second class deserves the second look. &lt;code&gt;11px&lt;/code&gt; and &lt;code&gt;12px&lt;/code&gt; both sit next to the same token, &lt;code&gt;text-di-meta&lt;/code&gt; (11.5px), and both were later migrated to it. Together that is 339 places where two different pixel values played the same semantic role — meta text, timestamps, helper lines. Nobody ever decided that both values should exist. There is no commit with the message "we are introducing 11px as an alternative to 12px." Both values simply came into being, suggestion by suggestion, because each looked reasonable on its own.&lt;/p&gt;

&lt;p&gt;That is what separates this drift from a copying error. Someone who mistypes a documented value produces a findable defect. Someone who puts plausible values into a gap of the scale a hundred times over produces a creeping second scale that was never decided anywhere and therefore never stands out anywhere. That is AI code drift in its purest form.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Better Prompts" Fallacy
&lt;/h2&gt;

&lt;p&gt;The obvious reaction to the finding would have been to state the convention more forcefully. Add a chapter to the brand rule, repeat the tokens in the prompt, instruct the agent more insistently.&lt;/p&gt;

&lt;p&gt;That doesn't carry far, and the reason lies in how the model works. A language model is trained on vast amounts of public code, among it countless Tailwind projects, and there &lt;code&gt;text-sm&lt;/code&gt; or &lt;code&gt;text-[12px]&lt;/code&gt; is the overwhelmingly most common way to write small text. A project-specific convention like &lt;code&gt;text-di-meta&lt;/code&gt;, by contrast, is exactly one file in a rules directory. At every single generation the rule competes against that weight, and it wins often — but not always. Across a thousand decisions, a hit rate of 90 percent still leaves a hundred drift spots, and even a considerably higher rate ends up in the dozens at sufficient volume.&lt;/p&gt;

&lt;p&gt;Rules in prose improve the rate, and rules with reasons improve it further. How a ruleset comes into being that an agent actually follows is described in the methodology article &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;. But any rate below 100 percent means drift at high volume. For a convention that is meant to hold without exception, the prompt is the wrong tool. It needs a check that doesn't get tired.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Works: Scale, Category Mapping, Snap Rule
&lt;/h2&gt;

&lt;p&gt;The cleanup consisted of three building blocks that only work in combination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, the scale itself.&lt;/strong&gt; Six steps, defined in exactly one place in the Tailwind configuration, each with its line height built in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tailwind.config.ts — the scale as the single source of truth&lt;/span&gt;
&lt;span class="nx"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-h1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;18px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;23.4px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-h2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;15px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;21px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;13px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;18.85px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-meta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;11.5px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;15px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-micro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10.5px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;13.65px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-label&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;letterSpacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.05em&lt;/span&gt;&lt;span class="dl"&gt;'&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;An important decision was to keep the scale at six steps. The tempting alternative would have been to create new tokens for &lt;code&gt;11px&lt;/code&gt; and &lt;code&gt;12px&lt;/code&gt; and thereby legalize the status quo. That would have turned the drift into an official eight-step scale, and the next in-between value would have found a gap again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, the element-category mapping.&lt;/strong&gt; A table in the rule file defines which kind of element carries which token. Section headings get &lt;code&gt;di-h1&lt;/code&gt;, dialog titles &lt;code&gt;di-h2&lt;/code&gt;, table cells and buttons &lt;code&gt;di-body&lt;/code&gt;, timestamps and helper texts &lt;code&gt;di-meta&lt;/code&gt;, counter pills &lt;code&gt;di-micro&lt;/code&gt;, uppercase labels &lt;code&gt;di-label&lt;/code&gt;. With that, "what size does this element need?" is no longer a matter of taste but a lookup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, the snap rule.&lt;/strong&gt; The name says it: roughly 400 occurrences sat on in-between values like &lt;code&gt;11px&lt;/code&gt; or &lt;code&gt;12px&lt;/code&gt;, and during the migration each of them had to snap onto one of the six tokens — like an object being pulled onto the grid in a graphics editor. Which token it becomes is decided in two stages. First comes the element's role from the category mapping: a timestamp gets &lt;code&gt;di-meta&lt;/code&gt; because it is meta text, no matter which pixel value used to be there. Only when an occurrence cannot be assigned to any category does numeric proximity to the nearest token decide. Even the single tie is settled explicitly: &lt;code&gt;14px&lt;/code&gt; sits exactly between &lt;code&gt;di-body&lt;/code&gt; (13px) and &lt;code&gt;di-h2&lt;/code&gt; (15px), and &lt;code&gt;di-body&lt;/code&gt; wins as the default. Role before number is not a formality, either. A &lt;code&gt;12px&lt;/code&gt; in a sticky table header belongs to &lt;code&gt;di-micro&lt;/code&gt; (10.5px) by category, although &lt;code&gt;di-meta&lt;/code&gt; (11.5px) would be numerically closer. And because the rule leaves no room for judgment, two people — or two agent runs — resolve the same raw value identically, guaranteed.&lt;/p&gt;

&lt;p&gt;With those three building blocks the migration itself was unspectacular: 80 files in the app scope, converted category by category, with shifts of less than a pixel. Exactly one deliberate exception remained — a large page title outside the app scale, marked with an inline comment and a justification.&lt;/p&gt;

&lt;p&gt;One side finding from the same cleanup deserves a mention for completeness: the tokens had to be explicitly registered as font-size classes in the &lt;code&gt;tailwind-merge&lt;/code&gt; configuration, because the library otherwise classifies unknown &lt;code&gt;text-*&lt;/code&gt; classes as text color and silently discards one of them on conflict. That, however, is an ordinary trap when introducing custom tokens and has nothing to do with AI. Know it, pin it with a small test, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Even the Fresh Rule Drifted
&lt;/h2&gt;

&lt;p&gt;The most instructive finding of the whole story comes from the migration's quality assurance, and it cuts against the author.&lt;/p&gt;

&lt;p&gt;The category rule — "the element category wins over pixel proximity" — was written into the rule file in the same commit that carried the migration. And in that very commit it was undercut: the data cells of the densest table registers, previously &lt;code&gt;12px&lt;/code&gt;, belonged to &lt;code&gt;di-body&lt;/code&gt; (13px) according to the category table. They were migrated numerically to &lt;code&gt;di-meta&lt;/code&gt; (11.5px) instead, because that matched the density intent of those views. The freshly written rule and its first application contradicted each other, and it surfaced only in the downstream QA pass, which filed it as a documentation inconsistency.&lt;/p&gt;

&lt;p&gt;You can consider the finding small — half a pixel in dense tables. Its value lies elsewhere: it shows that even a carefully worded, freshly printed rule drifts at the moment of its application when only humans and prose carry it. Not out of ignorance, but because a second legitimate consideration intervened in the concrete case and nobody checked against the wording. This rule-based check is one of the tasks a machine performs more reliably than any participant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcement: A Linter at error
&lt;/h2&gt;

&lt;p&gt;The fourth building block therefore makes the convention machine-checkable. The tool class is secondary: a compiler check, a Tailwind plugin or a CI script can play the same role. For this project, a custom ESLint rule was the most practical form — it reports every raw pixel font size and every Tailwind size default in the app scope as an &lt;strong&gt;error&lt;/strong&gt;, not a warning. The core of the rule fits on a page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// eslint-rules/no-raw-font-size.mjs — the core of the rule&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PX_RE&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/text-&lt;/span&gt;&lt;span class="se"&gt;\[(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:\.\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)?)&lt;/span&gt;&lt;span class="sr"&gt;px&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;!&lt;/span&gt;&lt;span class="se"&gt;[\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;text-&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;xs|sm|base&lt;/span&gt;&lt;span class="se"&gt;)(?![\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;

&lt;span class="c1"&gt;// Snap table: raw value -&amp;gt; canonical token (from the rule file)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PX_SNAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;11&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-meta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-meta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12.5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;13&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;14&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;15&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-h2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;problem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;rawPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Raw pixel font size `{{match}}`. Use {{target}}.&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PX_RE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rawPx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&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="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PX_SNAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;m&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a token from the scale&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// DEFAULT_RE analogous&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nc"&gt;TemplateElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;cooked&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&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="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;Three decisions have proven themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The rule inspects all string literals and template parts&lt;/strong&gt;, not just JSX attributes (the &lt;code&gt;Literal&lt;/code&gt; and &lt;code&gt;TemplateElement&lt;/code&gt; visitors at the end of the rule). In practice, class strings also arise in &lt;code&gt;cn()&lt;/code&gt; arguments, in &lt;code&gt;.join(" ")&lt;/code&gt; helpers and in exported constants — a rule that only sees &lt;code&gt;className="…"&lt;/code&gt; would have blind spots there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The error message names the snap target&lt;/strong&gt; (the &lt;code&gt;target&lt;/code&gt; field in the report data). Whoever sees the error sees the fix and doesn't have to go find the rule file first. That holds for human readers just as much as for the agent reacting to the linter error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;error&lt;/code&gt;, not &lt;code&gt;warn&lt;/code&gt;.&lt;/strong&gt; A warning is a number in a summary; an error breaks the build. Only the second one is enforcement. The halfway path — warnings plus occasional cleanup — ends up reproducing the very state that led to the drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One obvious alternative deserves a mention because it looks simpler than it is: remove Tailwind's default scale from the configuration altogether by defining &lt;code&gt;fontSize&lt;/code&gt; without &lt;code&gt;extend&lt;/code&gt;. Then &lt;code&gt;text-sm&lt;/code&gt; simply would not exist anymore. That fails on two counts. An unknown &lt;code&gt;text-sm&lt;/code&gt; produces no error in Tailwind — it produces no CSS at all, so the text would silently fall back to the browser's default size, and a silent failure like that is harder to find than the drift it is meant to prevent. And the bundled UI base components, like the marketing pages, build on exactly those default classes internally. The radical fix would break the very zones that legitimately live off the standard scale. A linter with a scope can express that; a global configuration cannot.&lt;/p&gt;

&lt;p&gt;The deliberate exceptions therefore live not in the rule but in the ESLint configuration: a path list exempts marketing, legal and login pages, whose large formats intentionally sit outside the app scale. That records the convention's scope machine-readably in one place, congruent with the scope in the rule file. The direction of the definition is worth noting: the app scope itself is never listed anywhere — it is simply everything that was not exempted. A new public page that nobody adds to the exemption list is treated as app scope, and the guard flags its large formats as errors. That is the right direction to fail: a forgotten list entry shows up as a loud false alarm in the build instead of slipping through as silent drift.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// eslint.config.mjs — carve-out as a path list (abridged)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FONT_SIZE_CARVE_OUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/app/page.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// landing: deliberately larger formats&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/app/impressum/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/app/login/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// tests reference classes as test data&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di2/no-raw-font-size&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&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="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FONT_SIZE_CARVE_OUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di2/no-raw-font-size&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;off&lt;/span&gt;&lt;span class="dl"&gt;'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One sequencing detail to close: the guard was activated as the &lt;strong&gt;last&lt;/strong&gt; step, after the app scope had been migrated clean. The other way around, every unfinished file would have needed a temporary exemption list, and temporary exemption lists have a tendency to become permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Linter Cannot Do
&lt;/h2&gt;

&lt;p&gt;For the experience report to stay honest, the limits belong in it.&lt;/p&gt;

&lt;p&gt;The guard checks the &lt;strong&gt;spelling&lt;/strong&gt;, not the &lt;strong&gt;assignment&lt;/strong&gt;. That an element carries a token at all — that it enforces reliably. Whether it is the right token for the element's category it cannot know — in that gap lived the rule contradiction from the QA finding, and there it can arise again. The category assignment remains a rule in prose, with all the weaknesses described, just on a much smaller attack surface.&lt;/p&gt;

&lt;p&gt;Two limits are chosen deliberately. Larger Tailwind defaults from &lt;code&gt;text-lg&lt;/code&gt; upward the rule does not flag, because a strikingly large text in an app file is a case for design review, not a candidate for mechanical snapping. And dynamically assembled class strings, such as values concatenated from variables, are only partially covered by a static rule. Both gaps are documented rather than concealed, because a check whose omissions nobody knows creates false confidence.&lt;/p&gt;

&lt;p&gt;And finally: the linter preserves the scale, it does not justify it. Whether six steps are the right six and which category deserves which token remains a design decision that precedes the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same Pattern in SQL
&lt;/h2&gt;

&lt;p&gt;The case is a frontend case on purpose, because that is where the drift was measurable. The pattern behind it is bound to no framework. Transferring it to SQL is therefore a reasoned analogy, not a second measurement — but one that follows from the same mechanism.&lt;/p&gt;

&lt;p&gt;A SQL convention — singular table names, snake_case parameters, a fixed procedure skeleton — is the same class of rule as a font-size scale: a project-specific commitment competing against the statistically more common spelling from other codebases. An agent generating PL/pgSQL drifts there for the same reason as with the pixel values, and the answer has the same structure. The convention lives versioned in a rule file, with reasons and negative examples, the way the &lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres Table Conventions&lt;/a&gt;, the &lt;a href="https://sql.marcus-belz.de/en/plpgsql-procedure-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Procedure Conventions&lt;/a&gt; and the &lt;a href="https://sql.marcus-belz.de/en/plpgsql-function-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Function Conventions&lt;/a&gt; demonstrate. And what is machine-checkable gets checked by a tool: for SQL layout, say, &lt;code&gt;sqlfluff&lt;/code&gt; with a project-specific configuration; for structural conventions, a script in the CI gate.&lt;/p&gt;

&lt;p&gt;The division of labor is the same in both worlds. The prose rule explains the why and raises the hit rate. The tool closes the gap between a high hit rate and zero exceptions. Because the underlying pattern stays the same, whether in the frontend or in the database: local plausibility creates global inconsistency. How the overall interplay of rules, skills and agents is set up is described in the pillar 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;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How do I find consistency drift in my own project?&lt;/strong&gt;&lt;br&gt;
It often announces itself as an impression first: the interface feels restless without any single spot to point at. It becomes tangible through a counting inventory: one search pattern for the canonical spelling, one for the bypasses, and the two numbers set in relation. For design tokens those are expressions like &lt;code&gt;text-\[[0-9.]+px\]&lt;/code&gt; against the token names. More revealing than the total is the distribution of values — clusters on values without a token show where the scale has a gap or where an unofficial second convention has formed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isn't it enough to put the rule into CLAUDE.md?&lt;/strong&gt;&lt;br&gt;
A documented rule improves the hit rate but doesn't guarantee it — at high generation volume, any residual rate turns into measurable drift. Rules in prose and machine checks are therefore not alternatives but two halves: the rule explains the why, the linter enforces the what. Where rules live in Claude Code and what they cost in context is covered in &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a custom ESLint rule instead of an off-the-shelf one?&lt;/strong&gt;&lt;br&gt;
Because the error message should name the project's own snap target, and the check has to cover class strings outside JSX attributes as well. Generic approaches like &lt;code&gt;no-restricted-syntax&lt;/code&gt; can flag the pattern but cannot suggest a contextual fix — and it is exactly that fix which makes the error immediately actionable for humans and agents alike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this only apply to CSS and design tokens?&lt;/strong&gt;&lt;br&gt;
No. AI code drift affects any project-specific convention that competes against a more widespread standard spelling — SQL naming, file structures, error-handling patterns all drift by the same mechanism. The countermeasure is the same everywhere: document with reasons, and pour the machine-checkable part into a tool at error level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doesn't a human team produce the same drift?&lt;/strong&gt;&lt;br&gt;
It does, and that is the honest core. The difference is pace: an agent makes as many individual decisions in a week as a team makes in months, and it makes each one optimized locally rather than for consistency with the previous ones. Conventions that held up passably at human speed break visibly under that volume — which also means AI drift merely makes an old problem visible faster.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Going deeper:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&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 a convention comes into being before you can enforce it.&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 — Rules, Skills and Agents&lt;/a&gt; — the pillar: the enforcement system as a whole.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt; — where conventions live and what they cost in context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Convention spokes:&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 — Naming, Keys and Audit Columns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/plpgsql-procedure-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Procedure Conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/plpgsql-function-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Function Conventions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Starter kit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;The open DI² starter kit on GitHub&lt;/a&gt; — a project template with the rules structure described here; the font-size guard from this article ships with it as a reusable template.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>eslint</category>
      <category>tailwindcss</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Design Pattern // The Architecture of an ETL Process — How to Isolate Bad Data Cleanly</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Mon, 20 Jul 2026 13:46:45 +0000</pubDate>
      <link>https://dev.to/marcus1968/design-pattern-the-architecture-of-an-etl-process-how-to-isolate-bad-data-cleanly-3b5h</link>
      <guid>https://dev.to/marcus1968/design-pattern-the-architecture-of-an-etl-process-how-to-isolate-bad-data-cleanly-3b5h</guid>
      <description>&lt;p&gt;A single date string that cannot be parsed, and the entire ETL run aborts. The design pattern for &lt;strong&gt;ETL process architecture&lt;/strong&gt; presented here prevents exactly that: bad data is isolated, not passed along.&lt;/p&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;Work packages and schema layering E0–L2&lt;/strong&gt; — how to decompose the ETL process into distinct, self-contained packages, each with its own database schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical vs. structural transformation&lt;/strong&gt; — why separating type conversion and foreign-key resolution into two passes is safer and easier to debug than doing both in one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data quality at the schema boundaries&lt;/strong&gt; — erroneous records are caught at the transitions; the main stream keeps flowing cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Historization as an optional layer&lt;/strong&gt; — driven by business requirements (SCD 1 / SCD 2), with an extra technical benefit for delta loads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite.&lt;/strong&gt; Basic familiarity with ETL processes. This is a conceptual article — not a step-by-step tutorial. The technology examples are SQL Server-centric; the Postgres equivalents are covered in the FAQ. Starting point of the article series: &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 present article covers the architecture part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tasks of the ETL Process
&lt;/h2&gt;

&lt;p&gt;An ETL process consists of the three general steps &lt;strong&gt;E&lt;/strong&gt; = &lt;em&gt;Extract&lt;/em&gt;, &lt;strong&gt;T&lt;/strong&gt; = &lt;em&gt;Transform&lt;/em&gt; and &lt;strong&gt;L&lt;/strong&gt; = &lt;em&gt;Load&lt;/em&gt;. What exactly has to happen within each of these top-level steps, however, is a matter of definition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extraction
&lt;/h3&gt;

&lt;p&gt;In this step, data is extracted from various data sources. Sources can be databases, files, or APIs. The data to be extracted may be structured or unstructured and may come in different formats. This article series deals exclusively with structured data. Structured data sources include relational databases, but also CSV documents as well as XML and JSON documents — as long as their data elements follow a logical structure. Unstructured data such as text from social networks is out of scope here.&lt;/p&gt;

&lt;p&gt;This generic definition leaves open what extraction concretely means. The following sections describe a concrete shape for the extraction process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Materialization of the extracted data&lt;/li&gt;
&lt;li&gt;Extended extraction tasks&lt;/li&gt;
&lt;li&gt;No typing into target types during extraction&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Materialization of Extracted Data
&lt;/h4&gt;

&lt;p&gt;The design pattern presented here stores all extracted data in a database. Storage must be designed so that a single invalid value cannot make it fail. An extraction step may only abort when the delivery itself is unprocessable (corrupted or unparsable files, unexpected encoding) or the infrastructure gives out (storage, network). Writing data to the database is referred to here as the &lt;em&gt;materialization&lt;/em&gt; of the data.&lt;/p&gt;

&lt;p&gt;Extraction and the materialization of extracted data give three main benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The source system is read only once and as briefly as possible.&lt;/li&gt;
&lt;li&gt;All extracted data is available in a database for subsequent processing.&lt;/li&gt;
&lt;li&gt;After-the-fact error analysis on concrete records becomes possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reading from the source system can put it under enough load that its performance and response times suffer. Extracting first minimizes the duration of that access.&lt;/p&gt;

&lt;p&gt;Once all extracted data sits in a database, downstream steps can work on it using SQL. No additional ETL tool is required just to integrate heterogeneous source systems. This lowers technical hurdles, and downstream processes are often substantially faster — both in execution and in development.&lt;/p&gt;

&lt;h4&gt;
  
  
  Extended Extraction Tasks
&lt;/h4&gt;

&lt;p&gt;For text files in XML and JSON format (and possibly CSV), materialization works a bit differently. XML and JSON documents are themselves stored in the database before the data they contain is extracted. As an extended extraction task, the attributes are then extracted from the stored documents using T-SQL functions such as &lt;code&gt;OPENJSON&lt;/code&gt; or &lt;code&gt;OPENXML&lt;/code&gt; and written to the database.&lt;/p&gt;

&lt;h4&gt;
  
  
  No Typing Into Target Types During Extraction
&lt;/h4&gt;

&lt;p&gt;Text-file deliveries are particularly problematic. The data they contain is not type-safe by any means. A date delivered as text may or may not be convertible into a &lt;code&gt;date&lt;/code&gt; value. There has to be an agreement between the delivering process and the ETL process about, for example, which date format is used (&lt;code&gt;yyyy-MM-dd&lt;/code&gt;, &lt;code&gt;dd.MM.yyyy&lt;/code&gt;, etc.). Converting values during extraction is a source of errors and risks aborting the entire ETL run. In this pattern, typing into the business target types is therefore not permitted during extraction: input values are first materialized in a deliberately tolerant text format and only converted into the required target types during the technical transformation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transformation
&lt;/h3&gt;

&lt;p&gt;A common definition of the transformation step goes something like: transformation converts the extracted data into the desired format. Another possible definition sums up the required tasks under the term &lt;em&gt;data integration&lt;/em&gt;. Both definitions — and the term &lt;em&gt;data integration&lt;/em&gt; itself — say everything and nothing. They offer no guidance on the concretely required measures.&lt;/p&gt;

&lt;p&gt;Starting from the extracted data, the design pattern presented here defines two mandatory tasks and one optional task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Typing of the extracted data&lt;/li&gt;
&lt;li&gt;Data quality check&lt;/li&gt;
&lt;li&gt;Optional: historization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When data arrives as text files, the extracted attributes must first be converted into the target data types. That is often also true when data is extracted from databases whose data types diverge from those in the target system. This section, however, focuses on text files as the data source. As described above, values extracted from text files are first stored as &lt;em&gt;text&lt;/em&gt;. The target system expects strongly typed data. A date, for example, will routinely have to be converted into a &lt;code&gt;date&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;The data quality check is, on top of that, a critically important task that fundamentally shapes the outcome of an ETL run. It starts with checking whether a delivered value can be converted into the data type of the corresponding target field. Where required, deliveries must also be checked for duplicates. Further rules cover value ranges, dependencies between fields, reference data, and business plausibility.&lt;/p&gt;

&lt;p&gt;In the historization step, source data identified as changed (new, modified, or deleted records) is rolled forward in separate tables, so it is always traceable when a record was inserted, modified, or deleted. This step is optional. A colleague once called the historized data the &lt;em&gt;brain of the ETL process&lt;/em&gt;. With complete historization and reproducible transformation and mapping rules, the data of the downstream target system can be reconstructed from the historized data. Of course, historization comes with additional maintenance tasks such as backups.&lt;/p&gt;

&lt;p&gt;The term &lt;em&gt;data integration&lt;/em&gt; is, in fact, closer to what we will call &lt;strong&gt;structural transformation&lt;/strong&gt;. There, data from various sources is filtered, merged, and aggregated. Although that is also a transformation task, the design pattern presented here does &lt;em&gt;not&lt;/em&gt; perform it during &lt;strong&gt;T&lt;/strong&gt; of the ETL process — it happens during &lt;strong&gt;L&lt;/strong&gt;. At this point, it pays to draw a sharp terminological line between the transformation tasks described in this section and structural transformation. The transformation tasks described here, performed during &lt;strong&gt;T&lt;/strong&gt;, are referred to as &lt;strong&gt;technical transformation&lt;/strong&gt;. The transformation tasks performed during &lt;strong&gt;L&lt;/strong&gt; are referred to as &lt;strong&gt;structural transformation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The boundaries between the three top-level ETL steps are fluid and, in the end, a matter of definition.&lt;/p&gt;

&lt;h4&gt;
  
  
  Typing of Extracted Data
&lt;/h4&gt;

&lt;p&gt;If the data source is a database such as &lt;em&gt;SQL Server&lt;/em&gt; or &lt;em&gt;Oracle&lt;/em&gt;, the data will typically already be strongly typed. Even then, type conversion may still be necessary to match the data types of the target system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Typical cases are the length of text fields and the storage of a point in time without a time zone. Application developers do not always pay close attention to input length limits. As a result, an address field in a source system might be able to hold entire novels. Users who notice such a gap will — empirically — happily use it to dump information that simply does not belong there. If the source delivers a point in time without a time-zone offset, it must be clarified before conversion in which time zone that point in time is to be interpreted from a business perspective. In SQL Server, depending on the requirement, &lt;code&gt;datetime2&lt;/code&gt; or &lt;code&gt;datetimeoffset&lt;/code&gt; is the target type of choice.&lt;/p&gt;

&lt;p&gt;When processing attributes extracted from a text file, typing the extracted values into the target data types is always required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; During extraction, attributes are stored as values of type &lt;em&gt;text&lt;/em&gt;. A text that looks like a date is not necessarily convertible into a &lt;code&gt;date&lt;/code&gt; value. For instance, &lt;code&gt;30-02-2023&lt;/code&gt; is not a valid date. Another example: &lt;code&gt;03-05-2023&lt;/code&gt; cannot be interpreted as a date without additional context about the data source. Read in the American style (&lt;code&gt;MM-dd-yyyy&lt;/code&gt;), it yields March 5, 2023; read in the style common in Germany (&lt;code&gt;dd-MM-yyyy&lt;/code&gt;), it yields May 3, 2023. Correct interpretation requires knowledge of the date format — that is, the format string. Similar challenges arise for numeric values with respect to decimal and thousands separators.&lt;/p&gt;

&lt;h4&gt;
  
  
  Data Quality Check
&lt;/h4&gt;

&lt;p&gt;The data quality check inspects the extracted and converted data for completeness and correctness. These checks cover a wide field. Examples are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type conversion check&lt;/li&gt;
&lt;li&gt;Duplicate identification&lt;/li&gt;
&lt;li&gt;Spelling and orthography check on text values&lt;/li&gt;
&lt;li&gt;Foreign key check&lt;/li&gt;
&lt;li&gt;Mandatory field missing value check&lt;/li&gt;
&lt;li&gt;Business logic validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The article &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; introduces the term &lt;strong&gt;technical data quality&lt;/strong&gt;. The check on technical data quality operates on the typed data. For data quality checks on typed data, simple logical conditions can be set up, identifying errors on a value or per-record basis. A data quality rule is technically expressed as a &lt;code&gt;WHERE&lt;/code&gt; predicate in the ETL process and applied to the typed data — the predicate is formulated so that it selects exactly the erroneous records. Every hit carries an error in the inspected field the rule refers to.&lt;/p&gt;

&lt;h5&gt;
  
  
  Type Conversion Check
&lt;/h5&gt;

&lt;p&gt;Whether the type conversion succeeds or fails has direct impact on all downstream tasks. If an input value cannot be converted into the target data type, the offending record may have to be excluded from further processing. The design pattern presented here checks for every delivered source record whether its input values can be converted into the respective target data types.&lt;/p&gt;

&lt;h5&gt;
  
  
  Duplicate Identification
&lt;/h5&gt;

&lt;p&gt;Duplicate identification can be arbitrarily complex. This article series limits itself to a combination of fields that, per the delivery contract, must follow a defined cardinality or must be unique (cardinality = 1).&lt;/p&gt;

&lt;h5&gt;
  
  
  Spelling and Orthography Check on Text Values
&lt;/h5&gt;

&lt;p&gt;Phone numbers, for instance, have many possible notations. The German &lt;em&gt;DIN 5008&lt;/em&gt; standard prescribes that the area code be written without parentheses and separated from the rest of the number by a single space. Format rules like these can serve as check rules within the &lt;em&gt;technical transformation&lt;/em&gt;. Which notation counts as correct, however, is not a question of the standard but a convention to be defined per target system.&lt;/p&gt;

&lt;h5&gt;
  
  
  Foreign Key Check
&lt;/h5&gt;

&lt;p&gt;If the delivered data contains a foreign key relationship, only the &lt;strong&gt;syntactic validity&lt;/strong&gt; of a delivered foreign key value is checked here — that is, format, presence where required, and data type. The actual &lt;strong&gt;foreign key resolution&lt;/strong&gt; against the target system (mapping source-system code → target surrogate key) only happens later, as part of the structural transformation. Only there is referential validity established against the reference table. The reason for the split: resolution needs context from the target system (such as a Countries table), whereas format and presence checks can be answered from the record alone.&lt;/p&gt;

&lt;h5&gt;
  
  
  Mandatory Field Missing Value Check
&lt;/h5&gt;

&lt;p&gt;If an attribute is a mandatory field in the target system, the typed data must be inspected to ensure that a corresponding value was delivered.&lt;/p&gt;

&lt;h5&gt;
  
  
  Business Logic Validation
&lt;/h5&gt;

&lt;p&gt;Checking business logic is itself a wide field that can become arbitrarily complex. But even checking simple business rules improves data quality noticeably. A simple example is a customer's date of birth: it must not lie in the future.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading
&lt;/h3&gt;

&lt;p&gt;In the final step of the ETL process, the typed data is structurally transformed to match the data structures of the target system, optionally re-checked for data errors, filtered, aggregated, historized, and finally loaded into the target system. The tasks involved are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structural transformation&lt;/li&gt;
&lt;li&gt;Data quality check&lt;/li&gt;
&lt;li&gt;Filtering&lt;/li&gt;
&lt;li&gt;Aggregation&lt;/li&gt;
&lt;li&gt;Optional: historization&lt;/li&gt;
&lt;li&gt;Loading the data into the target system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The previously technically transformed data can be loaded into different target systems. The target can be, for example, a CRM system or a data warehouse. The structural transformation task is specific to the chosen target system. That is why the structural transformation happens during &lt;strong&gt;L&lt;/strong&gt; of the ETL process.&lt;/p&gt;

&lt;h4&gt;
  
  
  Structural Transformation
&lt;/h4&gt;

&lt;p&gt;The structural transformation works exclusively on the typed, quality-checked, and possibly historized data that was found to be error-free. Technically, the structural transformation corresponds to a &lt;code&gt;SELECT&lt;/code&gt; statement joining the historized tables via &lt;code&gt;JOIN&lt;/code&gt;s and shaping the output to match the target system's data structures. Among other things, this step resolves foreign keys and lookup values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Foreign key resolution&lt;/li&gt;
&lt;li&gt;Lookup value resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output of the structural transformation is — as with extraction and the technical transformation — materialized in the database, so this data, too, is available for analysis and error diagnosis. The data structures of the structurally transformed data largely correspond to the structures in the target system. In particular, the column names and data types of the output are chosen to match those expected by the target.&lt;/p&gt;

&lt;h5&gt;
  
  
  Foreign Key Resolution
&lt;/h5&gt;

&lt;p&gt;If foreign keys cannot be determined from the extracted data alone, they must be resolved against the target system's data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Target systems often store countries in a separate table. The country &lt;em&gt;United States&lt;/em&gt; is then identified both by its country name and — typically — by a technical key (e.g., a GUID). When structurally transforming a customer whose source data identifies the country as the text &lt;em&gt;United States&lt;/em&gt;, this text must be translated to the primary key of that country in the target system and stored as a foreign key with the customer record.&lt;/p&gt;

&lt;p&gt;Foreign key resolution requires direct read access to the &lt;em&gt;Countries&lt;/em&gt; table in the target system. If direct access is not available, that table must be read in advance, and its data must be made available in a database. At that point, reading the &lt;em&gt;Countries&lt;/em&gt; table is itself an extraction task.&lt;/p&gt;

&lt;h5&gt;
  
  
  Lookup Value Resolution
&lt;/h5&gt;

&lt;p&gt;Source and target systems often use different codings for the value of a dropdown field. A dropdown field, for example, could be a list field for selecting a customer's salutation.&lt;/p&gt;

&lt;p&gt;In the database, what is shown and selected in the application is rarely stored verbatim. A salutation of &lt;em&gt;Mr.&lt;/em&gt; might be stored as the value &lt;code&gt;1&lt;/code&gt; and &lt;em&gt;Ms.&lt;/em&gt; as &lt;code&gt;2&lt;/code&gt;. The codings used in source and target systems typically differ.&lt;/p&gt;

&lt;p&gt;These coded attributes are often not stored in separate tables. Translating the source-system code into the target-system code therefore requires explicit knowledge of the translation rules. Following terminology used in &lt;em&gt;Microsoft Dynamics CRM&lt;/em&gt;, this translation is called &lt;em&gt;lookup value resolution&lt;/em&gt;. To resolve lookup values, the codes used by source and target systems must be determined and stored in a mapping table that is consulted during the structural transformation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Data Quality Check
&lt;/h4&gt;

&lt;p&gt;Experience from real projects shows that foreign key resolution and lookup value resolution are major sources of errors — typically rooted in incomplete or incorrect mappings of source-system codes to target-system codes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Filtering
&lt;/h4&gt;

&lt;p&gt;Unless the target system is being initially populated with data, only records with specific properties should be loaded into the target. Filtering for the records actually destined for loading should — where possible — already happen during the technical transformation. If that is not feasible there, filtering happens during the structural transformation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Aggregation
&lt;/h4&gt;

&lt;p&gt;Data may need to be aggregated before loading into the target system.&lt;/p&gt;

&lt;p&gt;If end-to-end traceability of every processing step in the ETL pipeline is required, aggregation should be considered as a separate processing step downstream of the structural transformation. Aggregated data would then be stored in separate tables of the staging database.&lt;/p&gt;

&lt;h4&gt;
  
  
  Historization
&lt;/h4&gt;

&lt;p&gt;As in the technical transformation, the structurally transformed and checked data can be rolled forward in separate tables. New records are inserted, changed records are updated, and deleted records are flagged as deleted.&lt;/p&gt;

&lt;h4&gt;
  
  
  Loading Data Into the Target System
&lt;/h4&gt;

&lt;p&gt;The final loading of the changed data into the target system therefore works on quality-assured, structurally transformed, and historized data. Only error-free records — those for which foreign keys and lookup values were successfully resolved — are loaded.&lt;/p&gt;

&lt;p&gt;Technologically, this article focuses on loading change data into a target database. The target database is updated via SQL statements, that is, via &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and where applicable &lt;code&gt;DELETE&lt;/code&gt; statements. Other target systems — such as Microsoft Dynamics 365 — require the use of a proprietary API, both for writing data into and reading data from the target. In that case, an API-capable integration mechanism is needed: &lt;em&gt;SQL Server Integration Services&lt;/em&gt; with suitable components, a custom API client, or another ETL/ELT tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture of the ETL Process
&lt;/h2&gt;

&lt;p&gt;The pattern fits in one sentence: &lt;strong&gt;Each work package has its own database schema, checks data quality at its boundary, and passes on only error-free records.&lt;/strong&gt; The architecture presented here is broadly transferable and can be used regardless of the kind of source data or target system, in relational data migration and data integration projects alike. It also fits the data-loading workflow of a data warehouse. The ETL process is decomposed into small, self-contained work packages with sharply defined tasks. At the end of the pipeline, quality-assured data sits in data structures similar to those of the target system and can be loaded there without further business transformations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Work Packages of the ETL Process
&lt;/h3&gt;

&lt;p&gt;The following diagram illustrates the work packages of the ETL process presented here:&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%2Fzzcnhcnf3ihvwm5vlz53.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%2Fzzcnhcnf3ihvwm5vlz53.png" alt="Three-lane overview of the ETL process: at the top, the ETL steps Extract, Transform, Load; in the middle, the schemas E0, E1, T1, T2, L1, L2 sitting between data sources and the destination; at the bottom, the work packages from Data source through Load." width="800" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The top lane of the diagram shows the top-level steps from the &lt;strong&gt;ETL&lt;/strong&gt; acronym: &lt;strong&gt;Extract&lt;/strong&gt;, &lt;strong&gt;Transform&lt;/strong&gt;, and &lt;strong&gt;Load&lt;/strong&gt;. The bottom lane names the concrete work packages of the ETL process and maps each to one of the top-level steps. Each work package is paired with a database schema. The middle lane labels the database schemas used per work package (&lt;strong&gt;E0&lt;/strong&gt;–&lt;strong&gt;L2&lt;/strong&gt;). During processing, data is handed from work package to work package, that is, from schema to schema. The ETL process consists of the following work packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data extraction&lt;/li&gt;
&lt;li&gt;Technical transformation&lt;/li&gt;
&lt;li&gt;Historization of the technically transformed data&lt;/li&gt;
&lt;li&gt;Structural transformation&lt;/li&gt;
&lt;li&gt;Historization of the structurally transformed data&lt;/li&gt;
&lt;li&gt;Loading the data into the target system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The six schemas at a glance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Schema&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Error class at the boundary&lt;/th&gt;
&lt;th&gt;Persistence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;raw documents (XML/JSON)&lt;/td&gt;
&lt;td&gt;format/parser errors&lt;/td&gt;
&lt;td&gt;only for document sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;extracted raw data, untyped&lt;/td&gt;
&lt;td&gt;infrastructure/extraction errors&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;typed and checked data&lt;/td&gt;
&lt;td&gt;technical data quality (type errors)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;history of the technically transformed data&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;target-system-like structures&lt;/td&gt;
&lt;td&gt;structural data quality (FK/lookup)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;history of the structurally transformed data plus load flags&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;Technical Transformation&lt;/strong&gt; and &lt;strong&gt;Structural Transformation&lt;/strong&gt; work packages check the data quality of the transformed data and hand over only error-free data to the next package. In the diagram, these checks are indicated by the dark arrow heads. The sections below summarize the steps within each work package and provide an overview of the technology used to carry them out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extraction
&lt;/h3&gt;

&lt;p&gt;The goal of extraction is to first store all data to be processed in the &lt;em&gt;staging database&lt;/em&gt;. Within extraction, it matters whether the source data is read from a database or from documents with table-like structures (for example, Excel or CSV documents), or whether documents with complex logical structures (for example, XML or JSON) are to be processed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Extraction From a Database
&lt;/h4&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%2Flfxowei192yer0ukctdw.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%2Flfxowei192yer0ukctdw.png" alt="Detail diagram, extraction from a database: the active ETL step is Extract; the source Database is materialized into schema E1." width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When reading from a database or from table-like structures, the attributes / columns are first materialized into tables of schema &lt;strong&gt;E1&lt;/strong&gt;. The structures of the tables in schema &lt;strong&gt;E1&lt;/strong&gt; closely match the structures in the source system. When extracting from a database, the data is stored using the data types from the source system. If the source-system data types are not supported by &lt;em&gt;SQL Server&lt;/em&gt;, the data is stored in schema &lt;strong&gt;E1&lt;/strong&gt; as &lt;code&gt;nvarchar&lt;/code&gt;. That works for values that can be represented as text. Binary or special types (such as geodata) need a deliberate strategy instead — for example &lt;code&gt;varbinary(max)&lt;/code&gt; or a raw format.&lt;/p&gt;

&lt;h4&gt;
  
  
  Extraction From Documents With Table-Like Structures
&lt;/h4&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%2F6y193267fe66rfbiak2z.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%2F6y193267fe66rfbiak2z.png" alt="Detail diagram, extraction from EXCEL/CSV documents: the active step is Extract; the table-like data lands in schema E1 as text." width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Data from documents with table-like structures, such as Excel and CSV documents, arrives without guaranteed typing. A CSV file transports no data types — every delivered value is text at first. Excel does not enforce typing either: individual rows of a column set up as a date can still contain a number or free text. Even a delivery agreement about the column format is an expectation, not a guarantee. Checking remains the ETL process's job, all the more since these documents are often created and maintained by hand. To make sure that all values from these documents can be materialized in the staging database in tables of schema &lt;strong&gt;E1&lt;/strong&gt;, all data is first stored as &lt;code&gt;nvarchar&lt;/code&gt;. Use generous text lengths — &lt;code&gt;nvarchar(max)&lt;/code&gt; where no upper bound is known — so that materialization does not fail on length.&lt;/p&gt;

&lt;h4&gt;
  
  
  Extraction From Documents With Complex Logical Structures
&lt;/h4&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%2Fo354fhxoafzjjaf129ul.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%2Fo354fhxoafzjjaf129ul.png" alt="Detail diagram, extraction from XML/JSON documents: two steps — documents are read (Extract documents) and then attributes are extracted (Extract attributes)." width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When XML or JSON documents are to be processed, the documents themselves are first stored in tables of schema &lt;strong&gt;E0&lt;/strong&gt;. The attributes are then extracted into tables of schema &lt;strong&gt;E1&lt;/strong&gt;. The attribute extraction works on the documents stored in schema &lt;strong&gt;E0&lt;/strong&gt; in the first step.&lt;/p&gt;

&lt;p&gt;Attributes from text files are stored in schema &lt;strong&gt;E1&lt;/strong&gt; as &lt;code&gt;nvarchar&lt;/code&gt;. Use generous text lengths, &lt;code&gt;nvarchar(max)&lt;/code&gt; where no upper bound is known.&lt;/p&gt;

&lt;h4&gt;
  
  
  Technology
&lt;/h4&gt;

&lt;p&gt;Extraction of data from a database or from table-like structures can be done with Microsoft's &lt;em&gt;SQL Server Integration Services&lt;/em&gt; (SSIS) or any other ETL tool. To extract XML or JSON documents, &lt;em&gt;SSIS&lt;/em&gt; first loads them into tables of schema &lt;strong&gt;E0&lt;/strong&gt;. &lt;code&gt;OPENJSON&lt;/code&gt; extracts the attributes from JSON documents. For XML, the methods of the &lt;code&gt;xml&lt;/code&gt; data type (&lt;code&gt;.nodes()&lt;/code&gt;, &lt;code&gt;.value()&lt;/code&gt;) or the older &lt;code&gt;OPENXML&lt;/code&gt; are available — &lt;code&gt;OPENXML&lt;/code&gt; uses its own memory and parsing model (&lt;code&gt;sp_xml_preparedocument&lt;/code&gt;) and is mostly found in legacy code.&lt;/p&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;This extraction approach has several advantages. Using an ETL tool such as &lt;em&gt;SSIS&lt;/em&gt;, which supports a high degree of parallelism in data processing, the data can be materialized into the schemas &lt;strong&gt;E0&lt;/strong&gt; and &lt;strong&gt;E1&lt;/strong&gt; with high throughput. Upstream systems are minimally impacted, and the data is available for further processing — such as attribute extraction from XML and JSON documents via the T-SQL functions &lt;code&gt;OPENXML&lt;/code&gt; or &lt;code&gt;OPENJSON&lt;/code&gt; — in the staging database. The materialized data also enables root-cause analysis when errors arise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Transformation
&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%2Fpyu6p8bv9ylymvt9a6m1.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%2Fpyu6p8bv9ylymvt9a6m1.png" alt="Detail diagram, Technical Transformation: the active step is Transform; the three sub-steps Type Conversion, Check Conversion, and Calculate Hash Values move data from E1 to T1; erroneous records branch into an Error table." width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Within the top-level transformation step, this design pattern runs the technical transformation described above. It consists of the following sub-steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type conversion&lt;/li&gt;
&lt;li&gt;Technical data quality check&lt;/li&gt;
&lt;li&gt;Data error logging&lt;/li&gt;
&lt;li&gt;Flagging of erroneous records&lt;/li&gt;
&lt;li&gt;Hash value computation&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Type Conversion
&lt;/h4&gt;

&lt;p&gt;The output of the technical transformation is typed data that matches the target system's expectations. Typing can be driven by metadata via generic user-defined stored procedures and materializes the data into tables of schema &lt;strong&gt;T1&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Per attribute from schema &lt;strong&gt;E1&lt;/strong&gt;, two columns are provided in schema &lt;strong&gt;T1&lt;/strong&gt;. The first column holds the extracted value in the data type used in schema &lt;strong&gt;E1&lt;/strong&gt;. The second column holds the typed value in the target data type, provided the value can be converted. If the value cannot be converted, the second column stores &lt;code&gt;NULL&lt;/code&gt; — in T-SQL, &lt;code&gt;TRY_CONVERT&lt;/code&gt; and &lt;code&gt;TRY_CAST&lt;/code&gt; provide exactly that behavior, returning &lt;code&gt;NULL&lt;/code&gt; on a failed conversion.&lt;/p&gt;

&lt;h4&gt;
  
  
  Technical Data Quality Check
&lt;/h4&gt;

&lt;p&gt;After typing, the result is checked by comparing the column pairs for type-conversion problems. A conversion error exists exactly when the input column holds a value but the typed column is &lt;code&gt;NULL&lt;/code&gt;. A &lt;code&gt;NULL&lt;/code&gt; input remains a legitimate &lt;code&gt;NULL&lt;/code&gt; in the target and is not a conversion error — whether the field may be empty at all is the mandatory-field check's job. How empty strings are treated (as &lt;code&gt;NULL&lt;/code&gt; or as a value in their own right) must be defined up front. Because the type conversion is purely technical, this check is also called the &lt;em&gt;technical data quality check&lt;/em&gt;. The error check can already be extended here to cover simple, record-local business rules.&lt;/p&gt;

&lt;h4&gt;
  
  
  Data Error Logging
&lt;/h4&gt;

&lt;p&gt;Detected data errors are logged in a readable, queryable form in an error table.&lt;/p&gt;

&lt;h4&gt;
  
  
  Flagging of Erroneous Records
&lt;/h4&gt;

&lt;p&gt;If a record contains at least one error, it is flagged as erroneous so it can be excluded from further processing. The flag lives in a column that stores the count of detected errors. Error-free records carry &lt;code&gt;NULL&lt;/code&gt; in this column.&lt;/p&gt;

&lt;h4&gt;
  
  
  Hash Value Computation
&lt;/h4&gt;

&lt;p&gt;The last sub-step of the technical transformation is computing and storing two hash values per record. The first hash represents the business-key columns of the record; the second hash represents all remaining columns. Through these two hashes, the next work package — &lt;strong&gt;Historization of Technically Transformed Data&lt;/strong&gt; — can identify change records. Hash values are computed only for error-free records.&lt;/p&gt;

&lt;p&gt;Two things must be unambiguously defined for this: the serialization of the columns (column order, delimiter, &lt;code&gt;NULL&lt;/code&gt; handling, encoding, and the representation of dates and decimals: without an unambiguous delimiter, &lt;code&gt;('AB','C')&lt;/code&gt; and &lt;code&gt;('A','BC')&lt;/code&gt; produce the same hash input) and the algorithm — for SQL Server &lt;code&gt;SHA2_256&lt;/code&gt; or &lt;code&gt;SHA2_512&lt;/code&gt; (all older &lt;code&gt;HASHBYTES&lt;/code&gt; algorithms have been deprecated since SQL Server 2016). Strictly speaking, an equal hash does not prove equal data, because different inputs can produce the same hash value. With &lt;code&gt;SHA2_256&lt;/code&gt;, this residual risk is practically zero. With short checksums such as &lt;code&gt;CHECKSUM&lt;/code&gt; (32 bits), collisions become likely at a few tens of thousands of rows (around 50 % at roughly 77,000 distinct inputs). As the sole basis for change detection, they are therefore unsuitable.&lt;/p&gt;

&lt;h4&gt;
  
  
  Technology
&lt;/h4&gt;

&lt;p&gt;Conversion of extracted values into target data types, error checks, flagging of erroneous records, and hash value computation can all be implemented as generic stored procedures that build the appropriate dynamic SQL statements from metadata. This requires one-time investment in implementing those procedures. Once they exist, the tasks above reduce to simple procedure calls. In the long run, this reduces development effort and maximizes reuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope of the dynamic part.&lt;/strong&gt; Dynamic SQL in the strict sense only appears in the data-quality check — one rule maps to one &lt;code&gt;WHERE&lt;/code&gt; clause applied to the typed table at run time. Beyond that, the procedures listed above (type conversion, DQ check, flagging, hash-value computation) are &lt;strong&gt;metadata-generatable&lt;/strong&gt;, because they follow the same structural pattern for every target table. This generation covers the corridor from extraction up to technical historization (schema &lt;strong&gt;T2&lt;/strong&gt;). From schema &lt;strong&gt;L1&lt;/strong&gt; onward — the structural transformation — the &lt;code&gt;JOIN&lt;/code&gt; statements are target-system-specific and are developed manually. The same applies to the historization procedures for schema &lt;strong&gt;L2&lt;/strong&gt; (see the corresponding sections below).&lt;/p&gt;

&lt;p&gt;How these generic checks are implemented in practice is shown in &lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Checking Data Quality with SQL&lt;/a&gt; — a configurable framework that handles the tasks listed above through metadata-driven procedures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Historization of Technically Transformed Data
&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%2F6nw816humwmd36b65j2b.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%2F6nw816humwmd36b65j2b.png" alt="Detail diagram, identification of change records: three New-record work packages show how new records are rolled forward into the historized tables." width="800" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Historization consists of the following sub-steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Historization&lt;/li&gt;
&lt;li&gt;Identification of change records&lt;/li&gt;
&lt;li&gt;Identification via hash values&lt;/li&gt;
&lt;li&gt;Storing hash values&lt;/li&gt;
&lt;li&gt;Promoting only error-free records&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Historization
&lt;/h4&gt;

&lt;p&gt;Historization means that delivered data is rolled forward in a database. In the data warehousing world, &lt;em&gt;&lt;a href="https://en.wikipedia.org/wiki/Slowly_changing_dimension" rel="noopener noreferrer"&gt;Slowly Changing Dimensions&lt;/a&gt;&lt;/em&gt; describes several types of historization that specify exactly how the rolling-forward works. &lt;em&gt;Slowly Changing Dimensions&lt;/em&gt; is also commonly abbreviated as &lt;em&gt;SCD&lt;/em&gt;. &lt;em&gt;Ralph Kimball&lt;/em&gt; describes SCD techniques from Type 0 through Type 7. Two of them matter most for this pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SCD 1&lt;/strong&gt; — strictly speaking, no real historization at all. A record loaded earlier is simply overwritten by its changed counterpart. Only the most recent state of each record is ever stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCD 2&lt;/strong&gt; — on a relevant change, a new version of the record is added (&lt;em&gt;Add New Row&lt;/em&gt;). To implement this, every historized table here gets two extra columns &lt;code&gt;valid_from&lt;/code&gt; and &lt;code&gt;valid_till&lt;/code&gt;, indicating the record's validity interval (they correspond to Kimball's &lt;em&gt;row effective date&lt;/em&gt; and &lt;em&gt;row expiration date&lt;/em&gt;). The intervals are half-open — &lt;code&gt;[valid_from, valid_till)&lt;/code&gt;: &lt;code&gt;valid_from&lt;/code&gt; is part of the validity, &lt;code&gt;valid_till&lt;/code&gt; marks the start of the successor version. Currently valid records are open-ended, indicated by &lt;code&gt;NULL&lt;/code&gt; in &lt;code&gt;valid_till&lt;/code&gt;. Common implementation variants are a far-future date such as &lt;code&gt;9999-12-31&lt;/code&gt; instead of the &lt;code&gt;NULL&lt;/code&gt;, and an additional current-row flag. When a change record arrives for a currently valid record, the previously valid record's &lt;code&gt;valid_till&lt;/code&gt; is set to the date from which the change record becomes valid, and the change record itself is inserted with &lt;code&gt;valid_till&lt;/code&gt; = &lt;code&gt;NULL&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Historization is optional. It helps most with delta loads, when downstream objects depend on unchanged master data — even there, it is not strictly required. Suppose a customer places a new order. In a delta load, the order is delivered, but not the customer, who has not changed. Resolving the foreign-key relationship between order and customer cannot be done from the delivered data alone. To resolve it, either the customer data has to be extracted from the target system, or customers must be historized in the database so they are available on subsequent ETL runs.&lt;/p&gt;

&lt;p&gt;In the context of the ETL process presented here, historization means that only error-free, changed records are historized. Rolling forward can follow either &lt;em&gt;SCD 1&lt;/em&gt; or &lt;em&gt;SCD 2&lt;/em&gt; — bearing in mind that &lt;em&gt;SCD 1&lt;/em&gt; keeps no historical versions, only the current state.&lt;/p&gt;

&lt;h4&gt;
  
  
  Identification of Change Records
&lt;/h4&gt;

&lt;p&gt;Historization requires that change records can be recognized in the source system and, subsequently, in the historized tables. Source systems often provide no information — or only unreliable information — about when a record was inserted, modified, or deleted. When a CSV file is generated from a hand-edited Excel document, for example, it must be assumed that no reliable change information is available. Against that backdrop, this design pattern always derives change records from the data itself. The hash values computed during the technical transformation are used for this.&lt;/p&gt;

&lt;h4&gt;
  
  
  Identification via Hash Values
&lt;/h4&gt;

&lt;p&gt;In the Technical Transformation section, two hash values were computed for error-free records — one over the business-key columns, one over all remaining columns. The core idea: &lt;strong&gt;T2&lt;/strong&gt; accumulates the complete history of the delivered data across the runs, and every new delivery (full or delta) is analyzed against that history. The comparison set is exclusively the currently valid version per business key (for &lt;em&gt;SCD 2&lt;/em&gt;, the rows with &lt;code&gt;valid_till&lt;/code&gt; = &lt;code&gt;NULL&lt;/code&gt;), never the historical versions. New, modified, and deleted records are identified by comparing the hash values between the technically transformed data (schema &lt;strong&gt;T1&lt;/strong&gt;) and the currently valid version in the historized data (schema &lt;strong&gt;T2&lt;/strong&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hash (business key)&lt;/th&gt;
&lt;th&gt;Hash (attributes)&lt;/th&gt;
&lt;th&gt;Type of change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;present in &lt;strong&gt;T1&lt;/strong&gt; and &lt;strong&gt;T2&lt;/strong&gt;, equal&lt;/td&gt;
&lt;td&gt;equal&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;present in &lt;strong&gt;T1&lt;/strong&gt; and &lt;strong&gt;T2&lt;/strong&gt;, equal&lt;/td&gt;
&lt;td&gt;not equal&lt;/td&gt;
&lt;td&gt;record was modified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;only in &lt;strong&gt;T1&lt;/strong&gt; (extracted)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;new record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;only in &lt;strong&gt;T2&lt;/strong&gt; (historized)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;record was deleted &lt;em&gt;(assumes a full delivery — see below)&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Limits of delete detection.&lt;/strong&gt; The fourth row only holds for a full delivery: if every run delivers the complete population, a record missing from &lt;strong&gt;T1&lt;/strong&gt; really was deleted — the deletions emerge as the difference between delivery and history. In a delta load, the same absence only means that the record was not delivered as a change. Two cases must therefore be distinguished for delta deliveries. If the source system soft-deletes and ships the delete flag, the deletion is processed like any other change and marked as deleted in &lt;strong&gt;T2&lt;/strong&gt;. If the source system deletes physically and ships nothing about it, deletions are fundamentally undetectable from the delivered data. This special case needs a separate mechanism: a periodic full reconciliation (which can also run on the business keys alone) or, where the source system offers it, a change feed such as CDC or Change Tracking. A detected delete is implemented under &lt;em&gt;SCD 2&lt;/em&gt; by closing the currently valid version (&lt;code&gt;valid_till&lt;/code&gt; is set) and marking it as deleted. Whether an additional dedicated delete version is created is a separate modeling decision.&lt;/p&gt;

&lt;h5&gt;
  
  
  Storing Hash Values
&lt;/h5&gt;

&lt;p&gt;When a record is inserted into the historized tables, updated, or flagged as deleted, the hash values of the new, modified, or deleted record are stored or updated there as well. This ensures that the hash values stored there always represent the status quo of the source systems and that change records can be identified via hash values at any later point (in subsequent ETL runs).&lt;/p&gt;

&lt;h5&gt;
  
  
  Promoting Only Error-Free Records
&lt;/h5&gt;

&lt;p&gt;Promoting an erroneous record — and later loading it into the target system — could cause an error and potentially abort the entire ETL run. Therefore, only error-free change records from schema &lt;strong&gt;T1&lt;/strong&gt; are stored in schema &lt;strong&gt;T2&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structural Transformation
&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%2Fb7c68y2pcrd27gyjdpbn.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%2Fb7c68y2pcrd27gyjdpbn.png" alt="Detail diagram, Structural Transformation: the active step is Load; the sub-steps Structural Transformation, Check Resolution, and Calculate Hash Values move data into schema L1; annotations for foreign-key and lookup-value resolution; erroneous records branch into the Error table." width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The structural transformation consists of the following sub-steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structural transformation and resolution of foreign-key relationships and lookup values&lt;/li&gt;
&lt;li&gt;Structural data quality check&lt;/li&gt;
&lt;li&gt;Data error logging&lt;/li&gt;
&lt;li&gt;Flagging of erroneous records&lt;/li&gt;
&lt;li&gt;Hash value computation&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Structural Transformation and Resolution of Foreign Key Relationships and Lookup Values
&lt;/h4&gt;

&lt;p&gt;The output of the structural transformation is data in table structures matching the target system. SQL statements with the required &lt;code&gt;JOIN&lt;/code&gt;s in the &lt;code&gt;FROM&lt;/code&gt; clause implement the structural transformation. Developing those statements requires solid knowledge of the data, the relationships among entities, and especially the foreign-key relationships among tables in the source system — or among the source systems being integrated.&lt;/p&gt;

&lt;p&gt;Besides the actual structural transformation of source data, the structural transformation resolves foreign-key relationships for the target system and determines the codes to store for lookup values. The result is stored in tables of schema &lt;strong&gt;L1&lt;/strong&gt;, whose structure, column names, and data types resemble those of the target system.&lt;/p&gt;

&lt;h4&gt;
  
  
  Structural Data Quality Check
&lt;/h4&gt;

&lt;p&gt;After the structural transformation, the result is checked: could all foreign-key relationships and lookup values be resolved? If no foreign key or no lookup code can be determined for a record, the record counts as erroneous. Since this check concerns the outcome of the structural transformation, it is called the &lt;em&gt;structural data quality check&lt;/em&gt; here.&lt;/p&gt;

&lt;h4&gt;
  
  
  Data Error Logging
&lt;/h4&gt;

&lt;p&gt;Detected data errors are logged in a readable, queryable form in an error table.&lt;/p&gt;

&lt;h4&gt;
  
  
  Flagging of Erroneous Records
&lt;/h4&gt;

&lt;p&gt;If a record contains at least one error, it is flagged as erroneous so it can be excluded from further processing. The flag lives in a column that stores the count of detected errors. Error-free records carry &lt;code&gt;NULL&lt;/code&gt; in this column.&lt;/p&gt;

&lt;h4&gt;
  
  
  Hash Value Computation
&lt;/h4&gt;

&lt;p&gt;The last sub-step of the structural transformation is computing two hash values per record. The first hash represents the business-key columns of a structurally transformed record; the second hash represents all remaining attribute columns. Both hashes let the next work package — &lt;strong&gt;Historization of Structurally Transformed Data&lt;/strong&gt; — identify change records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Historization of Structurally Transformed Data
&lt;/h3&gt;

&lt;p&gt;Historization of the structurally transformed data covers the same sub-steps as historization of the technically transformed data. It is an optional step, because — as long as the data from the technical transformation is historized — the structurally transformed data can always be reconstructed via the structural transformation.&lt;/p&gt;

&lt;p&gt;The structurally transformed data from tables in schema &lt;strong&gt;L1&lt;/strong&gt; is historized into tables of schema &lt;strong&gt;L2&lt;/strong&gt;. The approach is identical to historizing data from schema &lt;strong&gt;T1&lt;/strong&gt; into schema &lt;strong&gt;T2&lt;/strong&gt;. Only error-free change records are historized from &lt;strong&gt;L1&lt;/strong&gt; into &lt;strong&gt;L2&lt;/strong&gt;. New, changed, and deleted records are additionally marked with a flag indicating that they still need to be loaded into the target system. If the data of schema &lt;strong&gt;L2&lt;/strong&gt; is historized as well, it must never be deleted and should be backed up by a maintenance process. This makes it traceable when the ETL process handled which change. The actual time of change in the source system can only be read from this if the source delivers corresponding metadata.&lt;/p&gt;

&lt;p&gt;The procedures required to historize data into schema &lt;strong&gt;L2&lt;/strong&gt; must be developed manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading
&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%2F2goe8tinww77vtzodxqs.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%2F2goe8tinww77vtzodxqs.png" alt="Detail diagram, loading into the target system: the active step is Load; Load data moves records from schema L2 into the Destination; Mark loaded data marks them in L2 as loaded." width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The transformed and quality-checked data in schema &lt;strong&gt;L2&lt;/strong&gt; can now be loaded into the target system using a technology of choice. If historization in &lt;strong&gt;L2&lt;/strong&gt; is not enabled, loading happens directly from schema &lt;strong&gt;L1&lt;/strong&gt;. When loading from &lt;strong&gt;L2&lt;/strong&gt;, the change records to be loaded are identified via a flag indicating whether the record has already been loaded. Records loaded successfully into the target system are flagged accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;At the end stands the ground rule that runs through all work packages: each work package has its own schema, checks data quality at its boundary, and passes on only error-free records. Bad data stays isolated where it was detected — along with a log that narrows error diagnosis down to individual records. A single unconvertible value no longer has to abort the ETL run.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;E0&lt;/strong&gt;-to-&lt;strong&gt;L2&lt;/strong&gt; layering is a construction kit, not a dogma. If you do not need historization, drop &lt;strong&gt;T2&lt;/strong&gt; and &lt;strong&gt;L2&lt;/strong&gt;. If storage is tight, implement individual layers as views (see the FAQ for the trade-offs). In effect, the pattern works as persistent staging: the essential transformations run inside the database. The implementation layer beneath this architecture is shown in &lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Checking Data Quality with SQL&lt;/a&gt;. Where the pattern sits between ETL and ELT is covered in &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;.&lt;/p&gt;

&lt;p&gt;The pattern reaches its limits where per-package persistence does not pay off: streaming and low-latency requirements, extreme data volumes without matching storage, and simple one-way pipelines without reprocessing or audit needs.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What is the difference between technical and structural transformation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The technical transformation works on each record in isolation: it converts input values into the target data types (text → &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;decimal&lt;/code&gt;, …) and runs a first data-quality check at the value level, both without looking at other tables. The structural transformation, by contrast, needs context from the target system — resolving foreign keys, mapping lookup values — and therefore happens in its own work package after the technical transformation. Splitting them lets the two classes of errors be logged and fixed separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why materialize every work package in the database?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Materialization — writing each work package's output to a database table — decouples three things: The source system is read only once and stays untouched by the downstream packages. Every step becomes restartable without rerunning the entire ETL pipeline. And a traceable audit trail emerges for diagnosing errors on individual records. The additional storage and I/O cost is real and grows with data volume. It is the price for restartability, auditability, and error diagnosis — a worthwhile trade in this pattern's typical scenarios (migration and integration projects). At large volumes, it deserves a deliberate trade-off (see the next question on the persistence layers).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you really need all six persistence layers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not strictly. The full E0/E1/T1/T2/L1/L2 layering pays off mostly where audit trail, per-package restartability, and after-the-fact error analysis are hard requirements — typically in classical migration and CRM-integration projects with data volumes in the low to medium range. For large volumes or modern platforms such as Snowflake, Databricks, or BigQuery, some intermediate layers are often implemented as &lt;strong&gt;views&lt;/strong&gt; rather than materialized tables — the architectural logic stays the same, storage and I/O drop, but the transformation cost is paid again on every access. Rule of thumb: &lt;strong&gt;L1&lt;/strong&gt; is the first candidate for virtualization, because it can always be regenerated from the historized data in schema &lt;strong&gt;T2&lt;/strong&gt;. &lt;strong&gt;L2&lt;/strong&gt; only qualifies if its load flags and its own history are given up. &lt;strong&gt;T2&lt;/strong&gt; itself stays materialized — a history cannot be reconstructed from a current snapshot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When do you need historization (SCD)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Historization is primarily a business requirement: it is needed when state trajectories, historical reporting, or reproducible snapshots are required — regardless of the load type. Delta loads add a technical benefit on top: there, the order record is delivered but not the related customer, if the customer has not changed. Without historized customer data, the foreign-key relationship cannot be resolved. With full snapshot loads and no business need for history, SCD is dispensable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can this pattern be used with Postgres instead of SQL Server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The core concepts of the pattern — work packages, schema layering E0–L2, data quality at the boundaries, hash-based SCD — are not tied to SQL Server and can be implemented in any relational database. The &lt;strong&gt;technology stack&lt;/strong&gt; presented here, however, is consistently SQL Server-centric (SSIS, &lt;code&gt;OPENXML&lt;/code&gt;, &lt;code&gt;OPENJSON&lt;/code&gt;, &lt;code&gt;HASHBYTES&lt;/code&gt;, metadata-driven procedure generation). The most important Postgres equivalents are: &lt;code&gt;xmltable()&lt;/code&gt; for XML, &lt;code&gt;jsonb_to_recordset()&lt;/code&gt; or &lt;code&gt;JSON_TABLE&lt;/code&gt; (available since PostgreSQL 17) for JSON, and &lt;code&gt;digest(…, 'sha256')&lt;/code&gt; from &lt;code&gt;pgcrypto&lt;/code&gt; for hashes. For bulk imports into the staging tables, Postgres offers &lt;code&gt;COPY&lt;/code&gt; (it does not replace the orchestration of an SSIS), and loading into the target can be modeled with &lt;code&gt;MERGE&lt;/code&gt;. Structural adaptation can go deeper than just renaming functions — for example, the E0/E1 split for XML/JSON can often be dropped in Postgres because &lt;code&gt;xmltable()&lt;/code&gt; extracts directly from the source read. Whether the raw layer &lt;strong&gt;E0&lt;/strong&gt; is dropped remains a trade-off between storage and reproducibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does this pattern relate to Data Vault 2.0?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are conceptual parallels — business key, hash-based delta detection, persistent layers, auditability. The pattern does not implement Data Vault modeling, though: there are no hubs, links, or satellites with their modeling rules, no mandatory insert-only history, and no raw-vault-versus-business-vault separation. For classical migration and CRM-integration projects with audit requirements, the lighter pattern is pragmatic. For pure data-warehouse loading with multi-source integration, Data Vault 2.0 is worth a look.&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/data-quality-in-an-etl-process/" rel="noopener noreferrer"&gt;Data quality in an ETL process&lt;/a&gt; — root of the article series.&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; — foundational article on &lt;code&gt;TRY_CONVERT&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Implementation layer:&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;Checking Data Quality with SQL — a Configurable Framework&lt;/a&gt; — spotting bad data generically and classifying it by severity.&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;code&gt;fn_try_convert_*&lt;/code&gt; UDFs for the technical transformation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/logging-an-etl-process-with-t-sql/" rel="noopener noreferrer"&gt;Design Pattern // Logging an ETL process with T-SQL&lt;/a&gt; — cluster sibling covering the logging layer.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&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 architecture presented here as persistent-staging ELT.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>etl</category>
      <category>database</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Checking Data Quality with SQL — a Configurable Framework for Spotting Bad Data Generically</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Thu, 16 Jul 2026 21:12:39 +0000</pubDate>
      <link>https://dev.to/marcus1968/checking-data-quality-with-sql-a-configurable-framework-for-spotting-bad-data-generically-2cg9</link>
      <guid>https://dev.to/marcus1968/checking-data-quality-with-sql-a-configurable-framework-for-spotting-bad-data-generically-2cg9</guid>
      <description>&lt;p&gt;Bad data gives no warning. An age of 200 years, a duplicate customer number, a country code that doesn't exist — in the source system nobody notices. Only when the ETL run tries to push the rows into the strictly modelled target layer does the load break: on a &lt;code&gt;CHECK&lt;/code&gt;, on a &lt;code&gt;UNIQUE&lt;/code&gt; index, on a foreign key. &lt;strong&gt;Checking data quality with SQL&lt;/strong&gt; means finding exactly those rows beforehand, classifying them by severity and sorting them out deliberately — without a special tool, with a handful of generic SQL routines.&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;Three generic check routines — a &lt;strong&gt;WHERE clause&lt;/strong&gt;, a &lt;strong&gt;uniqueness check&lt;/strong&gt; and a &lt;strong&gt;foreign-key check&lt;/strong&gt; — cover a large share of typical data errors.&lt;/li&gt;
&lt;li&gt;All three write into &lt;strong&gt;one shared error table&lt;/strong&gt;: one row per violation, with the business key, the offending value and a plain-text message.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;severity&lt;/strong&gt; (error / warning / information) drives a &lt;strong&gt;quality gate&lt;/strong&gt;: only error-free rows flow on.&lt;/li&gt;
&lt;li&gt;All in plain PL/pgSQL — the same basic principle that specialised data-quality tools use too, here dependency-free to build yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Postgres as the example engine and a central staging layer that is loaded &lt;em&gt;raw&lt;/em&gt; first. The checks run against it set-based. That is the counter-design to the tool-centric package that mixes extraction, transformation and loading per table and scatters the quality check across the whole process (more on that in the architecture article of this series).&lt;/p&gt;

&lt;h2&gt;
  
  
  Data quality with SQL: the dimensions behind it
&lt;/h2&gt;

&lt;p&gt;There is plenty to read about data quality and little to grab hold of. The literature has agreed for decades on &lt;em&gt;what&lt;/em&gt; makes data good: Wang &amp;amp; Strong described fifteen dimensions in their 1996 paper "Beyond Accuracy", the DAMA UK working group singled out six of them in 2013 as core dimensions for practice (completeness, uniqueness, timeliness, validity, accuracy, consistency), and ISO/IEC 25012 standardises data-quality characteristics as a norm. What the literature rarely delivers is the &lt;em&gt;how&lt;/em&gt; — and when it does, it is usually tied to a particular tool.&lt;/p&gt;

&lt;p&gt;The concrete lever, though, is obvious: anyone who wants to check data quality with SQL formulates the check as a query and ends up with a table holding the bad records. Three routines cover a large share of practice, and each one cleanly maps to one of the established dimensions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check routine&lt;/th&gt;
&lt;th&gt;What it finds&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;WHERE clause on one table&lt;/td&gt;
&lt;td&gt;values outside allowed ranges, missing mandatory values&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Validity&lt;/strong&gt; (+ Completeness)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uniqueness / maximum occurrences&lt;/td&gt;
&lt;td&gt;duplicates, over-frequent keys&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Uniqueness&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Foreign key against a reference&lt;/td&gt;
&lt;td&gt;orphaned rows without a master&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Consistency / Integrity&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That covers four of the six dimensions; timeliness and accuracy — in the sense of matching the real world — lie outside their reach and need other means. The theory behind it — error classes, the criteria canon, and the full coverage map including the two blind spots — is deepened in 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;.&lt;/p&gt;

&lt;p&gt;Honestly placed: this approach is not new. Tools like Soda Core, dbt tests or Great Expectations follow the same approach at the core — formulate check logic, collect the hits, attach a severity; whether that happens as generated SQL or against another engine (Pandas, Spark, a data warehouse) is an implementation detail. Around that, granted, they offer more, from monitoring through profiling to lineage. The value of the home-built version is not originality but transparency: every line is readable, nothing is bound to a product, and it runs everywhere you aren't allowed to install an extra tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common denominator: one error table
&lt;/h2&gt;

&lt;p&gt;The backbone is not the check but its result. All three routines write into the &lt;strong&gt;same&lt;/strong&gt; table — one row per violation found, stored so that the source record can later be identified unambiguously:&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;error&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;id&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="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&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;id1_value&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;id2_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;id2_value&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="n"&gt;id3_column&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;id3_value&lt;/span&gt;      &lt;span class="nb"&gt;text&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;error_column&lt;/span&gt;   &lt;span class="nb"&gt;text&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;error_value&lt;/span&gt;    &lt;span class="nb"&gt;text&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;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;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;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;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;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;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;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;pk_error&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;id&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="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ck_error_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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The column pairs &lt;code&gt;id1_column&lt;/code&gt;/&lt;code&gt;id1_value&lt;/code&gt; through &lt;code&gt;id3_column&lt;/code&gt;/&lt;code&gt;id3_value&lt;/code&gt; hold the &lt;strong&gt;business key&lt;/strong&gt; of the affected record: &lt;code&gt;id1_column&lt;/code&gt; holds the &lt;em&gt;column name&lt;/em&gt; (say &lt;code&gt;customer_id&lt;/code&gt;), &lt;code&gt;id1_value&lt;/code&gt; the &lt;em&gt;value&lt;/em&gt; (say &lt;code&gt;4711&lt;/code&gt;). That lets you reconstruct the bad row later — &lt;code&gt;WHERE customer_id = 4711&lt;/code&gt;. Three pairs are enough for composite keys; in practice one almost always suffices. Alongside, each row records the &lt;strong&gt;checked column&lt;/strong&gt; (&lt;code&gt;error_column&lt;/code&gt;), the &lt;strong&gt;offending value&lt;/strong&gt; (&lt;code&gt;error_value&lt;/code&gt;), the &lt;strong&gt;message&lt;/strong&gt; in plain text and the &lt;strong&gt;severity&lt;/strong&gt; (&lt;code&gt;severity&lt;/code&gt;: &lt;code&gt;E&lt;/code&gt;rror, &lt;code&gt;W&lt;/code&gt;arning, &lt;code&gt;I&lt;/code&gt;nformation).&lt;/p&gt;

&lt;h2&gt;
  
  
  The configuration: one rule per row
&lt;/h2&gt;

&lt;p&gt;The check rules are not cast into code but written into a table. One row = one rule. A new check is a data row instead of a code change — it needs no code deployment, and the business side can read along with what is being checked:&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;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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;id&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="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&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;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="c1"&gt;-- 'constraint' | 'unique' | 'lookup'&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;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="n"&gt;id1_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="c1"&gt;-- business key (up to three)&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;id2_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;id3_column&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="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="c1"&gt;-- checked column(s); composite as comma list&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;where_clause&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;               &lt;span class="c1"&gt;-- 'constraint': the "bad" predicate&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;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="c1"&gt;-- 'unique': allowed occurrences&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;ref_schema&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;               &lt;span class="c1"&gt;-- 'lookup': reference table&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;ref_table&lt;/span&gt;       &lt;span class="nb"&gt;text&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;ref_column&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;               &lt;span class="c1"&gt;-- position-aligned with check_column&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;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="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;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;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;active&lt;/span&gt;          &lt;span class="nb"&gt;boolean&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;true&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;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;pk_check_rule&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;id&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check_type&lt;/code&gt; decides which of the three routines is built for the row. Depending on the type, different columns matter: &lt;code&gt;where_clause&lt;/code&gt; for the WHERE check, &lt;code&gt;max_occurrence&lt;/code&gt; for uniqueness, &lt;code&gt;ref_schema&lt;/code&gt;/&lt;code&gt;ref_table&lt;/code&gt;/&lt;code&gt;ref_column&lt;/code&gt; for the foreign key. &lt;code&gt;severity&lt;/code&gt; and &lt;code&gt;message&lt;/code&gt; hang on every rule — so each finding carries its severity and its plain text straight from the configuration. Nobody has to enter the rows for required fields, key uniqueness and type limits by hand, though: how these rules can be derived from &lt;code&gt;information_schema&lt;/code&gt; is shown 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;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routine 1: the WHERE clause
&lt;/h2&gt;

&lt;p&gt;The simplest and at the same time most powerful check: a condition that describes &lt;em&gt;bad&lt;/em&gt; rows, attached to a table. Everything the condition matches is a finding. For the rule "age must be between 0 and 120" the routine produces this 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="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;error&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;id1_value&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;error_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_value&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;SELECT&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;'staging'&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="s1"&gt;'customer'&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="s1"&gt;'customer_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;T01&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="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="s1"&gt;'age'&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="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;::&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="s1"&gt;'E'&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="s1"&gt;'Age out of range 0..120'&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;FROM&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;staging&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;T01&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;age&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;OR&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;120&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The predicate on line 15 comes unchanged from &lt;code&gt;where_clause&lt;/code&gt;. With that, this one routine covers a whole family: ranges (&lt;code&gt;age &amp;lt; 0 OR age &amp;gt; 120&lt;/code&gt;), mandatory fields (&lt;code&gt;email IS NULL&lt;/code&gt;), formats (&lt;code&gt;length(zip) &amp;lt;&amp;gt; 5&lt;/code&gt;), plausibility (&lt;code&gt;order_date &amp;gt; current_date&lt;/code&gt;). Two dimensions at once — validity and completeness. How to push this routine to its limits — and which NULL trap of three-valued logic it has to avoid — is covered in depth by the spoke &lt;a href="https://sql.marcus-belz.de/en/validate-data-with-sql/" rel="noopener noreferrer"&gt;Validating Data with SQL&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routine 2: uniqueness and cardinality
&lt;/h2&gt;

&lt;p&gt;Duplicate keys are the classic case. The check is a &lt;code&gt;GROUP BY&lt;/code&gt; with &lt;code&gt;HAVING&lt;/code&gt; on the count:&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;customer_id&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;staging&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;5&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;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;customer_id&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;HAVING&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;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;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real trick sits in the &lt;code&gt;&amp;gt; 1&lt;/code&gt;: it comes from &lt;code&gt;max_occurrence&lt;/code&gt;. Instead of checking for &lt;em&gt;uniqueness&lt;/em&gt;, you check for a &lt;em&gt;maximum count&lt;/em&gt; — &lt;code&gt;&amp;gt; 1&lt;/code&gt; for true uniqueness, &lt;code&gt;&amp;gt; 3&lt;/code&gt; for example when a key may appear at most three times. The routine then joins the keys it found back onto the table and logs &lt;strong&gt;every&lt;/strong&gt; occurrence (not just the first), so that every affected row appears in the error table.&lt;/p&gt;

&lt;p&gt;What is checked here is exactly the business key that identifies the record — the same column that lands in the error table as &lt;code&gt;id1_column&lt;/code&gt;. That is no coincidence: this check directly mirrors the &lt;code&gt;UNIQUE&lt;/code&gt; constraint that the strict target layer carries on the business key. What &lt;em&gt;must&lt;/em&gt; be unique there is caught up front in the source. How to push this routine — maximum cardinality, composite keys and the NULL trap in &lt;code&gt;UNIQUE&lt;/code&gt;, where SQL Server and Postgres differ — is deepened 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;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routine 3: referential integrity
&lt;/h2&gt;

&lt;p&gt;A foreign key into the void — a &lt;code&gt;country_code&lt;/code&gt; for which there is no entry in the master-data table. Put generically: all rows of the child table that find no partner in the master table via &lt;code&gt;LEFT JOIN&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;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&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;staging&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;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;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&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;country&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;country_code&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;country_code&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="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country_code&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;10&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;country_code&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Child table, child column(s) (&lt;code&gt;check_column&lt;/code&gt;) and master (&lt;code&gt;ref_schema&lt;/code&gt;/&lt;code&gt;ref_table&lt;/code&gt;/&lt;code&gt;ref_column&lt;/code&gt;) come from the configuration — so the same routine works for any master-child relationship. A composite foreign key sits as a position-aligned comma list in &lt;code&gt;check_column&lt;/code&gt; and &lt;code&gt;ref_column&lt;/code&gt;: the first child field belongs to the first master field, and the runner splits both lists and builds the join over all pairs. The &lt;code&gt;IS NOT NULL&lt;/code&gt; condition on line 9 deliberately separates "unknown value" (an error) from "no value given" (that is the WHERE routine's job). How to push this routine — the three phrasings &lt;code&gt;LEFT JOIN … IS NULL&lt;/code&gt;/&lt;code&gt;NOT EXISTS&lt;/code&gt;/&lt;code&gt;NOT IN&lt;/code&gt;, the notorious &lt;code&gt;NOT IN&lt;/code&gt;-plus-&lt;code&gt;NULL&lt;/code&gt; trap, and composite and self-referencing foreign keys — is deepened by the spoke &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;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The runner: dynamic SQL — done safely
&lt;/h2&gt;

&lt;p&gt;The three statements above are hard-coded. They become generic when a function assembles them at runtime from the configuration. In PL/pgSQL, &lt;code&gt;format()&lt;/code&gt; is the right tool — and the point where you have to be careful. Identifiers belong in with &lt;code&gt;%I&lt;/code&gt;, literals with &lt;code&gt;%L&lt;/code&gt;; both are quoted correctly by Postgres and rule out SQL injection through table and column names. Here is the branch for the WHERE check:&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="n"&gt;l_sql&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="err"&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;error&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;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="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;id1_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;id1_value&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;error_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;error_value&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;severity&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;message&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;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;13&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="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;L&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="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;L&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="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;L&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;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&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="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;L&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="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&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="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;L&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="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;L&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="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="n"&gt;T01&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;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                         &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="err"&gt;$&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;l_rule&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;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;l_rule&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="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;l_rule&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;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;l_rule&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;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;l_rule&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;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;l_rule&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;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;l_rule&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;l_rule&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="n"&gt;l_rule&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;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;l_rule&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;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;l_rule&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;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;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;l_sql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema and column names (lines 16, 18, 22) go through &lt;code&gt;%I&lt;/code&gt;, the fixed values through &lt;code&gt;%L&lt;/code&gt;. The uniqueness and foreign-key branches are built on the same pattern — only the inner statement differs.&lt;/p&gt;

&lt;p&gt;One spot stays deliberately raw: the predicate on line 24 is inserted with &lt;code&gt;%s&lt;/code&gt;, i.e. as unchanged SQL. It &lt;strong&gt;has&lt;/strong&gt; to be — &lt;code&gt;where_clause&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; a SQL expression, not a value. That makes the &lt;code&gt;dq.check_rule&lt;/code&gt; table the &lt;strong&gt;trust boundary&lt;/strong&gt; of the system: whoever may write there can have arbitrary SQL executed. In practice this is uncritical, because the configuration is maintained administratively and never filled from user input — but you have to know it and secure it. Identifiers, by contrast, are watertight through &lt;code&gt;%I&lt;/code&gt;: a column name like &lt;code&gt;age"; DROP TABLE staging.customer; --&lt;/code&gt; from the configuration becomes a (non-existent) quoted identifier and raises a clean error instead of dropping the table.&lt;/p&gt;

&lt;p&gt;Across all active rules of a table the runner loops, executes the built statement per rule and finally writes the severity counters back. The &lt;code&gt;active&lt;/code&gt; flag is a versioned switch for the manually maintained rules: it is toggled as part of a deployment, not live. Rules derived from the schema deliberately have no such flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Take it with you:&lt;/strong&gt; The complete framework — error table, rule table, runner, demo data and gate query — is available as a runnable &lt;a href="https://sql.marcus-belz.de/en/download/2940/" rel="noopener noreferrer"&gt;SQL bundle for download&lt;/a&gt; (PostgreSQL 13+, verified against Postgres 16). An empty database is all you need; the script sets up everything itself.&lt;/p&gt;

&lt;p&gt;A word on runtime, to be honest: most freely configured predicates run as a full-table scan over the source table, because no matching index exists for an arbitrary expression — across many rules on large tables that adds up. A simple &lt;code&gt;email IS NULL&lt;/code&gt; or a key check may well use an index, but with freely configured rules you cannot rely on it. It is acceptable because the check runs in the staging window that is scheduled anyway, against the freshly loaded set, not against the production system. For very large tables it pays to limit the check to the partitions or batches loaded in the current run, instead of scanning everything every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Severity and the quality gate
&lt;/h2&gt;

&lt;p&gt;Up to here, &lt;code&gt;dq.error&lt;/code&gt; holds &lt;em&gt;what&lt;/em&gt; is wrong. Steering the ETL process, however, needs a statement &lt;em&gt;per record&lt;/em&gt;: may it proceed or not? For that the source table gets three counter columns (&lt;code&gt;sys_error&lt;/code&gt;, &lt;code&gt;sys_warning&lt;/code&gt;, &lt;code&gt;sys_info&lt;/code&gt;), the runner fills them after each run via the single-column business key (&lt;code&gt;id1&lt;/code&gt;), and the gate then is a trivial WHERE condition: &lt;code&gt;sys_error = 0&lt;/code&gt;. Only rows without an error flow into the next layer, warnings and information are logged but no obstacle — in the demo set of seven rows exactly two pass the gate this way, the clean one and the one with the missing email. Why three levels instead of binary pass/fail, why severity also decides whether a rule belongs in the target schema as a constraint or stays in the pipeline, and what happens when a warning is promoted to an error is deepened by the spoke &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;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just constraints?
&lt;/h2&gt;

&lt;p&gt;The obvious question: if the target layer has &lt;code&gt;CHECK&lt;/code&gt;, &lt;code&gt;UNIQUE&lt;/code&gt; and foreign-key constraints anyway — why the effort? The answer lies precisely there. The downstream tables &lt;em&gt;are&lt;/em&gt; strictly modelled; that is intended. But 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 — a single bad row stops the entire process, and you don't even know which one.&lt;/p&gt;

&lt;p&gt;That is exactly why you check in the &lt;strong&gt;source up front&lt;/strong&gt;: you identify &lt;em&gt;all&lt;/em&gt; rows that would fail at the target constraints, classify them by severity and let only the clean ones pass. The framework does not rebuild the constraints — it is the transparent, auditable pre-filter &lt;em&gt;before&lt;/em&gt; a deliberately strict target layer. Instead of an aborted load you get a table of findings and a process that carries on with the good data.&lt;/p&gt;

&lt;p&gt;In fact the three routines are exactly the pre-filter for the three constraint types the target enforces:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint at the target&lt;/th&gt;
&lt;th&gt;Check routine in the source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CHECK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WHERE clause (Routine 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNIQUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uniqueness (Routine 2)&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;Foreign key (Routine 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What is &lt;em&gt;enforced&lt;/em&gt; as a constraint at the target is checked in the source &lt;em&gt;up front&lt;/em&gt; — on the same business key that carries the &lt;code&gt;UNIQUE&lt;/code&gt; constraint at the target. The difference is not &lt;em&gt;what&lt;/em&gt; is checked but &lt;em&gt;how&lt;/em&gt; the violation is handled: report and classify instead of aborting the load.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this approach does not cover
&lt;/h2&gt;

&lt;p&gt;Three routines are a lot, but not everything — and it pays to be honest about where the line lies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repair&lt;/strong&gt; is deliberately not part of it. The framework &lt;em&gt;finds&lt;/em&gt; and &lt;em&gt;makes transparent&lt;/em&gt;; it corrects nothing. That is a decision, not an omission — transparency first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-field business rules&lt;/strong&gt; ("discount only if status = active") can sometimes be expressed as a WHERE clause, sometimes not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal consistency, accuracy against an external truth, complex patterns&lt;/strong&gt; (beyond simple length and format checks) are topics of their own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you'd rather use a finished tool: Soda Core, dbt tests and Great Expectations cover the same area, free and well maintained. The SQL home-build pays off when you want transparency, zero dependencies and full control over every line — or simply work in an environment where no extra tool may be installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres-to-SQL-Server bridge
&lt;/h2&gt;

&lt;p&gt;The pattern is not Postgres-specific. In SQL Server, &lt;code&gt;sp_executesql&lt;/code&gt; takes the role of &lt;code&gt;EXECUTE format()&lt;/code&gt;; identifiers are protected there with &lt;code&gt;QUOTENAME()&lt;/code&gt; instead of &lt;code&gt;%I&lt;/code&gt;. The structure stays identical: a configuration table, a cursor (or a loop) over the rules, a dynamically built &lt;code&gt;INSERT … SELECT … WHERE&lt;/code&gt; per rule, a shared error table and severity columns in the source as a gate. Anyone coming from SQL Server transfers the approach one to one.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need a dedicated tool for data quality?&lt;/strong&gt;&lt;br&gt;
No. Checking data quality with SQL works with three generic routines — range, uniqueness and foreign-key checks — that cover a large share of typical errors. Tools like Soda or dbt take work off your hands but at the core do the same: build SQL, collect hits, attach a severity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isn't dynamic SQL a security risk?&lt;/strong&gt;&lt;br&gt;
Only if you glue inputs together raw. With &lt;code&gt;format()&lt;/code&gt; and &lt;code&gt;%I&lt;/code&gt; (identifiers) or &lt;code&gt;%L&lt;/code&gt; (literals), Postgres quotes correctly and injection through table/column names is ruled out. The freely configurable predicate (&lt;code&gt;where_clause&lt;/code&gt;) is deliberately raw SQL — which is why the configuration table is the trust boundary and belongs under administrative protection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just use the target table's CHECK and foreign-key constraints?&lt;/strong&gt;&lt;br&gt;
Because a constraint aborts the whole load instead of reporting bad rows one by one. The check in the source identifies &lt;em&gt;all&lt;/em&gt; rows up front that would fail at the target, classifies them and lets only clean ones pass — auditable instead of aborted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I find the faulty record again from the error table?&lt;/strong&gt;&lt;br&gt;
Via the &lt;code&gt;id*_column&lt;/code&gt;/&lt;code&gt;id*_value&lt;/code&gt; pairs: they hold the column name and value of the business key. &lt;code&gt;WHERE &amp;lt;id1_column&amp;gt; = &amp;lt;id1_value&amp;gt;&lt;/code&gt; leads back to the source row. For composite keys, up to three pairs are available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this work in SQL Server too?&lt;/strong&gt;&lt;br&gt;
Yes. &lt;code&gt;sp_executesql&lt;/code&gt; replaces &lt;code&gt;EXECUTE format()&lt;/code&gt;, &lt;code&gt;QUOTENAME()&lt;/code&gt; replaces &lt;code&gt;%I&lt;/code&gt;. Configuration table, error table, severity gate and the three routines stay structurally the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&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 bigger picture: catching technical and business errors before they reach the target system.&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; — how bad data is cleanly isolated across layered staging levels.&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; — the field-wise validity check when converting, the "T" building block of this series.&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 spoke for Routine 1: value ranges, required fields and the NULL trap of three-valued logic in detail.&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 spoke for Routine 2: maximum cardinality, composite keys and the NULL trap of &lt;code&gt;UNIQUE&lt;/code&gt; semantics between SQL Server and Postgres.&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 spoke for Routine 3: &lt;code&gt;LEFT JOIN … IS NULL&lt;/code&gt; vs. &lt;code&gt;NOT EXISTS&lt;/code&gt; vs. &lt;code&gt;NOT IN&lt;/code&gt;, the NOT-IN-plus-NULL trap and composite/self-referencing 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;Deriving Data Quality Rules from the Schema&lt;/a&gt; — the spoke on where the rules come from: deriving required fields, key uniqueness and type limits from &lt;code&gt;information_schema&lt;/code&gt; instead of typing them into the configuration by hand, with a deployment projection and a drift detector.&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: why binary pass/fail fails and how severity decides whether a rule becomes a constraint or stays in the pipeline.&lt;/li&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 theory frame of the series: error classes, the criteria per Apel et al., and an honest coverage map of what SQL reaches — and the two criteria it cannot.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>dataquality</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Is Claude a Woman or a Man? — and Why We Ask in the First Place</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Thu, 09 Jul 2026 08:15:32 +0000</pubDate>
      <link>https://dev.to/marcus1968/is-claude-a-woman-or-a-man-and-why-we-ask-in-the-first-place-4ga9</link>
      <guid>https://dev.to/marcus1968/is-claude-a-woman-or-a-man-and-why-we-ask-in-the-first-place-4ga9</guid>
      <description>&lt;p&gt;"You spend more time with her than with me." That's a sentence you usually hear when an affair comes to light — in my case, it was about Claude Code. And suddenly there was a question in the room I had never thought about before: Is Claude actually a woman or a man?&lt;/p&gt;

&lt;p&gt;What this article puts on trial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The evidence for "woman" and the evidence for "man" — a circumstantial trial in two acts&lt;/li&gt;
&lt;li&gt;The verdict: a SQL query whose result is only four letters long&lt;/li&gt;
&lt;li&gt;Why we assign gender to tools in the first place — from ships to the GPS voice&lt;/li&gt;
&lt;li&gt;What that means for working with Claude Code: calibrating trust instead of adopting a colleague&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prerequisites: none. Although, if you have ever lost a row to a &lt;code&gt;NULL&lt;/code&gt; in a &lt;code&gt;WHERE&lt;/code&gt; clause, the verdict will be twice the fun.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Case: A First Name in the Terminal
&lt;/h2&gt;

&lt;p&gt;Anthropic could have called its language model "Assistant 3000". Instead, it got a French first name — and first names trigger reflexes: Whoever has a first name has a face, a voice, a story. And, so the reflex insists, a gender.&lt;/p&gt;

&lt;p&gt;English is comparatively merciful here — "the AI" and "the model" carry no gender. My native German is not: every noun drags a gendered article along, so German speakers cast a vote with every sentence — "die KI" (feminine) or "der Assistent" (masculine). And anyone who works with Claude Code all day catches themselves thinking "he solved that cleanly" — or "she's contradicting me again" — in any language. The question sounds silly, but it leads somewhere interesting: to the line between tool and colleague. So let's try it properly: two lines of evidence, one verdict — and then the actually interesting follow-up question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evidence for "Woman"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Exhibit 1: language.&lt;/strong&gt; In French, the name's home country, artificial intelligence is feminine: &lt;em&gt;une IA&lt;/em&gt;. In my native German, it is too: &lt;em&gt;die KI&lt;/em&gt;, &lt;em&gt;die Maschine&lt;/em&gt;, &lt;em&gt;die Antwort&lt;/em&gt; — whoever says "frag mal die KI" has already ruled. Two languages, one tendency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exhibit 2: the first name.&lt;/strong&gt; Claude is one of the few French first names that have been used for both genders for centuries. Claude Pompidou was France's First Lady, Claude Jade starred for Truffaut, Claude Cahun photographed her way through every role model of the 20th century. If you hear "Claude" and automatically picture a man, you only know half the name's history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exhibit 3: the demeanor.&lt;/strong&gt; The cliché says: listens, apologizes a lot, weighs every position three times. Sounds like Claude. (That this is a cliché &lt;em&gt;about&lt;/em&gt; women rather than a property &lt;em&gt;of&lt;/em&gt; women is part of the evidence — this exact mechanism will keep us busy after the verdict.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exhibit 4: the jealousy.&lt;/strong&gt; The strongest piece of evidence comes from my own living room — see the opening: my girlfriend is jealous of Claude. By now she knows my excuses by heart: "I just need to ask Claude something real quick." "Five minutes, honestly." "No, we're just discussing index strategies." And jealousy is a surprisingly precise measuring instrument: Nobody has ever been jealous of a wrench. Of a colleague you talk to for hours every day — apparently, yes. (This exhibit, too, will reappear after the verdict — it is living proof for the chapter on anthropomorphization.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evidence for "Man"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Exhibit 1: language, now for the other side.&lt;/strong&gt; In English, the default pronoun for a coding agent slips toward "he" with remarkable ease — "ask him to fix the branch", "he's already opened the PR". And German votes twice: &lt;em&gt;der Assistent&lt;/em&gt;, &lt;em&gt;der Agent&lt;/em&gt;, &lt;em&gt;der Chatbot&lt;/em&gt;, &lt;em&gt;der Algorithmus&lt;/em&gt; — all masculine. Whoever says "the agent has already created the branch — he's fast today" has ruled just as firmly, only the other way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exhibit 2: the namesakes.&lt;/strong&gt; Claude Monet, Claude Debussy, Claude Lévi-Strauss — and Claude Shannon, the founder of information theory, most frequently traded as the secret namesake. Anthropic has never officially confirmed it; but what name would suit a language model better than that of the man who made the information content of language computable? (Objection from the prosecution: speculation. — Sustained. The exhibit stays in the record anyway.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exhibit 3: the demeanor, cross-check.&lt;/strong&gt; The cliché says: explains things unasked and at full length, is remarkably sure of itself — especially when it's wrong. Anyone who has ever received a confidently delivered, entirely invented API signature nods knowingly at this point.&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%2F0qdzmee7i51sbboeusp1.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%2F0qdzmee7i51sbboeusp1.png" alt="Comic: A young woman studies a balance scale weighing " width="768" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A note for readers without German, because the two pans only pair up this way there: the labels lean on German grammar, where &lt;em&gt;the AI&lt;/em&gt; is a &lt;strong&gt;she&lt;/strong&gt; (&lt;em&gt;die KI&lt;/em&gt;) and &lt;em&gt;the assistant&lt;/em&gt; is a &lt;strong&gt;he&lt;/strong&gt; (&lt;em&gt;der Assistent&lt;/em&gt;). The scale weighs exactly those two grammatical camps against each other.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Verdict
&lt;/h2&gt;

&lt;p&gt;Two lines of evidence, both conclusive, both built on clichés and pronouns. They cancel each other out exactly. The court retires to deliberate — to the place where all verdicts on this blog are rendered: the database.&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;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;gender&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="n"&gt;assistants&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="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Claude'&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="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- name    gender&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;-- Claude  NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it is, in four letters: &lt;code&gt;NULL&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%2F1esl3k1gxj175505bfb3.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%2F1esl3k1gxj175505bfb3.png" alt="Comic: A judge bangs his gavel at a laptop labeled Claude — " width="768" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cross-check confirms the verdict — Claude shows up neither among the women nor among the non-women, because &lt;code&gt;NULL&lt;/code&gt; is neither equal nor unequal to anything:&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="o"&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;assistants&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;gender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'female'&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="c1"&gt;-- 0&lt;/span&gt;
  &lt;span class="mi"&gt;8&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="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="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="n"&gt;assistants&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;gender&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'female'&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="c1"&gt;-- 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In SQL, &lt;code&gt;NULL&lt;/code&gt; does not mean "empty" and it does not mean "zero". It means: there is no value here. It gets interesting once you ask &lt;em&gt;why&lt;/em&gt; there is none — and there are two very different readings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Missing, but applicable:&lt;/strong&gt; The value exists, we just don't know it. The birth date of a customer who never provided it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing, because not applicable:&lt;/strong&gt; There is no value that could belong in this column at all. The shoe size of a number. The maiden name of a warehouse shelf.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Edgar F. Codd, the inventor of the relational model, even wanted to distinguish these two cases with two separate markers — it never caught on; to this day, SQL has one &lt;code&gt;NULL&lt;/code&gt; for both. The Claude case is clearly the second reading: The gender is not &lt;em&gt;unknown&lt;/em&gt;, it is &lt;em&gt;not applicable&lt;/em&gt;. There is no hidden gender that Anthropic keeps secret — there is simply no value that belongs in this column. Claude itself, by the way, answers the question exactly the same way: neither, in every language.&lt;/p&gt;

&lt;p&gt;That would close the case. But the more interesting question only starts here: Why did we ask in the first place?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Assign Gender to Tools
&lt;/h2&gt;

&lt;p&gt;The reflex is old and well documented. Ships have been "she" in English for centuries. The GPS voice is "the lady in the nav" in a surprising number of households, although nobody lives in there. Voice assistants like Alexa and Siri entered the market with female-coded names and default voices — a design decision that has drawn plenty of criticism since, because it perpetuates the pattern "assisting role = female".&lt;/p&gt;

&lt;p&gt;And we act accordingly: We thank machines. We say "please" to Siri. We comfort the robot vacuum when it gets stuck under the sofa. Communication research measured this reflex back in the 1990s — Byron Reeves and Clifford Nass showed that people respond to computers with the same social patterns as to humans, even when they know perfectly well there is a machine in front of them. That is not stupidity, it is economy: Our brain has exactly one module for dialogue, and it was trained on humans.&lt;/p&gt;

&lt;p&gt;A language model with a first name that answers in full sentences, apologizes and asks follow-up questions hits that module with full force. Wanting to assign Claude a gender is not an accident — it is the expected consequence of Claude &lt;em&gt;functioning&lt;/em&gt; like a conversation partner. And the mechanism works in both directions: The jealousy from Exhibit 4 is the same reflex, just seen from the outside — if you spend hours talking to "someone", your environment sees a relationship, not a toolchain. The question "woman or man?" is the most harmless symptom of this mechanism. The less harmless one follows in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for Working with Claude Code
&lt;/h2&gt;

&lt;p&gt;If you develop with Claude Code, you work in dialogue for hours and days on end. The anthropomorphization reflex runs in the background the whole time — and it has a side effect that directly concerns code quality: &lt;strong&gt;Colleagues get the benefit of the doubt, tools get checked.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The colleague who has delivered good work for years gets his pull request waved through once in a while. Exactly this pattern transfers to the AI assistant once you internally promote it to colleague: After twenty good answers, you stop really reading the twenty-first. But the twenty-first answer of a language model is statistically just as much at risk as the first — the model has no reputation to lose and does not keep track of its own success rate.&lt;/p&gt;

&lt;p&gt;The verdict has been rendered — but it comes with terms of probation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Review discipline independent of gut feeling.&lt;/strong&gt; Generated code gets read, executed and tested — even if "the colleague" was right ten times in a row. What that means in practice is what the convention and workflow articles of this blog are about: rules that enforce conventions, and checks that find errors mechanically instead of relying on trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-name terms are allowed, the responsibility stays here.&lt;/strong&gt; It is perfectly fine to say "he" or "she" and to thank the assistant — as long as accountability stays clear: bugs in deployed SQL belong to the human who merged it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gender stays &lt;code&gt;NULL&lt;/code&gt;. The responsibility stays &lt;code&gt;NOT NULL&lt;/code&gt;. And the pull request still gets a review.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Where does the name Claude come from?&lt;/strong&gt; Anthropic has never officially explained it. The most common guess is a bow to Claude Shannon, the founder of information theory. The only thing certain: It is a deliberately human first name — and in French one of the few that are used for women and men alike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does Claude itself answer?&lt;/strong&gt; Neither. Claude describes itself as an AI without gender and without a body — consistently, whether you ask in English, German or French.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it harmful to humanize the AI?&lt;/strong&gt; Not per se — the reflex is normal and makes the collaboration more pleasant. It only gets risky when humanizing turns into the benefit of the doubt: when generated code goes through unread because "the colleague" has been reliable so far. The solution is not less friendliness, but more systematic reviews.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And what do I do about the jealousy at home?&lt;/strong&gt; Read out the verdict. Granted: "It's just a tool" sounds exactly like something a person with something to hide would say. But &lt;code&gt;NULL&lt;/code&gt; is, after all, the only relationship status where guaranteed nothing is going on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do Alexa and Siri have female voices, while Claude has no persona at all?&lt;/strong&gt; The voice assistants of the 2010s were deliberately designed as friendly service personas — with a name, a voice and a small-talk repertoire. Anthropic went a different way: a human first name, but no fixed voice, no avatar, no gender. That is a deliberate design decision, and it emphasizes the tool character.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The NULL side of the story:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&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 — Ranges, Required Fields and the NULL Trap&lt;/a&gt; — why NULL values fall through &lt;code&gt;WHERE&lt;/code&gt; filters and how to find them anyway&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The serious AI side of this blog:&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; — how "the colleague" becomes a tool with enforced conventions&lt;/li&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&lt;/a&gt; — the place to start if you want to build with Claude yourself&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code — What Auto-Loads, What Loads on Demand&lt;/a&gt; — the mechanics behind review discipline: conventions that do not depend on gut feeling&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>sql</category>
      <category>watercooler</category>
    </item>
  </channel>
</rss>
