Solutions · SQL Server & SSIS

Your pipeline lives in msdb, not in git

SSIS keeps parameters in SSISDB. SQL Agent keeps job steps in msdb. Transformation logic sits in stored procedures with no commit history. Connecting a repository does not make any of that visible — reading the server does.

Where the pipeline actually lives

The reflex when investigating an ETL failure is to open the repository. On a SQL Server estate that reflex is usually wrong, because the repository holds a minority of the things that determine what the pipeline does.

A typical nightly load is a SQL Agent job with eight steps. Step one runs a stored procedure. Step four executes an SSIS package with parameters supplied by an environment reference. Step six calls a command-line utility somebody wrote in 2018. The schedule lives in msdb, the parameters live in SSISDB, the procedure body lives in sys.sql_modules, and only the utility is in git.

This is not unusual or a sign of a badly run team. It is what SQL Server ETL looks like almost everywhere, and it is precisely why the modern data-observability tools cannot help — they assume a warehouse, a transformation framework, and a repository that describes both.

Where each part of a SQL Server pipeline is defined, and whether source control can see it
What Where it lives In git?
Job schedule and stepsmsdb.dbo.sysjobs, sysjobstepsNo
Execution historymsdb.dbo.sysjobhistoryNo
SSIS package logic.dtsx in a repo, deployed .ispac in SSISDBPartly
SSIS parameters & environmentsSSISDB.catalogNo
Transformation logicsys.sql_modules (stored procedures)Sometimes
Table & column schemasys.tables, sys.columnsOnly if migrations exist
Linked server definitionssys.serversNo
Deadlock evidencesystem_health extended eventsNo
Application ETL codeYour repositoryYes

Why connecting a repository doesn't help

Even when a repository exists and is well maintained, three gaps remain, and each one has caused incidents that took days to explain.

The deployed thing is not the committed thing

An .ispac deployed to SSISDB in March does not update because someone merged a change in June. The catalogue tells you what is actually deployed; the repository tells you what somebody intended.

Parameters are not code

The same package, executed with a different connection manager or batch size, is effectively a different pipeline. Environment references and parameter values are configuration held in SSISDB, and they are frequently changed without any corresponding commit.

Procedures often have no repository at all

A stored procedure altered directly in production leaves a modify_date and nothing else. There is no diff, no author, no pull request, and no way to correlate the change with an incident — unless you read the definition and compare it against what you expected.

How to find what actually ran

Five queries that answer most SQL Server ETL questions. Everything here is read-only and safe to run on a production instance during an incident.

  1. 1

    List the jobs and their steps

    Start in msdb. A SQL Agent job is an ordered list of steps, each with a subsystem and a command. The command column is the actual thing that executes — a stored procedure call, a package path, a command line. This is the closest thing to a pipeline definition that exists on most SQL Server estates.

    msdb — jobs and their steps
    SELECT  j.name           AS job_name,
            j.enabled,
            s.step_id,
            s.step_name,
            s.subsystem,          -- TSQL, SSIS, CmdExec, PowerShell...
            s.command
    FROM    msdb.dbo.sysjobs      AS j
    JOIN    msdb.dbo.sysjobsteps  AS s ON s.job_id = j.job_id
    WHERE   j.name LIKE '%ETL%'
    ORDER BY j.name, s.step_id;
  2. 2

    Pull the execution history

    sysjobhistory holds one row per step per execution, plus a summary row where step_id = 0. Two gotchas: run_date and run_time are integers, so use msdb.dbo.agent_datetime() to combine them; and run_duration is HHMMSS packed into an integer, not a count of seconds — 130 means one minute thirty, not two minutes ten.

    msdb — execution history for one job
    SELECT  j.name AS job_name,
            h.step_id,
            h.step_name,
            msdb.dbo.agent_datetime(h.run_date, h.run_time) AS started_at,
            h.run_duration,        -- HHMMSS as an integer, not seconds
            CASE h.run_status
                 WHEN 0 THEN 'Failed'
                 WHEN 1 THEN 'Succeeded'
                 WHEN 2 THEN 'Retry'
                 WHEN 3 THEN 'Canceled'
                 WHEN 4 THEN 'In progress'
            END AS status,
            h.message
    FROM    msdb.dbo.sysjobhistory AS h
    JOIN    msdb.dbo.sysjobs       AS j ON j.job_id = h.job_id
    WHERE   j.name = 'ETL - Transaction Import'
    ORDER BY h.instance_id DESC;
  3. 3

    Get the SSIS parameters that were actually used

    If a step's subsystem is SSIS, the package logic may be in source control but the values it ran with are not. SSISDB.catalog.execution_parameter_values records what each execution was actually given. When the same package behaves differently in two environments, this table usually holds the answer.

    SSISDB — executions and their parameter values
    SELECT  e.execution_id,
            e.folder_name,
            e.project_name,
            e.package_name,
            e.start_time,
            e.end_time,
            e.status              -- 4 = failed, 7 = succeeded
    FROM    SSISDB.catalog.executions AS e
    WHERE   e.package_name = 'LoadTransactions.dtsx'
    ORDER BY e.start_time DESC;
    
    -- The parameters that run actually used, which the .dtsx does not record
    SELECT  parameter_name,
            parameter_value,
            value_set
    FROM    SSISDB.catalog.execution_parameter_values
    WHERE   execution_id = 41821;
  4. 4

    Read the stored procedure

    On a large proportion of estates the real transformation is T-SQL inside a procedure that was never committed anywhere. sys.sql_modules holds the text. Searching its definition for a table name is the fastest way to answer “what writes to this?” — a question no repository can answer if the procedure isn't in one.

    sys.sql_modules — find and read procedure source
    -- Which procedure writes to this table?
    SELECT  o.name          AS procedure_name,
            o.modify_date,
            LEN(m.definition) AS definition_length
    FROM    sys.sql_modules AS m
    JOIN    sys.objects     AS o ON o.object_id = m.object_id
    WHERE   o.type = 'P'
      AND   m.definition LIKE '%dbo.SalesDaily%'
    ORDER BY o.modify_date DESC;
    
    -- Then read it. This is frequently the only copy that exists.
    SELECT  m.definition
    FROM    sys.sql_modules AS m
    JOIN    sys.objects     AS o ON o.object_id = m.object_id
    WHERE   o.name = 'usp_LoadSalesDaily';
  5. 5

    Reconcile the counts

    Finally, compare what arrived with what landed, and look at the distribution of whatever was rejected rather than the total. Random corruption spreads across values; a mapping gap concentrates on one. When a single discriminator accounts for most of the rejections, you are looking at a value the system has never seen before rather than a data quality problem.

    Rejected rows, grouped by discriminator
    SELECT  PaymentType,
            COUNT(*) AS Rejected
    FROM    dbo.RejectedTransactions
    WHERE   RunId = 18291
    GROUP BY PaymentType
    ORDER BY Rejected DESC;

