<?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: Tracepoint Data</title>
    <description>The latest articles on DEV Community by Tracepoint Data (@tracepointdata).</description>
    <link>https://dev.to/tracepointdata</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%2F4081752%2F70d71e7a-1bb7-417a-9d9e-d36ab5136579.png</url>
      <title>DEV Community: Tracepoint Data</title>
      <link>https://dev.to/tracepointdata</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tracepointdata"/>
    <language>en</language>
    <item>
      <title>QUERY_HISTORY has no PARENT_QUERY_ID. Here's how I traced which statement inside a Snowflake procedure regressed anyway.</title>
      <dc:creator>Tracepoint Data</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:15:00 +0000</pubDate>
      <link>https://dev.to/tracepointdata/queryhistory-has-no-parentqueryid-heres-how-i-traced-which-statement-inside-a-snowflake-el7</link>
      <guid>https://dev.to/tracepointdata/queryhistory-has-no-parentqueryid-heres-how-i-traced-which-statement-inside-a-snowflake-el7</guid>
      <description>&lt;p&gt;If you write ETL procedures in Snowflake but don't have &lt;code&gt;ACCOUNT_USAGE&lt;/code&gt;&lt;br&gt;
access, you've probably had this exact conversation: someone says a load&lt;br&gt;
job got slower, you can look at one query's profile if you already know&lt;br&gt;
which query, but &lt;code&gt;QUERY_HISTORY&lt;/code&gt; gives you zero help connecting a &lt;code&gt;CALL&lt;/code&gt;&lt;br&gt;
to the statements it ran internally. There's no &lt;code&gt;PARENT_QUERY_ID&lt;/code&gt;. No&lt;br&gt;
&lt;code&gt;ROOT_QUERY_ID&lt;/code&gt;. &lt;code&gt;TRANSACTION_ID&lt;/code&gt; doesn't match between parent and&lt;br&gt;
child. &lt;code&gt;QUERY_TAG&lt;/code&gt; is empty unless the caller happens to set it.&lt;/p&gt;

&lt;p&gt;So when a 7-statement procedure regresses, "which of the 7 got slower,&lt;br&gt;
and why" is a manual, undocumented process that lives in whoever's head&lt;br&gt;
has done it before. I got tired of doing it by hand, so I spent a few&lt;br&gt;
weeks building three SQL procedures that do it for me. This post is&lt;br&gt;
about the interesting part: how the attribution actually works, since&lt;br&gt;
Snowflake genuinely doesn't expose it and I couldn't find anyone else&lt;br&gt;
who'd written this up.&lt;/p&gt;
&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ACCOUNT_USAGE.QUERY_HISTORY&lt;/code&gt; has about 85 columns. I went through all&lt;br&gt;
of them looking for anything that links a &lt;code&gt;CALL&lt;/code&gt; to its child&lt;br&gt;
statements. Here's what doesn't work, in case it saves someone else the&lt;br&gt;
time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TRANSACTION_ID&lt;/code&gt;&lt;/strong&gt; - differs per child statement, doesn't match the
parent &lt;code&gt;CALL&lt;/code&gt; row (which is usually &lt;code&gt;0&lt;/code&gt;/null).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;QUERY_TAG&lt;/code&gt;&lt;/strong&gt; - empty unless the calling session sets it explicitly.
Not usable generically, since a tool like this doesn't control the
customer's session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CHILD_QUERIES_WAIT_TIME&lt;/code&gt;&lt;/strong&gt; - this one's interesting. It exists on
the parent &lt;code&gt;CALL&lt;/code&gt; row, and its mere existence proves Snowflake tracks
the parent/child relationship &lt;em&gt;internally&lt;/em&gt;. It just doesn't expose the
child query IDs, only an aggregate wait-time duration. So close.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The workaround: session ID + timestamp containment
&lt;/h2&gt;

&lt;p&gt;No FK column exists, but the data needed to reconstruct the relationship&lt;br&gt;
does: &lt;code&gt;SESSION_ID&lt;/code&gt;, &lt;code&gt;START_TIME&lt;/code&gt;, &lt;code&gt;END_TIME&lt;/code&gt;, &lt;code&gt;TOTAL_ELAPSED_TIME&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The core idea: a child statement belongs to whichever same-session query&lt;br&gt;
has the &lt;em&gt;tightest enclosing time window&lt;/em&gt;. Concretely, for any statement,&lt;br&gt;
find every same-session query whose window fully contains it&lt;br&gt;
(&lt;code&gt;candidate.START_TIME &amp;lt;= child.START_TIME AND candidate.END_TIME &amp;gt;=&lt;br&gt;
child.END_TIME&lt;/code&gt;), then pick the one with the &lt;strong&gt;minimum&lt;br&gt;
&lt;code&gt;TOTAL_ELAPSED_TIME&lt;/code&gt;&lt;/strong&gt; among those candidates. A nested window is always&lt;br&gt;
shorter than any window that contains it, so "shortest containing&lt;br&gt;
window" and "nearest enclosing window" are the same thing - no separate&lt;br&gt;
tree-walk needed, and no need to pre-filter candidates to&lt;br&gt;
&lt;code&gt;QUERY_TYPE = 'CALL'&lt;/code&gt; either, since single-session serial execution means&lt;br&gt;
unrelated sibling queries in the same session can never overlap at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- simplified shape of the correlation, not the full procedure&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_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;SELECT&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;query_history&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_time&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt;   &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;  &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&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;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_elapsed_time&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
        &lt;span class="k"&gt;LIMIT&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;parent_query_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;query_history&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole trick. Everything else is bookkeeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I expected this to break, and it didn't
