<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[bluu.dev]]></title><description><![CDATA[Tools for thought, ideas into action]]></description><link>https://bluu.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 14 Oct 2026 22:12:23 GMT</lastBuildDate><atom:link href="https://bluu.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Escaping the Attention Black Hole]]></title><description><![CDATA[Everyone talks about attention deficit. Not enough of it, can't hold it, phone keeps stealing it. That's not my worst problem.
My problem is the opposite. In engineering, the silent killer isn't the i]]></description><link>https://bluu.dev/attention-blackholes</link><guid isPermaLink="true">https://bluu.dev/attention-blackholes</guid><category><![CDATA[hyperfocus]]></category><category><![CDATA[AI Productivity]]></category><category><![CDATA[attention engineering]]></category><category><![CDATA[engineering productivity]]></category><category><![CDATA[attention]]></category><dc:creator><![CDATA[Brice Luu]]></dc:creator><pubDate>Tue, 24 Mar 2026 15:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/61b9d3bfd2a5c9036e6f2364/a8848585-21b5-4ec6-bd3e-85425aed35e1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Everyone talks about attention deficit. Not enough of it, can't hold it, phone keeps stealing it. That's not my worst problem.</p>
<p>My problem is the opposite. In engineering, the silent killer isn't the inability to start, it's the inability to stop. Call it hyperfocus, call it perseveration: when a problem catches, everything else drops. Other work, sleep, conversations. Gone. I don't resurface until either the thing is done, or I am.</p>
<p>I've learned to build guardrails. Social media was the easy one: an algorithmic feed is designed to keep you consuming, never surfacing. No exit by design. That loop has nothing worth losing sleep over, so I mostly stay away.</p>
<p>But work is different. The problem isn't going to solve itself, so I <em>have</em> to dive in. I've tried hard rules, like no work after dinner, to prevent those sleepless nights. But rules only hold until you're three errors deep at 11PM and the fix feels <em>right there</em>.</p>
<p>Here's the thing I kept sacrificing to stay functional: exploration. Not mindless scrolling, but actual cognitive wandering, letting attention drift sideways, follow tangents, consider the adjacent possibilities. That's where most of the interesting things come from: unexpected connections, better questions or simply the solutions you can only reach by first taking a step back. But wandering only works if you don't get lost in it. For me, that was never guaranteed. Once something caught, I tended to follow it all the way down. So I learned to keep a tighter leash on my own curiosity, just to get work done. Guardrails on everything, including the good stuff.</p>
<p>But that has changed with AI.
Not that I suddenly became disciplined. But AI has made a lot of those descents shallower, and that has quietly given me something back...</p>
<p>The old research loop used to be: query, ten tabs, three more queries, another page of results, compare sources, slowly build a mental model. Now it's much more often query, read the synthesis, then sources only to dig deeper where it actually matters. And for the technical rabbit holes, the effect is different but just as important. When I hit a gnarly implementation detail, a weird dependency issue, or some cursed edge case, I don't always have to go all the way down myself anymore. A coding agent can take care of that for me.</p>
<p>Which means my attention is freer than it used to be. Not because I suddenly stopped going deep, but because depth no longer swallows me whole.</p>
<p>I'm still figuring out what that actually means for me. But in the middle of last night, attention spinning in the dark, I landed on this: attention isn't binary. It's a dial. You can turn it up for depth, down for breadth. But the part that used to feel impossible: getting my attention back when I need to move on. That’s what actually feels new.</p>
<p>Now, if you'll excuse me, it's time to turn the dial down and step AFK.</p>
<hr />
<p><em>PS: the AI might be solving my daytime rabbit holes but at night, trying to sleep, I'm still left alone with my meaty brain, battling random 3AM inspiration spikes! That's rarely enjoyable: tomorrow-me always pays the price. If you've found a way to quiet your brain for the entire night, every night, and without the grogginess the next day, I'm genuinely all ears.</em></p>
]]></content:encoded></item><item><title><![CDATA[Easy Time‑Travel for your Data Warehouse with dbt snapshots]]></title><description><![CDATA[Why your "Sales by Region" keeps lying to you
If a "simple" KPI like Sales by Region keeps changing every time someone moves house or changes team, that's not your dashboard being flaky. That's your data model telling you it only remembers the latest...]]></description><link>https://bluu.dev/scd2-with-dbt-snapshots</link><guid isPermaLink="true">https://bluu.dev/scd2-with-dbt-snapshots</guid><category><![CDATA[point in time analysis]]></category><category><![CDATA[dbt-databuildtool]]></category><category><![CDATA[scd2]]></category><category><![CDATA[slowly changing dimension]]></category><category><![CDATA[change data capture]]></category><category><![CDATA[Data warehouse]]></category><category><![CDATA[data-modeling]]></category><category><![CDATA[SQL]]></category><category><![CDATA[analytics engineering]]></category><dc:creator><![CDATA[Brice Luu]]></dc:creator><pubDate>Sat, 31 Jan 2026 06:35:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ZaktiVjw2oM/upload/53dbda215f8243e53365fda28f9c35eb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-why-your-sales-by-region-keeps-lying-to-you">Why your "Sales by Region" keeps lying to you</h2>
<p>If a "simple" KPI like <strong>Sales by Region</strong> keeps changing every time someone moves house or changes team, that's not your dashboard being flaky. That's your data model telling you it only remembers the <strong>latest</strong> truth.</p>
<p>As long as dimension tables keep one row per customer/account/contract, they can only answer one question:</p>
<blockquote>
<p>"What does this entity look like <strong>right now</strong>?"</p>
</blockquote>
<p>The questions stakeholders actually ask are different:</p>
<ul>
<li><p>"What did this customer look like <strong>when they ordered</strong>?"</p>
</li>
<li><p>"Who owned this account <strong>when the deal closed</strong>?"</p>
</li>
<li><p>"What was the contract status <strong>at quarter-end</strong>?"</p>
</li>
</ul>
<p>To answer those, the warehouse needs some form of <strong>time travel</strong>.</p>
<p>In an ideal world, that comes from <strong>CDC (Change Data Capture)</strong>: reading the database's transaction logs (WAL, binlog, etc.) and replaying every insert, update, and delete as a continuous event stream.</p>
<p>In the real world, that often means begging infra for Debezium/Kafka/cloud‑$VENDOR$ you will never get.</p>
<p>This is where <strong>dbt snapshots</strong> are useful: a very simple, very boring way to add just enough history to make your warehouse behave like it remembers the past.</p>
<hr />
<h2 id="heading-1-the-real-problem-scd-type-2-solves">1. The real problem SCD Type 2 solves</h2>
<p><strong>TL;DR:</strong> Without SCD Type 2, the warehouse only remembers the latest state. With it, you can reconstruct what things looked like at any past date.</p>
<p>Think about a classic e‑commerce case: "Sales by Region" over the last year.</p>
<ul>
<li><p>A customer lives in <strong>New York</strong> for 6 months and buys 5 items.</p>
</li>
<li><p>Then they move to <strong>California</strong> and buy 3 more.</p>
</li>
</ul>
<p>If the <code>customers</code> dimension only keeps their <strong>current</strong> address:</p>
<ul>
<li><p>All 8 orders get attributed to California.</p>
</li>
<li><p>New York's historical revenue is under‑reported.</p>
</li>
<li><p>California looks more successful than it really was.</p>
</li>
</ul>
<p>The warehouse is effectively rewiring the past every time someone moves.</p>
<p>SCD Type 2 is the pattern that fixes this. It lets the dimensional model answer:</p>
<blockquote>
<p>"What was the state of this customer <strong>at the time of each order</strong>?"</p>
</blockquote>
<p>Once that exists, a whole class of questions becomes actually answerable:</p>
<ul>
<li><p>Sales attribution by region / rep at <strong>deal time</strong></p>
</li>
<li><p>Funnel conversion by <strong>segment at the time of signup</strong></p>
</li>
<li><p>Regulatory views of contracts <strong>as of reporting date</strong></p>
</li>
</ul>
<p>Without SCD2, those are guesswork. With it, they're just SQL.</p>
<hr />
<h2 id="heading-2-scd-type-2-in-two-columns">2. SCD Type 2 in two columns</h2>
<p><strong>TL;DR:</strong> Add <code>valid_from</code> and <code>valid_to</code> to the dimension, and stop overwriting rows.</p>
<p>Instead of overwriting a customer row when something changes, SCD Type 2 adds a <strong>new</strong> row and marks the old one as expired.</p>
<p>Minimal version of a customer address history table:</p>
<pre><code class="lang-text">customer_id | name | region | valid_from  | valid_to
----------- | ---- | ------ | ----------- | ----------
CUST-001    | John | NY     | 2024-01-01  | 2024-07-01
CUST-001    | John | CA     | 2024-07-01  | NULL
</code></pre>
<ul>
<li><p><code>customer_id</code>: same across all versions for the same person.</p>
</li>
<li><p><code>valid_from</code>: when this version became active.</p>
</li>
<li><p><code>valid_to</code>: when this version stopped being true. <code>NULL</code> = "still active".</p>
</li>
</ul>
<p>dbt will also add a surrogate key like <code>dbt_scd_id</code> under the hood, one per version, which is what you'd typically join from facts in a more "by‑the‑book" warehouse.</p>
<h3 id="heading-pointintime-lookups">Point‑in‑time lookups</h3>
<p>To know where John lived on March 15th:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> *
<span class="hljs-keyword">FROM</span> customer_scd2
<span class="hljs-keyword">WHERE</span> <span class="hljs-string">'2024-03-15'</span> <span class="hljs-keyword">BETWEEN</span> valid_from <span class="hljs-keyword">AND</span> <span class="hljs-keyword">COALESCE</span>(valid_to, <span class="hljs-keyword">CURRENT_DATE</span>);
</code></pre>
<p>March 15th sits inside the <code>[2024‑01‑01, 2024‑07‑01)</code> window, so you get the <strong>NY</strong> row.</p>
<p>That one pattern—"date BETWEEN valid_from AND valid_to (or COALESCE)"—is basically the entire mechanic.</p>
<hr />
<h2 id="heading-3-dbt-snapshots-an-easy-batch-scd2">3. dbt snapshots: an easy batch SCD2</h2>
<p><strong>TL;DR:</strong> dbt snapshots implement SCD Type 2 for you. You point them at a table or model, configure how to detect changes, and run them on a schedule.</p>
<h3 id="heading-what-snapshots-actually-do">What snapshots actually do</h3>
<p>On each <code>dbt snapshot</code> run, dbt:</p>
<ol>
<li><p>Reads the current state of your source/model.</p>
</li>
<li><p>Compares it to the existing snapshot table.</p>
</li>
<li><p>For rows that changed:</p>
<ul>
<li><p>Finds the existing "current" version (where <code>dbt_valid_to IS NULL</code>) for that entity.</p>
</li>
<li><p>Updates it by setting <code>dbt_valid_to</code> = snapshot timestamp.</p>
</li>
<li><p>Inserts a new version with <code>dbt_valid_from</code> = snapshot timestamp, <code>dbt_valid_to</code> = NULL.</p>
</li>
</ul>
</li>
<li><p>Leaves unchanged rows alone.</p>
</li>
<li><p>Keeps <strong>current</strong> rows with <code>dbt_valid_to = NULL</code> (or a configurable max date like <code>9999‑12‑31</code>).</p>
</li>
</ol>
<p>You get:</p>
<ul>
<li><p><code>dbt_valid_from</code></p>
</li>
<li><p><code>dbt_valid_to</code></p>
</li>
<li><p><code>dbt_scd_id</code> (surrogate key, unique per version)</p>
</li>
<li><p>Optional extras (like <code>dbt_is_deleted</code> if you enable hard deletes).</p>
</li>
</ul>
<h3 id="heading-how-to-configure-snapshots-modern-way">How to configure snapshots (modern way)</h3>
<p>As of dbt Core v1.9+, snapshots are configured in <strong>YAML</strong>, not in <code>.sql</code> blocks. If any transformation is needed, do it in a model (ephemeral or not), then snapshot that.</p>
<p>Example: snapshotting a raw <code>customers</code> table:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># snapshots/properties.yml</span>
<span class="hljs-attr">snapshots:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">customer_snapshot</span>
    <span class="hljs-attr">relation:</span> <span class="hljs-string">source('raw_database',</span> <span class="hljs-string">'customers'</span><span class="hljs-string">)</span>
    <span class="hljs-attr">config:</span>
      <span class="hljs-attr">schema:</span> <span class="hljs-string">snapshots</span>
      <span class="hljs-attr">unique_key:</span> <span class="hljs-string">customer_id</span>
      <span class="hljs-attr">strategy:</span> <span class="hljs-string">timestamp</span>
      <span class="hljs-attr">updated_at:</span> <span class="hljs-string">updated_at</span>
