<?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: Jung Kim</title>
    <description>The latest articles on DEV Community by Jung Kim (@jungalung).</description>
    <link>https://dev.to/jungalung</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%2F4014036%2F5a66e3b9-dd95-49ba-9f0e-cdc6ddb32c8f.png</url>
      <title>DEV Community: Jung Kim</title>
      <link>https://dev.to/jungalung</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jungalung"/>
    <language>en</language>
    <item>
      <title>Tracking Tableau Viewership with PostgreSQL</title>
      <dc:creator>Jung Kim</dc:creator>
      <pubDate>Fri, 03 Jul 2026 19:36:23 +0000</pubDate>
      <link>https://dev.to/jungalung/tracking-tableau-viewership-with-postgresql-2l7c</link>
      <guid>https://dev.to/jungalung/tracking-tableau-viewership-with-postgresql-2l7c</guid>
      <description>&lt;h1&gt;
  
  
  Tracking Tableau Viewership with PostgreSQL
&lt;/h1&gt;

&lt;p&gt;Tableau's native "Who has seen this view?" panel is a flat list — no trends, no way to tell if usage is real or just the publisher checking their own work. With direct access to the Tableau PostgreSQL repository, you can do a lot better: turn raw access events into aggregated viewership data your dashboard creators can actually act on. Here's the query I use to power that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SQL
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;access_events&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- Pull only "Access View" events from the last 90 days&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;he&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="n"&gt;he&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;he&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hist_view_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;he&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hist_workbook_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;he&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hist_actor_user_id&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;historical_events&lt;/span&gt; &lt;span class="n"&gt;he&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;historical_event_types&lt;/span&gt; &lt;span class="n"&gt;het&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;he&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;historical_event_type_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;het&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type_id&lt;/span&gt;   &lt;span class="c1"&gt;-- filter to the event type we care about&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;het&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;'Access View'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;he&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'90 days'&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;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;                           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;workbook_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- current workbook name, from the live content table&lt;/span&gt;
    &lt;span class="n"&gt;hu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;friendly_name&lt;/span&gt;                 &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;viewer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;-- NULL when the workbook has zero views&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ae&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;AS&lt;/span&gt; &lt;span class="n"&gt;view_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 0 when no matching access events&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ae&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&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;last_viewed&lt;/span&gt;          &lt;span class="c1"&gt;-- NULL when never viewed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_workbooks&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;                                            &lt;span class="c1"&gt;-- drive from the full content table...&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;hist_workbooks&lt;/span&gt; &lt;span class="n"&gt;hw&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;hw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workbook_id&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;      &lt;span class="c1"&gt;-- ...so workbooks with no history still show up&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;access_events&lt;/span&gt; &lt;span class="n"&gt;ae&lt;/span&gt;  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ae&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hist_workbook_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&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;hist_users&lt;/span&gt; &lt;span class="n"&gt;hu&lt;/span&gt;     &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;hu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ae&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hist_actor_user_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;friendly_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;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The use case
&lt;/h2&gt;

&lt;p&gt;I built this as the backbone of a self-service "Viewership Analytics" dashboard — KPI tiles, a time-series trend, top-viewers ranking, and a detail table with recency color-coding. The goal was to give dashboard creators direct visibility into how their own content is actually being used, so they can answer "is anyone using this?" on their own, anytime. Driving the query from the full workbook list also means unviewed content shows up right alongside everything else, instead of needing a second query to catch what never got seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to know before you use this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hist_workbooks.id&lt;/code&gt; is just the internal ID for that snapshot row, not the workbook itself.&lt;/strong&gt; If you need a stable way to track a specific workbook over time, use &lt;code&gt;hist_workbooks.workbook_id&lt;/code&gt; instead. This matters more than it sounds like it should: workbook names and even publishing locations can change during development — especially before a workbook lands in its final, published-to-prod state — so tracking by internal snapshot ID or by name alone can silently split what should be one workbook's history into several.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;friendly_name&lt;/code&gt; availability can vary.&lt;/strong&gt; It's present on &lt;code&gt;hist_users&lt;/code&gt; in this table, but in other parts of the repository it only lives on &lt;code&gt;system_users&lt;/code&gt; — worth confirming which table has it populated in your version before you build on top of this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-views aren't excluded here on purpose.&lt;/strong&gt; As written, this counts a publisher viewing their own workbook the same as anyone else. Depending on your use case that might be exactly what you want (e.g., an admin dashboard), or it might inflate "most viewed" rankings if you're handing this to creators. If you want to exclude them, add a join to the workbook's owner and filter &lt;code&gt;hist_actor_user_id != owner_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>tableau</category>
      <category>datavisualization</category>
    </item>
  </channel>
</rss>
