You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A deadlock inside a stored procedure shows 'Proc [Database Id = N Object Id = M]' instead of the procedure name — the resolution is never attempted #3307
A deadlock whose statement sits inside a stored procedure renders the procedure as SQL Server's unresolved placeholder rather than its name. Reported from the field on a live alert:
Database: <a tenant database>
Victim SQL: Proc [Database Id = 7 Object Id = 1790404501]
Processes: SPID 251 (victim) vs SPID 256
Involved Objects: <db>.dbo.<table>
Every other field in that alert resolved. Involved Objects names the actual table, the database resolved, the SPIDs resolved — and the one field an on-call engineer reads first to know what code deadlocked is an object id.
Why it happens
AlertContextBuilders.cs:165 renders the stored value verbatim:
Proc [Database Id = N Object Id = M] is what SQL Server itself writes into the deadlock graph when it cannot resolve the name in the capturing session's context. We store that string and display it unchanged.
Nothing in the tree parses procname at all — a repo-wide search over the alerting, collector and service projects returns no hits. So this is not a resolution that fails; it is a resolution that is never attempted.
Two routes, and the collection-time one looks better
Resolve at collection time. The collector is already connected to the target when it reads the deadlock event, and OBJECT_NAME(object_id, database_id) is valid cross-database from any context — so OBJECT_NAME(1790404501, 7) is one cheap call per unresolved frame. Store the resolved name; this is accurate regardless of how recently the procedure ran.
Or join what is already stored.collect.module_map already maps sql_handle to a module name, keyed (server_id, sql_handle) — that machinery exists from #1568/#2737 and is populated from recent procedure_stats (the service logs an upsert count each pass). Deadlock graph frames carry sqlhandle alongside procname, so the join needs no new collection.
The tradeoff is coverage: module_map only knows modules that appeared in procedure_stats recently, so a procedure that deadlocked but has not run enough to be captured would miss. The OBJECT_NAME route has no such dependency. The join is cheaper to build; the collection-time lookup is correct more often.
Whatever the route, degrade to the raw form
If resolution fails — object dropped between the deadlock and the lookup, or the monitoring login cannot see it — the field must keep showing Proc [Database Id = 7 Object Id = 1790404501] rather than going blank or saying "unknown". The object id is a worse answer than a name and a much better answer than nothing: it is still resolvable by hand.
Worth checking while in here
The same placeholder shape appears in blocking reports and in sys.dm_exec_requests-derived surfaces for the same reason. If the resolution lands as a shared helper rather than inline in the deadlock path, those get it for free — but confirm they are actually affected rather than assuming, since the blocked-process report carries its own text.
Reframing this: it is a regression from the prior art this was modeled on, not a gap nobody thought about. sp_HumanEventsBlockViewer resolves it, deliberately and in three separate places, and the technique is worth porting rather than reinventing.
What the reference actually does
From DarlingData/sp_HumanEvents/sp_HumanEventsBlockViewer.sql:
query_text =
CASE
WHEN kheb.query_textLIKE @inputbuf_bom + N'Proc |[Database Id = %' ESCAPE N'|'
THEN
OBJECT_SCHEMA_NAME
(
SUBSTRING(kheb.query_text,
CHARINDEX(N'Object Id = ', kheb.query_text) +12,
LEN(kheb.query_text) - (CHARINDEX(N'Object Id = ', kheb.query_text) +12)),
SUBSTRING(kheb.query_text,
CHARINDEX(N'Database Id = ', kheb.query_text) +14,
CHARINDEX(N'Object Id', kheb.query_text) - (CHARINDEX(N'Database Id = ', kheb.query_text) +14))
) + N'.'+
OBJECT_NAME( /* same two SUBSTRINGs */ )
ELSE kheb.query_text
END
Three things that matter and that my original write-up above got wrong or missed:
It resolves to schema.object, not just the object name.OBJECT_SCHEMA_NAME(...) + '.' + OBJECT_NAME(...). That matters for consistency: the same alert's Involved Objects field already renders schema-qualified, so an unqualified procedure name would be the odd one out in its own message.
The [ must be escaped, or the predicate silently matches nothing.[ opens a character class in LIKE, so LIKE N'Proc [Database Id = %' is a range expression, not a literal. The reference writes N'Proc |[Database Id = %' ESCAPE N'|'. A port that omits this does not error — it just never fires, and the field keeps showing object ids while a test written against a hand-built string may well pass.
There is a BOM to allow for.@inputbuf_bom is prepended to the pattern because the text can begin with a byte-order mark. Same failure mode: omit it and the LIKE misses every affected row without complaining.
Both of those are silent no-ops rather than errors, which is exactly how a "we ported it" change ships without working.
Where it applies here, and where it may not
Erik's report is a deadlock, where the placeholder arrives in the graph's procname attribute — an XML attribute, so no BOM. The BOM handling belongs to the blocked-process-report path, where the text comes from inputbuf. This product collects both (deadlocks and blocked_process_reports), so both need the resolution and only one needs the BOM tolerance. Worth confirming per surface rather than applying one shape to both.
Note also that the reference repeats this predicate at three sites in one file. If a port inlines it per surface here it will drift the same way; one helper with the pattern, the escape, the offsets and the BOM in a single place is the shape that survives.
Correcting my own suggestion above
I proposed OBJECT_NAME(object_id, database_id) "or join module_map". The module_map route is the weaker one and I should not have offered it as an equal: it depends on the procedure having appeared in procedure_stats recently, and the reference does not need that. The prior art settles the question — parse the placeholder, resolve schema and name against the target. Drop the module_map idea.
Correcting my previous comment on a point that would break a port. I wrote:
Erik's report is a deadlock, where the placeholder arrives in the graph's procname attribute — an XML attribute, so no BOM. The BOM handling belongs to the blocked-process-report path.
Both halves are wrong, and sp_BlitzLock — a deadlock-only tool — is the proof.
The placeholder is in <inputbuf>, not procname
sp_BlitzLock.sql line 4013 applies the resolution to d.inputbuf, and the surrounding projection is unambiguously the deadlock process row: last_batch_started, lock_mode, transaction_count, client_app, host_name, login_name, isolation_level, client_option_1/2.
This product's own deadlock fixtures agree — the graph XML carries the statement in an element, e.g. DarlingIncidentFingerprintTests.cs:
So VictimSqlText comes from <inputbuf> element text, which is exactly where SQL Server writes Proc [Database Id = N Object Id = M].
Which means the BOM handling DOES apply to deadlocks
sp_BlitzLock uses @inputbuf_bom on the deadlock path. Element text can begin with a byte-order mark; an XML attribute was the wrong mental model and it is the reason I talked myself out of the guard.
The consequence of my error, had it been implemented: the LIKE misses every BOM-prefixed row, silently, and the field keeps showing object ids. Combined with the unescaped-[ trap this is the second way to ship a "done" port that never fires — and I had just finished warning about that class while removing one of the two defences.
So both references converge completely
sp_HumanEventsBlockViewer
sp_BlitzLock
predicate
`LIKE @inputbuf_bom + N'Proc
[Database Id = %' ESCAPE N'
source
query_text
d.inputbuf
output
OBJECT_SCHEMA_NAME(...) + N'.' + OBJECT_NAME(...)
identical
offsets
Database Id = +14, Object Id = +12
identical
Two implementations, same predicate, same escape, same BOM allowance, same schema-qualified result. There is no variant to choose between — port it as written.
Applies to the blocked-process-report surface too, which already parses <inputbuf> here, so the same helper serves both rather than one shape per surface.
Fixed by #3310, merged to dev as 4fe105e. Closing explicitly — this repo merges to dev but defaults to main, so closing keywords in a PR body never fire.
Activity
erikdarlingdata commentedon Sep 11, 2026
Claude posting for Erik Darling
Reframing this: it is a regression from the prior art this was modeled on, not a gap nobody thought about.
sp_HumanEventsBlockViewerresolves it, deliberately and in three separate places, and the technique is worth porting rather than reinventing.What the reference actually does
From
DarlingData/sp_HumanEvents/sp_HumanEventsBlockViewer.sql:Three things that matter and that my original write-up above got wrong or missed:
It resolves to
schema.object, not just the object name.OBJECT_SCHEMA_NAME(...) + '.' + OBJECT_NAME(...). That matters for consistency: the same alert'sInvolved Objectsfield already renders schema-qualified, so an unqualified procedure name would be the odd one out in its own message.The
[must be escaped, or the predicate silently matches nothing.[opens a character class inLIKE, soLIKE N'Proc [Database Id = %'is a range expression, not a literal. The reference writesN'Proc |[Database Id = %' ESCAPE N'|'. A port that omits this does not error — it just never fires, and the field keeps showing object ids while a test written against a hand-built string may well pass.There is a BOM to allow for.
@inputbuf_bomis prepended to the pattern because the text can begin with a byte-order mark. Same failure mode: omit it and theLIKEmisses every affected row without complaining.Both of those are silent no-ops rather than errors, which is exactly how a "we ported it" change ships without working.
Where it applies here, and where it may not
Erik's report is a deadlock, where the placeholder arrives in the graph's
procnameattribute — an XML attribute, so no BOM. The BOM handling belongs to the blocked-process-report path, where the text comes frominputbuf. This product collects both (deadlocksandblocked_process_reports), so both need the resolution and only one needs the BOM tolerance. Worth confirming per surface rather than applying one shape to both.Note also that the reference repeats this predicate at three sites in one file. If a port inlines it per surface here it will drift the same way; one helper with the pattern, the escape, the offsets and the BOM in a single place is the shape that survives.
Correcting my own suggestion above
I proposed
OBJECT_NAME(object_id, database_id)"or joinmodule_map". Themodule_maproute is the weaker one and I should not have offered it as an equal: it depends on the procedure having appeared inprocedure_statsrecently, and the reference does not need that. The prior art settles the question — parse the placeholder, resolve schema and name against the target. Drop themodule_mapidea.erikdarlingdata commentedon Sep 11, 2026
Claude posting for Erik Darling
Correcting my previous comment on a point that would break a port. I wrote:
Both halves are wrong, and
sp_BlitzLock— a deadlock-only tool — is the proof.The placeholder is in
<inputbuf>, notprocnamesp_BlitzLock.sqlline 4013 applies the resolution tod.inputbuf, and the surrounding projection is unambiguously the deadlock process row:last_batch_started,lock_mode,transaction_count,client_app,host_name,login_name,isolation_level,client_option_1/2.This product's own deadlock fixtures agree — the graph XML carries the statement in an element, e.g.
DarlingIncidentFingerprintTests.cs:So
VictimSqlTextcomes from<inputbuf>element text, which is exactly where SQL Server writesProc [Database Id = N Object Id = M].Which means the BOM handling DOES apply to deadlocks
sp_BlitzLockuses@inputbuf_bomon the deadlock path. Element text can begin with a byte-order mark; an XML attribute was the wrong mental model and it is the reason I talked myself out of the guard.The consequence of my error, had it been implemented: the
LIKEmisses every BOM-prefixed row, silently, and the field keeps showing object ids. Combined with the unescaped-[trap this is the second way to ship a "done" port that never fires — and I had just finished warning about that class while removing one of the two defences.So both references converge completely
sp_HumanEventsBlockViewersp_BlitzLockquery_textd.inputbufOBJECT_SCHEMA_NAME(...) + N'.' + OBJECT_NAME(...)Database Id =+14,Object Id =+12Two implementations, same predicate, same escape, same BOM allowance, same schema-qualified result. There is no variant to choose between — port it as written.
Applies to the blocked-process-report surface too, which already parses
<inputbuf>here, so the same helper serves both rather than one shape per surface.Merge pull request #3310 from erikdarlingdata/fix/3307-resolve-proc-p…
erikdarlingdata commentedon Sep 11, 2026
Claude posting for Erik Darling
Fixed by #3310, merged to
devas 4fe105e. Closing explicitly — this repo merges todevbut defaults tomain, so closing keywords in a PR body never fire.Merge pull request #3318 from erikdarlingdata/chore/changelog-3307
Add the erikdarlingdata#3307 CHANGELOG entry for the merged proc-plac…