</code></pre>
<p>Key bits:</p>
<ul>
<li><p><code>relation</code>: what to snapshot (<code>source()</code> or <code>ref()</code>).</p>
</li>
<li><p><code>unique_key</code>: stable business identifier for the entity (identifies the "same customer across versions").</p>
</li>
<li><p><code>strategy</code>: how to detect changes.</p>
</li>
<li><p><code>updated_at</code>: which column to use for <code>timestamp</code> strategy.</p>
</li>
<li><p><code>schema</code>: where to store the snapshot table.</p>
</li>
</ul>
<p>Optional, but very useful:</p>
<ul>
<li><p><code>dbt_valid_to_current</code>: store a <strong>max date</strong> instead of <code>NULL</code> for current rows (e.g. <code>to_date('9999-12-31')</code>), so queries can use a plain <code>BETWEEN</code> without <code>COALESCE</code>.</p>
</li>
<li><p><code>hard_deletes: 'new_record'</code>: add rows for hard deletions with a <code>dbt_is_deleted</code> flag, instead of silently losing them.</p>
</li>
</ul>
<h3 id="heading-two-changedetection-strategies">Two change‑detection strategies</h3>
<p>dbt supports two built‑in strategies to decide when "this row is different enough to version it."</p>
<h4 id="heading-1-timestamp-use-this-whenever-you-can">1. <code>timestamp</code> (use this whenever you can)</h4>
<pre><code class="lang-yaml"><span class="hljs-attr">strategy:</span> <span class="hljs-string">timestamp</span>
<span class="hljs-attr">updated_at:</span> <span class="hljs-string">updated_at</span>
</code></pre>
<ul>
<li><p>Compares only the <code>updated_at</code> column.</p>
</li>
<li><p>If <code>updated_at</code> is newer than what's in the snapshot, dbt records a new version.</p>
</li>
<li><p>Handles schema changes gracefully—new columns don't require config changes.</p>
</li>
</ul>
<p>Constraint: <code>updated_at</code> must actually change whenever any tracked attribute changes. If the source only bumps it for some columns, you <em>will</em> miss changes in the others.</p>
<h4 id="heading-2-check-fallback-when-no-good-timestamp">2. <code>check</code> (fallback when no good timestamp)</h4>
<pre><code class="lang-yaml"><span class="hljs-attr">strategy:</span> <span class="hljs-string">check</span>
<span class="hljs-attr">check_cols:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">region</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">customer_segment</span>
</code></pre>
<ul>
<li><p>Compares the actual values of the listed columns against the snapshot.</p>
</li>
<li><p>If any differ, new version.</p>
</li>
</ul>
<p>Pros:</p>
<ul>
<li><p>Works when there's no reliable <code>updated_at</code>.</p>
</li>
<li><p>Explicit about which attributes you care about.</p>
</li>
</ul>
<p>Cons:</p>
<ul>
<li><p>Scales worse than <code>timestamp</code> as rows get wider and tables bigger, because more data must be compared per row.</p>
</li>
<li><p>Requires config maintenance when adding/removing columns.</p>
</li>
</ul>
<p>Good rule of thumb:</p>
<blockquote>
<p>If there is a trustworthy <code>updated_at</code>, use <code>timestamp</code>.<br />If there isn't, use <code>check</code> on a <strong>small, intentional</strong> subset of columns.</p>
</blockquote>
<h3 id="heading-how-often-to-run-snapshots">How often to run snapshots</h3>
<p>dbt's own recommendation: somewhere between <strong>hourly and daily</strong>. If you feel tempted to run snapshots every minute, you're trying to force a batch tool to behave like real‑time CDC.</p>
<ul>
<li><p>Run too rarely → you miss more behaviour between runs.</p>
</li>
<li><p>Run too often → you're paying a lot for a poor approximation of a log‑based system.</p>
</li>
</ul>
<h4 id="heading-running-snapshots-vs-dbt-run">Running snapshots vs `dbt run`</h4>
<p>Snapshots are <strong>not</strong> run as part of <code>dbt run</code>.</p>
<ul>
<li><p><code>dbt run</code> materializes your models.</p>
</li>
<li><p><code>dbt snapshot</code> runs <em>only</em> your snapshots.</p>
</li>
</ul>
<p>So you’ll generally want to schedule both:</p>
<ul>
<li><p><code>dbt snapshot</code> on your chosen cadence (hourly/daily), and</p>
</li>
<li><p><code>dbt run</code> for your transformation models.</p>
</li>
</ul>
<blockquote>
<p><strong>Note:</strong> Snapshots only track changes from the moment you create and start running them. They can't reconstruct history from before the first snapshot run.</p>
</blockquote>
<hr />
<h2 id="heading-4-putting-it-together-in-sql">4. Putting it together in SQL</h2>
<p><strong>TL;DR:</strong> The pattern is always "join facts to the snapshot where the fact's date is between <code>dbt_valid_from</code> and <code>dbt_valid_to</code> (or its <code>COALESCE</code>)."</p>
<h3 id="heading-pointintime-lookup">Point‑in‑time lookup</h3>
<p>Basic "state on a given date":</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> 
  customer_id,
  region
