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.
| What | Where it lives | In git? |
|---|---|---|
| Job schedule and steps | msdb.dbo.sysjobs, sysjobsteps | No |
| Execution history | msdb.dbo.sysjobhistory | No |
| SSIS package logic | .dtsx in a repo, deployed .ispac in SSISDB | Partly |
| SSIS parameters & environments | SSISDB.catalog | No |
| Transformation logic | sys.sql_modules (stored procedures) | Sometimes |
| Table & column schema | sys.tables, sys.columns | Only if migrations exist |
| Linked server definitions | sys.servers | No |
| Deadlock evidence | system_health extended events | No |
| Application ETL code | Your repository | Yes |
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
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
Pull the execution history
sysjobhistoryholds one row per step per execution, plus a summary row wherestep_id = 0. Two gotchas:run_dateandrun_timeare integers, so usemsdb.dbo.agent_datetime()to combine them; andrun_durationisHHMMSSpacked 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
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_valuesrecords 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
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_modulesholds the text. Searching itsdefinitionfor 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
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 rejection | Run reports success; target row count is short | Group the reject table by its discriminator column — concentration on one value means a mapping gap, not corruption |
| Retry duplicates | Duplicate key count exactly equals one batch size | Check sysjobhistory for the same step_id appearing twice within minutes |
| Deadlock victim | Msg 1205, job fails mid-load | Read the deadlock graph from system_health and identify the winning session |
| Empty input, clean exit | Run finishes in seconds, loads nothing, reports success | Compare run_duration against the trailing median for that step |
| Parameter drift | Same package, different behaviour per environment | Diff execution_parameter_values between the good run and the bad one |
| Schema change upstream | Parse or conversion errors on every row | Compare 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?
Do I have to put my stored procedures in source control first?
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?
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?
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?
What leaves my network?
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.