&lt;/h2&gt;

&lt;p&gt;I didn't trust "timestamp containment" until I'd tried to break it on&lt;br&gt;
purpose. Three cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nested procs&lt;/strong&gt; (proc A calls proc B calls a statement). The
grandchild's window can fit inside &lt;em&gt;both&lt;/em&gt; the inner and outer &lt;code&gt;CALL&lt;/code&gt;
windows simultaneously. This is exactly why "any containing window"
isn't enough and it has to be the &lt;em&gt;tightest&lt;/em&gt; one - confirmed against a
real &lt;code&gt;OUTER_PROC -&amp;gt; INNER_PROC -&amp;gt; table&lt;/code&gt; call tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent sessions&lt;/strong&gt; - same user, two worksheets, running the same
procedure at genuinely overlapping wall-clock times. Filtering to the
exact &lt;code&gt;SESSION_ID&lt;/code&gt; of the parent &lt;code&gt;CALL&lt;/code&gt; cleanly isolates its own
children even when a second session runs the identical procedure at
the same moment, because &lt;code&gt;SESSION_ID&lt;/code&gt; is per-connection, not per-user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rapid back-to-back &lt;code&gt;CALL&lt;/code&gt;s&lt;/strong&gt; submitted as a single batch in one
session. Still executed serially with clean, non-overlapping windows.
No interleaving to worry about.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also forced real warehouse contention (&lt;code&gt;MAX_CONCURRENCY_LEVEL = 1&lt;/code&gt;,&lt;br&gt;
two concurrent sessions) to check the attribution still holds when&lt;br&gt;
execution windows genuinely overlap under queuing pressure, not just&lt;br&gt;
when they're clean and sequential. It did: the queued statement still&lt;br&gt;
attributed to the right parent, and the flag for warehouse queuing (see&lt;br&gt;
below) fired correctly on the queued child, not the parent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it's wrapped into
&lt;/h2&gt;

&lt;p&gt;Three procedures, matching the manual workflow instead of one black-box&lt;br&gt;
call:&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;Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DIAGNOSE_PROCEDURE(proc_name, lookback_hours)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bulk scan: reconstructs the call tree for recent runs and flags statements for regression vs. their own history, poor pruning, spilling, non-sargable predicates, duplicate table scans, VARCHAR/NUMBER join-key mismatches, and warehouse queuing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET_STATEMENT_PROFILE(query_id)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-operator drill-down for one flagged statement, via &lt;code&gt;GET_QUERY_OPERATOR_STATS()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXPLAIN_WITH_CORTEX(query_id)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opt-in AI second opinion via Snowflake Cortex (&lt;code&gt;AI_COMPLETE&lt;/code&gt;), in-account, no external LLM call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of them specify &lt;code&gt;EXECUTE AS&lt;/code&gt;, so they run as owner's rights by&lt;br&gt;
Snowflake's default: install once with &lt;code&gt;ACCOUNT_USAGE&lt;/code&gt; access, then&lt;br&gt;
&lt;code&gt;GRANT USAGE&lt;/code&gt; on the procedures to anyone who needs to call them without&lt;br&gt;
ever giving them &lt;code&gt;ACCOUNT_USAGE&lt;/code&gt; themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do (yet)
&lt;/h2&gt;

&lt;p&gt;Procedures only, not views. View definitions get inlined into the query&lt;br&gt;
plan, so mapping a slow operator back to which view (and which part of&lt;br&gt;
it) caused it is a genuinely harder problem, and I decided to ship the&lt;br&gt;
procedure case well rather than both cases half-done.&lt;/p&gt;

&lt;p&gt;The regex-based flags (duplicate-scan detection, join-type mismatch) are&lt;br&gt;
text pattern matching, not a real parser, so they miss CTEs, subqueries,&lt;br&gt;
and multi-condition joins. They catch the common case, not everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;MIT licensed, three SQL files, nothing to install beyond pasting SQL&lt;br&gt;
into your own account: &lt;a href="https://github.com/TracepointData/QueryTrace" rel="noopener noreferrer"&gt;https://github.com/TracepointData/QueryTrace&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Curious if anyone else has fought this exact problem, and if so how you&lt;br&gt;
solved the attribution piece.&lt;/p&gt;

</description>
      <category>snowflake</category>
      <category>sql</category>
      <category>dataengineering</category>
      <category>database</category>
    </item>
  </channel>
</rss>