<span class="hljs-keyword">FROM</span> customer_snapshot
<span class="hljs-keyword">WHERE</span> 
  <span class="hljs-string">'2024-03-15'</span> <span class="hljs-keyword">BETWEEN</span> dbt_valid_from
                   <span class="hljs-keyword">AND</span> <span class="hljs-keyword">COALESCE</span>(dbt_valid_to, <span class="hljs-keyword">CURRENT_DATE</span>);
</code></pre>
<p>If you configure <code>dbt_valid_to_current</code> to store <code>9999‑12‑31</code> for current rows, the <code>COALESCE</code> can go away:</p>
<pre><code class="lang-sql">WHERE '2024-03-15' BETWEEN dbt_valid_from AND dbt_valid_to;
</code></pre>
<h3 id="heading-sales-by-customer-region-at-order-time">Sales by customer region at order time</h3>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span>
  cs.region,
  <span class="hljs-keyword">SUM</span>(o.order_total) <span class="hljs-keyword">AS</span> total_revenue
<span class="hljs-keyword">FROM</span> fact_orders <span class="hljs-keyword">AS</span> o
<span class="hljs-keyword">JOIN</span> customer_snapshot <span class="hljs-keyword">AS</span> cs
  <span class="hljs-keyword">ON</span> o.customer_id = cs.customer_id
 <span class="hljs-keyword">AND</span> o.order_date <span class="hljs-keyword">BETWEEN</span> cs.dbt_valid_from
                      <span class="hljs-keyword">AND</span> <span class="hljs-keyword">COALESCE</span>(cs.dbt_valid_to, <span class="hljs-keyword">CURRENT_DATE</span>)
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span>
  cs.region;
</code></pre>
<p>The join condition is doing all the temporal heavy lifting: each order row finds the <strong>version</strong> of the customer record that was valid on the order date.</p>
<p>The result:</p>
<ul>
<li><p>The 5 "New York" orders are counted for NY.</p>
</li>
<li><p>The 3 "California" orders are counted for CA.</p>
</li>
<li><p>No one has to hack around it in Looker/Power BI with window functions and CASE statements.</p>
</li>
</ul>
<hr />
<h2 id="heading-5-when-snapshots-are-enough-and-when-you-really-need-cdc">5. When snapshots are enough, and when you really need CDC</h2>
<p><strong>TL;DR:</strong> Snapshots are batch SCD2. They capture differences <strong>between runs</strong>, but not everything that happens in between those runs.</p>
<h3 id="heading-what-snapshots-are-genuinely-good-at">What snapshots are genuinely good at</h3>
<ul>
<li><p>Capturing <strong>most business‑relevant changes</strong> in CRUD‑style apps.</p>
</li>
<li><p>Making point‑in‑time joins trivial.</p>
</li>
<li><p>Giving analysts a reliable way to time‑travel dimensions using standard SQL.</p>
</li>
<li><p>Providing a simple audit trail for "what did this row look like before/after X?".</p>
</li>
</ul>
<p>All with:</p>
<ul>
<li><p>No log readers.</p>
</li>
<li><p>No Kafka, unless you otherwise need it.</p>
</li>
<li><p>No extra infra dependencies beyond "warehouse + dbt + scheduler".</p>
</li>
</ul>
<h3 id="heading-builtin-limitations-by-design">Built‑in limitations (by design)</h3>
<p>Snapshots cannot and will not:</p>
<ul>
<li><p>Show <strong>every intermediate state</strong> between runs. If a field flips A → B → A between two snapshots, the snapshot will never know it changed at all.</p>
</li>
<li><p>React in real time. Until the next run, the snapshot is out of date.</p>
</li>
<li><p>Magically turn a flaky <code>updated_at</code> into a reliable change log. Garbage in, garbage out.</p>
</li>
</ul>
<p>If a regulator, auditor, or core business requirement says:</p>
<blockquote>
<p>"Every change to this record must be permanently recorded, in order."</p>
</blockquote>
<p>then snapshots are the wrong tool. That's where <strong>real CDC</strong>—consuming transaction logs or event streams—earns its keep.</p>
<p>A healthy mental model:</p>
<ul>
<li><p>Snapshots = <strong>batch time‑travel</strong> for analytics.</p>
</li>
<li><p>CDC = <strong>continuous ledger</strong> for systems that must never forget anything.</p>
</li>
</ul>
<h3 id="heading-but-why-not-just-freeze-the-numbers">“But why not just freeze the numbers?”</h3>
<p>You can also solve some of these problems with incremental data marts:</p>
<ul>
<li><p>Each run upserts a new batch of results for the current period (for example, <code>sales_by_region_2024_Q1</code>).</p>
</li>
<li><p>Older batches are never updated, so your dashboards don’t change after the period closes.</p>
</li>
</ul>
<p>This works, but there’s a trade-off:</p>
<ul>
<li><p>If your business logic changes later (new definition of “active customer”, new region mapping, etc.), you simply can’t re-run the past without rebuilding the whole mart.</p>
</li>
<li><p>So you’d either live with inconsistent logic across periods, or you’d lose the original “frozen” numbers.</p>
</li>
</ul>
<p>Snapshots give you a different superpower: you preserve <strong>row-level history</strong> and can re-run models with new logic while still joining facts to the state that was true at the time.</p>
<hr />
<h2 id="heading-6-operations-keeping-history-without-drowning-in-it">6. Operations: keeping history without drowning in it</h2>
<p>Snapshots accumulate forever. That's their job. But large history tables can still hurt if left completely unmanaged.</p>
<p>Practical guardrails:</p>
<ul>
<li><p><strong>Partition</strong> by <code>dbt_valid_from</code> (or similar) so most queries only scan relevant periods.</p>
</li>
<li><p><strong>Cluster</strong> on a key like <code>customer_id</code> or <code>order_id</code> to speed up point‑in‑time joins.</p>
</li>
<li><p><strong>Retention rules:</strong> if no one cares about row‑level history older than a few years, consider archiving or aggregating old partitions.</p>
</li>
</ul>
<p>None of this is specific to dbt; it's just basic warehouse hygiene applied to a table that, by definition, only grows.</p>
<hr />
<h2 id="heading-summary">Summary</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Concept</td><td>Implementation</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Problem</strong></td><td>Warehouse only remembers the latest state; business questions need the past.</td></tr>
<tr>
<td><strong>Solution</strong></td><td>SCD Type 2: keep multiple versions per entity with validity windows.</td></tr>
<tr>
<td><strong>Setup</strong></td><td>dbt snapshot defined in YAML; <code>timestamp</code> strategy with a good <code>updated_at</code> when possible.</td></tr>
<tr>
<td><strong>Querying</strong></td><td><code>date BETWEEN dbt_valid_from AND COALESCE(dbt_valid_to, CURRENT_DATE)</code></td></tr>
<tr>
<td><strong>Maintenance</strong></td><td>Run <code>dbt snapshot</code> on a sensible schedule (hourly–daily), monitor table growth.</td></tr>
<tr>
<td><strong>Limitations</strong></td><td>No visibility into changes between runs; not a replacement for true CDC.</td></tr>
</tbody>
</table>
</div><h3 id="heading-next-steps">Next steps</h3>
<p>Set up a small snapshot on a single dimension (like customers), wire one existing report to use it, and watch how many "wait, why did this change?" conversations disappear. Once the pattern clicks, the rest of the team can use it everywhere without having to understand CDC or event logs.</p>
<blockquote>
<p><strong>Note:</strong> This article builds on the official dbt <a target="_blank" href="https://docs.getdbt.com/docs/build/snapshots">snapshots documentation</a> and <a target="_blank" href="https://docs.getdbt.com/reference/snapshot-configs">config reference</a> changed with dbt Core 1.9 and documented on the dbt Developer Hub as of January 2026 (current dbt Core version at time of writing: 1.11.2).</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Dad, what's an event stream??]]></title><description><![CDATA[Once upon a time, I had to explain change data capture to my son a client.
Here's the gist of how that went:

Change data capture (CDC) is basically reading the event stream of everything that has happened on a database. Any update, insert, delete (a...]]></description><link>https://bluu.dev/dad-whats-an-event-stream</link><guid isPermaLink="true">https://bluu.dev/dad-whats-an-event-stream</guid><category><![CDATA[event streaming]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[data streaming]]></category><category><![CDATA[change data capture]]></category><category><![CDATA[message queue]]></category><category><![CDATA[kafka]]></category><dc:creator><![CDATA[Brice Luu]]></dc:creator><pubDate>Thu, 21 Mar 2024 12:35:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/DCzpr09cTXY/upload/e8ace1fc3b85413cba22570282badbb9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Once upon a time, I had to explain change data capture to <s>my son</s> a client.</p>
<p>Here's the gist of how that went:</p>
<blockquote>
<p>Change data capture (CDC) is basically reading the event stream of everything that has happened on a database. Any update, insert, delete (and sometimes even DDL commands) that have been ran on the database. The database itself actually uses that same stream to apply the requested changes/events to the main node, as well as any replicas it might have. CDC therefor is already used internally by the database itself.</p>
</blockquote>
<p>That being said, when we talk about CDC as data engineers, more often then not, we're referring to reading that same event stream but for <em>"external"</em> consumption/replication (i.e towards a data warehouse or datalake, generally for post-hoc analytical purposes...)</p>
<p>And that got me thinking that the underlying concept: the event stream, is nebulous at best... Which is a shame, because it's really useful!! So it would be worth being able to explain it to <strong>anyone</strong>, even my <s>6yo</s> son. (now 8yo 🤦)</p>
<p><em>Anyhow, let's get on with the story, shall we?</em></p>
<blockquote>
<h3 id="heading-the-never-ending-book"><strong>The Never-Ending Book</strong></h3>
<p>Long ago, storytellers could only share tales by speaking out loud. If you wanted to hear their stories, you had to be there listening at that very moment. If you left or got distracted, you'd miss out on parts of the narrative.</p>
<p>But then, writing came along. Suddenly, storytellers could write their stories down in books. This allowed listeners to become readers who could pick up where they left off whenever they wanted. The storytellers no longer had to wait for an audience before sharing their tales.</p>
<p>Now, imagine there was a magical author who was so inspired, her stories never ended! She just kept writing and writing without ever stopping. To share this endless tale, she created a very special magical book.</p>
<p>This book was truly magical because no matter how many new pages the author added to it, every reader could see the full book at the same time! The author could keep writing forever, with new pages just appearing at the very end for all to see.</p>
<p>Since readers all experienced the story at their own pace, each one needed a special bookmark to keep their place. Thanks to the bookmarks, they could put the book down and come back to it later without losing their spot.</p>
<p>The author's inspiration was so endless that the book just kept growing bigger and bigger and bigger. Before long, it became too huge for anyone to pick up!</p>
<p>So the author cast another spell: This time, as soon as every single reader had finished with a page, that page would magically disappear from the front of the book. This kept the book from becoming too heavy to lift.</p>
<p>But what if a reader couldn't read for too long, or stopped altogether? If their bookmark didn't move for a while, it would eventually disappear too so it didn't slow down all the other readers.</p>
<p>And that, kids, is a lot like the "event stream" systems that power so many of the apps and websites we use today...</p>
</blockquote>
<p>There you have it, that's basically the same as an event stream. New pages (or events) keep getting added at the end. And pages that have been read by all readers (consumers) <strong>can</strong> be discarded along the way.</p>
<p>This illustrates:</p>
<ul>
<li><p>the need for asynchronous communication to start with</p>
</li>
<li><p>how event streams (or message queues) enable such communication</p>
</li>
<li><p>the never ending aspect of streams</p>
</li>
<li><p>as well as the basics around keeping it within reasonable size.</p>
</li>
</ul>
<p>However it does <strong>not</strong> explicit the fact that readers can consume concurrently, which is the <s>magic</s> scaling benefit of such systems (when configured properly). But I find it still captures most of the intuition around such a piece of stack.</p>
<p>And to close off the loop: that's also how the Postgres Write Ahead Log (WAL - used for CDC) generally works and can be configured...</p>
]]></content:encoded></item><item><title><![CDATA[Modern Data Modeling: Start with the End?]]></title><description><![CDATA[Introducing: dbt
So what's this dbt everyone's fussing about??
At its core, dbt (data build tool) is a "modern" data modeling framework using (Jinja) templated SQL. It comes with a CLI tool that allows to easily materialize your models as tables or v...]]></description><link>https://bluu.dev/modern-data-modeling-start-with-the-end</link><guid isPermaLink="true">https://bluu.dev/modern-data-modeling-start-with-the-end</guid><category><![CDATA[data]]></category><category><![CDATA[#DataModelling]]></category><category><![CDATA[dbt]]></category><dc:creator><![CDATA[Brice Luu]]></dc:creator><pubDate>Wed, 07 Feb 2024 08:02:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/7BpeiA0bhxs/upload/v1641548217403/qUGBGRtmDO.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introducing-dbt">Introducing: dbt</h2>
<p>So what's this dbt everyone's fussing about??</p>
<p>At its core, <a target="_blank" href="https://docs.getdbt.com/docs/introduction?ref=bluu.dev">dbt (data build tool)</a> is a "modern" data modeling framework using (<a target="_blank" href="https://jinja.palletsprojects.com/en/3.1.x/?ref=bluu.dev">Jinja</a>) templated SQL. It comes with a <a target="_blank" href="https://en.wikipedia.org/wiki/Command-line_interface?ref=bluu.dev">CLI</a> tool that allows to easily materialize your models as tables or views in your data warehouse.</p>
<p>The company behind it: <a target="_blank" href="https://www.getdbt.com/?ref=bluu.dev">dbt Labs</a> (formerly known as Fishtown Analytics) also produces a cloud-managed service revolving around the core dbt. They call it, quite simply, "dbt Cloud". Now, we won't get into that part here, just a thing to note: they've been central to the current "modern data stack" movement, for example coining new job titles like "analytics engineer" and, doing so, they've <a target="_blank" href="https://coalesce.getdbt.com/?ref=bluu.dev">coalesced</a> 😉 quite a <a target="_blank" href="https://www.getdbt.com/community/?ref=bluu.dev">following</a>.</p>
<p>What I mean (and think most people mean) by "modern data stack" is any service (open-sourced or not) that is native to cloud data warehouses (things like Snowflake, BigQuery and co...) and specifically targeting data teams.</p>
<p>What dbt allows is quite useful for data teams. Simply put, we're now easily able to adopt best practices software engineers take for granted (things like code versioning and reviewing, sandbox environments, testing, documentation, etc.) throughout our data warehouses and data modeling processes.</p>
<p>That makes things like unreproducible queries or inextricably weird numbers things of the past! Because instead of having a collection of interdependent views and/or dynamically generated bits of queries that do stuff all over our warehouse, it gives us (pretty) plain SQL <code>select</code> statements, cleanly versioned in a <a target="_blank" href="https://git-scm.com/about?ref=bluu.dev">git</a> code repository.</p>
<p>Along with a simple CLI to run these models, which will "materialize" (create or update) them in our data warehouse. It also has a bunch of nice additions that help us structure, test and document all those data models.</p>
<blockquote>
<p>Disclaimer: We'll cover how to gradually structure a dbt project. Not how to run it. When you're developing you can <a target="_blank" href="https://docs.getdbt.com/docs/get-started/installation?ref=bluu.dev">install the dbt CLI</a> to easily use it in your development environment. For deployment to production there are <a target="_blank" href="https://docs.getdbt.com/docs/deploy/deployments?ref=bluu.dev">different options</a>. If you're already using a data orchestrator like Airflow, Prefect or Dagster, running dbt is as simple as adding a task/step that runs <code>dbt build</code> in a shell environment containing the <a target="_blank" href="https://pypi.org/search/?q=dbt&amp;ref=bluu.dev">dbt pip package</a> you're using.</p>
</blockquote>
<h2 id="heading-with-great-power-comes-great-responsibility">With great power comes great responsibility</h2>
<p>The problem with frameworks that are so flexible is that they can be used in way too many ways. Some are good, some less so...</p>
<p>Anyway, we know we want to use dbt, but how should we go about it?</p>
<p>In terms of data modeling, there are multiple theories out there. Take one of the most prevalent still widely used today, <a target="_blank" href="https://en.wikipedia.org/wiki/Dimensional_modeling?ref=bluu.dev">dimensional modeling</a>: it was initially formulated back in the 90s’!</p>
<p>Now don't get me wrong, it has plenty of still very relevant and useful principles. The problem though, is that it takes too much conception time to pay up front. That's because it's mainly top-down design, at least for the initial fleshing out of the business entities and relationships.</p>
<p>In contrast, the approach I want to outline here is primarily bottom-up, in order to:</p>
<ul>
<li><p>deliver value and insights as quickly as possible</p>
</li>
<li><p>not pile up too much <a target="_blank" href="https://martinfowler.com/bliki/TechnicalDebt.html?ref=bluu.dev">(data modeling) technical debt</a> along the way.</p>
</li>
</ul>
<p>So, we'll start naively, and gradually factorize common patterns as they emerge.</p>
<h2 id="heading-quick-and-dirty-or-lets-say-humble-beginnings">Quick and dirty... or let's say: humble beginnings</h2>
<p>The quickest way to extract any insight from our data is to:</p>
<ol>
<li><p>have all our data sources readily available under the same roof: in our data warehouse (I’ve mentioned a couple options to help with that in <a target="_blank" href="https://bluu.dev/modern-data-stack-for-cto-99552102a5f9?ref=bluu.dev">a previous post</a>)</p>
</li>
<li><p>directly write each query that will join all the useful sources into a single table that we’ll then expose to our stakeholders (either in their own tools via <a target="_blank" href="https://medium.com/memory-leak/reverse-etl-a-primer-4e6694dcc7fb?ref=bluu.dev">reverse-ETL</a> or through a dashboard we'll create for them in our BI tool of choice)</p>
</li>
</ol>
<p>Of course, this isn’t sustainable! Because as soon as we have piled up a few such <em>usecases</em> , we'll surely need to copy/paste some parts from one query to the next... And this is always a slippery slope to get on!</p>
<p>Still! Go ahead and hack together your first couple of usecases in precisely this manner. The quicker we prove the value of data, the quicker we’ll start building support for “data” as a function/team within our organization. And that’ll be our basis for hiring more, spending more, etc. So raw speed is definitely something we want to aim for... in the beginning.</p>
<p>First off, <code>dbt init</code> your git repository. dbt will create a folder for you (named with the project name you’ll enter at prompt), which will contain the whole dbt project: meaning the <code>.sql</code> queries/files we’ll add, and a few <code>.yml</code> configurations to go with 'em.</p>
<p>That first command will have created a bunch of folders, most of which we won’t get into right now, but the main ones we can start working with are <code>analysis</code> and <code>models</code>.</p>
<ul>
<li><p><code>analysis</code> can hold all our ad-hoc queries that we'll want to manually run,</p>
</li>
<li><p><code>models</code> will contain all the queries that we actually want to <code>dbt run</code>, which will then materialize (as tables or views) in the data warehouse.</p>
</li>
</ul>
<p>Also, it loaded a couple of useless examples in the <code>models</code> folder. You can safely delete these. They only sparsely illustrate dbt’s main Jinja "macro" when you’re starting out: <code>{{ref(&lt;parent_model_of_current_query&gt;) }}</code> but we’ll get into that later 😉</p>
<p>You can simply start creating <code>.sql</code> files with your select queries for your first few usecases. These can go in the <code>models</code> folder if you want them to be materialized on your data warehouse when you execute <code>dbt run</code> from your CLI.</p>
<p>When running <code>dbt</code> commands, it isn’t really doing anything more than just executing our SQL files (just wrapped up in the correct <a target="_blank" href="https://en.wikipedia.org/wiki/Data_definition_language?ref=bluu.dev">DDL</a><a target="_blank" href="https://en.wikipedia.org/wiki/Boilerplate_code?ref=bluu.dev">boilerplate</a>) against our warehouse at this stage. We’ll get to the Jinja templating superpowers later!</p>
<p>If in doubt, please check out dbt’s documentation to get you up and running: <a target="_blank" href="https://docs.getdbt.com/docs/core/connect-data-platform/connection-profiles?ref=bluu.dev">Connection profiles</a>.</p>
<p>The only useful piece of advice at this stage: do start splitting out the logic within your query/ies in smaller bite-sized <a target="_blank" href="https://learnsql.com/blog/cte-with-examples/?ref=bluu.dev">CTEs</a>. This will be a huge time saver down the line.</p>
<p>But even with somewhat readable queries, just be aware that you’ll soon hit that slippery slope we mentioned earlier.</p>
<h2 id="heading-start-specifying-your-sources">Start specifying your sources</h2>
<p>So the next step we should take is to add a first <em>base</em> layer in our dbt project. I like calling it "base" as it’ll be the layer in which we simply select the data we need from our raw data sources.</p>
<p>So let’s create that <code>base</code> folder under <code>models/</code>. Then add a general <code>_sources.yml</code> inside. That configuration file will hold all the (input) references to our raw data. Effectively <a target="_blank" href="https://docs.getdbt.com/docs/building-a-dbt-project/using-sources?ref=bluu.dev">declaring the origin of your data sources</a> (read the dbt documentation linked to get the details of what that means and looks like).</p>
<p>You can now start creating simple <code>select</code> statements for the data sources that are actually used in the usecases you’ve previously fleshed out. The idea here is to be explicit with the columns you’re selecting. Try not to be tempted with just using <code>select *</code> because the main responsibility of this layer will be to explicitly declare the data your using at the column level.</p>
<p>Our <code>_&lt;source_name&gt;_sources.yml</code> declares <em>where</em> our raw data is, our <code>base</code> models declare <em>what</em> raw data we want to use.</p>
<p>Here’s a quick example (in Snowflake syntax):</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- base/mailchimp/base_mailchimp__members.sql</span>

<span class="hljs-keyword">with</span> parsed <span class="hljs-keyword">as</span> (
    <span class="hljs-keyword">select</span> *
      , parse_json(merge_fields) <span class="hljs-keyword">as</span> parsed_fields
    <span class="hljs-keyword">from</span> {{ <span class="hljs-keyword">source</span>(<span class="hljs-string">'mailchimp'</span>, <span class="hljs-string">'members'</span>) }}
)
<span class="hljs-keyword">select</span> <span class="hljs-keyword">id</span> <span class="hljs-keyword">as</span> member_id
    , <span class="hljs-keyword">status</span>
    , parsed_fields:<span class="hljs-string">'FNAME'</span> || <span class="hljs-string">' '</span> || parsed_fields:<span class="hljs-string">'LNAME'</span> <span class="hljs-keyword">as</span> full_name
    , parsed_fields:<span class="hljs-string">'REGISTERAT'</span> <span class="hljs-keyword">as</span> registered_at
<span class="hljs-keyword">from</span> parsed
</code></pre>
<p>We can then use this with <code>select * from {{ ref('base_mailchimp__members') }}</code> in a downstream model/query...</p>
<p>Just don’t forget to point your usecases to the new base models created here with that <code>ref()</code> macro.</p>
<p>Now, under this new <code>base</code> folder, we can also separate each data source into it’s own subfolder (as shown above). This allows to cleanly organize our base layer by source. That being said, we do <em>not</em> want to nest too many levels of folders. For the simple reason that dbt lives in a single namespace!</p>
<p>This means all our models (<code>models/**/*.sql</code> files) all need to have a <strong>unique</strong> name. A good idea is to prefix the filenames with the source’s name (as shown above). This will prevent most duplicate name clashes. Bonus: let's start writing down a style guide where we'll, ultimately, specify our preferences in terms writing SQL, but for now simply document this naming convention.</p>
<blockquote>
<p>i.e "base models filepath should be in the form: <code>base/&lt;source_name&gt;/base_&lt;source_name&gt;__&lt;model_name&gt;.sql</code>"</p>
</blockquote>
<p>Apart from explicitly detailing what data we want to use from our sources, any simple row-wise transformations can also go here in our base layer. Things like renaming columns to align names across models, type casting or even "case-when-ing" 😉 Here, we just want to avoid changing the grain of our raw data (no group bys nor joins) or filter anything out.</p>
<p>Last but not least, take the time to add a <code>_&lt;source_name&gt;_models.yml</code> file to each of the base subfolders. There, we’ll start specifying <a target="_blank" href="https://docs.getdbt.com/reference/model-properties?ref=bluu.dev">descriptions</a> and <a target="_blank" href="https://docs.getdbt.com/reference/resource-properties/tests?ref=bluu.dev">schema tests</a> for the most used models and their main columns. You don’t need to be exhaustive for the time being. Just describe and test the most important ones to start off with!</p>
<p>Now that we've got our bases covered, where to?</p>
<h2 id="heading-first-refactorings">First refactorings</h2>
<p>Now it’s time to add a middle layer (still under the <code>models/</code> folder). You can call it <code>core</code>, <code>staging</code>, <code>transform</code> or anything in between: this is ultimately where most of the magic will happen. The layer that our data team will keep full ownership of till the end of times! (no pressure 😆)</p>
<p>But for now, let’s start simple: is there any logic that you’ve already had to copy/paste in the different usecase queries you already have? Or CTEs you keep on re-writing in each new usecase?</p>
<p>If so, just isolate the repeated parts in their own <code>.sql</code> file in this folder and <code>ref()</code> it in the usecase models that use it.</p>
<p>For the time being, the main point is to start reducing the complexity and length of the usecase models by simply extracting the parts that are repeating. A good rule of thumb is the <a target="_blank" href="https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming)?ref=bluu.dev">rule of three</a>: if you have a logic that’s repeated 3 (or more) times in your project, then it should be isolated in it’s own model and can then be referenced by all the models that use it.</p>
<p>This way, if that logic needs to change down the line, it can be changed in a single place, instead of having to patch it systematically in all the places where it’s used. The risk being that it's easy to miss one of these places and therefor have differing logic when it should really be the same.</p>
<p>If your main <em>usecase</em> queries are still directly under the <code>models</code> folder. You can move them to <code>models/usecases/</code>. And, if you’re starting to have more than a few usecases, a nice addition to that folder is a further level of subfolders per stakeholder type. E.g one for <code>usecases/product</code> , another for <code>usecases/sales</code> or <code>usecases/marketing</code>, yet another for <code>usecases/finance</code> or <code>usescases/board</code>, ... You get the idea.</p>
<p>The ownership over each of these can ultimately be split out if and when data competency is spread in each department of our organization. But that’ll be the topic for another blog...😉</p>
<p>Nice: our project is getting a bit more structured!</p>
<p>So we can now start getting into the Jinja templating goodness of dbt. But let's use it wisely 😆</p>
<p>Good candidates for such templating are if you have long and predictable column lists that you want to systematically add. You can do so with a simple Jinja <code>for</code> loop. You can also check out the <code>dbt_utils</code><a target="_blank" href="https://github.com/dbt-labs/dbt-utils?ref=bluu.dev">package</a> for other ideas on what you can do more concisely with Jinja.</p>
<p>Any custom Jinja macros should go in the <code>macros</code> main level folder of your dbt project (same level as the <code>models</code> folder). I generally like separating out the macros that simply override dbt core ones in a <code>macros/overrides</code> subfolder. The first example of such, is generally the <code>generate_schema_name.sql</code> macro to <a target="_blank" href="https://docs.getdbt.com/docs/building-a-dbt-project/building-models/using-custom-schemas?ref=bluu.dev#how-does-dbt-generate-a-models-schema-name">specify different schema names</a> between locally/manually triggered runs and non-dev (prod/preprod) environments for example.</p>
<p>Here’s how I often end up doing that:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- macros/overrides/generate_schema_name.sql</span>

{% macro generate_schema_name(schema_name, node='Unknown node') -%}
    {%- if var('ENV') == 'dev' and target.schema -%}
        {<span class="hljs-comment"># -- use the target schema provided in profiles.yml for manually-triggered (dev) runs #}</span>
        {{ target.schema | trim }}
    {%- elif var('ENV') in ('prod', 'preprod') -%}
        {<span class="hljs-comment"># -- or use the schemas provided in configuration (in dbt_project.yml or individual models' config) #}</span>
        {{ schema_name | trim }}
    {%- else -%}
        {<span class="hljs-comment"># -- fail fast in case of unexpected configurations #}</span>
        {{ exceptions.raise_compiler_error("Local run but environment variable `DBT_DEFAULT_SCHEMA` is not set. Or environment is not known.") }}
    {%- endif -%}
{%- endmacro %}
</code></pre>
<p>Which requires adding to the end of your <code>dbt_project.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-comment">#...</span>
<span class="hljs-attr">vars:</span>
  <span class="hljs-attr">ENV:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ env_var('DBT_ENV', 'dev') }}</span>"</span>
</code></pre>
<p>And passing a <code>DBT_ENV</code> environment variable with the corresponding environment name.</p>
<p>Next up: shoot for the stars!</p>
<h2 id="heading-endgame-core-objects">Endgame: core objects</h2>
<p>Now that we’re getting the hang of it, it’s time to shuffle things around a bit more 😆</p>
<p>That middle layer we’ve introduced is actually the place where we’ll want to have our central logic manipulating the data.</p>
<p>The goal will be to aim towards <a target="_blank" href="https://www.fivetran.com/blog/star-schema-vs-obt?ref=bluu.dev">either</a>:</p>
<ul>
<li><p>a <a target="_blank" href="https://www.kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/?ref=bluu.dev">dimensional model</a>/<a target="_blank" href="https://en.wikipedia.org/wiki/Star_schema?ref=bluu.dev">star schema</a></p>
</li>
<li><p>or a limited collection of "<a target="_blank" href="https://hightouch.com/blog/data-warehouse-modelling-part-2?ref=bluu.dev">One Big Tables</a>"</p>
</li>
<li><p>(or any other top-down theory you’re most comfortable with...)</p>
</li>
</ul>
<p>where we’ll progressively define all our main business objects: users on the one hand, orders on the other, inventory in a third, etc</p>
<p>Each being at the grain of that object and having all the attributes useful to that particular object (via shared dimensions in the case of dimensional modeling).</p>
<p>These core objects will be the output of that layer, but will probably each need to be based on shared "preparation" models (the repeating CTEs we first factorized above...) Which can go in a <code>core/staging</code>subfolder when things become unwieldy.</p>
<p>As previously mentioned, we’ll generally want to keep central ownership on that layer.</p>
<p>And from these core objects, the <code>usecases</code> that are exposed (then outsourced) to our different stakeholders can simply select a limited number of columns/informations, relevant to each particular usecase.</p>
<p>So from <code>sources -&gt; base -&gt; core</code>, we're integrating data: the idea being to end up with fewer but wider (more columns) models.</p>
<p>And from <code>core -&gt; usecases</code>, we're doing a <a target="_blank" href="https://en.wikipedia.org/wiki/Fan-out_(software)?ref=bluu.dev">fan-out</a>, producing multiple variations from the core objects that only specify the data needed for each different type of stakeholder.</p>
<h2 id="heading-conclusion-dont-over-design-up-front">Conclusion: don't over design up-front</h2>
<blockquote>
<p>"Be like water my friend"</p>
</blockquote>
<ol>
<li><p>Start small but start fast</p>
</li>
<li><p>Progressively regroup repetitions</p>
</li>
<li><p>Only formalize once you have a good grasp of what's what</p>
</li>
<li><p>Rinse and repeat...</p>
</li>
</ol>
<p>Agile analytics engineering.</p>
<h3 id="heading-addendum-naming-is-importanthttpsmartinfowlercomblikitwohardthingshtmlrefbluudev">Addendum: <a target="_blank" href="https://martinfowler.com/bliki/TwoHardThings.html?ref=bluu.dev">Naming is important</a></h3>
<p>Why do I like these 3 folders for the first level under <code>models</code>?</p>
<ol>
<li><p><code>base</code></p>
</li>
<li><p><code>core</code> this folder we’ll always be our common thread as data modelers, our perpetual “work in progress”</p>
</li>
<li><p><code>usecases</code> these models are our end goals and we want the insights to actually be “useable”! (detail: I like the plural to insist on the transverse role of our data team: we strive to serve <strong>all</strong> the other teams!!)</p>
</li>
</ol>
<p>Ok, these names can seem a bit arbitrary, because they kind of are. But another thing I really like is that they’ll appear in that logical, sequential, alphabetical order anywhere you’ll list them: your cloud data warehouse UI (cf config in snippet below), your file explorer, your IDE of choice, you name it!</p>
<p>It just flows in the natural order: starting from our <code>base</code> models, passing through our <code>core</code> and finishing with the actual <code>usecases</code> we want to deliver.</p>
<p>Of course, I encourage you to align that first level of folder structure with your warehouse schemas. Here’s how in your <code>dbt_project.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">models:</span>
  <span class="hljs-string">&lt;your_dbt_project_name&gt;:</span>
    <span class="hljs-attr">base:</span>
      <span class="hljs-string">+schema:</span> <span class="hljs-string">"base"</span>
    <span class="hljs-attr">core:</span>
      <span class="hljs-string">+schema:</span> <span class="hljs-string">"core"</span>
    <span class="hljs-attr">usecases:</span>
      <span class="hljs-string">+schema:</span> <span class="hljs-string">"usecases"</span>
</code></pre>
<p>Note: this will only be the case in prod/preprod envs if your using <code>generate_schema_name</code> macro covered previously, when triggering locally (dev) everything will end up in a single schema (defined in your personal <code>~/.dbt/profiles.yml</code>)</p>
<h3 id="heading-published-by">Published by:</h3>
<p><a target="_blank" href="https://www.linkedin.com/in/brice-luu-data-eng/?ref=bluu.dev"><img src="https://media.licdn.com/dms/image/C4D03AQGPX9k-4qikog/profile-displayphoto-shrink_200_200/0/1571473413280?e=1712188800&amp;v=beta&amp;t=YpTzC9NXu8y-AT87g45oS2Tq86FbXhDr13oXp7w92uA" alt /></a></p>
]]></content:encoded></item><item><title><![CDATA[A Startup CTO’s Guide to the Modern Data Stack]]></title><description><![CDATA[Plugging your first BI tool on your first source of data (your production backend’s database) is already a step in the right direction. Well done!
Chances are you’ve tried both Metabase and Redash. Both being lightweight and open source alternatives ...]]></description><link>https://bluu.dev/modern-data-stack-for-cto-99552102a5f9</link><guid isPermaLink="true">https://bluu.dev/modern-data-stack-for-cto-99552102a5f9</guid><category><![CDATA[Data Science]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[tech ]]></category><dc:creator><![CDATA[Brice Luu]]></dc:creator><pubDate>Sun, 05 Dec 2021 14:33:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/IKHvOlZFCOg/upload/v1639739974365/XjhRUkbhD.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Plugging your first BI tool on your first source of data (your production backend’s database) is already a step in the right direction. Well done!</p>
<p>Chances are you’ve tried both <a target="_blank" href="https://www.metabase.com/">Metabase</a> and <a target="_blank" href="https://redash.io/">Redash</a>. Both being lightweight and open source alternatives to more full-fledged BI solutions like <a target="_blank" href="https://superset.apache.org/">Superset</a> (also open source), <a target="_blank" href="https://looker.com/">Looker</a>, <a target="_blank" href="https://mode.com/">Mode</a> or <a target="_blank" href="https://www.tableau.com/">Tableau</a>.</p>
<p>But if you’re lucky enough to get some traction internally on your brand new BI tool’s usage, you’ll soon end up with a sprawling collection of disparate queries and dashboards. Because everyone will be trying to find their own answers.</p>
<p>And that’s when things generally get messy… with other additional issues you’ll eventually encounter:</p>
<ol>
<li>Your production database won’t scale to handle the analytical workloads on top of your transactional ones.</li>
<li>Your production database won’t contain the data of your whole business, only from the app it’s serving (no data from your CRM, ERP, ad spend, etc)</li>
<li>Your production data won’t be ready for easy human consumption, quite the opposite, it should only care about being correctly normalized for your app’s consistent usage.</li>
</ol>
<p>Now, don’t get me wrong, you can get away with those for some time, by “simply” vertically scaling your replica, by adding other data sources as additional “connections” to your BI tool, and by educating all data users on each and every separate data source available.</p>
<p>Or, you can take the next real steps on your analytics journey: bootstrapping your Modern Data Stack.</p>
<p>With this article, I want to provide some simple — albeit opinionated — next steps you should take to build your data-driven company. The idea is to prevent you from getting lost in <a target="_blank" href="https://a16z.com/2020/10/15/the-emerging-architectures-for-modern-data-infrastructure/">too much</a> <a target="_blank" href="https://mattturck.com/data2020/">detail</a>!</p>
<p>Personally, <a target="_blank" href="https://www.linkedin.com/in/brice-luu-data-eng/">I’ve been</a> into data engineering for a few years now and have had the chance of setting up such stacks for a bunch of startups and scaleups over the past couple of years.</p>
<hr />
<h3 id="heading-step-1-set-up-a-cloud-data-warehouse">Step 1: set up a cloud data warehouse</h3>
<p>“Where they are put in boxes, and they come out all the same.”
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639571680778/XTeb9w0zX.jpeg" alt="Photo by [Sikai Gu](https://unsplash.com/@gentle_kay?utm_source=hashnode&amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=hashnode&amp;utm_medium=referral)" /></p>
<p>In order to decouple the scaling of your app’s transactional workloads from that of your analytics workload, the idea is to add a resource specifically tailored to the latter.</p>
<p>Furthermore, the aim will be to make it your central source of truth for what’s going on across your whole business.</p>
<p>In terms of options, general consensus among data engineers currently has it that you can’t go wrong with either <a target="_blank" href="https://cloud.google.com/bigquery/">Big Query</a> or <a target="_blank" href="https://www.snowflake.com/">Snowflake</a>.</p>
<p>Big Query:</p>
<ul>
<li>if you already rely on GCP for other stuff in your tech stack</li>
<li>or you have some machine learning use cases you already want to teeth out (as BQ is pretty tightly integrated with Google’s ML services, among others)</li>
<li>and/or you prefer a less hands-on approach to elasticity and scaling</li>
</ul>
<p>Snowflake:</p>
<ul>
<li>if none of the above really matters</li>
<li>and your team has some — even very limited — experience with database administration: you’ll feel right at home here (e.g user management through DDL statements)</li>
</ul>
<hr />
<h3 id="heading-step-2-extract-andamp-load-into-your-warehouse">Step 2: extract &amp; load into your warehouse</h3>
<p>“Lock and load.”
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639571682459/gOjKL3u3J.jpeg" alt="Photo by [Bernd Dittrich](https://unsplash.com/@hdbernd?utm_source=hashnode&amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=hashnode&amp;utm_medium=referral)" /></p>
<p>Now that you have a brand new and immaculate data warehouse, it’s time to start loading things in it.</p>
<p>While we’re at it, why not dump every potentially useful data source from across your business?</p>
<p>Typically, your future rockstar CMO might be interested in measuring your ads’ ROI (i.e Is our spending on Facebook Ads leading new users to sign up? Or maybe bringing past ones back? …)</p>
<p>You have quite a few options in terms of SaaS providers to do precisely that.</p>
<p>What you’ll generally get, are click-based UIs that allow to quickly connect your database(s) and/or a bunch of third party APIs as data sources to be loaded into your warehouse.</p>
<p>This even allows less technical folks to quickly become independent in dumping the data from their favorite tool right into the common warehouse.</p>
<p>Here, I’d highlight <a target="_blank" href="https://fivetran.com/">Fivetran</a> — current EL(T) market leader — as the more mature option, albeit currently <em>very</em> pricey.</p>
<p>Or <a target="_blank" href="https://airbyte.io/">Airbyte</a> as the latest but strongest upcoming contender. Especially if you’re comfortable hosting you’re own ingestion infrastructure: it’s open sourced and just a <code>docker-compose up</code> away.</p>
<hr />
<h3 id="heading-step-3-model-model-model">Step 3: model, model, model</h3>
<p>Also known as the ‘T’ in EL<strong>T</strong> (i.e “Transform”)
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639571684061/FunrXIG-B.jpeg" alt="Photo by [SwapnIl Dwivedi](https://unsplash.com/@momentance?utm_source=hashnode&amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=hashnode&amp;utm_medium=referral)" /></p>
<p>What you’ll need now is to get usable results — KPIs — from all of that data. Maybe merging data across different sources, aggregating it per user segment, and/or per day, week, month, quarter? Comparing that year over year, or over the previous period?</p>
<p>There are quite a few specialist terms involved because of a long history of techniques used to do that holistically.</p>
<p>You might have heard of things like <a target="_blank" href="https://www.kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/">Kimball</a>, <a target="_blank" href="https://en.wikipedia.org/wiki/Dimensional_modeling">Dimensional modeling</a>, <a target="_blank" href="https://en.wikipedia.org/wiki/Star_schema">Star schema</a> (all three are often used interchangeably) or others: <a target="_blank" href="https://en.wikipedia.org/wiki/Data_vault_modeling">Datavault</a>, <a target="_blank" href="https://towardsdatascience.com/the-new-unified-star-schema-paradigm-in-analytics-data-modeling-review-a245b2641dc8">Puppini bridge</a>, etc.</p>
<p>These techniques have roots back in the 80s’ when on-premises proprietary data warehouses were the norm (and E<strong>T</strong>L was a necessary evil). So the arguments focused on performance are way less relevant nowadays with cloud data warehouses being the norm.</p>
<p>But all the other benefits remain essential.</p>
<p>The problem is, in a start up environment where everything is going too fast, taking 6 months to design that perfect star schema from scratch will rarely be an option.</p>
<p>Hence my general advice in that context is: start from the use case and work your way back to the data sources (not the other way around).</p>
<p>Then you’ll refactor into more generic and intermediate layers/models once multiple — think <a target="_blank" href="https://en.wikipedia.org/wiki/Rule_of_three_%28computer_programming%29">rule of three</a> — use cases need similar transformation logic applied upstream.</p>
<p>A framework that really helps pinning all of that down is <a target="_blank" href="https://www.getdbt.com/">dbt</a>.</p>
<p>How does it help ?</p>
<p>It allows anyone that speaks SQL to contribute to your shared data warehouse construction. All of that in a versioned, tested and templated way, with builtin dependency resolution between all your models and sources.</p>
<h3 id="heading-then-whats-next">Then, what’s next?</h3>
<p>Give back!
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639571685661/-Uwi-1Tt7.jpeg" alt="Photo by [Damir Spanic](https://unsplash.com/@spanic?utm_source=hashnode&amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=hashnode&amp;utm_medium=referral)" /></p>
<p>This one should actually go hand in hand with that previous step. Focusing on the actual use case(s) is what will align the data team to quickly bring strong value to the table.</p>
<p>This can take multiple forms depending on the use case at hand:</p>
<ul>
<li>from dashboards (or alerts) to allow the whole company to gain insight on the business</li>
<li>to <a target="_blank" href="https://medium.com/memory-leak/reverse-etl-a-primer-4e6694dcc7fb">reverse ETLs</a> that’ll make that transformed data directly actionable by sharing it back to the operational tools the other teams are using (e.g calculate your segments in your data warehouse to then (re)load them in your email marketing tool, weighing clients at risk for your CRM, …)</li>
<li>or serving data APIs to feed some insights/recommendations/predictions into your product directly</li>
</ul>
<h3 id="heading-epilogue">Epilogue</h3>
<p>Something I could’ve mentioned: of course you’ll need to start recruiting some data specialists along the way. A data analyst or two for starters, helped by an <a target="_blank" href="https://blog.getdbt.com/analytics-engineering-for-everyone/">analytics engineer</a>.</p>
<p>I’ve held back on that one to highlight the fact that: there are quite a few things you <em>can</em> progress on, even before that very first data hire! That would definitely help with the hiring process, by conveying the importance you want to place on having a sustainable data stack. And it could actually enable them to make an impact from the get-go.</p>
<p>There you have it. You’re just a few steps away from data-enabled greatness! So start building ;-)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639571687153/aEHIowJsl.jpeg" alt="Photo by [Randy Fath](https://unsplash.com/@randyfath?utm_source=hashnode&amp;utm_medium=referral) on [Unsplash](https://unsplash.com/?utm_source=hashnode&amp;utm_medium=referral)" /></p>
]]></content:encoded></item></channel></rss>