Failure modes specific to SQL Server ETL

Across a lot of incidents the same half-dozen shapes recur. What makes them tractable is that each has a distinctive tell — a signature in the data that separates it from the others long before you understand the cause.

Failure mode The tell How to confirm it
Silent row rejectionRun reports success; target row count is shortGroup the reject table by its discriminator column — concentration on one value means a mapping gap, not corruption
Retry duplicatesDuplicate key count exactly equals one batch sizeCheck sysjobhistory for the same step_id appearing twice within minutes
Deadlock victimMsg 1205, job fails mid-loadRead the deadlock graph from system_health and identify the winning session
Empty input, clean exitRun finishes in seconds, loads nothing, reports successCompare run_duration against the trailing median for that step
Parameter driftSame package, different behaviour per environmentDiff execution_parameter_values between the good run and the bad one
Schema change upstreamParse or conversion errors on every rowCompare sys.columns against the last known-good run's landing table

The one that costs the most is the first. A run that reports success and diverts rows to a table nobody monitors will not alert, will not fail a data quality test — because the invalid rows never reached the table being tested — and will typically be discovered at month end by an accountant rather than an engineer.

We wrote about that pattern in detail in Your pipeline succeeded and lost 38% of the rows , including the query that identifies it in about ten seconds.

What Decim does with all this

Everything above is work you can do by hand, and experienced engineers do it by hand, at two in the morning, repeatedly. Decim performs the same steps automatically when an incident opens, and then does two things a person cannot do quickly.

It reconciles counts across every step

Rows in, rows out, rows diverted — per step, for the failing run and for the last several healthy ones. The step where those numbers stop matching is the answer, and it takes seconds rather than a sequence of ad-hoc queries.

It compares the declared pipeline against the observed one

Having read msdb, SSISDB, sys.sql_modules and your repository, Decim marks every node in the resulting topology by how well those sources agree. A target written on every run that appears in no definition anywhere is flagged as undocumented — and that is very often where the missing rows went.

Each conclusion links to the evidence behind it: the query that was run, the rows it returned, the log lines that matched, the procedure definition that was read. If the evidence does not support a conclusion, it says so rather than producing a confident guess.

Common questions

Does Decim need write access to my databases?
No. The agent connects with read-only credentials you issue, and executes only queries drawn from an approved catalogue that your team reviews. There is no free-text SQL path — a query that is not in the catalogue cannot run, whatever the investigation concludes it needs.
Do I have to put my stored procedures in source control first?
No, and that is much of the point. Decim reads procedure bodies from sys.sql_modules directly. Connecting a repository is useful when one exists, but a pipeline whose logic lives entirely in the database is still fully readable.
How does it handle SSIS packages?
It reads deployed packages and, more importantly, the execution parameter values from SSISDB.catalog. Package logic in a repository tells you what the package can do; the parameter values tell you what it actually did on the run that failed.
What about SQL Server 2012 or 2014?
The catalogue views used here — sysjobs, sysjobsteps, sysjobhistory, sys.sql_modules, SSISDB.catalog — have been stable since 2012. Older instances are supported; the constraint is usually network reachability from the agent, not the SQL version.
Does the agent need to run on the database server?
No. It needs network reachability to the instance and to whatever log files it is configured to read. Most deployments put it on the ETL host rather than the database host.
What leaves my network?
Structured evidence records, with secrets and personal data redacted at the agent before transmission. Connection strings, tokens, card numbers and configurable patterns are removed at source rather than on ingest. Raw query results are row-capped by the approved query definition.

Get started

Point it at a SQL Server job that failed last week

Bring an incident you already know the answer to. If Decim reaches the wrong conclusion, that tells you more about whether to trust it than any demo we could script.