<?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" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[The Analytics Engineer’s Handbook]]></title><description><![CDATA[The Analytics Engineer’s Handbook]]></description><link>https://markphammm.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!laP7!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fmarkphammm.substack.com%2Fimg%2Fsubstack.png</url><title>The Analytics Engineer’s Handbook</title><link>https://markphammm.substack.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 04 Nov 2026 06:27:21 GMT</lastBuildDate><atom:link href="iframe.php?url=https%3A%2F%2Fmarkphammm.substack.com%2Ffeed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Minh Phạm]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[markphammm@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[markphammm@substack.com]]></itunes:email><itunes:name><![CDATA[Mark Pham]]></itunes:name></itunes:owner><itunes:author><![CDATA[Mark Pham]]></itunes:author><googleplay:owner><![CDATA[markphammm@substack.com]]></googleplay:owner><googleplay:email><![CDATA[markphammm@substack.com]]></googleplay:email><googleplay:author><![CDATA[Mark Pham]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[I let a coding agent build my dashboard. I didn't let it touch the math.]]></title><description><![CDATA[Reviewing Anfra on 214k airline reviews.]]></description><link>https://markphammm.substack.com/p/i-let-a-coding-agent-build-my-dashboard</link><guid isPermaLink="false">https://markphammm.substack.com/p/i-let-a-coding-agent-build-my-dashboard</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Thu, 29 Oct 2026 14:23:41 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21EC5t%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>1. Intro</h2><p>Hi everyone, I'm Minh, an Analytics Engineer at <a href="iframe.php?url=https%3A%2F%2Finfinitelambda.com">Infinite Lambda</a>, an AI and Data consulting company headquartered in London. <a href="iframe.php?url=https%3A%2F%2Fwww.holistics.io">Holistics</a>, the team behind Holistics BI and <a href="iframe.php?url=https%3A%2F%2Fdbdiagram.io">dbdiagram.io</a>, asked me to beta test their new thing: <a href="iframe.php?url=https%3A%2F%2Fanfra.ai">anfra</a>. An open-source framework for building BI apps with coding agents. My first reaction was honestly pretty skeptical, partly because building the dashboard was never the hard part. Also, Anthropic recently launch Claude Dashboard, which also directly connect to your DWH.  So I put it on a real star schema, and tried my best to have a honest review</p><h3>TLDR</h3><ul><li><p>Tools like <code>anfra</code> are exactly where I think where BI&#8217;s heading.</p></li></ul><ul><li><p><code>anfra</code> is basically <strong>dbt for BI apps.</strong> The semantic layer is text in git, a coding agent writes the dashboard, the CLI checks it.</p></li><li><p>The agent writes the HTML. The metrics come from the semantic layer, so it can't invent its own math.</p></li><li><p>First app: one lazy prompt, about 3 minutes. The four-tab "one airline vs the rest" version on 214,335 reviews: 6 more prompts, about 35 minutes.</p></li><li><p>Interactions, lineage and exports come out of the box.</p></li><li><p>It also showed me a data bug in about five seconds, which is exactly what a dashboard should do.</p></li></ul><h2>2. What anfra is</h2><p>Three pieces:</p><ul><li><p><code>AML</code>, the modelling language. Models, dimensions, measures, relationships, datasets. This is your semantic layer, and it's just files.</p></li><li><p><code>AQL</code>, the query language. Not SQL. Apps ask for measures by name and AQL works out the joins and the aggregation.</p></li><li><p><code>Data Apps</code><strong>.</strong> One HTML file in <code>apps/</code>. That's the dashboard.</p></li></ul><p>Then a CLI (<code>anfra init</code>, <code>validate</code>, <code>show</code>, <code>query</code>, <code>serve</code>) and a <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fholistics%2Fanfra-skills">set of skills</a> for Claude Code, Cursor and Codex, so the agent actually knows how to write all three.</p><p>The flow is very dbt-shaped:</p><ol><li><p>Install the CLI + skills</p></li><li><p><code>anfra init</code></p></li><li><p>Connect the warehouse</p></li><li><p>Prompt the agent to build whatever app you want</p></li></ol><p>Back to my <a href="iframe.php?url=https%3A%2F%2Fmarkphammm.substack.com%2Fp%2Fi-tell-people-im-an-ae-so-wtf-does">kitchen analogy</a>. The warehouse is the kitchen, the dataset is the dish, the dashboard is the plate. With anfra, the agent can plate as many dashboards as you want, but the recipe lives in AML, in git, where someone reviewed it. That's the "trusted" in their tagline.</p><h2>3. Setup</h2><h3>3.1 The test data</h3><p>My repo is <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FMarkPhamm%2Fskytrax_reviews_data">skytrax_reviews_data</a>. Skytrax reviews (<a href="iframe.php?url=https%3A%2F%2Fwww.airlinequality.com">AirlineQuality.com</a>, closed now, so it's historical data) modelled with dbt into four star schemas: airline, airport, lounge and seat reviews. 214,335 fact rows, all in one DuckDB file on my laptop. Not huge, but real. Real nulls, real duplicates, real junk from the scrape. Good enough to break a tool.</p><h3>3.2 Install, init, connect</h3><pre><code>curl -fsSL https://anfra.ai/install.sh | bash   # one binary into ~/.anfra/bin
anfra skills install --agent claude             # the skills, for Claude Code
anfra init anfra                                # the project, in an anfra/ folder</code></pre><p>Connecting the warehouse is one YAML file:</p><pre><code># .anfra/data_sources.yml  (git-ignored, it holds credentials)
data_sources:
  skytrax:
    type: duckdb
    connection:
      path: &lt;repo&gt;/skytrax_reviews.duckdb</code></pre><p>First query through it, plain SQL to check the connection:</p><pre><code>anfra query -l sql -s skytrax "select count(*) as n_reviews, round(avg(average_rating),2) as avg_rating from airlines_reviews.fct_airline_review"</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21M_MW%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!M_MW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png 424w, https://substackcdn.com/image/fetch/$s_!M_MW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png 848w, https://substackcdn.com/image/fetch/$s_!M_MW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png 1272w, https://substackcdn.com/image/fetch/$s_!M_MW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21M_MW%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png" width="728" height="409.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!M_MW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png 424w, https://substackcdn.com/image/fetch/$s_!M_MW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png 848w, https://substackcdn.com/image/fetch/$s_!M_MW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png 1272w, https://substackcdn.com/image/fetch/$s_!M_MW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab54dd01-ac2e-44c7-a771-808e5567a8e8_2310x1242.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>156,077 reviews, average rating 2.6. Then the usual loop:</p><pre><code>anfra validate   # does the AML compile
anfra show       # the datasets, as the agent sees them
anfra serve      # the app, running locally</code></pre><p>No account, no "request access". Install to first query took about 3 minutes, and honestly I didn't do it. I pasted the setup steps into Claude Code and said "can you install and try this out? connect to our duckdb instance here".</p><h2>4. AML: where the math lives</h2><p>A model is a table plus the dimensions and measures you want people to use. The agent wrote these, I reviewed them. Trimmed:</p><pre><code>Model fct_airline_review {
  table_name: '"airlines_reviews"."fct_airline_review"'

  dimension recommended {
    type: 'truefalse'
    definition: @sql {{ #SOURCE.recommended }};;
  }

  measure recommend_rate {
    label: '% Recommended'
    description: 'Share of reviews where the reviewer recommends the airline.'
    definition: @sql CASE WHEN {{ recommended }} THEN 1.0 ELSE 0.0 END;;
    aggregation_type: 'avg'
  }
}</code></pre><p><code>% Recommended</code> <strong>is defined once</strong>, with a description, in a file I can review in a PR. Every chart that shows it asks for it by name. Nobody gets to redefine it in chart 4.</p><p>A dataset puts models together with their joins:</p><pre><code>Dataset airline_reviews {
  models: [fct_airline_review, dim_airline]
  relationships: [
    relationship(fct_airline_review.airline_id &gt; dim_airline.airline_id, true),
  ]
}</code></pre><p>I ended up with six models (four facts, <code>dim_airline</code>, <code>dim_airport</code>) and four datasets, one per review type. The part I liked most here: the plugin has a hook that runs <code>anfra validate</code> after every AML edit. So the agent writes a model, the CLI yells at it, the agent fixes it. It checks its own homework before I ever see it.</p><h2>5. AQL: asking for metrics, not writing SQL</h2><p>The dashboard queries look like this:</p><pre><code>explore {
  dimensions { period: fct_airline_review.date_submitted }
  measures { avg_rating: fct_airline_review.avg_rating }
  sorts { period asc }
}</code></pre><p>No joins, no <code>GROUP BY</code>. You name the dimensions and measures, AQL figures out the rest from the dataset.</p><p>Fair warning, AQL is not SQL, and the skill literally tells the agent not to write it from SQL knowledge. There's a whole reference behind it: level of detail, nested aggregation, window functions, ranking, period comparisons. There's even an <code>aql-writer</code> subagent in Claude Code that writes and validates AQL on its own, so the reference doesn't eat your main context. So you don't really need to learn AQL to start. But you do need to read it in review, the same way you read the SQL an agent writes.</p><h2>6. The dashboard</h2><h3>6.1 v1: one lazy prompt</h3><p>My entire spec for v1:</p><pre><code>okay, now try out anfra</code></pre><p>That's it. No metric, no chart, no layout, because I wanted to see what it does when I give it nothing. It picked something small on its own: avg rating by airline, a monthly trend, click an airline to filter the trend. Then it wrote the models, ran <code>anfra validate</code>, checked the numbers against a direct SQL query (156,077 reviews, 2.599 avg, both match), opened the app in a headless browser and clicked a bar to make sure the cross-filter worked. About 3 minutes.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21EC5t%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EC5t!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!EC5t!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!EC5t!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!EC5t!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21EC5t%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png" width="728" height="409.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Airline ratings v1: avg rating by airline next to avg rating over time, inside anfra serve&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Airline ratings v1: avg rating by airline next to avg rating over time, inside anfra serve" title="Airline ratings v1: avg rating by airline next to avg rating over time, inside anfra serve" srcset="https://substackcdn.com/image/fetch/$s_!EC5t!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!EC5t!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!EC5t!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!EC5t!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1b424ea-37ac-4ce7-a4bb-fe067bf95e9c_3024x1720.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Avg rating by airline on the left, trend on the right, a seat type filter and a month / quarter / year toggle on top. Click a bar and the trend filters to that airline. I didn't write any of that wiring. <code>anfra serve</code> wraps it in a small app shell: a sidebar to search your apps, refresh, and download buttons. It also reloads the page whenever the app file or the AML changes, so you just leave it open next to the agent.</p><h3>6.2 v2: one airline vs the rest</h3><p>v1 answered "who's rated highest", which is nice, but nobody makes a decision with that. The question a real airline would ask is "are we better or worse than everyone else, and on what?" So I asked for that. These are my actual prompts, typos included:</p><pre><code>i wants more metrics at the top and gotta be able to split it to compare a specific airlines vs how the rest of the airlines is doing
Let's create 4 dashboard, or one dashboard for 4 pages, i have 4 facts and i want to compare a specific airlines vs the rest on these 4 facts
rename call skytrax dashboard
change order from airlines to airport, to seat to lounge
add a date filter
can you add more charts and divides into sections? like avg rating, then a section for food and beverage, and section for ... a section for number of review</code></pre><p>Six prompts, about 35 minutes. v2 is four tabs, one per review type. Pick an airline (or airport), and every metric shows it next to all the others combined.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21aXqJ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aXqJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png 424w, https://substackcdn.com/image/fetch/$s_!aXqJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png 848w, https://substackcdn.com/image/fetch/$s_!aXqJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png 1272w, https://substackcdn.com/image/fetch/$s_!aXqJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21aXqJ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png" width="728" height="409.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Airline reviews tab: British Airways vs the rest, KPI cards on top, then a ranked bar chart and a trend line per metric&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Airline reviews tab: British Airways vs the rest, KPI cards on top, then a ranked bar chart and a trend line per metric" title="Airline reviews tab: British Airways vs the rest, KPI cards on top, then a ranked bar chart and a trend line per metric" srcset="https://substackcdn.com/image/fetch/$s_!aXqJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png 424w, https://substackcdn.com/image/fetch/$s_!aXqJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png 848w, https://substackcdn.com/image/fetch/$s_!aXqJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png 1272w, https://substackcdn.com/image/fetch/$s_!aXqJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98654c17-a14d-4181-aa89-cc9e0730a987_2332x8020.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>British Airways, for example:</p><ul><li><p>4,023 reviews, 2.6% of all airline reviews</p></li><li><p>avg rating 2.76 vs 2.59 for the rest</p></li><li><p>39.7% recommended vs 37.4%</p></li><li><p>biggest gap is ground service: 2.75 vs 2.36</p></li></ul><p>Same thing for airports. London Heathrow beats the rest on every metric, and shopping is the biggest gap: 3.43 vs 2.57. Very on brand.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21CEU9%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CEU9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png 424w, https://substackcdn.com/image/fetch/$s_!CEU9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png 848w, https://substackcdn.com/image/fetch/$s_!CEU9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png 1272w, https://substackcdn.com/image/fetch/$s_!CEU9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21CEU9%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png" width="728" height="409.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Airport reviews tab: London Heathrow vs the rest of airports&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Airport reviews tab: London Heathrow vs the rest of airports" title="Airport reviews tab: London Heathrow vs the rest of airports" srcset="https://substackcdn.com/image/fetch/$s_!CEU9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png 424w, https://substackcdn.com/image/fetch/$s_!CEU9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png 848w, https://substackcdn.com/image/fetch/$s_!CEU9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png 1272w, https://substackcdn.com/image/fetch/$s_!CEU9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe48ae3f6-d011-4e71-9384-f923a9b930ca_2332x8020.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Seat and lounge reviews get the same layout with their own sub-ratings (legroom, recline, seat width for seats; comfort, cleanliness, catering for lounges).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%213vrv%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3vrv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png 424w, https://substackcdn.com/image/fetch/$s_!3vrv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png 848w, https://substackcdn.com/image/fetch/$s_!3vrv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png 1272w, https://substackcdn.com/image/fetch/$s_!3vrv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%213vrv%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png" width="728" height="409.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Seat reviews tab&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Seat reviews tab" title="Seat reviews tab" srcset="https://substackcdn.com/image/fetch/$s_!3vrv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png 424w, https://substackcdn.com/image/fetch/$s_!3vrv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png 848w, https://substackcdn.com/image/fetch/$s_!3vrv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png 1272w, https://substackcdn.com/image/fetch/$s_!3vrv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b13976d-c2bf-47e0-b6ed-c508bfd0f1f8_2332x8020.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21RHFl%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RHFl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png 424w, https://substackcdn.com/image/fetch/$s_!RHFl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png 848w, https://substackcdn.com/image/fetch/$s_!RHFl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png 1272w, https://substackcdn.com/image/fetch/$s_!RHFl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21RHFl%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png" width="728" height="409.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Lounge reviews tab&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Lounge reviews tab" title="Lounge reviews tab" srcset="https://substackcdn.com/image/fetch/$s_!RHFl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png 424w, https://substackcdn.com/image/fetch/$s_!RHFl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png 848w, https://substackcdn.com/image/fetch/$s_!RHFl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png 1272w, https://substackcdn.com/image/fetch/$s_!RHFl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F821a5491-0680-4a1f-9e63-5a5a73259612_2332x8020.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>My favourite detail: "the rest" is not a separate model. It's the same query with the filter flipped from <code>is</code> to <code>is_not</code>:</p><pre><code>const app = Anfra.createApp({ title: 'Skytrax Dashboard' });
const grain = app.createDateDrill('grain', { default: 'quarter' });

const pick = app.createFilter('airline_pick', {
  dataset: 'airline_reviews', field: 'dim_airline.airline_name',
  default: { operator: 'is', values: ['british airways'] },
});
const rest = app.createFilter('airline_rest', {
  dataset: 'airline_reviews', field: 'dim_airline.airline_name',
  default: { operator: 'is_not', values: ['british airways'] },
});</code></pre><p>Each filter gets attached to its queries with one <code>app.mapControl(...)</code> call. Plus a traveller type filter, a date range with 1y / 3y / 5y shortcuts, and the grain toggle.</p><p>And the look. The default anfra style is <a href="iframe.php?url=https%3A%2F%2Fbasecoatui.com">Basecoat UI</a>, Inter, <a href="iframe.php?url=https%3A%2F%2Fecharts.apache.org">ECharts</a>, and one violet accent. Paper and ink, one purple pen. I didn't ask for a theme and it still looks better than most dashboards I've shipped by hand.</p><h3>6.3 The most-reviewed airline is "read more"</h3><p>Look at the reviews chart. The airline with the most reviews in my whole dataset is "read more", with 6,716 reviews. That's not an airline, that's a button. Somewhere in the scrape the "read more" link got saved as the airline name, and it's sitting in the top 15 airports too. I didn't even spot it first: the agent flagged it while testing v1, before I'd looked at a single chart.</p><p>anfra didn't cause that. It just put it on the plate, and honestly that's the best thing a dashboard can do. Garbage in, garbage with a nicer font, except this time the font made the garbage obvious in five seconds. The fix belongs in dbt, not the dashboard.</p><h2>7. What you get for free</h2><ul><li><p><strong>Interactions.</strong> Cross-filter on click, date drill, filters. All declared, none hand-wired.</p></li><li><p><strong>Lineage.</strong> "Where does this number come from?" traces a metric back to the fields, models and tables behind it. For an AE that's the whole job.</p></li><li><p><strong>The Inspect panel.</strong> In <code>anfra serve</code> it shows the page as a tree of blocks. You can point the agent at "this chart" instead of describing it in a paragraph.</p></li><li><p><strong>Right-click and downloads.</strong> Right-click a block for things like the underlying data. Any block or the whole page downloads as PNG or PDF, and a query can export its full result as CSV.</p></li><li><p><strong>Typos fail loud.</strong> Get a field name wrong in a filter and you get an error with a "did you mean", not a silently empty chart.</p></li></ul><h2>8. What I'd change</h2><p>It's a beta, and they asked for honest feedback, so here's the honest part.</p><ul><li><p><strong>I can't just send the link.</strong> My first question after v1 was "would that html work in GitHub Pages?" No. The page only works under <code>anfra serve</code>, which injects the SDK and runs every query against my local DuckDB. Static hosting gets you an empty page. To share it, you run the server somewhere (there's a Docker image), and the open-source server has no login, so anyone with the URL sees the data. I guess this is how they make money, by letting em host the dashboard for you</p></li><li><p><strong>Skills install guessed the wrong agent.</strong> <code>anfra skills install</code> put the skills in Codex only, because the <code>claude</code> CLI wasn't on my PATH. <code>--agent claude</code> fixed it, plus a <code>/reload-plugins</code>.</p></li><li><p><strong>Lineage can go stale.</strong> It reads a catalog, not the AML, so after an AML change you run <code>anfra ingest</code> or it shows the old definitions.</p></li><li><p><strong>Quotes inside quotes.</strong> <code>table_name: '"airlines_reviews"."fct_airline_review"'</code>. Small thing, but it's the kind of line that breaks at 5pm on a Friday.</p></li></ul><h2>9. Thoughts</h2><h3>It does the boring part first</h3><p>In my <a href="iframe.php?url=https%3A%2F%2Fmarkphammm.substack.com%2Fp%2Fpower-bi-or-tableau-wrong-question">Power BI vs Tableau post</a> I said the fix for bad dashboards is boring: define the decision before you touch SQL. The anfra build skill is written for this. Before writing the app, the agent is supposed to restate the request as one result you can check, ask what the business words mean and which date counts, and list every query and filter for you to approve.</p><p>In my run it skipped all of that, because I said "try out anfra" and nothing else. Fair. It picked a sensible small app instead of asking me ten questions. But look at what happened next. v1 was pretty and useless. v2 only got good once I said the actual decision out loud: one airline vs the rest. The agent can build fast, but it can't define the decision for you. That part is still your job.</p><h3>The bigger idea</h3><p>It's the same thing I keep writing about. If something gets edited, reviewed and shared over and over, it should be text, in git, with a history. The <a href="iframe.php?url=https%3A%2F%2Fmarkphammm.substack.com%2Fp%2Fpeople-version-control-everything">resume</a>, the <a href="iframe.php?url=https%3A%2F%2Fmarkphammm.substack.com%2Fp%2Fi-got-a-new-mac-and-refused-to-set">laptop setup</a>, the <a href="iframe.php?url=https%3A%2F%2Fmarkphammm.substack.com%2Fp%2Fi-just-wanted-to-send-someone-an">ERD</a>, and now the dashboard. The models are files. The datasets are files. The dashboard is one HTML file. v1 to v2 was a ~600-line diff, and I can read every line of it.</p><p>And honestly, I think this is where BI is going. Clicking and drag-and-drop made sense when the person building the dashboard couldn't write code. That's not the bottleneck anymore: you say what you want in plain English, the agent writes it as code, and you review the diff like any other PR. I believe drag-and-drop BI will eventually go the way of the hand-built Excel report. Not dead, just not where the trusted numbers live.</p><p>The tool is just where the buttons are. The semantic layer is the source of truth.</p><h3>Try it</h3><ul><li><p>Website: <a href="iframe.php?url=https%3A%2F%2Fanfra.ai">anfra.ai</a></p></li><li><p>Code: <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fholistics%2Fanfra">github.com/holistics/anfra</a></p></li><li><p>Agent skills: <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fholistics%2Fanfra-skills">github.com/holistics/anfra-skills</a></p></li><li><p>Demos: <a href="iframe.php?url=https%3A%2F%2Fdemo.anfra.ai">demo.anfra.ai</a></p></li><li><p>Holistics: <a href="iframe.php?url=https%3A%2F%2Fwww.holistics.io">holistics.io</a></p></li><li><p>My setup: <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FMarkPhamm%2Fskytrax_reviews_data">github.com/MarkPhamm/skytrax_reviews_data</a>, under <code>anfra/</code></p></li></ul><p>If you try it and something looks weird, tell the Holistics team. If you enjoyed this, please react or share so I know to keep going. Thanks for reading this far.</p><p>Feel free to <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fminhbphamm">connect with me on LinkedIn</a> and ask any question</p>]]></content:encoded></item><item><title><![CDATA[I just wanted to send someone an ERD, so I built a CLI and a VS Code extension]]></title><description><![CDATA[Quickdbd has ten tables for free, then everything turns into "Table 11". So I made my own.]]></description><link>https://markphammm.substack.com/p/i-just-wanted-to-send-someone-an</link><guid isPermaLink="false">https://markphammm.substack.com/p/i-just-wanted-to-send-someone-an</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Wed, 28 Oct 2026 15:14:05 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21J75F%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>1. Intro</strong></h2><p>Hi everyone, I&#8217;m Minh, an Analytics Engineer at Infinite Lambda, an AI and Data consulting company headquartered in London.</p><p>Every data project I have worked on has the same moment. Someone new joins, or a stakeholder asks &#8220;wait, how does <code>orders</code> connect to <code>customers</code> again?&#8221;, and I go looking for the ERD.</p><p>And the ERD is always one of these:</p><pre><code><code>erd.png                  (from 6 months ago, 3 tables missing)
erd_v2.png               (someone's screenshot, half cropped)
schema_FINAL.drawio      (nobody has edit access)
a link to a diagram tool (needs a login, or a paid seat)</code></code></pre><p>Surprisingly &#8220;un&#8221;professional &#175;\_(&#12484;)_/&#175;</p><p>The funny part is the same thing I complained about in my resume article. We version control SQL, dbt, Terraform, even BI now. But the one picture that explains how the whole warehouse fits together lives in a Slack thread, as a PNG, a painfully slow draw.io diagram and, most of the time,  it&#8217;s wrong.</p><p>So I built <strong>qdbd</strong> (<code>quickdbd-cli</code>). It&#8217;s a CLI that turns a schema written as plain text into an interactive ER diagram, one HTML file you can open anywhere, and it stays readable at 200+ tables. Then I built a VS Code extension on top of it, because I didn&#8217;t want to touch a terminal to look at a diagram either.</p><p>This post is about why, and how both of them work.</p><h2><strong>2. The pain point: sharing ERDs is weirdly hard</strong></h2><h3><strong>2.1 I actually like QuickDBD</strong></h3><p>I want to be clear first: I like <a href="iframe.php?url=https%3A%2F%2Fwww.quickdatabasediagrams.com%2F">QuickDBD</a>. You type something like this:</p><pre><code><code>Customer
-
CustomerId PK int
Email string UNIQUE

Order
-
OrderId PK int
CustomerId int FK &gt;- Customer.CustomerId</code></code></pre><p>and you get a diagram. No dragging boxes, no drawing arrows. Text in, picture out. For an AE, that&#8217;s exactly how it should work, because text is something I can put in git.</p><p>So why not <code>dbml</code> syntax? Well, I do like dbml, but I believe <code>qdbd</code> syntax is a bit more &#8220;simple&#8221; and &#8220;intuitive&#8221; . Also I was introduce to <code>quickdbd</code> before <code>dbml</code>, no naturally I&#8217;m a bit more used to using it.</p><h3><strong>2.2 Then you hit table 11</strong></h3><p>The free tier caps at 10 tables. From table 11 on, tables get renamed &#8220;Table N&#8221; and their columns become &#8220;...&#8221;.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21GY9N%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GY9N!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png 424w, https://substackcdn.com/image/fetch/$s_!GY9N!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png 848w, https://substackcdn.com/image/fetch/$s_!GY9N!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png 1272w, https://substackcdn.com/image/fetch/$s_!GY9N!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21GY9N%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png" width="1456" height="524" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:524,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:127234,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/222502471?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!GY9N!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png 424w, https://substackcdn.com/image/fetch/$s_!GY9N!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png 848w, https://substackcdn.com/image/fetch/$s_!GY9N!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png 1272w, https://substackcdn.com/image/fetch/$s_!GY9N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ab4d9ce-5b69-458c-be20-7454817501bb_2838x1022.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>10 tables. My <code>bronze</code> layer alone is more than 10 tables. Insurify&#8217;s dbt project was reaching 1500 models when I left. So either I ask em to fix the bug or I screenshot 10 tables at a time and stitch them together like it&#8217;s 2009.</p><h3><strong>2.3 Screenshots go stale the moment you send them</strong></h3><p>Even with a paid tool, the sharing flow is still: open the tool, export a PNG, drop it in Slack or Confluence.</p><p><strong>That PNG is frozen.</strong> Add a column next week and the picture is already lying. Nobody updates it, because updating it means opening the tool, finding the diagram, re-exporting, re-uploading. So the ERD in the docs slowly becomes fiction.</p><h3><strong>2.4 Generated diagrams are unreadable past ~30 tables</strong></h3><p>The other option is to generate the diagram from the database or the dbt project. That one stays current, which is great, until you open it and it&#8217;s 150 boxes and 300 lines on top of each other. Technically correct, practically useless.</p><p>So you pick one:</p><ul><li><p><strong>Hand-drawn:</strong> looks good, lives outside the repo, goes stale.</p></li><li><p><strong>Generated:</strong> stays current, unreadable at real size.</p></li></ul><p>I wanted both. A diagram that lives next to my code, and that I can still read at 200 tables.</p><h2><strong>3. What qdbd is</strong></h2><h3><strong>3.1 The idea in one line</strong></h3><p>Schema is a text file in git. qdbd checks it, lays it out, and builds <strong>one self-contained HTML page</strong> you can search, filter, click through and send to anyone.</p><p>No server, no login, no &#8220;request access&#8221;. It&#8217;s a file. Open it in a browser.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21xrUm%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xrUm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!xrUm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!xrUm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!xrUm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21xrUm%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png" width="1456" height="829" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:829,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:957064,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/222502471?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xrUm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!xrUm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!xrUm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!xrUm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7d71980-af7c-44b9-a687-ed5f05590930_3024x1722.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>3.2 It reads the syntax you already have</strong></h3><ul><li><p><strong>QuickDBD syntax.</strong> The parser mirrors QuickDBD&#8217;s own, error messages included. If a file passes <code>qdbd check --strict</code>, it pastes back into QuickDBD unchanged. That was the #1 rule when I built it: anything that works in QuickDBD must work here.</p></li><li><p><strong>DBML</strong>, the syntax from dbdiagram.io. <code>.dbml</code> files work as they are, and <code>qdbd convert</code> turns one syntax into the other.</p></li><li><p><strong>A dbt project.</strong> More on that in section 5, because that&#8217;s the part I use the most.</p></li></ul><h3><strong>3.3 Try it without installing anything</strong></h3><pre><code><code>curl -O https://raw.githubusercontent.com/MarkPhamm/quickdbd-cli/main/examples/quickdbd-sample.txt
uvx --from quickdbd-cli qdbd serve quickdbd-sample.txt</code></code></pre><p>A browser tab opens with the diagram and an editor next to it. Change the text and the diagram reloads. Drag tables around and the positions get saved.</p><p>To actually install it:</p><pre><code><code>pipx install quickdbd-cli   # or pip / uv tool install</code></code></pre><p>Python 3.11+, and that&#8217;s it. No Node, no Graphviz, no browser needed. I was pretty stubborn about this one, because &#8220;just install these 4 system dependencies first&#8221; is how a tool dies on a teammate&#8217;s laptop.</p><h2><strong>4. The CLI</strong></h2><h3><strong>4.1 </strong><code>qdbd build</code><strong>: the file you send</strong></h3><pre><code><code>qdbd build erd/ -o erd.html</code></code></pre><p>You can point it at one file or a whole folder. All files share one namespace, so a foreign key in <code>marts.txt</code> can point at a table in <code>staging.txt</code>.</p><p>What you get in the page:</p><ul><li><p><strong>Search</strong> and jump to any table</p></li><li><p><strong>Click a table</strong> to focus it and its neighbours, everything else fades out</p></li><li><p><strong>dbt-style selectors</strong> like <code>+orders+1</code>, <code>group:marts</code>, <code>tag:fact</code> to filter down to the part you care about</p></li><li><p><strong>Shaded regions</strong> per file or group, a minimap, and collapsed views (full / keys only / header only)</p></li><li><p><strong>A path finder</strong> between two tables, with the SQL <code>JOIN</code> written for you</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%219cIy%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9cIy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!9cIy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!9cIy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!9cIy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%219cIy%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png" width="1456" height="829" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:829,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:802765,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/222502471?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9cIy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!9cIy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!9cIy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!9cIy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a811d8-7653-4ef5-8308-05155f02b51a_3024x1722.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That last one is honestly my favourite. &#8220;How do I get from <code>order_items</code> to <code>customers</code>?&#8221; Click, click, here&#8217;s the join.</p><h3><strong>4.2 </strong><code>qdbd share</code><strong>: an actual link</strong></h3><pre><code><code>qdbd share erd/</code></code></pre><p>It uploads the page to a secret GitHub gist and prints a link. Run it again and it updates the <strong>same</strong> link, so the URL you pasted in Confluence last month is still the current diagram.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21gFeS%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gFeS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png 424w, https://substackcdn.com/image/fetch/$s_!gFeS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png 848w, https://substackcdn.com/image/fetch/$s_!gFeS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png 1272w, https://substackcdn.com/image/fetch/$s_!gFeS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21gFeS%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png" width="1456" height="877" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:877,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1132550,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/222502471?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!gFeS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png 424w, https://substackcdn.com/image/fetch/$s_!gFeS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png 848w, https://substackcdn.com/image/fetch/$s_!gFeS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png 1272w, https://substackcdn.com/image/fetch/$s_!gFeS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fa6eed1-0cba-4f56-a674-a5116e19ce04_3016x1816.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>This is literally the thing I wanted at the start.</strong> One command, one link, always up to date.</p><p>If you&#8217;d rather publish from CI, there&#8217;s a GitHub Action that deploys the diagram to GitHub Pages on every push.</p><h3><strong>4.3 </strong><code>qdbd diff</code><strong>: ERDs in pull requests</strong></h3><pre><code><code>qdbd diff --base main</code></code></pre><p>It tells you what a branch changed: new tables, changed columns, removed stuff. The GitHub Action posts that as a PR comment, and <code>qdbd build --diff-base main</code> colours the changes right in the diagram: new tables green, changed columns amber.</p><p>So reviewing a schema change is not &#8220;read 200 lines of YAML and imagine it&#8221;. You look at the picture.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21W6zg%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!W6zg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png 424w, https://substackcdn.com/image/fetch/$s_!W6zg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png 848w, https://substackcdn.com/image/fetch/$s_!W6zg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png 1272w, https://substackcdn.com/image/fetch/$s_!W6zg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21W6zg%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png" width="1456" height="831" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:831,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:815919,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/222502471?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!W6zg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png 424w, https://substackcdn.com/image/fetch/$s_!W6zg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png 848w, https://substackcdn.com/image/fetch/$s_!W6zg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png 1272w, https://substackcdn.com/image/fetch/$s_!W6zg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd814cf28-7730-4370-b03c-e77644ffce0f_3024x1726.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>4.4 </strong><code>qdbd export</code><strong>: when you really need a PNG</strong></h3><p>Sometimes you do need an image, for a slide or a doc. <code>qdbd export</code> writes PNG, SVG, or an editable draw.io file, and <code>--focus Table --depth 1</code> gives you just one table and its neighbours instead of the whole universe.</p><h2><strong>5. dbt: the part I use every day</strong></h2><h3><strong>5.1 Your dbt project already knows the ERD</strong></h3><p>If you write dbt tests, you already wrote down your keys:</p><ul><li><p><code>unique</code> + <code>not_null</code> on a column, that&#8217;s basically a primary key</p></li><li><p>a <code>relationships</code> test, that&#8217;s a foreign key</p></li></ul><p>So qdbd reads them:</p><pre><code><code>dbt docs generate
qdbd import dbt target/ -o erd/ --infer-fks</code></code></pre><p>You get one <code>.txt</code> file per dbt folder, with PKs from your tests, FKs from your <code>relationships</code> tests, and <code>--infer-fks</code> filling the gaps from column names. Model descriptions show up as tooltips.</p><h3><strong>5.2 Readable at 150 tables, for real</strong></h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%217PsD%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7PsD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!7PsD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!7PsD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!7PsD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%217PsD%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png" width="1456" height="828" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/aeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:828,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1049971,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/222502471?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7PsD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!7PsD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!7PsD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!7PsD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faeb7ad7e-bc97-4bd3-8790-80b7b855fe3d_3024x1720.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The live demo I like to show is OHDSI&#8217;s dbt-synthea project: 145 tables, 235 relationships, all from tests. It&#8217;s in the <a href="iframe.php?url=https%3A%2F%2Fqdbd.dev%2Fdemo%2F">demo gallery</a> if you want to click around. Without selectors that would be a wall of boxes. With <code>group:</code> and <code>+model+1</code>, it&#8217;s the five tables you actually wanted.</p><h2><strong>6. The VS Code extension</strong></h2><h3><strong>6.1 Why an extension</strong></h3><p>The CLI is great, but I spend my whole day in VS Code (well, Cursor). Alt-tabbing to a terminal to run <code>qdbd serve</code> every time I want to look at a table got old really fast.</p><p>So I built the extension. Search <strong>QuickDBD</strong> in the VS Code Marketplace, or on Open VSX if you&#8217;re on Cursor, Windsurf or VSCodium.</p><h3><strong>6.2 What you get</strong></h3><ul><li><p><strong>Syntax highlighting</strong> for QuickDBD and DBML</p></li><li><p><strong>Errors as you type</strong>, the same ones the CLI gives you</p></li><li><p><strong>Outline and F12</strong> go-to-definition across files</p></li><li><p><strong>The diagram beside your editor</strong>, following your cursor. Move to a table in the text and the diagram jumps to it</p></li><li><p><strong>A sidebar</strong> that finds every schema in your workspace, with buttons for export, share, compare with a git ref, convert to and from DBML, and dbt import</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21J75F%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!J75F!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png 424w, https://substackcdn.com/image/fetch/$s_!J75F!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png 848w, https://substackcdn.com/image/fetch/$s_!J75F!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png 1272w, https://substackcdn.com/image/fetch/$s_!J75F!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21J75F%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png" width="1456" height="980" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:980,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;A schema on the left, the live diagram on the right with the table under the cursor selected&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A schema on the left, the live diagram on the right with the table under the cursor selected" title="A schema on the left, the live diagram on the right with the table under the cursor selected" srcset="https://substackcdn.com/image/fetch/$s_!J75F!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png 424w, https://substackcdn.com/image/fetch/$s_!J75F!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png 848w, https://substackcdn.com/image/fetch/$s_!J75F!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png 1272w, https://substackcdn.com/image/fetch/$s_!J75F!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F491c9f14-ef87-43b0-abc9-af8525ec7c8c_1600x1077.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>6.3 No terminal, I mean it</strong></h3><p>If you don&#8217;t have <code>qdbd</code> installed, the extension installs it for you, into its own Python environment. You don&#8217;t need to know what pip is. Install the extension, open a <code>.txt</code> schema, the diagram shows up.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21rsHQ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rsHQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png 424w, https://substackcdn.com/image/fetch/$s_!rsHQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png 848w, https://substackcdn.com/image/fetch/$s_!rsHQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png 1272w, https://substackcdn.com/image/fetch/$s_!rsHQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21rsHQ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png" width="1456" height="1225" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1225,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;The QuickDBD sidebar: schemas, a table expanded to its columns with their types and keys, and the actions&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The QuickDBD sidebar: schemas, a table expanded to its columns with their types and keys, and the actions" title="The QuickDBD sidebar: schemas, a table expanded to its columns with their types and keys, and the actions" srcset="https://substackcdn.com/image/fetch/$s_!rsHQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png 424w, https://substackcdn.com/image/fetch/$s_!rsHQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png 848w, https://substackcdn.com/image/fetch/$s_!rsHQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png 1272w, https://substackcdn.com/image/fetch/$s_!rsHQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95054f5c-41bd-48c8-81d2-fc6e7b86abcf_1640x1380.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That matters a lot more than it sounds. The person who most needs to read the ERD is often not the person who wants to set up a Python environment.</p><h2><strong>7. Bonus: AI agents can read the schema too</strong></h2><p><code>qdbd mcp</code> exposes the schema to Claude, Copilot and Cursor as tools. The agent can list tables and columns, ask for the join path between two tables and get SQL back, check its own edits to the schema, and diff against <code>main</code>.</p><p>The extension registers it automatically in VS Code. So when I ask an agent to write a query, it doesn&#8217;t guess the join keys anymore. It asks the schema.</p><h2><strong>8. Thoughts</strong></h2><h3><strong>8.1 Is this over-engineering?</strong></h3><p>Probably, idk, you decide - If your project has 8 tables, open QuickDBD and you&#8217;re done. You don&#8217;t need any of this.</p><p>But if you have a dbt project with 100+ models, a team that keeps asking &#8220;how do these connect?&#8221;, and an ERD in Confluence that was last updated by someone who left the company, then I think a diagram that lives in git and builds itself is worth it.</p><h3><strong>8.2 The bigger idea</strong></h3><p>It&#8217;s the same thing I keep writing about. If something gets edited, reviewed and shared over and over, it should be text, in git, with a history. The resume, the laptop setup, and now the ERD.</p><p>The picture is just a build artifact. The text is the source of truth.</p><h3><strong>8.3 Try it</strong></h3><ul><li><p>Docs and live demos: <a href="iframe.php?url=https%3A%2F%2Fqdbd.dev%2F">qdbd.dev</a></p></li><li><p>PyPI: <a href="iframe.php?url=https%3A%2F%2Fpypi.org%2Fproject%2Fquickdbd-cli%2F">https://pypi.org/project/quickdbd-cli/</a></p></li><li><p>Code: <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FMarkPhamm%2Fquickdbd-cli">github.com/MarkPhamm/quickdbd-cli</a></p></li><li><p>Extension: search <strong>QuickDBD</strong> in VS Code, Cursor or Windsurf</p></li></ul><p>If you try it on your own dbt project and something looks weird, open an issue. Fair warning, I will probably ship the fix the same day, because I have zero chill about this project :&#8221;)</p><p>Feel free to dm me to learn more about it</p>]]></content:encoded></item><item><title><![CDATA[People version control everything except the file that gets them a job]]></title><description><![CDATA[You tell me if this is over-engineering or not]]></description><link>https://markphammm.substack.com/p/people-version-control-everything</link><guid isPermaLink="false">https://markphammm.substack.com/p/people-version-control-everything</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Sun, 20 Sep 2026 09:15:39 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vlY4%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><code>SQL</code> lives in Git. <code>dbt</code> lives in Git. <code>Terraform</code> lives in Git. BI becomes BI as code so we can version control and CI/CD them too.In my <a href="iframe.php?url=https%3A%2F%2Fsubstack.com%2Fhome%2Fpost%2Fp-215671396">last article</a>, I even started version controlling my MacBook setup so the next time I switch machines, I don&#8217;t have to remember what the hell I installed =))))</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21QFOM%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QFOM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png 424w, https://substackcdn.com/image/fetch/$s_!QFOM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png 848w, https://substackcdn.com/image/fetch/$s_!QFOM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png 1272w, https://substackcdn.com/image/fetch/$s_!QFOM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21QFOM%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png" width="1456" height="856" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:856,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:669573,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/216553867?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QFOM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png 424w, https://substackcdn.com/image/fetch/$s_!QFOM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png 848w, https://substackcdn.com/image/fetch/$s_!QFOM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png 1272w, https://substackcdn.com/image/fetch/$s_!QFOM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67f47c2a-6613-461e-9333-c1f331041cff_3024x1778.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>But there is one file I have probably edited dozens of times without ever thinking seriously about version control: <strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">my resume</mark></strong>.</p><p>A file where changing three or four lines can directly affect whether I get a recruiter screen, yet the standard workflow still somehow looks like this:</p><blockquote><p><code>resume.pdf</code><br><code>resume_new.pdf</code><br><code>resume_DA_final.pdf</code><br><code>resume_DE_final_v2.pdf</code><br><code>resume_AE_final_real.pdf</code></p></blockquote><p>Hi everyone, I&#8217;m Minh, an Analytics Engineer at Infinite Lambda, an AI and Data consulting company headquartered in London. Today I want to talk about a <em>slightly controversial</em>, and <em>probably slightly over-engineered</em> topic: <strong>version controlling your resume with Git</strong>.</p><p>Not because your resume needs CI/CD. Not because applying for jobs should become another project that you implement &#8220;Software engineering best pracetice&#8221;. I just think that if something is continuously iterated, tested, compared, and occasionally rolled back, then version control starts making a lot of sense.</p><h2>TLDR</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21RTXn%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RTXn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!RTXn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!RTXn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!RTXn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21RTXn%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f58df910-522b-465c-b976-16f2549b20cb_1920x1080.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1171940,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/216553867?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RTXn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!RTXn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!RTXn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!RTXn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff58df910-522b-465c-b976-16f2549b20cb_1920x1080.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The main benefits for me are pretty simple:</p><ul><li><p>I know exactly what changed between versions and why.</p></li><li><p>If an experiment does not work, I can roll it back.</p></li><li><p>I can maintain separate <strong>AE / DE / DA</strong> versions without duplicating a bunch of files.</p></li><li><p>Because the source is plain text, I can use Cursor or other AI agents <strong>directly inside the IDE.</strong></p></li></ul><p>The biggest benefit, though, is that Git turns resume iteration into a history I can actually inspect.</p><h2>Treat your resume as a product (product centric mindset)</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21g2NJ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!g2NJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png 424w, https://substackcdn.com/image/fetch/$s_!g2NJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png 848w, https://substackcdn.com/image/fetch/$s_!g2NJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png 1272w, https://substackcdn.com/image/fetch/$s_!g2NJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21g2NJ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png" width="1456" height="633" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:633,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:654188,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/216553867?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!g2NJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png 424w, https://substackcdn.com/image/fetch/$s_!g2NJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png 848w, https://substackcdn.com/image/fetch/$s_!g2NJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png 1272w, https://substackcdn.com/image/fetch/$s_!g2NJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a93b6bb-44c5-48cd-b582-895f94c8c2cf_2592x1126.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I think a resume is a product. Recruiter is your audience, your experiences/projects are the features that help the audience decide if they wanna hire you :&#8221;)</p><p>And if I continuously iterate a product, version controlling it feels pretty natural.</p><p>Yes, using LaTeX with <code>pdflatex</code>, Git, and Cursor requires slightly more technical knowledge than opening Word and editing a document. But for me, the one-hour-ish setup cost is completely worth it.</p><p>I&#8217;m also not going to argue that everyone should use LaTeX instead of Word for their resume. That depends entirely on your needs and preferences. Personally, I use LaTeX because I like the formatting and customization, but the interesting part here is not really LaTeX.</p><p>It is the <strong>history</strong>.</p><p>Suppose my current resume has a <strong>pretty decent interview rate.</strong> One day I decide to rewrite my entire Insurify experience, replace five bullets, remove a project, reorder my skills section, and push business impact much harder.</p><p>Then I apply to another 30 or 40 jobs and my interview rate seems worse. Is that statistically significant? Probably not =)))) I just feel like it got worse.</p><p>Obviously, I cannot conclude that the new resume caused it. The job market changes, the roles are different, the companies are different, timing is different, and there are a ridiculous number of confounding variables.</p><p>But the first question I still want to answer is:</p><p><strong>Okay, so what the hell did I change?</strong></p><p>If my resume is just a Word document or PDF that I keep overwriting, that question becomes surprisingly difficult to answer a few months later.</p><p>With Git, I can just look at my commit history:</p><pre><code><code>Rewrite Insurify bullets around business impact
Add Infinite Lambda experience
Remove older project
Tailor skills toward Analytics Engineering
Update changelog.md</code></code></pre><p>Then I can run <code>git diff</code> between two versions and see exactly what changed, line by line.</p><p>That is probably the biggest benefit for me. Git does not make my resume better. It does not magically improve my interview rate either. <strong>It makes the process of iterating on my resume inspectable.</strong></p><h2>Change &#8594; experiment &#8594; rollback</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vlY4%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vlY4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png 424w, https://substackcdn.com/image/fetch/$s_!vlY4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png 848w, https://substackcdn.com/image/fetch/$s_!vlY4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png 1272w, https://substackcdn.com/image/fetch/$s_!vlY4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vlY4%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png" width="1456" height="877" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:877,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:858298,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/216553867?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vlY4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png 424w, https://substackcdn.com/image/fetch/$s_!vlY4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png 848w, https://substackcdn.com/image/fetch/$s_!vlY4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png 1272w, https://substackcdn.com/image/fetch/$s_!vlY4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e79edfe-6654-413b-8a5f-924ecf44905e_3024x1822.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Let&#8217;s say I have a hypothesis:</p><blockquote><p>Recruiters care more about business impact than implementation details.</p></blockquote><p>So I rewrite my resume more aggressively, remove some of the technical details, and push the result to the front.</p><p>For example, maybe I change this:</p><blockquote><p>Built an event-driven partner onboarding pipeline using SQS, EventBridge, Kinesis, Airflow, and Redshift.</p></blockquote><p>into this:</p><blockquote><p>Reduced partner onboarding time by ~90%, scaling automated onboarding to 150+ partners supporting $10M+ revenue.</p></blockquote><p>Maybe the second one performs better. Maybe a technical hiring manager actually prefers the first one. Maybe there is literally no meaningful difference at all.</p><p>The point is that I am running an <strong>experiment</strong>.</p><p>If I test that version for a while and do not like the result, I do not have to sit there trying to remember, &#8220;Wait, how did I write that partner onboarding bullet two months ago?&#8221;</p><p>The old version is still there.</p><p>The mental model is basically the same as software:</p><blockquote><p><strong>change &#8594; ship &#8594; observe &#8594; keep / revert</strong></p></blockquote><p>Obviously, a resume is not a clean A/B testing environment. I do not have a proper control group, sample sizes are usually small, and interview rate is insanely noisy.</p><p>I&#8217;m not pretending I can attribute a change in response rate to a specific resume edit. But at least I preserve the <strong>state of each experiment</strong> instead of overwriting the baseline every time I try something new.</p><h2>Branching - git OP feature</h2><p>Most people probably do not have exactly one resume.</p><p>At different points in my job search, I was applying for <strong>Analytics Engineer, Data Engineer, and Data Analyst</strong> roles. Those jobs overlap quite a lot, but the emphasis is very different.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21r-NC%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!r-NC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png 424w, https://substackcdn.com/image/fetch/$s_!r-NC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png 848w, https://substackcdn.com/image/fetch/$s_!r-NC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png 1272w, https://substackcdn.com/image/fetch/$s_!r-NC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21r-NC%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png" width="1456" height="541" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:541,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:207089,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/216553867?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!r-NC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png 424w, https://substackcdn.com/image/fetch/$s_!r-NC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png 848w, https://substackcdn.com/image/fetch/$s_!r-NC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png 1272w, https://substackcdn.com/image/fetch/$s_!r-NC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdd521-2d80-4ec7-bef6-8eb211917a69_2634x978.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>My AE resume might push <strong>dbt, dimensional modeling, analytics engineering, CI/CD, and data quality</strong> much harder. The DE version might emphasize <strong>Airflow, AWS, ingestion, streaming, and infrastructure</strong>. The DA version might focus more on <strong>SQL, reporting, experimentation, stakeholders, and business impact</strong>.</p><p>The normal workflow is to copy the file:</p><pre><code><code>resume_AE.pdf
resume_DE.pdf
resume_DA.pdf</code></code></pre><p>That works perfectly fine until I add a new job, update education, fix a typo, change one achievement, or realize all three versions need the same experience update.</p><p>Then I have to remember to update all three files. A few months later, I no longer know which one is actually ahead of the others.</p><p>Git branching solves this pretty cleanly:</p><pre><code><code>main
analytics-engineer
data-engineer
data-analyst</code></code></pre><p><code>main</code> can hold the common version, while each branch contains role-specific changes. If I want to apply for a DE role, I check out <code>data-engineer</code>, compile the PDF, and use that version.</p><p>If I add a new experience that belongs in every resume, Git already has a workflow for bringing that change across branches.</p><p><strong>One resume, multiple variants, but the history stays connected.</strong></p><p>For me, this is probably where Git goes from &#8220;nice to have&#8221; to genuinely useful.</p><h2>LaTeX + Cursor makes the workflow much better</h2><p>Technically, Word and Google Docs already have version history, so I do not think people should move to LaTeX just because they want rollback.</p><p>I prefer LaTeX because the resume becomes <strong>plain text source code</strong>. Git diff becomes extremely clean, AI agents can read it properly, and the PDF is basically just a build artifact.</p><p>My setup is roughly:</p><pre><code>MINH_PHAM_RESUME/
&#9500;&#9472;&#9472; .claude/
&#9474;   &#9500;&#9472;&#9472; context/
&#9474;   &#9474;   &#9500;&#9472;&#9472; achievements.md
&#9474;   &#9474;   &#9492;&#9472;&#9472; resume-suggestions.md
&#9474;   &#9492;&#9472;&#9472; skills/
&#9474;       &#9492;&#9472;&#9472; update-resume/
&#9474;           &#9492;&#9472;&#9472; SKILL.md
&#9500;&#9472;&#9472; .vscode/
&#9474;   &#9492;&#9472;&#9472; tasks.json
&#9500;&#9472;&#9472; jd/
&#9474;   &#9492;&#9472;&#9472; demo_ae_jd
&#9500;&#9472;&#9472; legacy/
&#9474;   &#9492;&#9472;&#9472; minh_pham_resume.pdf
&#9500;&#9472;&#9472; CLAUDE.md
&#9500;&#9472;&#9472; minh_pham_resume.aux
&#9500;&#9472;&#9472; Minh_Pham_Resume.log
&#9500;&#9472;&#9472; minh_pham_resume.out
&#9500;&#9472;&#9472; minh_pham_resume.pdf
&#9500;&#9472;&#9472; Minh_Pham_Resume.tex
&#9492;&#9472;&#9472; README.md</code></pre><p>The workflow is basically:</p><pre><code><code>LaTeX source &#8594; compile &#8594; PDF
Git &#8594; track every change
GitHub &#8594; remote history
Cursor &#8594; edit + agent</code></code></pre><p>And plain text unlocks another thing I have started to like quite a lot: <strong>AI can work with the whole resume repository instead of just the final PDF</strong>.</p><p>I can ask Cursor something like:</p><blockquote><p>Compare my Analytics Engineer and Data Engineer branches. What bullets are different?</p></blockquote><p>Or:</p><blockquote><p>Show me everything I changed in my resume during the last month.</p></blockquote><p>Or:</p><blockquote><p>Rewrite this bullet, but keep the same structure and do not remove the Redshift/dbt implementation detail.</p></blockquote><p>The agent can see the source files, the Git diff, the commit history, and the changelog in the same context.</p><p>That is much more useful to me than uploading a PDF every time and re-explaining the entire background from scratch.</p><h2>Commit messages are surprisingly useful too</h2><p>One side effect I did not really expect is that Git forces me to be slightly more deliberate about why I am changing something.</p><p>Instead of just thinking, &#8220;This sentence sounds better,&#8221; I can commit something like:</p><pre><code><code>refactor: move business impact before implementation details</code></code></pre><p>or:</p><pre><code><code>test: remove older projects to keep resume to one page</code></code></pre><p>or:</p><pre><code><code>ae: emphasize dbt modeling and data quality work</code></code></pre><p>Yes, this is extremely nerdy =))))</p><p>But when I look back a few months later, I know not only <strong>what</strong> changed, but also <strong>why</strong> I changed it.</p><p>I also keep a very simple <code>changelog.md</code> for bigger experiments:</p><pre><code><code>2026-09-15

- Rewrote Insurify section around business impact
- Removed streaming project
- Added Infinite Lambda experience
- AE version only</code></code></pre><p>Is this necessary? Definitely not.</p><p>But if you are actively recruiting and changing your resume every week, I think having some record of the larger experiments is actually pretty useful.</p><h2>Is this over-engineering?</h2><p>Probably, idk, you decide. But is it worth it? Definitely yes!</p><p>If you update your resume twice a year, Word or Google Docs is completely fine. There is very little reason to introduce Git just for the sake of saying your resume is version controlled.</p><p>But if you are actively recruiting, maintaining multiple role variants, tailoring your resume often, and already know Git anyway, the incremental cost is basically zero.</p><p>What I find funny is that people in Data and Engineering are completely comfortable with the idea that:</p><blockquote><p>&#8220;Code should be version controlled.&#8221;</p></blockquote><p>We version control SQL. We version control dbt models. We version control infrastructure. We are increasingly version controlling BI as well.</p><p>Then the document that might decide whether we even get into the interview process is sitting in a folder called:</p><pre><code><code>resume_final_final_v7_REAL.pdf</code></code></pre><h2>Thoughts</h2><p>The interesting part here is not really LaTeX. LaTeX is just the implementation I happen to prefer.</p><p>The bigger idea is that I do not think a resume should be treated as a static file that I open every few months, change a few lines in, and forget again. <strong>It is an artifact I continuously iterate based on feedback from the market.</strong></p><p>Once I look at it that way, a lot of software engineering practices start making sense. Version control tells me what changed. Commit messages document why I changed it. Branches let me maintain multiple variants. Rollback preserves the baseline when an experiment does not work. And AI agents can use the history to help me iterate faster.</p><p>I am still not at the point where I open a pull request and self-approve my own resume.</p><p>But Git? I think that part is completely worth it.</p><p><strong>If data teams already version control transformations, infrastructure, and increasingly the BI layer, then the file that decides whether I get an interview probably deserves a </strong><code>git log</code><strong> too.</strong></p>]]></content:encoded></item><item><title><![CDATA[I rebuilt my ingestion pipeline with zero infrastructure: Part 3 - CDC, and the two numbers that came out wrong]]></title><description><![CDATA[Change five rows in Postgres, click nothing, and follow them to the reporting tables. Then work out why the revenue is off.]]></description><link>https://markphammm.substack.com/p/i-rebuilt-my-ingestion-pipeline-with-85c</link><guid isPermaLink="false">https://markphammm.substack.com/p/i-rebuilt-my-ingestion-pipeline-with-85c</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Sat, 19 Sep 2026 09:13:40 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21c37Q%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1><strong>Intro</strong></h1><p>This is the last part. In <a href="iframe.php?url=https%3A%2F%2Fsubstack.com%2F%40markphammm%2Fp-214887139">part 1 </a>I set up a Neon Postgres source, a Snowflake trial, and a Fivetran connection reading Postgres&#8217; write-ahead log. In <a href="iframe.php?url=https%3A%2F%2Fsubstack.com%2Fhome%2Fpost%2Fp-215043072">part 2</a> I let Fivetran clone my GitHub repo and run the dbt project on a schedule, with the daily job triggered by the ingestion sync finishing.</p><p>Everything is built. So this part is the exercise that proves it: <strong>edit a few rows in Neon and confirm they reach </strong><code>FIVETRAN_DEMO.SERVE</code><strong> without touching Fivetran, Snowflake, or dbt.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21c37Q%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!c37Q!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png 424w, https://substackcdn.com/image/fetch/$s_!c37Q!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png 848w, https://substackcdn.com/image/fetch/$s_!c37Q!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png 1272w, https://substackcdn.com/image/fetch/$s_!c37Q!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21c37Q%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png" width="1456" height="728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:728,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1245440,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215169789?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!c37Q!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png 424w, https://substackcdn.com/image/fetch/$s_!c37Q!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png 848w, https://substackcdn.com/image/fetch/$s_!c37Q!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png 1272w, https://substackcdn.com/image/fetch/$s_!c37Q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17b20589-f4ae-40d9-8bd4-4f26378d2e46_2172x1086.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>They do reach it. Five rows changed in Postgres, six rows moved through the sync, the dbt job fired on its own, and the reporting tables rebuilt.</p><p>And two of the numbers came out wrong.</p><p>Not wrong because the pipeline broke. Wrong because a Fivetran landing zone is not the same thing as a copy of your source table, and every model in this project quietly assumes it is. <strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">This is the part I would have gotten wrong in production, so it is the part I most wanted to write down.</mark></strong></p><h1><strong>Takeaway</strong></h1><ul><li><p>What an incremental CDC sync actually looks like: six rows instead of 2,747</p></li><li><p><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">The two columns Fivetran adds to your tables, </mark><code>_FIVETRAN_SYNCED</code><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> and </mark><code>_FIVETRAN_DELETED</code><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">, and what they mean</mark></p></li><li><p>Why <strong>a single </strong><code>UPDATE</code><strong> arrives as two rows</strong> when your source table has no primary key</p></li><li><p>Why <strong>soft deletes</strong> silently keep a deleted order line in your revenue total</p></li><li><p>The one-line filter every staging model on a Fivetran landing zone should have, and why this project deliberately leaves it out</p></li><li><p>Why &#8220;the row is still there&#8221; and &#8220;the row is still current&#8221; are different questions</p></li></ul><h1><strong>Prerequisite</strong></h1><p>Parts 1 and 2, finished and working. Specifically: the Fivetran connection syncs <code>l1_landing</code> into <code>FIVETRAN_DEMO</code>, and the <code>Daily-after-landing</code> dbt job builds <code>TRANSFORM</code> and <code>SERVE</code>.</p><p>Check the <strong>Connections</strong> column on the Transformations list before you start. If <code>Daily-after-landing</code> says <code>None</code>, the connection ID in <code>deployment.yml</code> is wrong, the integrated schedule never resolved, and the interesting half of this test will not happen on its own.</p><p>Three scripts do all the work:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/TLLbg/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/35a9188c-ae49-4806-90e3-fd64c0fd41d4_1220x558.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4c2e9a98-5441-477f-a0b1-7d56e1fa98a0_1220x558.png&quot;,&quot;height&quot;:273,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FTLLbg%2F1%2F" width="730" height="273" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><h1><strong>What is and is not incremental</strong></h1><p>Worth being precise about this before we start, because &#8220;the pipeline is incremental&#8221; is only half true.</p><p><strong>The ingestion half is incremental.</strong> Postgres writes your <code>UPDATE</code>, <code>INSERT</code>, and <code>DELETE</code> to the WAL. Fivetran reads the change stream through <code>replication_slot_01</code> and ships only those rows. It never re-reads the 1,651 rows in <code>orderitems</code>.</p><p><strong>The transformation half is not.</strong> Every model in this project is <code>materialized='table'</code>, so the dbt job drops and rebuilds all of them from scratch. Five models, a few thousand rows, a couple of seconds. At this size, incremental models would be complexity with no payoff.</p><p>That split is normal and it is usually the right one. Incremental ingestion is where the savings are, because that is the part that scans a production database. Incremental transformation is where the bugs are.</p><h1><strong>Step 1: Take a baseline</strong></h1><p>Run section 1 of <code>cdc_verify.sql</code> in Snowsight <strong>before you change anything</strong>:</p><pre><code><code>SELECT CUSTOMERID, CUSTOMERNAME, ORDERCOUNT, REVENUE
FROM FIVETRAN_DEMO.SERVE.CUSTOMERREVENUE
WHERE CUSTOMERID = '28222';</code></code></pre><p>Customer 28222 owns exactly one order, <code>800143</code>, which keeps the arithmetic easy to check by hand.</p><p>Also write down the revenue for customer 27613, who is unrelated:</p><pre><code><code>SELECT CUSTOMERID, REVENUE
FROM FIVETRAN_DEMO.SERVE.CUSTOMERREVENUE
WHERE CUSTOMERID = '27613';</code></code></pre><p>27613 owns order <code>800000</code>, and we are going to delete one <code>300.00</code> line from it. Keeping the update and the delete on different customers means we can track them separately when things get weird.</p><h1><strong>Step 2: Change the source</strong></h1><p>Run <code>cdc_test.sql</code> in the Neon SQL Editor, <strong>on </strong><code>fivetran_source</code><strong>, not </strong><code>neondb</code>.</p><p>Nothing in that file is special. There is no Fivetran API call, no trigger, no CDC-aware syntax. It is three ordinary DML statements, which is exactly the point of log-based CDC: the source application does not know or care that a pipeline is watching.</p><p><strong>An update</strong>, so a change is visible at the far end. <code>CustomerName</code> in <code>SERVE.CUSTOMERREVENUE</code> is built from first and last name, so a rename travels the full length of the pipeline:</p><pre><code><code>UPDATE l1_landing.customers
SET lastname = 'Perez-CDC',
    updated_at = now()
WHERE customerid = '28222';</code></code></pre><p><strong>An insert</strong>, a new order for the same customer with two lines worth <code>115.00</code> total:</p><pre><code><code>INSERT INTO l1_landing.orders (orderid, orderdate, customerid, employeeid, storeid, status, updated_at)
VALUES (900001, CURRENT_DATE, '28222', 507279, '1000', '02', now());

INSERT INTO l1_landing.orderitems (orderid, orderitemid, productid, quantity, unitprice, updated_at)
VALUES (900001, 1, 74900, 2, 40.00, now()),
       (900001, 2, 70518, 1, 35.00, now());</code></code></pre><p>So 28222 should move from 1 order at <code>1509.00</code> to 2 orders at <code>1624.00</code>.</p><p>Two details in that insert are deliberate. <code>storeid</code> is <code>1000</code>, and <code>orders_stg</code> maps store 1000 to <code>'Online'</code> while every seed store uses a different id, so this becomes the only Online row in the entire warehouse and is trivially easy to find. <code>employeeid</code> 507279 is a real employee, so the weekly model can still join it.</p><p><strong>A delete</strong>, one line off the unrelated order:</p><pre><code><code>DELETE FROM l1_landing.orderitems
WHERE orderid = 800000
  AND orderitemid = 4;</code></code></pre><p>That line is worth <code>300.00</code>. Naively, revenue for customer 27613 should drop by 300. Hold that thought.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21ezKQ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ezKQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png 424w, https://substackcdn.com/image/fetch/$s_!ezKQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png 848w, https://substackcdn.com/image/fetch/$s_!ezKQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png 1272w, https://substackcdn.com/image/fetch/$s_!ezKQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21ezKQ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png" width="1456" height="831" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:831,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:609556,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215169789?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ezKQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png 424w, https://substackcdn.com/image/fetch/$s_!ezKQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png 848w, https://substackcdn.com/image/fetch/$s_!ezKQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png 1272w, https://substackcdn.com/image/fetch/$s_!ezKQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cdef4d8-95d6-4f4c-8124-efaee5d64668_3024x1726.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The result tabs along the bottom are the per-statement receipts: <code>UPDATE 1</code>, <code>INSERT 1</code>, <code>INSERT 2</code>, <code>DELETE 1</code>, then the three source-side checks. <strong>Five rows changed</strong>, and Postgres has written every one of them to the WAL.</p><h1><strong>Step 3: Sync, and then do nothing</strong></h1><p>Open the Postgres connection in Fivetran and click <strong>Sync now</strong>.</p><p><strong>Use Sync now, not Re-sync.</strong> Re-sync throws away the slot bookmark and reloads all 2,747 rows, which defeats the entire experiment. You can also just skip this click: the connection syncs on its own schedule, six hours by default, under <strong>Setup &#8594; Sync frequency</strong>. I click it because I am impatient, not because it is required.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vmp-%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vmp-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!vmp-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!vmp-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!vmp-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vmp-%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png" width="1456" height="829" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:829,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:456504,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215169789?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vmp-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!vmp-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!vmp-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!vmp-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92e4f9ee-eaea-4a22-9730-87b066c27623_3024x1722.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Read that sync detail before you go anywhere near Snowflake, because it is the whole argument for logical replication in one screenshot.</p><p><strong>Three tables. Six rows extracted, six loaded. About twenty seconds.</strong></p><p>Compare it to the historical sync in part 1: eight tables, 2,747 rows. Fivetran did not re-read <code>orderitems</code>. It did not even look at <code>products</code>, <code>dates</code>, <code>suppliers</code>, or <code>stores</code>, because nothing in those tables generated a WAL event. It read the change stream through <code>replication_slot_01</code>, found events for three tables, and shipped exactly those rows.</p><p>Now the breakdown, which is the first thing that does not add up:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/8x0NA/2/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a2254a9f-2c90-454d-95b6-f8a4ec7bc836_1220x430.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1d61b707-8263-4a93-9875-5e541a54543b_1220x430.png&quot;,&quot;height&quot;:207,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2F8x0NA%2F2%2F" width="730" height="207" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>I changed five rows. Six moved. <mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">One </mark><code>UPDATE</code><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> produced two rows. Park that too; it gets explained in a moment.</mark></p><p>Then <strong>do nothing</strong>. <code>Daily-after-landing</code> uses the integrated schedule from part 2, so Fivetran queues the dbt job the instant the sync finishes. No cron waiting, no manual trigger, no orchestrator. That is the payoff for putting the connection ID in <code>deployment.yml</code>.</p><p>If it never fires, that ID is wrong.</p><h1><strong>Step 4: Read the landing tables</strong></h1><p>Section 2 of <code>cdc_verify.sql</code>. This is where the Fivetran-generated columns start earning their keep.</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/UFvDx/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/be6b04f1-a2a6-49fe-a020-86a8f09b5837_1220x622.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a0846e3d-2587-4e2d-9e42-f9f82acd229c_1220x622.png&quot;,&quot;height&quot;:306,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FUFvDx%2F1%2F" width="730" height="306" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><h2><strong>The insert</strong></h2><pre><code><code>SELECT ORDERID, CUSTOMERID, EMPLOYEEID, STOREID, STATUS
FROM FIVETRAN_DEMO.POSTGRES_DEMO_L1_LANDING.ORDERS
WHERE ORDERID = 900001;</code></code></pre><p>One row, exactly as expected. Inserts are boring, which is the correct behavior for inserts.</p><h2><strong>The delete</strong></h2><pre><code><code>SELECT ORDERITEMID, QUANTITY, UNITPRICE, _FIVETRAN_DELETED
FROM FIVETRAN_DEMO.POSTGRES_DEMO_L1_LANDING.ORDERITEMS
WHERE ORDERID = 800000
ORDER BY ORDERITEMID;</code></code></pre><p>Order item 4 is <strong>still there</strong>, flagged <code>_FIVETRAN_DELETED = TRUE</code>.</p><p>This is Fivetran&#8217;s default <a href="iframe.php?url=https%3A%2F%2Ffivetran.com%2Fdocs%2Fcore-concepts%2Fsyncoverview%2Fsync-modes%2Fsoft-delete">soft delete mode</a>, and once you see it, it makes sense. Deleting rows from the warehouse destroys history: you could never answer &#8220;how many order lines did we have last Tuesday,&#8221; and a bad delete on the source would be unrecoverable downstream. Flagging preserves everything.</p><p>It also means <strong>your row count no longer matches the source</strong>, and any model that does not know about the flag is now summing a line that no longer exists.</p><h2><strong>The update, which is the interesting one</strong></h2><pre><code><code>SELECT CUSTOMERID, LASTNAME, _FIVETRAN_DELETED, _FIVETRAN_SYNCED
FROM FIVETRAN_DEMO.POSTGRES_DEMO_L1_LANDING.CUSTOMERS
WHERE CUSTOMERID = '28222'
ORDER BY _FIVETRAN_DELETED;</code></code></pre><p>Customer 28222 comes back <strong>twice</strong>, and the table now holds 101 rows for 100 customers:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/W4a0K/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7c4a75f1-35f6-44d0-af60-b64465500aa3_1220x324.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ad019afb-7aab-49d6-a69f-0a42b279f605_1220x324.png&quot;,&quot;height&quot;:153,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FW4a0K%2F1%2F" width="730" height="153" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Pe0_%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Pe0_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png 424w, https://substackcdn.com/image/fetch/$s_!Pe0_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png 848w, https://substackcdn.com/image/fetch/$s_!Pe0_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!Pe0_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Pe0_%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png" width="1456" height="830" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:830,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:301703,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215169789?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Pe0_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png 424w, https://substackcdn.com/image/fetch/$s_!Pe0_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png 848w, https://substackcdn.com/image/fetch/$s_!Pe0_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!Pe0_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2dae26-8da7-4756-a626-3566d5df0c52_3020x1722.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Both rows carry the <strong>same</strong> <code>_FIVETRAN_SYNCED</code>, because one sync wrote both of them.</p><p>Here is why, and it goes all the way back to a detail from part 1.</p><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">These tables have no primary key.</mark></strong> So Fivetran cannot key rows on <code>customerid</code>; nobody told it that column is unique. Instead it generates <code>_FIVETRAN_ID</code>, a hash of every column value in the row.</p><p>Now change <code>lastname</code>. The hash changes. From Fivetran&#8217;s point of view this is not the same row with a new value, it is a row it has never seen, and the row it used to have is gone. In soft delete mode the only correct response is: insert the new version, flag the old one.</p><p>That is why one <code>UPDATE</code> reported two rows in the sync detail.</p><p>This is also, finally, why <code>replication.sql</code> set <code>REPLICA IDENTITY FULL</code> back in part 1. Without it the WAL would not carry the complete old row, and Fivetran could not work out which version to retire. Every piece of that setup was for this moment.</p><p><strong>Give the source table a primary key and none of this happens.</strong> You get one updated row, like you expected. The chain is: no primary key &#8594; hash-based identity &#8594; updates become insert-plus-soft-delete &#8594; your dimension table has multiple versions of every row that has ever changed.</p><h1><strong>Step 5: Confirm in SERVE</strong></h1><p>Section 3 of <code>cdc_verify.sql</code>, after the dbt job has run itself.</p><pre><code><code>SELECT CUSTOMERID, CUSTOMERNAME, ORDERCOUNT, REVENUE
FROM FIVETRAN_DEMO.SERVE.CUSTOMERREVENUE
WHERE CUSTOMERID = '28222';</code></code></pre><p><strong>Both changes travelled.</strong> The baseline was 1 order at <code>1509.00</code>, and it is now 2 orders at <code>1624.00</code>, so the insert made it through the sync, through the staging models, through <code>orders_fact</code>, and into the mart. The rename is there too. Five rows changed in Postgres, and with one click on Sync everything downstream updated itself.</p><p>That is the thing working.</p><p>And the customer is in the report twice.</p><p>The new order is easy to find, since it is the only Online row in the warehouse:</p><pre><code><code>SELECT ORDERID, CUSTOMERID, ORDER_CHANNEL
FROM FIVETRAN_DEMO.TRANSFORM.ORDERS_STG
WHERE ORDER_CHANNEL = 'Online';</code></code></pre><p>Then check 27613, the customer whose <code>300.00</code> order line we deleted:</p><pre><code><code>SELECT CUSTOMERID, REVENUE
FROM FIVETRAN_DEMO.SERVE.CUSTOMERREVENUE
WHERE CUSTOMERID = '27613';</code></code></pre><p><strong>Unchanged.</strong> The deleted line is still in the revenue total.</p><h1><strong>Why both numbers are wrong</strong></h1><p>Two surprises, one cause: <strong>no model in this project reads </strong><code>_FIVETRAN_DELETED</code><strong>.</strong></p><h2><strong>The double-counted customer</strong></h2><p><code>customers_stg</code> is this:</p><pre><code><code>SELECT
    CustomerID,
    ...
    CONCAT(FirstName, ' ', LastName) AS CustomerName
FROM
    {{ source('landing', 'customers') }}
</code></code></pre><p>No filter. So it carries <strong>both</strong> versions of customer 28222 through to the mart, the current <code>Perez-CDC</code> row and the retired <code>Perez</code> row.</p><p><code>customerrevenue</code> then joins that to <code>orders_fact</code> on <code>CustomerID</code>:</p><pre><code><code>FROM {{ ref('orders_fact') }} OS
JOIN {{ ref('customers_stg') }} C ON OS.CustomerID = C.CustomerID
GROUP BY OS.CustomerID, C.CustomerName</code></code></pre><p>Two customer rows share <code>CustomerID = '28222'</code>, so the join fans out: every order matches both versions. Grouping by <code>CustomerName</code> then splits the result into two rows, each showing the full <code>1624.00</code>.</p><p><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Total revenue in this table is now overstated by </mark><code>1624.00</code><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">, and a dashboard built on it shows one person twice under two slightly different names. Nothing errored. Nothing looked wrong in Fivetran. The job said Succeeded.</mark></p><h2><strong>The delete that did not reduce revenue</strong></h2><p><code>orderitems_stg</code> has the same shape and the same gap:</p><pre><code><code>SELECT
    ...
    Quantity * UnitPrice AS TotalPrice
FROM
    {{ source('landing', 'orderitems') }}
</code></code></pre><p>Order item 4 is still in the landing table with <code>_FIVETRAN_DELETED = TRUE</code>. The model does not look at that column, so it sums <code>300.00</code> that no longer exists in the source.</p><h2><strong>The fix</strong></h2><p>One line in each staging model:</p><pre><code><code>WHERE COALESCE(_FIVETRAN_DELETED, FALSE) = FALSE</code></code></pre><p><code>COALESCE</code> because the column is <code>NULL</code> on rows that were never touched by a delete, and <code>NULL = FALSE</code> is not <code>TRUE</code> in SQL. Get that wrong and you filter out your entire table, which is a fun ten minutes.</p><p><code>cdc_verify.sql</code> shows what the models would have seen:</p><pre><code><code>SELECT COUNT(*) AS current_customer_rows
FROM FIVETRAN_DEMO.POSTGRES_DEMO_L1_LANDING.CUSTOMERS
WHERE COALESCE(_FIVETRAN_DELETED, FALSE) = FALSE;</code></code></pre><ol start="100"><li><p>Exactly right.</p></li></ol><p>The alternative is switching the Fivetran connection to hard deletes, which drops rows instead of flagging them. That makes the landing tables match the source and the models correct as written, at the cost of throwing away the history that made soft deletes attractive in the first place.</p><h2><strong>Why this project leaves the bug in</strong></h2><p>The models come from <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fsleekdata%2Foms_fivetran_transforms">upstream</a> unchanged, and I decided to keep them that way, because this is a more useful thing to see than a working dashboard.</p><p>The lesson generalizes past Fivetran. <strong>A landing zone is not a copy of your source table. It is an append-mostly log of every version of every row your source has ever had.</strong> On that kind of table, &#8220;the row is still there&#8221; and &#8220;the row is still current&#8221; are different questions, and only <code>_FIVETRAN_DELETED</code> answers the second one.</p><p>Every staging model reading a Fivetran landing zone needs to answer the second question. Every one of these does not. Neither model is broken in a way that fails a test or throws an error; they are broken in the way that shows up as a number somebody trusts.</p><h1><strong>Step 6: Clean up</strong></h1><p>Run <code>cdc_revert.sql</code> in Neon and sync again. It undoes all three changes.</p><p>Notice that it restores <strong>exact</strong> seed values, <code>updated_at</code> included:</p><pre><code><code>UPDATE l1_landing.customers
SET lastname = 'Perez',
    updated_at = '2025-06-14 18:29:55'
WHERE customerid = '28222';
</code></code></pre><p>Not <code>now()</code>. That timestamp is deliberate, and it follows directly from everything above. Fivetran keys these rows on a hash of all their values, so restoring the row to <em>byte-identical</em> seed values makes it hash back to the <code>_FIVETRAN_ID</code> of the row that was soft-deleted earlier, and Fivetran can revive that row instead of appending a third version. Use <code>now()</code> and you get customer 28222 three times.</p><p>That is a nice demonstration of hash-based identity, and also a warning: on a table with no primary key, an accidental round-trip edit is indistinguishable from no edit at all.</p><p>Even so, <code>SERVE</code> does not return to the exact baseline. The landing tables keep every version they retired during the test, so <code>customerrevenue</code> keeps double-counting. For a genuinely clean copy, re-sync just the <code>customers</code> and <code>orderitems</code> tables from the connection&#8217;s schema tab, which drops and reloads those two, then rerun the dbt job.</p><h2><strong>If nothing changed at all</strong></h2><p>SymptomUsual causeNothing moved in SnowflakeYou ran the edits against <code>neondb</code> instead of <code>fivetran_source</code>Landing queries return nothingYour landing schema is not called <code>POSTGRES_DEMO_L1_LANDING</code>The dbt job never firedWrong connection ID in <code>deployment.yml</code>; Connections column says <code>None</code>Fivetran reports a slot errorRe-check <code>replication_slot_01</code> with the queries at the bottom of <code>replication.sql</code></p><h1><strong>What I took away from the whole series</strong></h1><p>Three parts, no Python, no Docker, no Terraform, no orchestrator. A Postgres source streaming its write-ahead log, a warehouse with a least-privilege service identity, a dbt project run from Git by the same vendor doing the ingestion, and a change in the source reaching a reporting table with one click and then rebuilding itself.</p><p>Against the version I built with Airflow, S3, and Terraform, I wrote something like two percent as much code. But the work did not disappear, it moved, and that is the actual lesson:</p><p><strong>What I stopped having to know:</strong> how to schedule a DAG, how to write an IAM trust policy, how <code>sts:AssumeRole</code> works, how to make a scraper idempotent, how to partition a landing directory by date.</p><p><strong>What I had to learn instead:</strong> what a replication slot is and why an abandoned one eats your disk, why replica identity exists, how Snowflake&#8217;s role hierarchy separates <code>SECURITYADMIN</code> from <code>SYSADMIN</code>, why service users use key pairs, and, most of all, what a CDC landing zone actually contains.</p><p>The second list is more transferable. IAM trust policies are an AWS fact. &#8220;Your warehouse table holds every version of every row and you have to filter for the current one&#8221; is true of Fivetran, and Airbyte, and Debezium, and anything else that reads a change log.</p><p>The managed stack did not make the pipeline simpler. It made the plumbing disappear and left me alone with the data modelling, which turned out to be where the only real bug was.</p><p>If you enjoyed this series, please react or share so I know to write more of them. Thanks for reading all three parts.</p><p>The repo, with every script and screenshot: <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FMarkPhamm%2Ffivetran_snowflake_dbt_demo">MarkPhamm/fivetran_snowflake_dbt_demo</a>.</p><ul><li><p><a href="iframe.php?url=https%3A%2F%2Fsubstack.com%2Fhome%2Fpost%2Fp-214887139">Part 1 - Ingestion: Neon, Snowflake, Fivetran</a></p></li><li><p><a href="iframe.php?url=https%3A%2F%2Fsubstack.com%2Fhome%2Fpost%2Fp-215043072">Part 2 - dbt, run by Fivetran</a></p></li><li><p>Part 3 - CDC (you are here)</p></li></ul>]]></content:encoded></item><item><title><![CDATA[I got a new Mac and refused to set it up by hand again]]></title><description><![CDATA[One repo, four scripts, and about twenty minutes from a factory-fresh laptop to a working analytics engineering machine.]]></description><link>https://markphammm.substack.com/p/i-got-a-new-mac-and-refused-to-set</link><guid isPermaLink="false">https://markphammm.substack.com/p/i-got-a-new-mac-and-refused-to-set</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Mon, 14 Sep 2026 15:23:02 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21mdzG%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Intro</strong></h2><p>Hi everyone, my name is Mark, an Analytics Engineer at Infinite Lambda, graduated from TCU in the summer of 2025, majoring in Computer System Analyst with minors in Math and Fintech.</p><p>I got a new M5 pro this week, and the moment I opened the lid I felt the specific dread of doing this again<strong>. Install the Command Line Tools.</strong> Wait. <strong>Install Homebrew</strong>. Wait. <strong>Try to remember whether I use eza or exa now</strong>. Find the Oh My Zsh install line. Google the zsh-autosuggestions clone URL for what must be the ninth time in my life. Open my old laptop, scroll through its <code>.zshrc</code>, and copy chunks across while hoping I do not also copy the three aliases that only worked because of a Docker container ID that no longer exists.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21mdzG%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mdzG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png 424w, https://substackcdn.com/image/fetch/$s_!mdzG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png 848w, https://substackcdn.com/image/fetch/$s_!mdzG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png 1272w, https://substackcdn.com/image/fetch/$s_!mdzG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21mdzG%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png" width="1456" height="890" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:890,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:520968,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215671396?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mdzG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png 424w, https://substackcdn.com/image/fetch/$s_!mdzG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png 848w, https://substackcdn.com/image/fetch/$s_!mdzG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png 1272w, https://substackcdn.com/image/fetch/$s_!mdzG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F853be277-4d99-46e2-9df1-ce920005e249_3024x1848.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I have done that four times: personal laptop, school laptop, first work laptop, and now this one. Every time it takes the better part of a day, and every time the result is slightly different from the last one. That is the part that actually bothers me. Not the hours, the drift. I spend my working life telling people that if a transformation is not in version control it does not really exist, and then I configure the machine that runs those transformations by hand, from memory, with no record of what I did.</p><p>I also wrote about the productivity tools I used as an AE <a href="iframe.php?url=https%3A%2F%2Fmarkphammm.substack.com%2Fp%2Fthe-productivity-stack-i-actually">here</a>.</p><p>So this time I wrote it down as code. The repo is <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FMarkPhamm%2Fmac_setup">mac_setup</a>. It is not clever. There is no Ansible, no Nix, no 800-line install framework. It is a <code>Brewfile</code>, two zsh files, two JSON files for Cursor, and four bash scripts, and it takes a fresh Mac to a working machine in about twenty minutes, most of which is Homebrew downloading things while I make coffee.</p><h2><strong>Takeaway</strong></h2><ul><li><p>Why &#8220;my dotfiles are in a gist&#8221; is not the same thing as a reproducible machine</p></li><li><p>What actually belongs in a setup repo and what does not (spoiler: your employer&#8217;s file paths do not)</p></li><li><p>The four-script structure: bootstrap, link, configure, verify, and why verify matters more than it sounds</p></li><li><p>How a <code>Brewfile</code> replaces the entire &#8220;install these 25 things&#8221; section of an onboarding doc</p></li><li><p>The one trick I added this round: every git repo gets its own terminal background colour</p></li></ul><h2><strong>The problem with how I used to do it</strong></h2><p>My old approach was a Google Doc. It had a list of apps, a few terminal commands, and a copy of my <code>.zshrc</code> pasted in at the bottom. It was genuinely better than nothing.</p><p><strong>It also rots.</strong> A doc cannot tell you that Terraform left Homebrew core after the license change and now lives in <code>hashicorp/tap</code>. It cannot tell you that the <code>docker_start_mwaa</code> alias you have been carrying since 2024 refers to a container that stopped existing when you changed jobs. It cannot tell you that you wrote down Powerlevel10k but have actually been running the Jonathan theme this whole time, which I only discovered when I diffed the doc against the real file. <strong>A doc records intentions. A repo records the machine.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21qsUK%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qsUK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png 424w, https://substackcdn.com/image/fetch/$s_!qsUK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png 848w, https://substackcdn.com/image/fetch/$s_!qsUK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png 1272w, https://substackcdn.com/image/fetch/$s_!qsUK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21qsUK%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png" width="1456" height="836" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:836,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:462567,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215671396?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qsUK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png 424w, https://substackcdn.com/image/fetch/$s_!qsUK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png 848w, https://substackcdn.com/image/fetch/$s_!qsUK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png 1272w, https://substackcdn.com/image/fetch/$s_!qsUK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534a6508-464e-4890-82fd-6bac7684f158_2542x1460.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The other problem is that a doc cannot be run. Every line in it is a thing you have to read, understand, and retype at the exact moment you are least patient, which is hour three of setting up a laptop you just want to start working on.</p><h2><strong>What it installs</strong></h2><p>Everything lives in one <code>Brewfile</code>, which is Homebrew&#8217;s manifest format. One file, one command, whole machine.</p><p>The core: Git, GitHub CLI, uv for Python, Node, AWS CLI, and Terraform from the HashiCorp tap.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21wUEG%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wUEG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png 424w, https://substackcdn.com/image/fetch/$s_!wUEG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png 848w, https://substackcdn.com/image/fetch/$s_!wUEG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png 1272w, https://substackcdn.com/image/fetch/$s_!wUEG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21wUEG%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png" width="1456" height="1426" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1426,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:256754,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215671396?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!wUEG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png 424w, https://substackcdn.com/image/fetch/$s_!wUEG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png 848w, https://substackcdn.com/image/fetch/$s_!wUEG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png 1272w, https://substackcdn.com/image/fetch/$s_!wUEG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc43344b-f565-4238-a5de-76d7576b1553_1534x1502.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The terminal layer, which is the part I would miss most if it disappeared: Warp as the terminal, <strong>Oh My Zsh</strong> on top of <strong>zsh, zoxide, eza, fzf, ripgrep, bat, jq, tree, and lazygit.</strong></p><p>The apps: <strong>Cursor</strong>, <strong>OrbStack</strong> instead o<strong>f Docker Desktop</strong>, <strong>Raycast</strong> instead of <strong>Alfred</strong>, <strong>Rectangle</strong>, <strong>1Password</strong>, <strong>DBeaver</strong> instead of <strong>SQLWorkbench</strong>, and <strong>Obsidian</strong>.</p><p>And the Python and SQL tooling that I want available everywhere rather than per-project: Black and SQLFluff, installed with <code>uv tool install</code> so they live in their own environments and never fight with a project&#8217;s dependencies.</p><p>A few of those are deliberate replacements for what my older setup notes recommended. DBeaver instead of SQLWorkbench, because SQLWorkbench wants you to manage a Java install and DBeaver ships its own runtime. OrbStack instead of Docker Desktop, because it starts in about two seconds and does not eat the battery. Raycast instead of Alfred. Cursor instead of whatever editor the doc said in 2023. I wrote those decisions down in <code>docs/MIGRATION_NOTES.md</code> rather than silently swapping them, because the next person to read the repo is me in two years, and I will want to know why.</p><h2><strong>How it runs</strong></h2><p>Four scripts, in order. There is a <code>Makefile</code> too, so <code>make all</code> runs the whole chain, but I usually go one at a time on a new machine so I can watch what happens.</p><p><strong>Step 0</strong> is the one thing that cannot be automated. Apple&#8217;s Command Line Tools ship a GUI installer and there is no honest way around it:</p><pre><code><code>xcode-select --install</code></code></pre><p>Then clone the repo:</p><pre><code><code>git clone https://github.com/MarkPhamm/mac_setup ~/mac_setup
cd ~/mac_setup</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21x-yF%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!x-yF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png 424w, https://substackcdn.com/image/fetch/$s_!x-yF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png 848w, https://substackcdn.com/image/fetch/$s_!x-yF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png 1272w, https://substackcdn.com/image/fetch/$s_!x-yF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21x-yF%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png" width="1456" height="591" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c8af9392-5433-415c-8841-cc733c9187cc_1522x618.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:591,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:89102,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215671396?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!x-yF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png 424w, https://substackcdn.com/image/fetch/$s_!x-yF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png 848w, https://substackcdn.com/image/fetch/$s_!x-yF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png 1272w, https://substackcdn.com/image/fetch/$s_!x-yF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8af9392-5433-415c-8841-cc733c9187cc_1522x618.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>Step 1: </strong><code>./scripts/bootstrap.sh</code><strong>.</strong> This is the long one, and the only one that touches the network much. It checks that the Command Line Tools are actually there and stops with a useful message if they are not, installs Homebrew if it is missing, runs <code>brew bundle</code> against the <code>Brewfile</code>, installs Oh My Zsh, clones the two zsh plugins that are not Homebrew packages, installs Black and SQLFluff via <code>uv tool</code>, and installs the Codex CLI from npm. Every step is guarded by an existence check, so running it twice is safe and fast. That matters more than it sounds, because the first run of a setup script almost never is the only run.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." &amp;&amp; pwd)"

echo "==&gt; mac_setup bootstrap"

if ! xcode-select -p &gt;/dev/null 2&gt;&amp;1; then
  echo
  echo "Apple Command Line Tools are not installed yet."
  echo "Run:"
  echo "  xcode-select --install"
  echo
  echo "Finish the macOS installer, then run this script again."
  exit 1
fi

echo "==&gt; Apple Command Line Tools found: $(xcode-select -p)"

if ! command -v brew &gt;/dev/null 2&gt;&amp;1; then
  echo "==&gt; Installing Homebrew"
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi

# Homebrew's standard Apple Silicon location.
if [[ -x /opt/homebrew/bin/brew ]]; then
  eval "$(/opt/homebrew/bin/brew shellenv)"
fi

echo "==&gt; Homebrew: $(brew --version | head -n 1)"

echo "==&gt; Installing Brewfile packages"
brew bundle --file="$REPO_ROOT/Brewfile"

if [[ ! -d "$HOME/.oh-my-zsh" ]]; then
  echo "==&gt; Installing Oh My Zsh"
  RUNZSH=no CHSH=no KEEP_ZSHRC=yes \
    sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
else
  echo "==&gt; Oh My Zsh already installed"
fi

ZSH_CUSTOM_DIR="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"

if [[ ! -d "$ZSH_CUSTOM_DIR/plugins/zsh-autosuggestions" ]]; then
  echo "==&gt; Installing zsh-autosuggestions"
  git clone https://github.com/zsh-users/zsh-autosuggestions \
    "$ZSH_CUSTOM_DIR/plugins/zsh-autosuggestions"
fi

if [[ ! -d "$ZSH_CUSTOM_DIR/plugins/zsh-syntax-highlighting" ]]; then
  echo "==&gt; Installing zsh-syntax-highlighting"
  git clone https://github.com/zsh-users/zsh-syntax-highlighting.git \
    "$ZSH_CUSTOM_DIR/plugins/zsh-syntax-highlighting"
fi

echo "==&gt; Installing global formatter/linter tools with uv"
uv tool install --upgrade black || uv tool upgrade black
uv tool install --upgrade sqlfluff || uv tool upgrade sqlfluff

if ! command -v codex &gt;/dev/null 2&gt;&amp;1; then
  echo "==&gt; Installing Codex CLI"
  npm install -g @openai/codex
else
  echo "==&gt; Codex CLI already installed"
fi

# Ensure uv tool executables are discoverable.
uv tool update-shell || true

echo
echo "Bootstrap complete."
echo "Next:"
echo "  ./scripts/link-dotfiles.sh"
echo "  ./scripts/configure-git.sh"
echo "  exec zsh"
echo "  gh auth login"
</code></pre></div><p><strong>Step 2: </strong><code>./scripts/link-dotfiles.sh</code><strong>.</strong> This symlinks <code>~/.zshrc</code> and <code>~/.zsh_aliases</code> to the copies in the repo. Symlinks, not copies, and that is the whole point: when I improve an alias six months from now, I edit it in the repo, and the change is version controlled by construction. There is nothing to remember to sync back. Anything already sitting at those paths gets moved to a timestamped <code>.backup</code> first rather than clobbered.</p><p>It also touches an empty <code>~/.zshrc.local</code>, which is where every secret, API key, employer-specific path, and machine-specific export goes. That file is never committed. The rule I follow is simple: if it would be wrong for a stranger to read it, or if it is only true on this one laptop, it goes in <code>.zshrc.local</code>. Everything else goes in the repo. My old <code>.zshrc</code> failed that test badly, which is exactly how a company file path ends up in a public gist.</p><p><strong>Step 3: </strong><code>./scripts/configure-git.sh</code><strong>.</strong> Small script. Sets up <code>git lg</code>, the pretty graph log, and prints my current git identity so I remember to set it. Then <code>gh auth login</code> and GitHub is wired up.</p><p><strong>Step 4: </strong><code>./scripts/verify.sh</code><strong>.</strong> This is the one I would tell you to steal if you steal nothing else. It loops through every command the setup is supposed to provide, prints OK or MISSING for each, checks whether <code>~/.zshrc</code> is actually a symlink and where it points, and checks that each GUI app exists in <code>/Applications</code>.</p><p>It sounds trivial, and it is trivial. It is also the difference between &#8220;I think the setup worked&#8221; and knowing. The failure mode of a setup script is not a loud crash, it is one package quietly not installing, which you discover three weeks later in the middle of something else. Twenty lines of bash turns that into a table you read once.</p><h2><strong>The one new thing I added</strong></h2><p>Every repo now tints my terminal background its own colour.</p><p>I work across several repos in a normal day, usually with several Warp tabs open, and I have absolutely run a command in the wrong one. The fix is fifteen lines in <code>.zshrc</code>: hash the git repo root, turn the first three bytes of that hash into an RGB value clamped to a dark range so text stays readable, and set the background with an OSC 11 escape sequence. Outside a repo it resets.</p><pre><code><code>_project_bg() {
  local git_root
  git_root=$(git rev-parse --show-toplevel 2&gt;/dev/null) || {
    printf '\e]111\e\\'
    return
  }
  local hash=$(printf '%s' "$git_root" | md5 -q)
  local r=$(( 16#${hash:0:2} % 71 + 30 ))
  local g=$(( 16#${hash:2:2} % 71 + 30 ))
  local b=$(( 16#${hash:4:2} % 71 + 30 ))
  printf '\e]11;#%02x%02x%02x\e\\' $r $g $b
}

autoload -Uz add-zsh-hook
add-zsh-hook chpwd _project_bg
_project_bg
</code></code></pre><p>Because the colour comes from a hash of the path, it is deterministic. The dbt repo is the same slightly-green black on every machine I clone it to, forever, and I recognise a tab before I read the prompt. It hooks into <code>chpwd</code>, so it fires on every directory change. Two notes if you copy it: your terminal has to support OSC 11, which Warp, iTerm2, WezTerm, and Kitty do and Apple&#8217;s Terminal does not, and <code>md5</code> is macOS, so on Linux you want <code>md5sum</code>.</p><p>This is the kind of thing I would never have bothered to set up when configuration was a manual chore, because it would have evaporated at the next laptop. When the config is a repo, a fifteen-line quality-of-life hack is permanent, and the calculation of what is worth doing changes completely.</p><h2><strong>What I deliberately left out</strong></h2><p>The base setup is generic on purpose. No dbt adapters, no Airflow, no Spark, no Kubernetes CLI. Those are all things I use, and none of them are in the <code>Brewfile</code>.</p><p>The reason is that the warehouse adapter is a project decision, not a machine decision. The day I put <code>dbt-snowflake</code> in the base setup is the day the repo stops being about my Mac and starts being about one job. I keep a <code>docs/NEXT_LAYER.md</code> listing what the data-platform layer would contain when a project needs it. A list of candidates is cheap. An install you have to un-choose later is not.</p><p>Same logic for anything employer-shaped. The dbt aliases in the repo default to a state directory of <code>prod_dbt_artifacts</code> but read <code>$DBT_STATE_DIR</code>, so a project overrides it in <code>.zshrc.local</code>. The alias that downloads a production manifest reads <code>$DBT_MANIFEST_URL</code> instead of a hardcoded company link. The alias is portable; the URL is yours.</p><h2><strong>Was it worth it</strong></h2><p>The repo took an evening to write with Claude. The setup it replaces took a day, and I did it four times.</p><p>But the hours are not really the argument, because I would have spent that evening fiddling with dotfiles regardless, and everyone who writes a setup repo knows it does not pay for itself as fast as they claim. The argument is that my machine is now a thing I can read, diff, and roll back. When I change a tool, there is a commit explaining it. When something breaks, <code>verify.sh</code> tells me what. When I get the next laptop, or a contractor asks how I have my environment set up, the answer is a URL instead of an afternoon.</p><p>Which is, word for word, the argument I make about data pipelines all day. It felt overdue to apply it to the laptop the pipelines are written on.</p><p>The repo is <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FMarkPhamm%2Fmac_setup">here</a>. Fork it, rip out my apps, put yours in. The <code>Brewfile</code> is the only file most people need to change.</p><p></p>]]></content:encoded></item><item><title><![CDATA[I rebuilt my ingestion pipeline with zero infrastructure: Part 2 - dbt, run by Fivetran]]></title><description><![CDATA[Your ingestion vendor can also be your dbt orchestrator. No dbt Cloud, no Airflow, no server.]]></description><link>https://markphammm.substack.com/p/i-rebuilt-my-ingestion-pipeline-with-02e</link><guid isPermaLink="false">https://markphammm.substack.com/p/i-rebuilt-my-ingestion-pipeline-with-02e</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Sun, 13 Sep 2026 08:49:10 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%219MJy%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1><strong>Intro</strong></h1><p>Hi again everyone, my name is <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fminhbphamm%2F">Mark</a>, an Analytics Engineer at Infinite Lambda, graduated from TCU in the summer of 2025, majoring in Computer System Analyst with minors in Math and Fintech. In this series, I&#8217;m rebuilding my ingestion pipeline with zero infrastructure.</p><p>If you missed <a href="iframe.php?url=https%3A%2F%2Fsubstack.com%2F%40markphammm%2Fp-214887139">part 1</a> - ingestion, here&#8217;s the short version: </p><p>I have an OMS sample database on <strong>Neon Postgres</strong>, a <strong>Snowflake trial</strong>, and a <strong>Fivetran connection</strong> streaming eight tables between them using logical replication. Snowflake currently holds an exact, uncleaned copy of the source in a schema called <code>POSTGRES_DEMO_L1_LANDING</code>.</p><p><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">That&#8217;s the </mark><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">EL</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> in ELT. Now we do the </mark><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">T</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> in ELT.</mark></p><p>The part I did not expect when I started this project: <strong>I never install dbt to run this pipeline.</strong> Fivetran Transformations for dbt Core clones my GitHub repo over SSH, generates its own <code>profiles.yml</code> from the Snowflake destination credentials I already entered, and runs the jobs I define in a YAML file at the repo root. There is no dbt Cloud account. There is no Airflow. There is no VM.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%219MJy%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9MJy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png 424w, https://substackcdn.com/image/fetch/$s_!9MJy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png 848w, https://substackcdn.com/image/fetch/$s_!9MJy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png 1272w, https://substackcdn.com/image/fetch/$s_!9MJy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%219MJy%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png" width="1456" height="728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:728,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1245440,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215043072?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9MJy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png 424w, https://substackcdn.com/image/fetch/$s_!9MJy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png 848w, https://substackcdn.com/image/fetch/$s_!9MJy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png 1272w, https://substackcdn.com/image/fetch/$s_!9MJy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9738438-a043-4abb-81b2-e6515fe62397_2172x1086.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>My last project needed Astronomer and Docker to get a scheduler. </p><p>This one needs a deploy key and a 27-line YAML file.</p><p>This part covers the Snowflake schemas dbt writes into, the GitHub deploy key setup, connecting the project in Fivetran, and the <code>deployment.yml</code> that defines the jobs. At the end there is an optional section on running dbt locally, which you only need if you want to test model changes before pushing them.</p><h1><strong>Takeaway</strong></h1><ul><li><p>How <strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Fivetran Transformations for dbt Core</mark></strong> works, and how it differs from<mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">dbt Cloud</mark></strong></p></li><li><p>Why the identity that <em>runs</em> your models is the destination user, and what breaks when you forget that</p></li><li><p><strong>Deploy keys</strong>: giving a service read-only access to one repository instead of handing it your GitHub account</p></li><li><p>Defining jobs in <code>deployment.yml</code>, including a schedule that fires when an ingestion sync finishes rather than on a clock</p></li><li><p>dbt <strong>tags and selectors</strong>, and why <code>+tag:daily</code> builds five models instead of one</p></li><li><p>The two failure modes you will hit, and how to tell a permission problem from a naming problem</p></li></ul><h1><strong>Prerequisite</strong></h1><p>Finish <a href="iframe.php?url=https%3A%2F%2Ffile%2B.vscode-resource.vscode-cdn.net%2FUsers%2Fminhpham%2Fprojects%2Ffivetran_snowflake_dbt_demo%2Farticles%2Fpart-1-ingestion.md">part 1</a>. Every model here reads the landing schema, so if the initial sync has not run, all of them fail on a missing table.<mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> You should also have some previous exposure to dbt, if not, checkout my dbt fundamental repo that run dbt on a local duckdb instance </mark><a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FMarkPhamm%2Fdbt-Fundamental"><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">here</mark></a><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> .</mark></p><p>You will also need three things carried over from part 1:</p><ol><li><p>The <strong>landing schema name</strong> exactly as <code>SHOW SCHEMAS IN DATABASE FIVETRAN_DEMO</code> reported it. Mine is <code>POSTGRES_DEMO_L1_LANDING</code>.</p></li><li><p>The Postgres <strong>connection ID</strong>, the two-word system name Fivetran gave the connection. Mine is <code>verdure_rephrase</code>. Not the display name.</p></li><li><p>Admin rights on a GitHub repository, because you have to add a deploy key. Fork <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FMarkPhamm%2Ffivetran_snowflake_dbt_demo">the repo</a> or push your own copy.</p></li></ol><h1><strong>Overview</strong></h1><h2><strong>What dbt actually is here</strong></h2><p><strong>dbt turns SQL files into tables.</strong> As I&#8217;ve mentioned before, I do expect some knowledge about dbt before this article so I won&#8217;t dig too deep into the dbt part. You write <code>SELECT</code> statements, reference other models with <code>ref()</code> and raw sources with <code>source()</code>, and dbt figures out the dependency order, runs everything against your warehouse in that order, and can test the results.</p><p>That is the whole idea. It does not extract, it does not load, it does not move data between systems. It sends SQL to Snowflake and Snowflake does the work.</p><h2><strong>The shape of the transformation layer</strong></h2><pre><code><code>FIVETRAN_DEMO.POSTGRES_DEMO_L1_LANDING   Fivetran-synced OMS tables (part 1)
        |
        v
FIVETRAN_DEMO.TRANSFORM      staging models + orders_fact
        |
        v
FIVETRAN_DEMO.SERVE          customerrevenue, emp_weekly_sales
</code></code></pre><p>Three layers, seven models. The models come from <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fsleekdata%2Foms_fivetran_transforms">sleekdata/oms_fivetran_transforms</a> and are deliberately simple, because the interesting part of this article is the orchestration, not the SQL.</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/KpSBA/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a83dcbe5-ba77-4df5-9049-5a2624f9d57a_1220x918.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e07f07c5-06aa-4393-a6aa-7d0bbf8f8263_1220x918.png&quot;,&quot;height&quot;:456,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FKpSBA%2F1%2F" width="730" height="456" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>The staging layer is thin on purpose: rename, cast, add one derived column, nothing else. <code>customers_stg</code> is the entire pattern:</p><pre><code><code>SELECT
    CustomerID,
    FirstName,
    LastName,
    ...
    CONCAT(FirstName, ' ', LastName) AS CustomerName
FROM
    {{ source('landing', 'customers') }}
</code></code></pre><p>That <code>source()</code> call is what points at the Fivetran replica. Look closely at it, because it is where part 3 goes wrong.</p><h2><strong>What Fivetran Transformations is</strong></h2><p>This is the piece I want to be precise about, because the naming is confusing. There&#8217;re 3 main things we need to differentiate:</p><ol><li><p><strong>dbt Core</strong> is the free open-source CLI. </p></li><li><p><strong>dbt Cloud</strong> is dbt Labs&#8217; paid hosted product with a scheduler, an IDE, and a UI. <strong>Fivetran </strong></p></li><li><p><strong>Transformations for dbt Core</strong> is a third thing: Fivetran runs dbt Core for you, on their infrastructure, from your Git repo.</p></li></ol><p><strong>Concretely, on every project sync Fivetran:</strong></p><ol><li><p>Clones your repository over SSH using a deploy key you install</p></li><li><p>Generates a <code>profiles.yml</code> from the <strong>Snowflake destination credentials you already entered in part 1</strong></p></li><li><p>Reads <code>deployment.yml</code> from your repo root and creates or updates the jobs it finds</p></li><li><p>Runs those jobs on their schedules, capturing the dbt output into a run log</p></li></ol><p>Point 2 has a consequence that causes the most common failure in this article, so I will state it twice, once here and once when it matters: </p><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Your scheduled models are built by </mark></strong><code>FIVETRAN_USER</code><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> and </mark></strong><code>FIVETRAN_ROLE</code><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">, not by a dbt user.</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>Fivetran reuses the destination identity. Your local <code>profiles.yml</code> is never read by Fivetran and is gitignored.</p><p>Point 3 means your schedules are version controlled next to your models, which I like a lot more than clicking around a scheduler UI.</p><h1><strong>Step 1: Create the dbt schemas in Snowflake</strong></h1><p>First confirm part 1 actually finished:</p><pre><code><code>SELECT COUNT(*) FROM FIVETRAN_DEMO.POSTGRES_DEMO_L1_LANDING.CUSTOMERS;</code></code></pre><p>100 rows, or stop and go fix part 1.</p><p>Now run <code>snowflake/sql/dbt_setup.sql</code> in Snowsight, selecting the whole script. It creates:</p><ul><li><p>Schemas <code>TRANSFORM</code> and <code>SERVE</code> inside <code>FIVETRAN_DEMO</code></p></li><li><p><code>DBT_ROLE</code> and <code>DBT_USER</code>, for optional local development</p></li><li><p>Read grants across the whole database, including <strong>future</strong> schemas and tables</p></li><li><p>Write grants on <code>TRANSFORM</code> and <code>SERVE</code> for <strong>both</strong> <code>DBT_ROLE</code> and <code>FIVETRAN_ROLE</code></p></li></ul><p>Three things in that script deserve attention.</p><h2><strong>The future grants</strong></h2><pre><code><code>GRANT USAGE ON FUTURE SCHEMAS IN DATABASE FIVETRAN_DEMO TO ROLE DBT_ROLE;
GRANT SELECT ON FUTURE TABLES IN DATABASE FIVETRAN_DEMO TO ROLE DBT_ROLE;</code></code></pre><p>Fivetran creates schemas and tables on its own schedule, after this script runs. Without future grants you would have to re-grant every time a connector added a table. This is a small thing that saves real annoyance.</p><h2><strong>The </strong><code>FIVETRAN_ROLE</code><strong> grants on </strong><code>TRANSFORM</code><strong> and </strong><code>SERVE</code></h2><pre><code><code>GRANT USAGE ON SCHEMA TRANSFORM TO ROLE FIVETRAN_ROLE;
GRANT CREATE TABLE, CREATE VIEW ON SCHEMA TRANSFORM TO ROLE FIVETRAN_ROLE;</code></code></pre><p>Here is the second telling. <code>FIVETRAN_ROLE</code> was created in part 1 to <em>land raw data</em>. It has no reason to write into your transformation schemas, and it would not occur to you to grant it, right up until your first dbt job dies on:</p><pre><code><code>Object does not exist or not authorized</code></code></pre><p>Because Fivetran generates its profile from the destination credentials, the scheduled dbt run authenticates as <code>FIVETRAN_USER</code>. If that role cannot create tables in <code>TRANSFORM</code>, nothing builds. This is the number one reason a first Fivetran dbt job fails, and the error message points at an object rather than at the actual cause.</p><h2><strong>Ownership</strong></h2><p>Snowflake only lets the <strong>owning</strong> role replace a table, and whichever identity builds a model first owns it.</p><p><code>dbt_setup.sql</code> grants <code>DBT_ROLE</code> to <code>FIVETRAN_ROLE</code>, so if you build models locally first, Fivetran inherits and can take them over. The reverse is not automatic. If Fivetran built them first and you now want to run dbt locally, hand the objects over once:</p><pre><code><code>USE ROLE ACCOUNTADMIN;
GRANT OWNERSHIP ON ALL TABLES IN SCHEMA FIVETRAN_DEMO.TRANSFORM TO ROLE DBT_ROLE COPY CURRENT GRANTS;
GRANT OWNERSHIP ON ALL VIEWS  IN SCHEMA FIVETRAN_DEMO.TRANSFORM TO ROLE DBT_ROLE COPY CURRENT GRANTS;
GRANT OWNERSHIP ON ALL TABLES IN SCHEMA FIVETRAN_DEMO.SERVE     TO ROLE DBT_ROLE COPY CURRENT GRANTS;
GRANT OWNERSHIP ON ALL VIEWS  IN SCHEMA FIVETRAN_DEMO.SERVE     TO ROLE DBT_ROLE COPY CURRENT GRANTS;</code></code></pre><p>Or drop the two schemas and re-run <code>dbt_setup.sql</code>. Nothing in <code>TRANSFORM</code> or <code>SERVE</code> is a source of truth; they rebuild from the landing schema in seconds.</p><h1><strong>Step 2: Point the project at your landing schema</strong></h1><p>This is a one-line edit and it is the difference between everything working and every model failing.</p><p>Open <code>dbt_project.yml</code>:</p><pre><code><code>vars:
  fivetran_database: FIVETRAN_DEMO
  fivetran_schema: POSTGRES_DEMO_L1_LANDING</code></code></pre><p>Those variables feed <code>models/customer_rev/source.yml</code>:</p><pre><code><code>sources:
  - name: landing
    database: "{{ var('fivetran_database') }}"
    schema: "{{ var('fivetran_schema') }}"</code></code></pre><p>The value ships as <code>POSTGRES_DEMO_L1_LANDING</code> because my Fivetran connection is named <code>postgres_demo</code> and Fivetran prefixes the destination schema with the connection name. <strong>If you named your connection something else, or chose Fivetran&#8217;s source-naming option, yours is different.</strong> Use whatever <code>SHOW SCHEMAS IN DATABASE FIVETRAN_DEMO</code> reported in part 1.</p><p>Get this wrong and you see:</p><pre><code><code>Schema 'FIVETRAN_DEMO.L1_LANDING' does not exist or not authorized</code></code></pre><p>Read that message carefully when it happens. &#8220;does not exist <strong>or</strong> not authorized&#8221; is Snowflake being unable to distinguish the two, and the instinct is to go re-check grants. It is almost always the name.</p><h2><strong>The schema override macro</strong></h2><p>While you are in <code>dbt_project.yml</code>, notice this:</p><pre><code><code>models:
  oms_dbt_proj:
    customer_rev:
      +schema: "{{ 'TRANSFORM' }}"
      customerrevenue:
        +schema: "{{ 'SERVE' }}"
        +tags: ['daily']</code></code></pre><p>By default dbt would take those and <em>concatenate</em> them onto your target schema, so <code>TRANSFORM</code> becomes something like <code>PROD_TRANSFORM</code>. That default exists for good reasons on teams, but here it would mean the schemas I created in step 1 are not the schemas dbt writes to.</p><p><code>macros/generate_schema_name.sql</code> turns that off:</p><pre><code><code>{% macro generate_schema_name(custom_schema_name, node) -%}
    {{ custom_schema_name if custom_schema_name else target.schema }}
{%- endmacro %}</code></code></pre><p>Use the custom schema name verbatim if there is one, otherwise the target&#8217;s. This is why the <strong>Default Schema Name</strong> field we fill in Fivetran shortly barely matters: every model in this project overrides it.</p><p>Commit and push before you connect the project in Fivetran.</p><h1><strong>Step 3: Give Fivetran read access to your repo</strong></h1><p><strong>Transformations &#8594; Add transformation &#8594; select your Snowflake destination.</strong> On the project type screen, hover <strong>dbt Core</strong> and click <strong>Connect project</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21xsgB%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xsgB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png 424w, https://substackcdn.com/image/fetch/$s_!xsgB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png 848w, https://substackcdn.com/image/fetch/$s_!xsgB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!xsgB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21xsgB%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png" width="1456" height="831" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:831,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:364023,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215043072?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xsgB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png 424w, https://substackcdn.com/image/fetch/$s_!xsgB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png 848w, https://substackcdn.com/image/fetch/$s_!xsgB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!xsgB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F114c6b69-2ab9-4492-a0cb-968bd98ae822_3014x1720.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>The <strong>Quickstart data models</strong> block at the top of that page is a different feature, prebuilt packages for common SaaS sources. Ignore it. What you want is under <em>Orchestrate your custom data models</em>.</p><h2><strong>The deploy key</strong></h2><p><strong>Fivetran generates its own SSH key pair for Git access and shows you the public half.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Fsgq%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Fsgq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!Fsgq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!Fsgq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!Fsgq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Fsgq%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png" width="1456" height="828" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:828,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:600351,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215043072?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Fsgq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!Fsgq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!Fsgq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!Fsgq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff541e7f6-a218-4cb8-b30d-6f9c9f990e32_3024x1720.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>This is the <strong>third</strong> key pair in this project and it is unrelated to the other two. Worth keeping straight:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/kg75L/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e374dbeb-d0f2-4f93-91e4-b338ec6842da_1220x504.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/417cf88f-b152-42fc-beca-0f141412ae02_1220x504.png&quot;,&quot;height&quot;:243,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fkg75L%2F1%2F" width="730" height="243" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>Nothing is reused between them, and that is the point. Each one can be revoked on its own.</p><p>Copy the public key, then in GitHub go to <strong>your repository &#8594; Settings &#8594; Deploy keys &#8594; Add deploy key</strong>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%210R57%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0R57!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png 424w, https://substackcdn.com/image/fetch/$s_!0R57!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png 848w, https://substackcdn.com/image/fetch/$s_!0R57!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!0R57!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%210R57%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png" width="1456" height="827" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:827,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:566177,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215043072?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0R57!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png 424w, https://substackcdn.com/image/fetch/$s_!0R57!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png 848w, https://substackcdn.com/image/fetch/$s_!0R57!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!0R57!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6c8ac6f-195f-458a-8eb4-8b0d8dffff26_3024x1718.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Title it <code>fivetran</code>, paste the key, and <strong>leave &#8220;Allow write access&#8221; unchecked.</strong> Fivetran only clones. It has no reason to push, and an unchecked box is the cheapest security control in this entire project.</p><p>A deploy key is scoped to one repository, which is the whole reason to use one instead of a personal access token. If it leaks, someone can read this repo. That is the blast radius.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21bDqU%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bDqU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png 424w, https://substackcdn.com/image/fetch/$s_!bDqU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png 848w, https://substackcdn.com/image/fetch/$s_!bDqU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png 1272w, https://substackcdn.com/image/fetch/$s_!bDqU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21bDqU%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png" width="1456" height="831" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:831,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:422941,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215043072?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!bDqU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png 424w, https://substackcdn.com/image/fetch/$s_!bDqU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png 848w, https://substackcdn.com/image/fetch/$s_!bDqU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png 1272w, https://substackcdn.com/image/fetch/$s_!bDqU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feaff1431-26e3-4a8d-acbb-aa7f813322f0_3024x1726.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Finish the configuration</strong></h2><p>Back in Fivetran:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/XOPmp/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/16382a39-cb4b-46c5-a641-536b64db1bc1_1220x918.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5320922b-06e4-45db-89a2-88899e7bab82_1220x918.png&quot;,&quot;height&quot;:456,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FXOPmp%2F1%2F" width="730" height="456" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>The SSH URL matters. The HTTPS URL will not authenticate with a deploy key, and the error you get is a generic clone failure.</p><p>Leave <strong>Project path</strong> empty because <code>dbt_project.yml</code> is at the repo root, which is where Fivetran looks by default. Some people nest their dbt project in a subdirectory; if you do, that field is where you say so.</p><p><code>dbt deps</code> is a no-op here since there is no <code>packages.yml</code>, but leaving it on costs nothing and means you do not have to remember it the day you add a package.</p><p><strong>Save &amp; Test</strong>, then <strong>Done</strong>. Fivetran syncs the project in a couple of minutes.</p><h1><strong>Step 4: Define the jobs in </strong><code>deployment.yml</code></h1><p>Jobs live in <code>deployment.yml</code> at the repo root. Fivetran re-parses this file on every project sync and creates or updates the jobs it finds. This is my favorite part of the setup: the schedule lives in Git next to the models, and changing it is a pull request rather than a click in someone&#8217;s UI.</p><p>The whole file:</p><pre><code><code>jobs:
  - name: Daily-after-landing
    schedule:
      type: integrated
      integrations:
        - verdure_rephrase # replace with your postgres connection id
    steps:
      - name: run daily models
        command: dbt run --select +tag:daily

  - name: Weekly-3am
    schedule:
      type: cron
      entries: ["0 3 * * 0"]
    steps:
      - name: run weekly models
        command: dbt run --select +tag:weekly</code></code></pre><p><strong>You must change one value:</strong> replace <code>verdure_rephrase</code> with your own Postgres connection ID, the two-word system name from part 1. Find it under <strong>Connections &#8594; your Postgres connection &#8594; Setup</strong>, or read it out of the browser URL. It is not <code>postgres_demo</code>.</p><h2><strong>Why </strong><code>type: integrated</code><strong> is the interesting one</strong></h2><p>The cron job is ordinary. Sunday at 3am, run the weekly models.</p><p><code>type: integrated</code> is different: it does not run on a clock at all. It runs <strong>when that Fivetran connection finishes a sync</strong>. New data lands, models rebuild. That is the correct trigger, and it is the thing that is genuinely hard to build yourself. </p><p>In my Airflow project I approximated it with Datasets and a lot of careful DAG wiring. Here it is three lines of YAML, because the tool doing the ingestion is the same tool running the transformation and it simply knows when the sync finished.</p><p>This is also the mechanism that makes part 3 work without me clicking anything.</p><h2><strong>Why </strong><code>+tag:daily</code><strong> builds five models</strong></h2><p>The tags come from <code>dbt_project.yml</code>: <code>customerrevenue</code> is tagged <code>daily</code>, <code>emp_weekly_sales</code> is tagged <code>weekly</code>. Only two models are tagged at all.</p><p>But the selector is <code>+tag:daily</code>, not <code>tag:daily</code>. The leading <code>+</code> means &#8220;this model <strong>and everything it depends on</strong>.&#8221; So the daily job builds:</p><pre><code><code>customers_stg  &#9472;&#9488;
orders_stg     &#9472;&#9532;&#9472;&#8594; orders_fact &#9472;&#8594; customerrevenue
orderitems_stg &#9472;&#9496;
</code></code></pre><p>Five models. <code>employees_stg</code> and <code>emp_weekly_sales</code> are not in that dependency graph, so they sit out and wait for Sunday.</p><p>This is dbt&#8217;s actual value in one line of config. I did not write the build order anywhere. dbt read the <code>ref()</code> calls in the SQL, built the DAG, and worked out that <code>orders_fact</code> cannot run before <code>orderitems_stg</code>.</p><p>Commit and push. The jobs appear under Transformations after the next project sync.</p><h1><strong>Step 5: Run it &#8212; part 2 checkpoint</strong></h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21B6lS%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B6lS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!B6lS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!B6lS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!B6lS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21B6lS%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png" width="1456" height="829" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:829,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:383259,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215043072?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!B6lS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!B6lS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!B6lS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!B6lS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7f8b81-dc5e-4361-bd7f-944189e165ee_3024x1722.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Trigger <code>Daily-after-landing</code> manually from the Transformations tab rather than waiting for the next Neon sync</p><p>Both jobs should end on <strong>Succeeded</strong>.</p><p>Now look at the <strong>Connections</strong> column, because it is the fastest way to check that step 4 actually worked. <code>Daily-after-landing</code> shows <code>postgres_demo</code>, which means the integrated schedule resolved your connection ID. <strong>If it says </strong><code>None</code><strong>, the ID in </strong><code>deployment.yml</code><strong> is wrong</strong> and the job will never fire on its own. It will still run when you click the button, which makes this a quiet failure that you would not notice until part 3.</p><p><code>Weekly-3am</code> showing <code>None</code> is correct. It is on cron and is not tied to a connection.</p><p>Open the job and read the <strong>Run log</strong>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vl8X%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vl8X!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png 424w, https://substackcdn.com/image/fetch/$s_!vl8X!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png 848w, https://substackcdn.com/image/fetch/$s_!vl8X!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!vl8X!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vl8X%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png" width="1456" height="829" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:829,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:505606,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215043072?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vl8X!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png 424w, https://substackcdn.com/image/fetch/$s_!vl8X!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png 848w, https://substackcdn.com/image/fetch/$s_!vl8X!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!vl8X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04b39c08-626b-4f5f-b1c8-7542f6e07f0f_3022x1720.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Two details in there are worth noticing.</p><p><code>target='prod'</code><strong>.</strong> That is Fivetran&#8217;s generated profile, not the <code>dev</code> target from any local <code>profiles.yml</code>. Concrete proof that Fivetran built its own connection config from the destination and never looked at your file.</p><p><strong>Five models, not seven.</strong> <code>PASS=5</code>. Exactly the dependency closure of <code>+tag:daily</code> we worked out above.</p><p>Confirm in Snowsight:</p><pre><code><code>SELECT COUNT(*) FROM FIVETRAN_DEMO.SERVE.CUSTOMERREVENUE;
</code></code></pre><p>The pipeline is now end to end: a row changes in Neon, Fivetran ships it to Snowflake, the sync completing triggers the dbt job, and the reporting tables rebuild. Nobody ran anything.</p><h2><strong>When it fails</strong></h2><p>Almost every failure is one of two things, and they present identically:</p><pre><code><code>Object does not exist or not authorized</code></code></pre><p>Read <strong>which object</strong> the message names.</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/xCVcL/2/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/68577ed8-cbe3-4770-927d-85e28b50efdc_1220x388.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cad41596-9da6-4069-9fa6-33f6f66fcd25_1220x388.png&quot;,&quot;height&quot;:186,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FxCVcL%2F2%2F" width="730" height="186" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>The first is a permission problem. The second is a naming problem. Snowflake refuses to tell you which, so the object name is your only signal.</p><h1><strong>Step 6 (optional): Run dbt locally while you develop</strong></h1><p>You only need this to test model changes before pushing them. <strong>Scheduled builds never touch this setup.</strong> Skip it entirely if you are just following along.</p><h2><strong>A key pair for </strong><code>DBT_USER</code></h2><p>Same pattern as part 1, different file. Do not reuse Fivetran&#8217;s key:</p><pre><code><code>mkdir -p ~/.snowflake
openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out ~/.snowflake/dbt_rsa_key.p8 -nocrypt
openssl rsa -in ~/.snowflake/dbt_rsa_key.p8 -pubout -out ~/.snowflake/dbt_rsa_key.pub
chmod 600 ~/.snowflake/dbt_rsa_key.p8</code></code></pre><p>Copy the public key body, one line, no <code>BEGIN</code> / <code>END</code>:</p><pre><code><code>grep -v -- '-----' ~/.snowflake/dbt_rsa_key.pub | tr -d '\n' | pbcopy</code></code></pre><p>Paste it into <code>snowflake/sql/dbt_keypair.sql</code> and run as <code>SECURITYADMIN</code>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21UZxz%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UZxz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png 424w, https://substackcdn.com/image/fetch/$s_!UZxz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png 848w, https://substackcdn.com/image/fetch/$s_!UZxz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png 1272w, https://substackcdn.com/image/fetch/$s_!UZxz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21UZxz%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png" width="1456" height="830" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:830,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:348406,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215043072?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UZxz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png 424w, https://substackcdn.com/image/fetch/$s_!UZxz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png 848w, https://substackcdn.com/image/fetch/$s_!UZxz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png 1272w, https://substackcdn.com/image/fetch/$s_!UZxz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58a2a13-a62d-40e4-86f2-3b11063f8d3e_3024x1724.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>DBT_USER</code> exists so your laptop never holds the destination&#8217;s private key. If your machine is compromised, the attacker gets a role that can rebuild two schemas, not the credential that writes your entire warehouse.</p><h2><strong>Install dbt in the repo</strong></h2><p>Python 3.10 to 3.13; the project pins 3.12. Install into a virtualenv in the repo, not system-wide:</p><pre><code><code>uv sync
source .venv/bin/activate</code></code></pre><p>Or with the stdlib: <code>python3.12 -m venv .venv</code>, activate, <code>python -m pip install -r requirements.txt</code>. On Windows the activate script is <code>.venv\Scripts\activate</code>. Check with <code>which dbt</code>, which should end in <code>.venv/bin/dbt</code>.</p><h2><strong>Point dbt at Snowflake</strong></h2><p>Copy <code>profiles.yml.example</code> to <code>profiles.yml</code> in the repo root (already gitignored) or to <code>~/.dbt/profiles.yml</code>. Set <code>account</code> to your <code>&lt;ORG&gt;-&lt;ACCOUNT&gt;</code> and give an <strong>absolute</strong> path to the private key:</p><pre><code><code>authenticator: snowflake_jwt
private_key_path: /Users/YOUR_USER/.snowflake/dbt_rsa_key.p8</code></code></pre><p>dbt reads the key by path. Do not paste the key into the YAML. The profile name must stay <code>oms_dbt_proj</code> to match <code>dbt_project.yml</code>.</p><h2><strong>Build</strong></h2><pre><code><code>dbt debug
dbt run
dbt test</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21B4-y%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B4-y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png 424w, https://substackcdn.com/image/fetch/$s_!B4-y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png 848w, https://substackcdn.com/image/fetch/$s_!B4-y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png 1272w, https://substackcdn.com/image/fetch/$s_!B4-y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21B4-y%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png" width="1456" height="1286" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1286,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:334841,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/215043072?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!B4-y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png 424w, https://substackcdn.com/image/fetch/$s_!B4-y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png 848w, https://substackcdn.com/image/fetch/$s_!B4-y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png 1272w, https://substackcdn.com/image/fetch/$s_!B4-y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a91b6fe-6d56-4bbd-81e7-81aeddcacf50_1562x1380.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>dbt debug</code> should end in <code>All checks passed!</code> with <code>user: DBT_USER</code> and <code>role: DBT_ROLE</code>. Compare that to the <code>target='prod'</code> in the Fivetran run log: two different identities, same tables.</p><p><code>dbt run</code> builds all seven models. <code>dbt test</code> is currently a no-op, because <code>models/schema.yml</code> has descriptions but no tests yet. That is a gap, and adding <code>unique</code> and <code>not_null</code> tests to the staging models is the obvious first contribution if you want to extend this project.</p><p>One thing to be ready for: local and scheduled runs write the same tables, and Snowflake only lets the owning role replace a table. If a local run fails on ownership after Fivetran has built the models, see the ownership handover in step 1.</p><h1><strong>What I actually think about this setup</strong></h1><p>The honest comparison against how I would have done it before:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/zxE9Z/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7c7de84c-aa58-441f-8666-1317b3f39d5b_1220x652.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5c2f3d9f-8345-4ae9-ba34-b69e55141ef7_1220x652.png&quot;,&quot;height&quot;:320,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FzxE9Z%2F1%2F" width="730" height="320" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>That last row is the trade. Fivetran Transformations runs dbt commands. It does not run your Python, it does not call an API, it does not branch on a condition. The moment your pipeline needs a step that is not dbt, you are back to needing an orchestrator.</p><p>For a pipeline that is <em>only</em> &#8220;sync, then transform,&#8221; which describes a genuinely large share of analytics work, giving up that flexibility in exchange for deleting the entire scheduling layer is a good deal. For anything more complex it is not, and it is worth knowing which one you are building before you pick.</p><h1><strong>Outro</strong></h1><p>You now have a complete pipeline. Data changes in Neon, Fivetran replicates it into Snowflake, the sync completing triggers a dbt job, and the reporting tables in <code>SERVE</code> rebuild, all without a server of your own and without a line of Python.</p><p>The obvious question is whether it actually works, and that is <a href="iframe.php?url=https%3A%2F%2Ffile%2B.vscode-resource.vscode-cdn.net%2FUsers%2Fminhpham%2Fprojects%2Ffivetran_snowflake_dbt_demo%2Farticles%2Fpart-3-cdc.md">part 3</a>. We go back to Neon, change five rows, click nothing, and follow those rows all the way to <code>SERVE</code>.</p><p>The changes arrive. But two of the numbers are wrong, in ways that are entirely the models&#8217; fault and not the pipeline&#8217;s, and figuring out why is the most useful thing in this whole series.</p><p>If you enjoyed this, please react or share. See you in part 3.</p><p><span>Feel free to connect with </span><a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fminhbphamm%2F">me</a><span> and ask any question</span></p>]]></content:encoded></item><item><title><![CDATA[I rebuilt my ingestion pipeline with zero infrastructure: Part 1 - Neon Postgres WAL, Snowflake Credential & Fivetran CDC]]></title><description><![CDATA[A week-day project where a vendor writes the extract-load code for you, and you only write SQL.]]></description><link>https://markphammm.substack.com/p/i-rebuilt-my-ingestion-pipeline-with</link><guid isPermaLink="false">https://markphammm.substack.com/p/i-rebuilt-my-ingestion-pipeline-with</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Wed, 09 Sep 2026 14:27:31 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21HvqD%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Intro</h1><p>Hi everyone, my name is <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fminhbphamm%2F">Mark</a>, an Analytics Engineer at Infinite Lambda, graduated from TCU in the summer of 2025, majoring in Computer System Analyst with minors in Math and Fintech.</p><p>Last time I wrote about rebuilding my junior-year project: a scraper, an S3 landing zone, Terraform for the infrastructure, Airflow to orchestrate it, and a <code>COPY INTO</code> at the end to get the data into Snowflake. It worked, and I learned a lot writing it. <strong>It was also about 1,500 lines of Python, YAML, and HCL standing between a source system and a warehouse table.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21dFLU%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dFLU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png 424w, https://substackcdn.com/image/fetch/$s_!dFLU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png 848w, https://substackcdn.com/image/fetch/$s_!dFLU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png 1272w, https://substackcdn.com/image/fetch/$s_!dFLU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21dFLU%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png" width="1456" height="826" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:826,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:969442,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dFLU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png 424w, https://substackcdn.com/image/fetch/$s_!dFLU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png 848w, https://substackcdn.com/image/fetch/$s_!dFLU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png 1272w, https://substackcdn.com/image/fetch/$s_!dFLU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45f5af39-0997-491b-ac99-d8cb2a3496b3_3024x1716.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>So this project asks the opposite question: <strong>how much of that can I delete if I let a managed vendor do the extract and the load?</strong></p><p>The answer turned out to be <strong>&#8220;almost all of it, and the interesting problems move somewhere else.&#8221;</strong> There is no Python in this repo. <strong>There is no Airflow, no Docker, no Terraform.</strong> There are SQL scripts, dbt models, and a YAML file. The work that remains is <strong>permissions, key pairs,</strong> and understanding what a <strong>change-data-capture (CDC) stream </strong>actually does to your tables, which is honestly the part I understood least before I started.</p><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">This is part 1 of three</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">. </mark>In this part we do the <strong>ingestion</strong>: stand up a Postgres source on Neon, create a Snowflake trial and the objects Fivetran needs to write into, and connect the two so eight tables land in the warehouse.</p><p>Note: the data here is the SleekMart OMS educational sample. It is completely unrelated to Infinite Lambda&#8217;s business.</p><h1><strong>Takeaway</strong></h1><p>The full repo is <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FMarkPhamm%2Ffivetran_snowflake_dbt_demo">here</a>. This part&#8217;s takeaway:</p><ul><li><p>What <strong>ELT</strong> actually means in practice, and why the &#8220;T&#8221; moving into the warehouse changes how you set up the &#8220;EL&#8221;</p></li><li><p>How <strong>Postgres logical replication</strong> works: <strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">WAL, publications, replication slots, pgoutput, and replica identity.</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>These four words are the whole ingestion layer</p></li><li><p>Why you create a <strong>dedicated role, user, warehouse, and database</strong> for a third-party tool instead of handing it your admin login</p></li><li><p><strong>Key-pair authentication</strong> for a service account, which is what Snowflake now expects and what most tutorials still skip</p></li><li><p>How a managed connector differs from a pipeline you wrote yourself: what you gain, and what you now have to reason about instead</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21MWws%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fc4d0297d-9660-4513-9e47-997683216cbd_1200x800.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MWws!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d0297d-9660-4513-9e47-997683216cbd_1200x800.png 424w, https://substackcdn.com/image/fetch/$s_!MWws!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d0297d-9660-4513-9e47-997683216cbd_1200x800.png 848w, https://substackcdn.com/image/fetch/$s_!MWws!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d0297d-9660-4513-9e47-997683216cbd_1200x800.png 1272w, https://substackcdn.com/image/fetch/$s_!MWws!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d0297d-9660-4513-9e47-997683216cbd_1200x800.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21MWws%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fc4d0297d-9660-4513-9e47-997683216cbd_1200x800.png" width="1200" height="800" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c4d0297d-9660-4513-9e47-997683216cbd_1200x800.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:800,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;What is Write-Ahead Logging in PostgreSQL? | by ANKUSH THAVALI | Medium&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="What is Write-Ahead Logging in PostgreSQL? | by ANKUSH THAVALI | Medium" title="What is Write-Ahead Logging in PostgreSQL? | by ANKUSH THAVALI | Medium" srcset="https://substackcdn.com/image/fetch/$s_!MWws!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d0297d-9660-4513-9e47-997683216cbd_1200x800.png 424w, https://substackcdn.com/image/fetch/$s_!MWws!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d0297d-9660-4513-9e47-997683216cbd_1200x800.png 848w, https://substackcdn.com/image/fetch/$s_!MWws!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d0297d-9660-4513-9e47-997683216cbd_1200x800.png 1272w, https://substackcdn.com/image/fetch/$s_!MWws!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d0297d-9660-4513-9e47-997683216cbd_1200x800.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>My junior-year self would have solved this by writing a Python script with <code>psycopg2</code> and <code>pandas.to_sql</code>, running it on a cron, and re-reading every table every night. That works until the table has ten million rows. What Fivetran does instead is read Postgres&#8217; own change log (WAL), which is a fundamentally different idea and worth understanding even if you never pay for Fivetran. </p><h1><strong>Prerequisite</strong></h1><p><strong>You do not need Python. You do not need Docker. You do not need to know Terraform.</strong></p><p>You need to be comfortable opening a browser, pasting SQL into a console, and running two or three <code>openssl</code> commands in a terminal. Basic SQL helps for the verification queries, but you are mostly copying scripts out of the repo.</p><p>You will create three free accounts as we go: <strong><a href="iframe.php?url=https%3A%2F%2Fneon.com%2F"><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Neon</mark></a><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">, </mark><a href="iframe.php?url=https%3A%2F%2Fwww.snowflake.com%2Fen%2F"><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Snowflake</mark></a><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">, and </mark><a href="iframe.php?url=https%3A%2F%2Fwww.fivetran.com%2F"><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Fivetran</mark></a><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">. None of them ask for a credit card to start.</mark></strong></p><h1><strong>Overview</strong></h1><p>Clone the repo <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FMarkPhamm%2Ffivetran_snowflake_dbt_demo">here</a> to follow along. Here is the whole thing:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21HvqD%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HvqD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png 424w, https://substackcdn.com/image/fetch/$s_!HvqD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png 848w, https://substackcdn.com/image/fetch/$s_!HvqD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png 1272w, https://substackcdn.com/image/fetch/$s_!HvqD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21HvqD%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png" width="1456" height="728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:728,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1245440,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HvqD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png 424w, https://substackcdn.com/image/fetch/$s_!HvqD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png 848w, https://substackcdn.com/image/fetch/$s_!HvqD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png 1272w, https://substackcdn.com/image/fetch/$s_!HvqD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d5f73c0-9e17-4c39-a5cf-48f8add407e9_2172x1086.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>Reading it left to right:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/B9SwJ/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/856855e1-db38-42dd-a012-32ff77388f29_1220x866.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d1a59a5a-168a-4e74-9ea1-3777147e0884_1220x866.png&quot;,&quot;height&quot;:431,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FB9SwJ%2F1%2F" width="730" height="431" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>Two things about that diagram are worth pointing out before we start.</p><ol><li><p><strong>Neon is only the source.</strong> dbt never talks to Neon. It only ever reads the Snowflake replica. If you come from the ETL world where transformation happens in flight, this feels backwards, and that reversal is the entire point of ELT: copy first, cheaply and dumbly, then reshape inside a warehouse that is good at reshaping.</p></li><li><p><strong>Fivetran appears twice.</strong> It does the extract-load in part 1, and in part 2 it also clones your Git repo and runs your dbt project on a schedule. That surprised me. I had assumed I would need dbt Cloud or an orchestrator. You do not.</p></li></ol><p>The series is split like this:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/kCkDA/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/71c8a31b-4286-4209-b4c6-49c09b96f6db_1220x622.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0bc576f-220f-4928-928e-9745b46248d9_1220x622.png&quot;,&quot;height&quot;:306,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FkCkDA%2F1%2F" width="730" height="306" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>Finish part 1 before you start part 2. Every dbt model in part 2 fails on a missing table if the landing schema is not there.</p><h1><strong>Step 1: Create the Neon source database</strong></h1><p><strong><a href="iframe.php?url=https%3A%2F%2Fneon.com%2Fdocs%2Fget-started%2Fwhy-neon">Neon</a></strong> is serverless Postgres. It is not a Postgres-flavored thing, it is actual PostgreSQL with the storage layer separated from the compute layer, which is what lets it branch instantly and scale to zero when idle. It is now part of <strong>Databricks</strong>. For us it is just &#8220;a real Postgres that is always reachable and free,&#8221; which is exactly what <strong>Fivetran</strong> needs to extract from.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21H9Gr%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!H9Gr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png 424w, https://substackcdn.com/image/fetch/$s_!H9Gr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png 848w, https://substackcdn.com/image/fetch/$s_!H9Gr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!H9Gr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21H9Gr%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png" width="1456" height="827" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:827,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4191766,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!H9Gr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png 424w, https://substackcdn.com/image/fetch/$s_!H9Gr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png 848w, https://substackcdn.com/image/fetch/$s_!H9Gr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!H9Gr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7417753-adb1-4f1a-a676-b88e49d1e32c_3024x1718.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Why not run Postgres locally in Docker? Because Fivetran is a SaaS product. It has to be able to reach your database from the internet, and tunneling into your laptop is a much worse afternoon than clicking &#8220;create project.&#8221;</p><ol><li><p>Sign up at <a href="iframe.php?url=https%3A%2F%2Fconsole.neon.tech%2Fsignup">console.neon.tech/signup</a>. GitHub, Google, or email all work.</p></li><li><p>Create a project (I called mine <code>fivetran demo</code>) and pick a region near you. My project is on AWS <code>us-east-2</code>, and I mention that because you will want Fivetran&#8217;s processing region to be nearby later.</p></li><li><p>Your project starts with a default branch (<code>production</code>) and a default database called <code>neondb</code>.</p></li></ol><p>Now create the database we actually use. <strong>Do not use </strong><code>neondb</code><strong>.</strong> Under <strong>Postgres Database &#8594; Databases &#8594; Add database</strong>, name it <code>fivetran_source</code> and keep the default owner.</p><p>You cannot create it from inside a <code>fivetran_source</code> session, obviously, so do it in the Console. If you would rather use SQL, run this while connected to <code>neondb</code>:</p><pre><code><code>CREATE DATABASE fivetran_source;</code></code></pre><p>I am being heavy-handed about this because running SQL against <code>neondb</code> instead of <code>fivetran_source</code> is the single most common way to lose twenty minutes in this tutorial. Every time something later &#8220;did not work,&#8221; check the database dropdown first.</p><h1><strong>Step 2: Load the OMS sample data</strong></h1><p>The sample is <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fsleekdata%2Foms-db-setup">SleekMart OMS</a>, an order management system: customers, orders, order lines, products, suppliers, stores, employees, and a date dimension. Eight tables, 2,747 rows. Small enough to sync in half a minute, big enough that the joins in part 2 are real joins.</p><p>Everything lives in a schema called <code>l1_landing</code>. Identifiers are unquoted and lowercase so they match Postgres&#8217; folding rules.</p><p>Open <strong>Postgres Database &#8594; SQL Editor</strong> and <strong>set the database dropdown to </strong><code>fivetran_source</code>.</p><h3><strong>Create the tables</strong></h3><p>Paste <code>source/sql/schema.sql</code> and run it. You should get <strong>Statement executed successfully</strong> for each <code>CREATE</code>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vYVa%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vYVa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!vYVa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!vYVa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!vYVa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vYVa%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png" width="1456" height="828" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:828,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:477013,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vYVa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!vYVa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!vYVa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!vYVa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8702bb1-706a-4d4c-8dc3-803a62189206_3024x1720.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>One thing to notice in that file: <strong>none of these tables have a primary key.</strong></p><pre><code><code>CREATE TABLE IF NOT EXISTS l1_landing.customers (
    customerid VARCHAR(10),
    firstname VARCHAR(50),
    lastname VARCHAR(50),
    ...
    updated_at TIMESTAMP
);
</code></code></pre><p><code>customerid</code> looks like a key and behaves like one, but Postgres has not been told that. <strong>This is a completely ordinary thing to find in a real source system, and it has consequences that show up three steps from now and again in part 3.</strong> Keep it in the back of your head.</p><h3><strong>Insert the rows</strong></h3><p>Still on <code>fivetran_source</code>, paste <code>source/sql/insert.sql</code> and run it. It is about 180k characters, so the editor may warn you that history will be truncated. The inserts still run.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%213fGs%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3fGs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!3fGs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!3fGs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!3fGs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%213fGs%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png" width="1456" height="828" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:828,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:799610,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3fGs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!3fGs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!3fGs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!3fGs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09a9e454-71cb-477f-8af2-55911be2f68d_3024x1720.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Expected counts:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/XqBW0/2/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/689a289c-36c2-446f-8d7a-8d41769c3d9b_1220x768.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2e336d95-c8c0-4606-b445-b79d80e5d326_1220x838.png&quot;,&quot;height&quot;:378,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FXqBW0%2F2%2F" width="730" height="378" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><h3><strong>Confirm</strong></h3><pre><code><code>SELECT * FROM l1_landing.customers;</code></code></pre><p>100 rows. That is the checkpoint for the source.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vqry%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vqry!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png 424w, https://substackcdn.com/image/fetch/$s_!vqry!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png 848w, https://substackcdn.com/image/fetch/$s_!vqry!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!vqry!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vqry%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png" width="1456" height="830" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:830,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:409345,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vqry!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png 424w, https://substackcdn.com/image/fetch/$s_!vqry!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png 848w, https://substackcdn.com/image/fetch/$s_!vqry!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!vqry!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dab38ca-33a3-4f6e-86ab-85379cb7ae36_3012x1718.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>If you get nothing, you are on <code>neondb</code>. <strong>I warned you.</strong></p><h1><strong>Step 3: Turn on logical replication</strong></h1><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">This is the step that makes the whole thing incremental,</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>and it is the step worth actually understanding rather than copy-pasting.</p><h2><strong>The problem</strong></h2><p>Fivetran has to answer one question on every sync: <em>what changed since last time?</em></p><p>The naive answer is query-based sync: <code>SELECT</code> from the tables and diff against what you already have, using Postgres metadata like <code>xmin</code> to narrow it down. It works. It also means scanning the source on a schedule forever, and it is bad at detecting deletes, because a row that is gone does not show up in a <code>SELECT</code>.</p><p>The better answer is to stop asking Postgres what changed and instead <strong>read the log Postgres already writes</strong>.</p><h2><strong>WAL</strong></h2><p>Every Postgres instance maintains a <strong>write-ahead log</strong>. Before a change hits the table, it goes into the WAL. Postgres needs this for crash recovery and physical replication. Conceptually it is an activity log:</p><pre><code><code>10:01 INSERT order 101
10:05 INSERT order 102
10:10 UPDATE order 101 NEW -&gt; PAID
10:15 DELETE order 102
</code></code></pre><p>That is exactly the stream a data pipeline wants. <strong>Change data capture</strong> is nothing more than &#8220;read that log instead of scanning the tables.&#8221;</p><p>The catch is that by default the WAL records physical changes (&#8221;bytes on page 387 changed&#8221;), which is useless to an external tool. Setting <code>wal_level = logical</code> tells Postgres to record enough detail that the row-level events can be reconstructed. That is all that setting does, and it is why Neon exposes it as a project-level switch rather than something you can run as SQL.</p><h2><strong>Publication and replication slot</strong></h2><p>Two more objects, and the distinction between them is the thing to memorize:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/A3KJA/2/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e1c5018f-54a4-4876-a58f-4786062ec5af_1220x324.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0179468c-d083-48f6-a5ab-49605f0eb56b_1220x324.png&quot;,&quot;height&quot;:153,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FA3KJA%2F2%2F" width="730" height="153" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>The publication is an allow-list. This demo uses <code>FOR ALL TABLES</code> because it is a demo; in production you would name the tables.</p><p>The slot is a bookmark. Say the WAL events are numbered 100 through 105 and Fivetran has consumed through 102. The slot records that. Next sync, Fivetran reconnects and Postgres hands it 103 onward. Just as importantly, <strong>Postgres will not discard WAL that the slot still needs</strong>, so Fivetran can be offline for a while without losing data.</p><p>That guarantee is also the operational risk. Create a slot, then abandon it, and Postgres keeps every WAL segment since the bookmark, forever, and your storage grows:</p><pre><code><code>day 1:  &#9608;
day 2:  &#9608;&#9608;&#9608;
day 3:  &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;
day 4:  &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;
</code></code></pre><p>So: one connector per slot, and clean up slots you stop using.</p><p><code>pgoutput</code> is the last piece. The WAL is not a convenient stream of rows, so something has to decode it. <code>pgoutput</code><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> is Postgres&#8217; built-in logical decoding plugin and the one Fivetran uses.</mark></strong></p><p>The whole picture:</p><pre><code><code>                      POSTGRES
                         &#9474;
                         &#9474; rows change
                         &#9660;
                       WAL
                         &#9474;
              wal_level = logical
                         &#9474;
                         &#9660;
                     pgoutput
                         &#9474;
            &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
            &#9660;                         &#9660;
      publication_01         replication_slot_01
       "WHAT tables?"          "WHERE am I?"
            &#9474;                         &#9474;
            &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                         &#9660;
                      Fivetran
                         &#9474;
                         &#9660;
                      Snowflake
</code></code></pre><p>If that diagram makes sense, you understand the ingestion layer. The longer version with more diagrams is in <code>source/postgres_cdc.md</code>; you do not need it to finish the setup, but read it afterwards.</p><h2><strong>Enable it in Neon</strong></h2><p><strong>Settings &#8594; Logical Replication &#8594; Enable.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21EnEV%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EnEV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png 424w, https://substackcdn.com/image/fetch/$s_!EnEV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png 848w, https://substackcdn.com/image/fetch/$s_!EnEV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png 1272w, https://substackcdn.com/image/fetch/$s_!EnEV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21EnEV%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png" width="1456" height="832" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:832,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:419860,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!EnEV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png 424w, https://substackcdn.com/image/fetch/$s_!EnEV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png 848w, https://substackcdn.com/image/fetch/$s_!EnEV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png 1272w, https://substackcdn.com/image/fetch/$s_!EnEV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F953247c2-e5b8-421f-80b7-777f17313977_3024x1728.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Two warnings that are real and not boilerplate:</p><ul><li><p><strong>This cannot be reverted.</strong> It changes <code>wal_level</code> for the whole project.</p></li><li><p>It restarts compute, so you get a brief disconnect, and a connected subscriber keeps compute from scaling to zero, which uses more of your free credits.</p></li></ul><p>Confirm from the SQL Editor on <code>fivetran_source</code>:</p><pre><code><code>SHOW wal_level;</code></code></pre><p>You want <code>logical</code>.</p><h2><strong>Create the publication and slot</strong></h2><p>Paste and run <code>source/sql/replication.sql</code>. It does four things:</p><p><strong>1. Sets </strong><code>REPLICA IDENTITY FULL</code><strong> on all eight tables.</strong> <strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Remember the missing primary keys? Here is where that bites.</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>When an <code>UPDATE</code> or <code>DELETE</code> hits the WAL, Postgres needs to describe <em>which</em> row changed. With a primary key that is easy: &#8220;row with <code>customerid = 123</code>.&#8221; Without one, Postgres by default has nothing useful to log.</p><p><code>REPLICA IDENTITY FULL</code> says: log the <strong>entire old row</strong>. Then a downstream consumer can match on all the values instead of a key.</p><pre><code><code>ALTER TABLE l1_landing.customers REPLICA IDENTITY FULL;</code></code></pre><p>The cost is more WAL volume per change, which for 2,747 rows is nothing and for a billion-row production table would be a real decision. The right fix in production is a primary key.</p><p><strong>2. Creates the publication:</strong></p><pre><code><code>DROP PUBLICATION IF EXISTS publication_01;
CREATE PUBLICATION publication_01 FOR ALL TABLES;</code></code></pre><p><strong>3. Creates the slot:</strong></p><pre><code><code>SELECT *
FROM pg_create_logical_replication_slot('replication_slot_01', 'pgoutput');</code></code></pre><p>Publication <strong>before</strong> slot. That is the order Fivetran&#8217;s setup guide requires. Postgres itself does not strictly care, but follow the documented sequence.</p><p><strong>4. Prints sanity checks</strong> for <code>wal_level</code>, the publication, and the slot.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21NC-y%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NC-y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png 424w, https://substackcdn.com/image/fetch/$s_!NC-y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png 848w, https://substackcdn.com/image/fetch/$s_!NC-y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png 1272w, https://substackcdn.com/image/fetch/$s_!NC-y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21NC-y%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png" width="1456" height="826" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:826,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:642740,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NC-y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png 424w, https://substackcdn.com/image/fetch/$s_!NC-y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png 848w, https://substackcdn.com/image/fetch/$s_!NC-y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png 1272w, https://substackcdn.com/image/fetch/$s_!NC-y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e1009c1-5723-456e-9c5c-3cc8d0ae647d_3024x1716.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Write down these two names exactly. You type them into Fivetran later, and <code>publication_1</code> will not find <code>publication_01</code>:</p><ul><li><p><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Publication:</mark> <code>publication_01</code></p></li><li><p><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Slot: </mark><code>replication_slot_01</code></p></li></ul><h1><strong>Step 4: Create a Snowflake trial</strong></h1><p>Snowflake&#8217;s trial is 30 days with about <strong>$400 in credits</strong>, no credit card. That is enormously more than this project uses.</p><p>Pick the <strong>AI Data Cloud</strong> trial, not the Cortex Code CLI one. You need a real warehouse so Fivetran can write tables.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21SEB1%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SEB1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!SEB1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!SEB1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!SEB1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21SEB1%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png" width="1456" height="829" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:829,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2101948,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!SEB1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!SEB1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!SEB1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!SEB1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c2dc35e-5c77-41ae-baf7-1f7a79cfda7d_3024x1722.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ol><li><p>Go to <a href="iframe.php?url=https%3A%2F%2Fsignup.snowflake.com%2F">signup.snowflake.com</a> and leave <strong>AI Data Cloud For Enterprise</strong> selected.</p></li><li><p>Fill in name, work email, and why you are signing up. A personal name is fine.</p></li><li><p>Click the activation link in the email (check spam).</p></li><li><p>Choose a <strong>cloud</strong>, <strong>region</strong>, and <strong>edition</strong>. Cloud and region <strong>cannot be changed later</strong>, so pick something close to you and to your Neon project. Enterprise is the normal trial edition and is fine.</p></li><li><p>Set your admin username and password. This is <em>your</em> login, not the service user we create next.</p></li><li><p>Sign in at <a href="iframe.php?url=https%3A%2F%2Fapp.snowflake.com%2F">app.snowflake.com</a>.</p></li></ol><p>You land in Snowsight as <code>ACCOUNTADMIN</code>, with a default warehouse called <code>COMPUTE_WH</code> and some sample data. Credits expire after 30 days and the account suspends, so do not put anything you care about here.</p><h1><strong>Step 5: Create the Snowflake objects Fivetran writes into</strong></h1><p>Snowflake is empty. Before Fivetran can load anything, it needs somewhere to load <em>to</em> and an identity to load <em>as</em>.</p><p>The temptation is to hand Fivetran your <code>ACCOUNTADMIN</code> login and move on. Do not. Fivetran is a third party with a persistent connection to your warehouse, and the whole point of this step is that if that credential leaks, the blast radius is one database and one extra-small warehouse.</p><p>Open a Snowsight worksheet, paste <code>snowflake/sql/fivetran_setup.sql</code>, <strong>select the entire script</strong> (Snowsight only runs what is highlighted), and Run.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21tGbh%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tGbh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!tGbh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!tGbh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!tGbh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21tGbh%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png" width="1456" height="828" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:828,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:458897,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tGbh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png 424w, https://substackcdn.com/image/fetch/$s_!tGbh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png 848w, https://substackcdn.com/image/fetch/$s_!tGbh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!tGbh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff43cf590-fa33-4347-83df-2f5f10e0a44a_3024x1720.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>Four objects come out of it:</strong></p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/XWR6g/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f7781527-6475-446d-abd2-46f2daf38229_1220x696.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f35f59ff-c43d-43de-b0d9-c68d86a3bd44_1220x696.png&quot;,&quot;height&quot;:344,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FXWR6g%2F1%2F" width="730" height="344" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>A few decisions in that script that are worth calling out.</p><p><strong>Its own warehouse.</strong> Do not point Fivetran at <code>COMPUTE_WH</code>. A dedicated XSMALL warehouse that suspends after 60 seconds means Fivetran&#8217;s loads never queue behind your ad-hoc queries, and you can see exactly what the pipeline costs. Given that trial credits are finite, <code>AUTO_SUSPEND = 60</code> is the single most useful line in the file.</p><p><strong>A narrow set of grants.</strong> <code>USAGE</code> on the warehouse, then <code>CREATE SCHEMA</code>, <code>MONITOR</code>, <code>USAGE</code> on the database. That is it. Fivetran creates the landing schema itself, so you do not pre-create <code>l1_landing</code>.</p><p><strong>Load format defaults:</strong></p><pre><code><code>ALTER USER IDENTIFIER($USER_NAME) SET BINARY_INPUT_FORMAT = 'BASE64';
ALTER USER IDENTIFIER($USER_NAME) SET TIMESTAMP_INPUT_FORMAT = 'AUTO';</code></code></pre><p>These two prevent a class of failure that presents as &#8220;bad data&#8221; rather than &#8220;missing privilege,&#8221; which is a much more annoying thing to debug.</p><p><strong>Role granted to </strong><code>SYSADMIN</code>, so you can inspect Fivetran&#8217;s objects from a normal admin session without becoming the service user.</p><h2><strong>Attach a key pair</strong></h2><p><code>FIVETRAN_USER</code> is a service user with no password, and Snowflake is discontinuing username/password auth for destinations anyway. So it authenticates with an RSA key pair.</p><p>The mental model that took me a minute: <strong>Snowflake never generates or downloads a private key for you.</strong> You generate the pair on your machine, register the <em>public</em> half with Snowflake, and give the <em>private</em> half to Fivetran.</p><pre><code><code>mkdir -p ~/.snowflake
openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out ~/.snowflake/fivetran_rsa_key.p8 -nocrypt
openssl rsa -in ~/.snowflake/fivetran_rsa_key.p8 -pubout -out 
~/.snowflake/fivetran_rsa_key.pub
chmod 600 ~/.snowflake/fivetran_rsa_key.p8</code></code></pre><p>Copy the <strong>public key body</strong> to your clipboard, without the <code>BEGIN</code> / <code>END</code> lines, on one line:</p><pre><code><code>grep -v -- '-----' ~/.snowflake/fivetran_rsa_key.pub | tr -d '\n' | pbcopy</code></code></pre><p>Paste it into <code>snowflake/sql/fivetran_keypair.sql</code> and run it as <code>SECURITYADMIN</code>:</p><pre><code><code>USE ROLE SECURITYADMIN;
ALTER USER FIVETRAN_USER SET RSA_PUBLIC_KEY = '&lt;PASTE_PUBLIC_KEY_HERE&gt;';</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%214UwC%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4UwC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png 424w, https://substackcdn.com/image/fetch/$s_!4UwC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png 848w, https://substackcdn.com/image/fetch/$s_!4UwC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!4UwC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%214UwC%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png" width="1456" height="829" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:829,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:343320,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4UwC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png 424w, https://substackcdn.com/image/fetch/$s_!4UwC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png 848w, https://substackcdn.com/image/fetch/$s_!4UwC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!4UwC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9753466-d2fc-4790-b61c-6ce77d003f44_3020x1720.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In part 2 we generate a <em>second</em> pair for a dbt user. Do not reuse this one. Separate pairs mean you can rotate one without breaking the other, and it is the difference between &#8220;rotate the Fivetran key&#8221; and &#8220;take the whole pipeline down.&#8221;</p><h2><strong>Verify and grab your host</strong></h2><p>Run <code>snowflake/sql/verify.sql</code>. It confirms the four objects and the grants, then prints the account identifiers:</p><pre><code><code>SELECT
    CURRENT_ORGANIZATION_NAME() AS organization_name,
    CURRENT_ACCOUNT_NAME() AS account_name,
    CURRENT_ACCOUNT() AS account_locator,
    CURRENT_REGION() AS region;</code></code></pre><p>Your Fivetran <strong>Host</strong> is:</p><pre><code><code>&lt;organization_name&gt;-&lt;account_name&gt;.snowflakecomputing.com</code></code></pre><p><strong>A hyphen between org and account, not a dot.</strong> This trips up a lot of people, me included. There is an older <code>&lt;locator&gt;.&lt;region&gt;</code> form floating around in blog posts that will fail the host test.</p><h1><strong>Step 6: Connect the Postgres source in Fivetran</strong></h1><p>Sign up for a <a href="iframe.php?url=https%3A%2F%2Ffivetran.com%2Fsignup">free Fivetran account</a>, then <strong>Connections &#8594; Add connection &#8594; PostgreSQL</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21AueQ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AueQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png 424w, https://substackcdn.com/image/fetch/$s_!AueQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png 848w, https://substackcdn.com/image/fetch/$s_!AueQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!AueQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21AueQ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png" width="1456" height="830" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:830,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:411398,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AueQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png 424w, https://substackcdn.com/image/fetch/$s_!AueQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png 848w, https://substackcdn.com/image/fetch/$s_!AueQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!AueQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb4e977-f328-44cd-b6ac-214475abd1f5_3022x1722.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Before you start, open two browser tabs: Neon&#8217;s <strong>Connect</strong> modal, and the output of <code>verify.sql</code>. You will be copying from both.</p><h2><strong>Connection name</strong></h2><p>I used <code>postgres_demo</code>. <strong>Pick carefully, because this name becomes part of your destination schema name and cannot be changed later.</strong></p><p>By default Fivetran prefixes the destination schema with the connection name, so Neon&#8217;s <code>l1_landing</code> arrives in Snowflake as <code>POSTGRES_DEMO_L1_LANDING</code>. This matters a lot in part 2: the dbt project has that name hardcoded in a variable, and if yours differs, every model fails.</p><h2><strong>Networking</strong></h2><p>Open <strong>Connect</strong> in the Neon sidebar. Branch <code>production</code>, database <code>fivetran_source</code>, role <code>neondb_owner</code>, and <strong>turn connection pooling off</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21lBQ6%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lBQ6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png 424w, https://substackcdn.com/image/fetch/$s_!lBQ6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png 848w, https://substackcdn.com/image/fetch/$s_!lBQ6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!lBQ6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21lBQ6%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png" width="1456" height="827" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:827,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:453345,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lBQ6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png 424w, https://substackcdn.com/image/fetch/$s_!lBQ6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png 848w, https://substackcdn.com/image/fetch/$s_!lBQ6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!lBQ6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7198ae0b-2b49-4498-a923-2fd773ca4016_3024x1718.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>You get a URI like:</p><pre><code><code>postgresql://neondb_owner:PASSWORD@ep-....us-east-2.aws.neon.tech/fivetran_source?sslmode=require
</code></code></pre><p><strong>Do not paste that whole string into one Fivetran field.</strong> Split it:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/EMO6D/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6de980d2-633e-43e5-a8ea-43e1d2312d97_1220x546.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/327c7978-c57c-4833-9314-5cbb226bf7a0_1220x546.png&quot;,&quot;height&quot;:266,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FEMO6D%2F1%2F" width="730" height="266" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>Connection method is <strong>Connect directly</strong>.</p><p>The hostname <strong>must not contain </strong><code>-pooler</code>. Neon&#8217;s pooled endpoint runs through PgBouncer, and logical replication needs a persistent session, so the two are incompatible. If pooling was on when you copied the URI, go back and turn it off rather than editing the string by hand.</p><h2><strong>Database access</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21z0E1%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!z0E1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png 424w, https://substackcdn.com/image/fetch/$s_!z0E1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png 848w, https://substackcdn.com/image/fetch/$s_!z0E1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png 1272w, https://substackcdn.com/image/fetch/$s_!z0E1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21z0E1%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png" width="1456" height="830" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:830,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:478361,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!z0E1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png 424w, https://substackcdn.com/image/fetch/$s_!z0E1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png 848w, https://substackcdn.com/image/fetch/$s_!z0E1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png 1272w, https://substackcdn.com/image/fetch/$s_!z0E1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab67b63a-4b09-4422-aa42-63c8ddeb1220_3024x1724.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/6V0Tc/2/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5cf219f7-2995-4cda-92a2-28dd90f1bb84_1220x478.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ccecd1d9-612a-4cc3-b029-6109f6dbff1b_1220x478.png&quot;,&quot;height&quot;:229,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2F6V0Tc%2F2%2F" width="730" height="229" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>Look at the <strong>Database</strong> field in that screenshot: it starts <strong>empty</strong>. It does not default to anything. Leave it blank and the connection test fails with a message that does not obviously say &#8220;you left the database blank.&#8221;</p><p>Set <strong>Data processing location</strong> near your Neon project. Mine is AWS <code>us-east-2</code>, so I picked the closest US AWS region Fivetran offers.</p><h2><strong>Incremental sync</strong></h2><p>Here is where the names from step 3 go:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/h5irU/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/348c91d1-4866-45a8-b8f0-f2d486e170a1_1220x398.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/07c89d3b-25d4-408a-a08e-6d51270668b7_1220x398.png&quot;,&quot;height&quot;:191,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fh5irU%2F1%2F" width="730" height="191" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>Fivetran is literally going to look up those objects in Postgres. <strong>Exact match or nothing.</strong></p><h2><strong>Save and test</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21gyQZ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gyQZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!gyQZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!gyQZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!gyQZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21gyQZ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png" width="1456" height="829" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:829,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:314366,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!gyQZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png 424w, https://substackcdn.com/image/fetch/$s_!gyQZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png 848w, https://substackcdn.com/image/fetch/$s_!gyQZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!gyQZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe007df45-8f6c-45ae-a1ed-4ab670be4eab_3024x1722.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>You want green on the certificate, connecting, setup, and <strong>Testing logical replication</strong>.</p><p><strong>Checking configuration values</strong> will probably warn you, and on Neon Free that is expected:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/oQUIa/2/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/60783452-ecd4-4529-809a-30d323b74baa_1220x356.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7d299763-0a96-4e04-8cc8-c32ce669bd56_1220x356.png&quot;,&quot;height&quot;:170,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FoQUIa%2F2%2F" width="730" height="170" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>You cannot change these on Neon the way you could on self-hosted Postgres. Click <strong>Continue</strong>. <code>Creating BIT_XOR aggregate</code> showing a dash is also fine.</p><p>If something actually fails:</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/TKfzr/2/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a1e8d9f7-d08f-4d7f-9e99-fdf2c84443d9_1220x388.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/421e7eff-4100-47f6-9340-59123ef8caa2_1220x388.png&quot;,&quot;height&quot;:186,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FTKfzr%2F2%2F" width="730" height="186" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><h1><strong>Step 7: Connect the Snowflake destination</strong></h1><p><strong>Destinations &#8594; Add destination &#8594; Snowflake.</strong> Note this is a <em>destination</em>, not a source connector; they are different parts of the Fivetran UI.</p><p>Copy the full private key, <code>BEGIN</code> and <code>END</code> lines included:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21OybO%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OybO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png 424w, https://substackcdn.com/image/fetch/$s_!OybO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png 848w, https://substackcdn.com/image/fetch/$s_!OybO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!OybO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21OybO%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png" width="1456" height="828" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:828,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:617152,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OybO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png 424w, https://substackcdn.com/image/fetch/$s_!OybO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png 848w, https://substackcdn.com/image/fetch/$s_!OybO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!OybO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc7142d4-6ac3-448c-b4b8-bb82ef342000_3022x1718.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><pre><code><code>pbcopy &lt; ~/.snowflake/fivetran_rsa_key.p8</code></code></pre><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/nrcMN/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/82e846c5-129d-4420-bb02-5359d88c569b_1220x874.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d686255a-bb74-4c30-a4c3-24522e106c45_1220x874.png&quot;,&quot;height&quot;:432,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FnrcMN%2F1%2F" width="730" height="432" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><p>Two mistakes to avoid. <strong>First, this is the </strong><code>.p8</code><strong> file, not the </strong><code>.pub</code><strong>.</strong> Snowflake already has the public half; Fivetran needs the private one. Second, Role is technically optional in the form, but fill it in anyway so Fivetran is not silently relying on the user&#8217;s default role.</p><p><strong>Save &amp; Test.</strong> You want all six:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21MWgc%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MWgc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png 424w, https://substackcdn.com/image/fetch/$s_!MWgc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png 848w, https://substackcdn.com/image/fetch/$s_!MWgc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!MWgc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21MWgc%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png" width="1456" height="830" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:830,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:181675,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MWgc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png 424w, https://substackcdn.com/image/fetch/$s_!MWgc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png 848w, https://substackcdn.com/image/fetch/$s_!MWgc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png 1272w, https://substackcdn.com/image/fetch/$s_!MWgc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2600bd-72b4-4f4e-bdca-5949704b7bde_3022x1722.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Debugging guide: <strong>Host Connection</strong> failing is almost always the hostname format. <strong>Permission Test</strong> failing means re-run <code>fivetran_setup.sql</code>. <strong>Private key rejected</strong> means you pasted the <code>.pub</code>, or the public key in Snowflake does not match this private key.</p><h1><strong>Step 8: The first sync</strong></h1><p>Both connections test clean, but nothing has moved yet. The Postgres connection sits <strong>Paused</strong> until you tell it which tables to take.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21zZN8%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zZN8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png 424w, https://substackcdn.com/image/fetch/$s_!zZN8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png 848w, https://substackcdn.com/image/fetch/$s_!zZN8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png 1272w, https://substackcdn.com/image/fetch/$s_!zZN8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21zZN8%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png" width="1456" height="830" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:830,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:470622,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zZN8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png 424w, https://substackcdn.com/image/fetch/$s_!zZN8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png 848w, https://substackcdn.com/image/fetch/$s_!zZN8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png 1272w, https://substackcdn.com/image/fetch/$s_!zZN8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f04d11c-f29c-4db9-8bfd-23fcc283a066_3024x1724.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ol><li><p>Click <strong>Review connection schema</strong>.</p></li><li><p>Select the <code>l1_landing</code> schema, or the eight OMS tables individually. Leave Fivetran&#8217;s system tables alone.</p></li><li><p>Save, then unpause and sync.</p></li></ol><p>This is where the design pays off. Fivetran does a <strong>historical sync</strong> first, a straight copy of everything currently in the tables, and then switches to reading the WAL for everything after. Phase one is the snapshot, phase two is the change stream, and you never think about the handoff.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Eyij%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Eyij!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png 424w, https://substackcdn.com/image/fetch/$s_!Eyij!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png 848w, https://substackcdn.com/image/fetch/$s_!Eyij!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png 1272w, https://substackcdn.com/image/fetch/$s_!Eyij!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Eyij%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png" width="1456" height="830" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/da95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:830,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:486765,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Eyij!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png 424w, https://substackcdn.com/image/fetch/$s_!Eyij!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png 848w, https://substackcdn.com/image/fetch/$s_!Eyij!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png 1272w, https://substackcdn.com/image/fetch/$s_!Eyij!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda95ebd8-1f44-44d0-b8e0-1713c48bfb4a_3024x1724.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Roughly half a minute, eight tables, <strong>2,747 rows</strong> extracted and loaded.</p><h2><strong>Part 1 checkpoint</strong></h2><p>In Snowsight:</p><pre><code><code>SHOW SCHEMAS IN DATABASE FIVETRAN_DEMO;

SELECT COUNT(*) FROM FIVETRAN_DEMO.POSTGRES_DEMO_L1_LANDING.CUSTOMERS;</code></code></pre><p>You want <code>100</code>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Bg-_%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Bg-_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png 424w, https://substackcdn.com/image/fetch/$s_!Bg-_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png 848w, https://substackcdn.com/image/fetch/$s_!Bg-_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!Bg-_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Bg-_%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png" width="1456" height="830" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:830,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:668552,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/214887139?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Bg-_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png 424w, https://substackcdn.com/image/fetch/$s_!Bg-_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png 848w, https://substackcdn.com/image/fetch/$s_!Bg-_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png 1272w, https://substackcdn.com/image/fetch/$s_!Bg-_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04efeede-888c-497a-971a-0b94e91d41b5_3014x1718.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>SHOW SCHEMAS</code><strong> is the important one.</strong> Write down the landing schema name <em>exactly</em> as Snowflake reports it. Because my connection is called <code>postgres_demo</code>, mine is <code>POSTGRES_DEMO_L1_LANDING</code>, not <code>L1_LANDING</code>. Part 2 has to be told which one you got, and if you skip this you spend an hour reading <code>Schema 'FIVETRAN_DEMO.L1_LANDING' does not exist or not authorized</code> and looking for a grant problem that is actually a naming problem.</p><p>While you are in Fivetran, note the connection&#8217;s <strong>ID</strong>: the two-word system name Fivetran assigned it, visible in the connection&#8217;s Setup tab and in the browser URL. Mine is <code>verdure_rephrase</code>. That is <em>not</em> the display name <code>postgres_demo</code>, and part 2 needs it to trigger dbt off this connection.</p><p>You will also see a <code>FIVETRAN_*_STAGING</code> schema appear. That is Fivetran&#8217;s internal staging area. Ignore it.</p><p>Look at that screenshot one more time and notice the first column of the result set: <code>_FIVETRAN_ID</code>. Fivetran added columns to your tables. Because these tables have no primary key, Fivetran generates <code>_FIVETRAN_ID</code> as a hash of every column value and uses it to identify rows. There is also <code>_FIVETRAN_SYNCED</code> and <code>_FIVETRAN_DELETED</code>.</p><p>Those three columns are the most interesting thing in this entire project, and part 3 is essentially one long argument about them. For now, just notice they are there.</p><h1><strong>What this cost, in effort</strong></h1><p>Comparing honestly against my last project&#8217;s ingestion layer:</p><p>The work does not disappear, it relocates. I did not write a line of extract-load logic, and in exchange I had to actually learn what a replication slot is and how Snowflake&#8217;s role hierarchy works. I think that is a good trade, and I think the second set of things is more transferable than the first.</p><div id="datawrapper-iframe" class="datawrapper-wrap outer" data-attrs="{&quot;url&quot;:&quot;https://datawrapper.dwcdn.net/fe48D/1/&quot;,&quot;thumbnail_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bdf5e369-3668-48f9-9df4-e110d81d9540_1220x806.png&quot;,&quot;thumbnail_url_full&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4536f9a9-4bee-4948-aabd-f1bf29eff1bd_1220x806.png&quot;,&quot;height&quot;:400,&quot;title&quot;:&quot;Created with Datawrapper&quot;,&quot;description&quot;:&quot;&quot;,&quot;belowTheFold&quot;:true}" data-component-name="DatawrapperToDOM"><iframe id="iframe-datawrapper" class="datawrapper-iframe" src="iframe.php?url=https%3A%2F%2Fdatawrapper.dwcdn.net%2Ffe48D%2F1%2F" width="730" height="400" frameborder="0" scrolling="no" loading="lazy"></iframe><script type="text/javascript">!function(){"use strict";window.addEventListener("message",(function(e){if(void 0!==e.data["datawrapper-height"]){var t=document.querySelectorAll("iframe");for(var a in e.data["datawrapper-height"])for(var r=0;r<t.length;r++){if(t[r].contentWindow===e.source)t[r].style.height=e.data["datawrapper-height"][a]+"px"}}}))}();</script></div><h1><strong>Outro</strong></h1><p>That is the ingestion half. You have a real Postgres source streaming its change log, a warehouse with a dedicated least-privilege service identity, and eight tables replicating between them with no code and no server of your own.</p><p>Right now Snowflake holds an exact, uncleaned copy of Neon. In part 2 we turn that into reporting tables with dbt, and the twist is that we do not run dbt ourselves: Fivetran clones the Git repo and runs it on a schedule, using a deploy key and a <code>deployment.yml</code>. No dbt Cloud, no orchestrator.</p><p>Then in part 3 we go back to Neon, change five rows, and follow them all the way to the reporting tables. That is where <code>_FIVETRAN_DELETED</code> stops being trivia and starts being a bug in the dashboard.</p><p>If you enjoyed this, please react or share so I know to keep going. Thanks for reading this far.</p><p>Feel free to connect with <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fminhbphamm%2F">me</a> and ask any question </p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Power BI or Tableau? Wrong question.]]></title><description><![CDATA[The dashboard principles that survive every tool migration]]></description><link>https://markphammm.substack.com/p/power-bi-or-tableau-wrong-question</link><guid isPermaLink="false">https://markphammm.substack.com/p/power-bi-or-tableau-wrong-question</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Thu, 06 Aug 2026 19:58:46 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21BS5X%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>People keep asking me the same thing: should I learn Power BI or Tableau?</p><p><strong>And I honestly don&#8217;t think it matters that much.</strong></p><p>Unless you&#8217;re joining a company tomorrow and you already know they run Power BI, don&#8217;t lose sleep over it. You will switch BI tools multiple times in your career whether you like it or not.</p><p>I&#8217;ve used Tableau. I&#8217;ve used Power BI. I&#8217;ve dashboarded in Streamlit like a psychopath. I&#8217;ve used Mode and Hex, and those two are my favorites. And tomorrow my company could decide to migrate everything to Sigma for all I know.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21BS5X%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BS5X!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png 424w, https://substackcdn.com/image/fetch/$s_!BS5X!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png 848w, https://substackcdn.com/image/fetch/$s_!BS5X!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png 1272w, https://substackcdn.com/image/fetch/$s_!BS5X!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21BS5X%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png" width="1456" height="841" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:841,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;BI Tools&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="BI Tools" title="BI Tools" srcset="https://substackcdn.com/image/fetch/$s_!BS5X!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png 424w, https://substackcdn.com/image/fetch/$s_!BS5X!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png 848w, https://substackcdn.com/image/fetch/$s_!BS5X!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png 1272w, https://substackcdn.com/image/fetch/$s_!BS5X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4cda521-b14a-47b1-9cb7-de40b756fc22_1680x970.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The tool changes. The principles don&#8217;t.</p><p>It&#8217;s the same reason I don&#8217;t tell people to memorize Spark syntax before understanding distributed computing. Learn the fundamentals first. The rest is just finding where the buttons are.</p><p>Because every dashboard, in every tool, is trying to answer exactly one question:</p><p><strong>Can someone make a better decision because of what you built?</strong></p><p>If yes, good dashboard. If no, you made expensive wall art.</p><h2>So what even is a dashboard</h2><p>Anytime you&#8217;re displaying information to help someone make a decision, congrats, you&#8217;re building a dashboard. Doesn&#8217;t matter if it&#8217;s Tableau, a Google Sheet, or a sticky note on a monitor.</p><p>Back to my kitchen analogy: the warehouse is the kitchen, the dataset is the dish, and the dashboard is the plate that actually hits the table.<mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">It&#8217;s the only part of your work most people will ever see.</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>Nobody walks into the kitchen to compliment your beautiful incremental model. They judge the plate. And if the dataset underneath is garbage, the dashboard is just garbage with a nicer font. I&#8217;ve said this before, I&#8217;ll say it forever.</p><h2>ACES: what a good dashboard looks like</h2><p>The best framework I&#8217;ve found comes from Matt David&#8217;s book <em>How to Design a Dashboard</em> (it&#8217;s free from The Data School, go read it). He says an optimal dashboard is <strong>Accurate, Clear, Empowering, and Succinct</strong>. ACES.</p><p>Here&#8217;s what each one means when you&#8217;re the one building.</p><h3>1. Accurate (A)</h3><p>A dashboard lives and dies on trust. If viewers doubt the accuracy, they will not use it to make decisions. Full stop. And it&#8217;s actually worse than that: one broken dashboard makes people doubt ALL your data. Then everyone crawls back to their personal spreadsheets and now there are six versions of revenue in every meeting. Congratulations, you just un-invented the data warehouse.</p><p>Accuracy breaks down into three things.</p><h4>1.1 Data Quality</h4><p>Two questions to ask about every dashboard:</p><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Is the data displayed correctly?</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>Every single number on that screen needs to be right. Not &#8220;directionally right.&#8221; Right. Viewers can&#8217;t verify your numbers, they take them on faith, and faith does not survive a second incident. And if a dashboard IS broken and you can&#8217;t fix it right now, flag it loudly: slap [BROKEN] or [UNDER REPAIR] in the title. People assume any dashboard they stumble onto is correct unless you tell them otherwise.</p><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Is the data displayed ALL of the data?</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>Most dashboards are not real time. Batch processing says hi. If your pipeline runs every 12 hours and someone opens the dashboard at 11am, today&#8217;s numbers look like the business drove off a cliff. Cue the panic Slack thread. The fix costs you one text box: &#8220;Last updated 6am, refreshes every 12 hours.&#8221; That single line will save you a hundred &#8220;why is revenue down 60%???&#8221; messages that are really just &#8220;the dbt run hasn&#8217;t finished yet.&#8221;</p><h4>1.2 Metric Comprehension</h4><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">A metric has to be understood before it can be interpreted accurately.</mark></strong> &#8220;Revenue&#8221; sounds universal until finance, marketing, and product each calculate it differently. Gross or net? Refunds in or out? Booked or recognized?</p><p>Same with &#8220;conversion rate.&#8221; Converting from what to what? Sessions to leads? Leads to sales? Every team fills in that blank differently and then argues about it in a meeting like it&#8217;s a personality trait.</p><p>The best practice from the book: put the formula, note, or definition for any non-standard metric directly below the metric. On the dashboard itself. Not in a wiki nobody reads, not in your head. If someone has to DM you to know what a number means, that number might as well not exist.</p><h4>1.3 Visualization Design</h4><p>Here&#8217;s the sneaky one: you can display perfectly correct data and still lie with the chart. Most of the crime happens on the Y-axis.</p><p><strong>Bar charts start at zero. No exceptions.</strong> Bars communicate through size. Truncate the axis to start at 80 and a bar of 84 looks double a bar of 82, when the real difference is 2%. That&#8217;s not &#8220;emphasis,&#8221; that&#8217;s bias baked into pixels.</p><p><strong>Line charts get a &#8220;good range&#8221; instead.</strong> Lines communicate through slope, not size, so zooming in is fine, actually encouraged. A line plotted on a 0 to 1M axis when the data lives between 8K and 24K is just a flat line at the bottom of the chart. All the variability, which is the entire point of a line chart, becomes invisible.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21WcR3%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WcR3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png 424w, https://substackcdn.com/image/fetch/$s_!WcR3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png 848w, https://substackcdn.com/image/fetch/$s_!WcR3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png 1272w, https://substackcdn.com/image/fetch/$s_!WcR3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21WcR3%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png" width="1188" height="1504" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1504,&quot;width&quot;:1188,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:231447,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WcR3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png 424w, https://substackcdn.com/image/fetch/$s_!WcR3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png 848w, https://substackcdn.com/image/fetch/$s_!WcR3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png 1272w, https://substackcdn.com/image/fetch/$s_!WcR3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff215c5f1-588a-43fe-bb22-58f049961aef_1188x1504.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>2. Clear (C)</h3><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Clarity is speed to insight.</mark></strong> Every second someone spends squinting, decoding your color scheme, or DMing you &#8220;wait, what am I looking at&#8221; is the dashboard failing at its job. Four things to get right.</p><h4>2.1 Fonts</h4><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Eq0q!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png 424w, https://substackcdn.com/image/fetch/$s_!Eq0q!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png 848w, https://substackcdn.com/image/fetch/$s_!Eq0q!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png 1272w, https://substackcdn.com/image/fetch/$s_!Eq0q!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Eq0q%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png" width="626" height="186" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:186,&quot;width&quot;:626,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:55700,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Eq0q!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png 424w, https://substackcdn.com/image/fetch/$s_!Eq0q!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png 848w, https://substackcdn.com/image/fetch/$s_!Eq0q!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png 1272w, https://substackcdn.com/image/fetch/$s_!Eq0q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fbcdb3f-b7f9-48d2-9c30-9a877a1999d8_626x186.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Fonts should not be decorative.</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>The goal is legibility, this is a dashboard, not a wedding invitation. Boring sans serif (Arial, Helvetica, Verdana), sized so nobody has to squint and nothing is shouting. If anyone ever notices your font choice, you chose wrong.</p><h4>2.2 Colors</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21XSGc%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XSGc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png 424w, https://substackcdn.com/image/fetch/$s_!XSGc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png 848w, https://substackcdn.com/image/fetch/$s_!XSGc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png 1272w, https://substackcdn.com/image/fetch/$s_!XSGc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21XSGc%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png" width="1188" height="438" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:438,&quot;width&quot;:1188,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35887,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!XSGc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png 424w, https://substackcdn.com/image/fetch/$s_!XSGc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png 848w, https://substackcdn.com/image/fetch/$s_!XSGc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png 1272w, https://substackcdn.com/image/fetch/$s_!XSGc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecc0b2bb-3852-4bfc-98e8-743705be5981_1188x438.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Two rules: easy and consistent.</mark></strong></p><p><strong>Easy:</strong> muted tones, easy on the eyes, and no single color visually overweighting the others. Paint one series neon red and everyone reads it as an incident, even when it&#8217;s just &#8220;the West region.&#8221;</p><p><strong>Consistent:</strong> same metric, same color, every chart. If paid search is blue in one chart, it better be blue in every chart, because your brain connects data across charts through color whether you want it to or not. And don&#8217;t freestyle hex codes at 5pm on a Friday, use a color picker tool.</p><h4>2.3 Context</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21zv-M%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zv-M!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png 424w, https://substackcdn.com/image/fetch/$s_!zv-M!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png 848w, https://substackcdn.com/image/fetch/$s_!zv-M!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png 1272w, https://substackcdn.com/image/fetch/$s_!zv-M!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21zv-M%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png" width="1112" height="938" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:938,&quot;width&quot;:1112,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:154275,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zv-M!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png 424w, https://substackcdn.com/image/fetch/$s_!zv-M!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png 848w, https://substackcdn.com/image/fetch/$s_!zv-M!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png 1272w, https://substackcdn.com/image/fetch/$s_!zv-M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb48b1480-0afc-47da-84f1-a4b4db4842d6_1112x938.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">A chart with no context is a Rorschach test.</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>Give people:</p><ul><li><p>A descriptive title. &#8220;Operations Cost over Time by Department&#8221; beats &#8220;Chart 3&#8221;.</p></li><li><p>Categorical labels, so nobody&#8217;s guessing which slice is which.</p></li><li><p>Value labels when it&#8217;s hard to eyeball against the axis.</p></li></ul><p>And the rule from the book that I think about constantly: the more explanation a dashboard needs, the less effective it is.</p><h4>2.4 Layout</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21K0DE%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!K0DE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png 424w, https://substackcdn.com/image/fetch/$s_!K0DE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png 848w, https://substackcdn.com/image/fetch/$s_!K0DE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png 1272w, https://substackcdn.com/image/fetch/$s_!K0DE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21K0DE%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png" width="1196" height="814" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:814,&quot;width&quot;:1196,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:245773,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!K0DE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png 424w, https://substackcdn.com/image/fetch/$s_!K0DE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png 848w, https://substackcdn.com/image/fetch/$s_!K0DE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png 1272w, https://substackcdn.com/image/fetch/$s_!K0DE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfd4c2a1-d19c-4601-9a24-44ccb01104ec_1196x814.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">People in most countries read left to right, top to bottom.</mark></strong> Your dashboard gets read the same way whether you designed for it or not. So design for it: most important number goes top left. Least important goes bottom right, the retirement home for charts nobody asked about.</p><h3>3. Empowering (E)</h3><p>This is the one nobody measures, and it&#8217;s the entire point. <strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Does anyone actually USE this thing to make decisions?</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>The end users are the judges here, not you. Two ways to check.</p><h4>3.1 Usage</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21GzGu%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GzGu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png 424w, https://substackcdn.com/image/fetch/$s_!GzGu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png 848w, https://substackcdn.com/image/fetch/$s_!GzGu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png 1272w, https://substackcdn.com/image/fetch/$s_!GzGu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21GzGu%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png" width="1192" height="548" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:548,&quot;width&quot;:1192,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:57806,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!GzGu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png 424w, https://substackcdn.com/image/fetch/$s_!GzGu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png 848w, https://substackcdn.com/image/fetch/$s_!GzGu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png 1272w, https://substackcdn.com/image/fetch/$s_!GzGu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7849a5fc-d85a-4045-9f01-d37d24332f64_1192x548.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Do they view it regularly?</mark></strong> Most BI tools give you view logs, so actually check them. If views are dropping, don&#8217;t take it personally, take it as data, and go ask:</p><ul><li><p>Is the dashboard still useful to them?</p></li><li><p>Is it missing something critical?</p></li><li><p>Where are they getting their numbers instead? (There is ALWAYS an instead. It&#8217;s usually a spreadsheet. Find it.)</p></li><li><p>What would make this thing serve them better?</p></li></ul><p>A dashboard nobody opens is just compute burning money on a schedule.</p><h4>3.2 Decision Making</h4><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Does it factor into actual decisions?</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);"> </mark>My favorite test in the whole book: ask the viewer, &#8220;if this number went to zero tomorrow, or doubled, what would you do?&#8221;</p><p>If the answer is &#8220;nothing,&#8221; that chart is decoration. Cut it or rethink it. If the answer is &#8220;I&#8217;d reallocate budget by Friday,&#8221; congrats, you built a real dashboard.</p><h3>4. Succinct (S)</h3><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">If your dashboard has stuff hiding below the fold, you have too much stuff.</mark></strong></p><p>The entire value proposition of a dashboard is seeing everything at once so your brain can compare. Human working memory is tiny. The moment someone scrolls, chart 1 evaporates from their head while they read chart 7. At that point you&#8217;ve built a report with extra steps.</p><p>Two questions to ask about every chart on the page:</p><p><strong>Is this data necessary?</strong> Not interesting. Necessary. (See the zero-or-doubled test above.)</p><p><strong>Can it live in a smaller space without losing clarity?</strong> A giant line chart that exists to communicate one number should be a single value stat. Shrink it and free up the real estate.</p><p>And keep it relevant: every chart should relate to the dashboard&#8217;s purpose, and related data should sit close together. A weather widget is useful. A spend forecast is useful. They do not belong on the same dashboard. If people need more granularity, link out to a drill-down dashboard instead of cramming everything onto one screen.</p><h2>The process: Define, Prototype, Build, Deploy</h2><p>ACES tells you what good looks like. This process is how you actually get there. The book breaks it into four steps, <strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Define &#8594; Prototype &#8594; Build &#8594; Deploy</mark></strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">, </mark>and the whole trick is that SQL doesn&#8217;t show up until step three.</p><p>Because here&#8217;s what actually happens in the wild: someone asks for a dashboard, you get excited, you start pulling data, and fifteen charts later you&#8217;ve shipped a Frankenstein wall of bar charts that answers everything in general and nothing in particular. I&#8217;ve done it. You&#8217;ve done it. The fix is boring as hell: slow down at the start.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21dLWJ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dLWJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp 424w, https://substackcdn.com/image/fetch/$s_!dLWJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp 848w, https://substackcdn.com/image/fetch/$s_!dLWJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp 1272w, https://substackcdn.com/image/fetch/$s_!dLWJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21dLWJ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp" width="1022" height="354" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:354,&quot;width&quot;:1022,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:12668,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dLWJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp 424w, https://substackcdn.com/image/fetch/$s_!dLWJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp 848w, https://substackcdn.com/image/fetch/$s_!dLWJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp 1272w, https://substackcdn.com/image/fetch/$s_!dLWJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10380ede-25d0-44f2-8593-cfab6069a495_1022x354.webp 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>1. Define</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21UhV1%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UhV1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png 424w, https://substackcdn.com/image/fetch/$s_!UhV1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png 848w, https://substackcdn.com/image/fetch/$s_!UhV1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png 1272w, https://substackcdn.com/image/fetch/$s_!UhV1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21UhV1%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png" width="984" height="366" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:366,&quot;width&quot;:984,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:167979,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UhV1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png 424w, https://substackcdn.com/image/fetch/$s_!UhV1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png 848w, https://substackcdn.com/image/fetch/$s_!UhV1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png 1272w, https://substackcdn.com/image/fetch/$s_!UhV1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e10d3e-ccd8-4ce9-8462-e2dfae07e49d_984x366.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Figure out who this is for and what decision it supports before you touch a keyboard.</mark></strong></p><p>Every dashboard project has four characters: you (the designer), the audience, the <strong>Point Person</strong> (the one who requested it and owns the business need), and the <strong>Data Gatekeeper</strong> (the data team member who knows the schema and where the bodies are buried; at my company that role is literally me).</p><p>The Point Person will show up with vibes: &#8220;I want visibility into the marketing funnel.&#8221; Vibes are not metrics. Your job is the interrogation: What does success look like? What decision are you trying to make? What would you actually DO if this number doubled?</p><p>Then squeeze every metric into something queryable: formula, aggregation, group by, filters. Write it all down. The book calls this a Metric Spreadsheet, and it&#8217;s exactly as unsexy and as useful as it sounds. &#8220;SUM(amount), grouped by month, excluding free trials&#8221; is a spec. &#8220;Show me revenue&#8221; is a prayer.</p><h3>2. Prototype</h3><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Pen. Paper. No SQL.</mark></strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21dwHE%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F869199f3-83df-476f-a779-265892378b85_990x660.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dwHE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869199f3-83df-476f-a779-265892378b85_990x660.png 424w, https://substackcdn.com/image/fetch/$s_!dwHE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869199f3-83df-476f-a779-265892378b85_990x660.png 848w, https://substackcdn.com/image/fetch/$s_!dwHE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869199f3-83df-476f-a779-265892378b85_990x660.png 1272w, https://substackcdn.com/image/fetch/$s_!dwHE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869199f3-83df-476f-a779-265892378b85_990x660.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21dwHE%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F869199f3-83df-476f-a779-265892378b85_990x660.png" width="990" height="660" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/869199f3-83df-476f-a779-265892378b85_990x660.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:660,&quot;width&quot;:990,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:57243,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869199f3-83df-476f-a779-265892378b85_990x660.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dwHE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869199f3-83df-476f-a779-265892378b85_990x660.png 424w, https://substackcdn.com/image/fetch/$s_!dwHE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869199f3-83df-476f-a779-265892378b85_990x660.png 848w, https://substackcdn.com/image/fetch/$s_!dwHE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869199f3-83df-476f-a779-265892378b85_990x660.png 1272w, https://substackcdn.com/image/fetch/$s_!dwHE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869199f3-83df-476f-a779-265892378b85_990x660.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Not gonna lie, hand-drawing bar charts like a kindergartner feels ridiculous. But a bad idea costs 30 seconds on paper and three days in a BI tool. Sketching also forces you to think about design instead of getting sucked into whether the numbers are right. That part comes later.</p><p>Sketch it, show the Point Person, and ask the uncomfortable questions: Do you actually need to know this? Is the takeaway of each chart obvious? Which chart grabs your eye first, and should it? What&#8217;s missing?</p><p>Then iterate. The goal is a useful dashboard, not defending your first draft. Check your ego at the whiteboard.</p><p>Quick chart-picking cheat sheet while you sketch: single value for headline KPIs, bar for comparing categories, line for trends over time, bar-line when you need both. Keep it to about three chart types per dashboard, viewership actually drops off after that. And 3D charts are banned. Forever.</p><h3>3. Build</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21IEg3%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IEg3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png 424w, https://substackcdn.com/image/fetch/$s_!IEg3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png 848w, https://substackcdn.com/image/fetch/$s_!IEg3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png 1272w, https://substackcdn.com/image/fetch/$s_!IEg3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21IEg3%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png" width="1036" height="912" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:912,&quot;width&quot;:1036,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:233979,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IEg3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png 424w, https://substackcdn.com/image/fetch/$s_!IEg3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png 848w, https://substackcdn.com/image/fetch/$s_!IEg3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png 1272w, https://substackcdn.com/image/fetch/$s_!IEg3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ac96b92-ce3b-4634-ad25-96ab36a2dd72_1036x912.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">NOW you write SQL.</mark></strong></p><p>First, find the data. Three scenarios: you have it, it&#8217;s messy, or it doesn&#8217;t exist. It is almost always messy, so budget time for the boring checks: nulls and blanks, obviously fake values (John Doe, 123-456-7890, birthdays before 1919), outliers, and my personal enemy, category misspellings, because SQL will happily treat &#8216;Hamburger&#8217;, &#8216;hamburger&#8217;, and &#8216;Hanburger&#8217; as three different businesses.</p><p>Whatever you decide to do with the mess (ignore, delete, impute), document the decision so the next person can reproduce your numbers instead of discovering them like ancient ruins.</p><p>If the data straight up doesn&#8217;t exist, you either instrument new tracking (talk to engineering, set expectations, it will take a while) or you use a proxy metric. Be careful with proxies. Measure &#8220;return visits&#8221; as a stand-in for &#8220;customers like us&#8221; and you&#8217;ll slowly start optimizing for visits instead of happiness. Proxy metrics quietly redirect entire companies.</p><p>And get a code review on your queries. You review dbt PRs, review your dashboard SQL too. The Data Gatekeeper exists for exactly this.</p><h3>4. Deploy</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21yEPP%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yEPP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png 424w, https://substackcdn.com/image/fetch/$s_!yEPP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png 848w, https://substackcdn.com/image/fetch/$s_!yEPP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png 1272w, https://substackcdn.com/image/fetch/$s_!yEPP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21yEPP%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png" width="1006" height="382" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/add2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:382,&quot;width&quot;:1006,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:102128,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/210109104?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yEPP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png 424w, https://substackcdn.com/image/fetch/$s_!yEPP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png 848w, https://substackcdn.com/image/fetch/$s_!yEPP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png 1272w, https://substackcdn.com/image/fetch/$s_!yEPP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fadd2c518-6621-4f03-93c6-655c4cd6e13e_1006x382.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Shipping is not the end. It&#8217;s the start of a maintenance contract you didn&#8217;t know you signed.</p><p><strong>Match delivery to decision cadence.</strong> Daily decisions? Scheduled 8am email. Ad hoc decisions? Just share the link, don&#8217;t spam inboxes. Threshold-based decisions? Set an alert, and pick the threshold carefully or you&#8217;ll train everyone to ignore it.</p><p><strong>Then maintain the damn thing.</strong> Schemas change, sources get re-architected, someone renames a column and your chart quietly starts serving garbage with full confidence. Put a recurring review on the calendar: are the queries still right, is anyone still looking, is anything slow enough to optimize. And archive dashboards nobody has viewed in 90 days (after a warning email). Dead dashboards still run queries, and your warehouse pays for every single one.</p><h2>TLDR</h2><p>Stop stressing about which BI tool to learn. Learn this instead:</p><ul><li><p><strong>A dashboard exists to support a decision.</strong> No decision, no dashboard.</p></li><li><p><strong>ACES:</strong> Accurate (data quality + metric definitions + honest axes), Clear (fonts, colors, context, layout), Empowering (usage + the &#8220;would you do anything if this doubled&#8221; test), Succinct (nothing below the fold).</p></li><li><p><strong>Define &#8594; Prototype &#8594; Build &#8594; Deploy.</strong> Design on paper before you write a single line of SQL.</p></li><li><p>The dashboard is the plate that hits the table. Serve a dish, not a buffet.</p></li></ul><p>The tool is just where the buttons are. This is the actual job.</p>]]></content:encoded></item><item><title><![CDATA[I re-learn Spark (Again) - Part 2]]></title><description><![CDATA[This time it finally clicks]]></description><link>https://markphammm.substack.com/p/i-re-learn-spark-again-part-2</link><guid isPermaLink="false">https://markphammm.substack.com/p/i-re-learn-spark-again-part-2</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Sun, 02 Aug 2026 00:10:29 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F809cd217-c82f-4dc7-9df4-7d78dbfb83e7_567x406.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve been reading Vu Trinh Spark articles for, whoever knows how many times and still couldn&#8217;t gasp the concepts. So today, with an interview coming up, I decided to use the white board method, write everything out and everything that i&#8217;ve read before just, click.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21usRq%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!usRq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 424w, https://substackcdn.com/image/fetch/$s_!usRq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 848w, https://substackcdn.com/image/fetch/$s_!usRq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!usRq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21usRq%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg" width="1456" height="1941" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1941,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:255571,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/207997209?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!usRq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 424w, https://substackcdn.com/image/fetch/$s_!usRq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 848w, https://substackcdn.com/image/fetch/$s_!usRq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!usRq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In case you wonder wtf is this. Hey, I&#8217;ve never said that my drawing (or my hand writing) was any good. But well, it got the job done. Things finally &#8220;click&#8221; when you spent time draw it out and tried to understand everything through a wall of text. I STRONGLY recommend this method for anyone who&#8217;s studying Data Engineering concepts or any technical concepts in general. Grab a dashboard, imagine you&#8217;re teaching someone, then explain everything clearly.</p><p>This article is a way that I revise my knowledge about Spark for people who have tried to learn Spark before but just, couldn&#8217;t get it (me before this article). I not the best at drawing to I will just let ChatGPT do the work. </p><p>I also assume that you understand the fundamental different of distributed computing (why it exists) and how Spark is different from Hadoop (writing to disk vs processing in memory). This article is a bit more deep dive on the architecture of Spark itself. For each topic, I would go over the details and highlight the key TLDR by bolding it.</p><p>Well, enough said, let&#8217;s dig into the flow how I spend 3 hours on this topic. This article will cover the first part of shuffling - Aggregation</p><h1>Shuffling -  Aggregation</h1><p>There are two main type of aggregation in Spark</p><ul><li><p><strong>Hash aggregate </strong></p></li><li><p><strong>Sort aggregate</strong></p></li></ul><h1>The key idea</h1><ul><li><p>HashAggregate: <em><strong>"I need to remember every group because groups can appear anywhere."</strong></em></p></li></ul><ul><li><p>SortAggregate: <em><strong>&#8220;I only need to remember the current group because sorting guarantees I will never see it again after I move on.&#8221;</strong></em></p></li></ul><h1>Tradeoff</h1><p>So SortAggregate trades:</p><pre><code><code>extra CPU (sorting)
        +
        &#8595;
less memory usage</code></code></pre><p>for- HashAggregate:</p><pre><code><code>more memory
        +
        &#8595;
faster aggregation</code></code></pre><p>This is why Spark has both strategies. They solve the same aggregation problem but with different memory/CPU tradeoffs.</p><h1>Key idea of Aggregation</h1><p>Aggregation (<code>GROUP BY</code>, <code>COUNT</code>, <code>SUM</code>, etc.) typically happens in three stages:</p><ol><li><p><strong>Partial aggregation</strong> &#8211; Each input partition aggregates its local rows.</p><ol><li><p>Usually uses <strong>Hash Aggregate</strong> when possible.</p></li><li><p>Falls back to <strong>Sort Aggregate</strong> if hashing isn&#8217;t appropriate or memory is constrained.</p></li></ol></li><li><p><strong>Shuffle</strong> &#8211; Data is repartitioned by the grouping key.</p></li><li><p><strong>Final aggregation</strong> &#8211; Partial results are merged to produce the final output.</p></li></ol><h1>Hash Aggregation</h1><p>Let&#8217;s understand Hash Aggregation by having a super simple example.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT state, COUNT(*)
FROM customers
GROUP BY state;</code></pre></div><h2>1. The input partition:</h2><p>Given that our input partition look like the following:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Partition 1
-----------
CA
TX
CA

Partition 2
-----------
CA
WA
TX

Partition 3
-----------
TX
WA
CA</code></pre></div><h2>2. Partial Aggregation Step </h2><p>Let&#8217;s just take a look at partition 1: </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Partition 1
-----------
CA
TX
CA</code></pre></div><p>the expected output would be</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">CA: 2
TX: 1</code></pre></div><p>The question is how does Spark do this, with data up to billions of rows? In hash map case, it will go over each individual values and build the hash map in memory.</p><p><strong>Start with an empty HashMap:</strong></p><pre><code><code>{}</code></code></pre><p><strong>Read </strong><code>CA</code><strong>:</strong></p><p><code>CA</code> does not exist &#8594; create entry</p><pre><code><code>{
  CA: 1
}
</code></code></pre><p><strong>Read </strong><code>TX</code><strong>:</strong></p><p><code>TX</code> does not exist &#8594; create entry</p><pre><code><code>{
  CA: 1,
  TX: 1
}
</code></code></pre><p><strong>Read </strong><code>CA</code><strong>:</strong></p><p><code>CA</code> already exists &#8594; update count</p><pre><code><code>{
  CA: 2,
  TX: 1
}
</code></code></pre><p><strong>Final Partial Aggregation:</strong></p><pre><code><code>CA &#8594; 2
TX &#8594; 1</code></code></pre><p>This same process happens independently on all input partitions before the shuffle stage. So at the end, each partition will have its own local HashMap:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Partition 1
-----------
CA &#8594; 2
TX &#8594; 1


Partition 2
-----------
CA &#8594; 1
WA &#8594; 1
TX &#8594; 1


Partition 3
-----------
TX &#8594; 1
WA &#8594; 1
CA &#8594; 1</code></pre></div><h2>3. Shuffle Step</h2><p><strong>The problem:</strong></p><blockquote><p>The same key exists across multiple partitions. <strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Spark needs to bring all values for the same key together before it can do the final aggregation.</mark></strong></p></blockquote><p>For example:</p><pre><code><code>CA &#8594; 2   (from Partition 1)
CA &#8594; 1   (from Partition 2)
CA &#8594; 1   (from Partition 3)</code></code></pre><p>must end up in the same place.</p><p><strong>The solutions</strong></p><p>Therefore, shuffle is needed. Spark usually applies a <strong>partitioning function</strong> (usually hash partitioning) on the grouping key:</p><pre><code><code>shuffle_partition = hash(state) % number_of_shuffle_partitions -- default to 200</code></code></pre><p>Spark default the  <code>number_of_shuffle_partitions </code>to 200, but for the sake of simplicity, let&#8217;s only look at 3. We will talk about how AQE coalesce these (shuffle) partitions into smaller number of shuffle partitions in later part.</p><p>Example with 3 shuffle partitions:</p><pre><code><code>hash(CA) % 3 &#8594; Shuffle Partition 1
hash(TX) % 3 &#8594; Shuffle Partition 2
hash(WA) % 3 &#8594; Shuffle Partition 3</code></code></pre><p>Now Spark redistributes the data:</p><pre><code><code>Shuffle Partition 1
-------------------
CA &#8594; 2
CA &#8594; 1
CA &#8594; 1


Shuffle Partition 2
-------------------
TX &#8594; 1
TX &#8594; 1
TX &#8594; 1


Shuffle Partition 3
-------------------
WA &#8594; 1
WA &#8594; 1</code></code></pre><p>The key idea:</p><blockquote><p><strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Shuffle does not aggregate. It only moves data so that all rows with the same grouping key are located in the same partition.</mark></strong></p></blockquote><h2>4. Final Aggregation Step</h2><p>After the shuffle phase, all records with the same grouping key are now in the same shuffle partition:</p><pre><code><code>Shuffle Partition 1
-------------------
CA &#8594; 2
CA &#8594; 1
CA &#8594; 1


Shuffle Partition 2
-------------------
TX &#8594; 1
TX &#8594; 1
TX &#8594; 1


Shuffle Partition 3
-------------------
WA &#8594; 1
WA &#8594; 1</code></code></pre><p>Now Spark performs the <strong>final aggregation</strong>. Each shuffle partition builds another HashMap and combines the partial results:</p><pre><code><code>Shuffle Partition 1

Start with empty HashMap:

{}

Read CA &#8594; 2:

{
  CA: 2
}

Read CA &#8594; 1:

{
  CA: 3
}

Read CA &#8594; 1:

{
  CA: 4
}</code></code></pre><p>Final result from Shuffle Partition 1:</p><pre><code><code>CA &#8594; 4</code></code></pre><p>The same process happens for the other shuffle partitions:</p><pre><code><code>Shuffle Partition 2

TX &#8594; 1 + 1 + 1 = 3


Shuffle Partition 3

WA &#8594; 1 + 1 = 2</code></code></pre><p>Final aggregated dataframe:</p><pre><code><code>State | Count
-------------
CA    | 4
TX    | 3
WA    | 2</code></code></pre><h1>Sort Aggregation</h1><p>Now that we understand Hash Aggregation, let&#8217;s focus on the key difference.</p><p>With Hash Aggregation, Spark builds an in-memory HashMap:</p><pre><code><code>state &#8594; count</code></code></pre><p>Example:</p><pre><code><code>{
  CA: 2,
  TX: 1
}</code></code></pre><p>With Sort Aggregation, <strong><mark data-color="#ffff00" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">Spark does not build a HashMap.</mark></strong></p><p>Instead, Spark:</p><ol><li><p>Sorts the rows by the grouping key.</p></li><li><p>Scans the sorted rows and aggregates consecutive values.<br></p></li></ol><p><strong>Example:</strong></p><p>Before sorting:</p><pre><code><code>CA
TX
CA</code></code></pre><p>After sorting:</p><pre><code><code>CA
CA
TX</code></code></pre><p>Spark then scans the sorted data:</p><pre><code><code>Read CA:
First value &#8594; start a new group

Current Group:
CA &#8594; 1

Read CA:
Same key as previous row &#8594; increment count

Current Group:
CA &#8594; 2

Read TX:
New key detected &#8594; finish previous group and start a new one

Output:
CA &#8594; 2

Current Group:
TX &#8594; 1</code></code></pre><p>End of partition:</p><p>Final Partial Aggregation:</p><pre><code><code>CA &#8594; 2
TX &#8594; 1</code></code></pre><p>The shuffle and final aggregation steps are the same as Hash Aggregation. With this, Spark does not have to keep a hash map in memory, but the trade off is that it will need to sort the data in each input partition first</p><h1>Comparison</h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%214hp4%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4hp4!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 424w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 848w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 1272w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%214hp4%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_lossy%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif" width="1280" height="532" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:532,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Apache Spark Aggregation Methods: Hash-based Vs. Sort-based&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Apache Spark Aggregation Methods: Hash-based Vs. Sort-based" title="Apache Spark Aggregation Methods: Hash-based Vs. Sort-based" srcset="https://substackcdn.com/image/fetch/$s_!4hp4!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 424w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 848w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 1272w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>Hash Aggregate:</strong></p><ul><li><p><strong>HashAggregate</strong><span> builds an in-memory hash table keyed by the group key. The value is an aggregation buffer updated in-place (e.g. running SUM). No sorting needed &#8212; fast and preferred.</span></p></li><li><p><span>For </span><code>HashAggregate</code><span> to work efficiently, Spark uses the </span><strong>UnsafeRow</strong><span> format &#8212; an off-heap binary representation that bypasses the JVM garbage collector (part of Project Tungsten).</span></p></li><li><p><span>This format works best with </span><strong>fixed-size types</strong><span> that can be updated in-place in the hash table: integers, longs, doubles, dates, booleans.</span></p></li></ul><p><strong>Sort aggregation</strong></p><ul><li><p><strong>SortAggregate</strong><span> sorts the input data by the group key first, then iterates over it linearly to accumulate groups. Slower because of the sort pass.</span></p></li><li><p><strong>Variable-length types like String are not easily mutable in a fixed-size memory slot.</strong><span> When any aggregation buffer holds a String (or ArrayType, MapType), Spark falls back to SortAggregate.</span></p></li></ul><p>Hope this is all helpful :) In the next part we will still gonna cover shuffling, but with a different beast - Joins</p>]]></content:encoded></item><item><title><![CDATA[I re-learn Spark (Again) - Part 1]]></title><description><![CDATA[This time it finally clicks]]></description><link>https://markphammm.substack.com/p/i-re-learn-spark-again-part-1</link><guid isPermaLink="false">https://markphammm.substack.com/p/i-re-learn-spark-again-part-1</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Thu, 23 Jul 2026 02:52:50 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64fbe2bd-34f4-45a8-99f9-c4f8414eeaa5_567x406.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve been reading Vu Trinh Spark articles for, whoever knows how many times and still couldn&#8217;t gasp the concepts. So today, with an interview coming up, I decided to use the white board method, write everything out and everything that i&#8217;ve read before just, click.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21usRq%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!usRq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 424w, https://substackcdn.com/image/fetch/$s_!usRq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 848w, https://substackcdn.com/image/fetch/$s_!usRq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!usRq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21usRq%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg" width="1456" height="1941" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1941,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:255571,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/207997209?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!usRq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 424w, https://substackcdn.com/image/fetch/$s_!usRq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 848w, https://substackcdn.com/image/fetch/$s_!usRq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!usRq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc53da4a-be44-421b-9035-bef6fe2aff76_1536x2048.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In case you wonder wtf is this. Hey, I&#8217;ve never said that my drawing (or my hand writing) was any good. But well, it got the job done. Things finally &#8220;click&#8221; when you spent time draw it out and tried to understand everything through a wall of text. I STRONGLY recommend this method for anyone who&#8217;s studying Data Engineering concepts or any technical concepts in general. Grab a dashboard, imagine you&#8217;re teaching someone, then explain everything clearly.</p><p>This article is a way that I revise my knowledge about Spark for people who have tried to learn Spark before but just, couldn&#8217;t get it (me before this article). I not the best at drawing to I will just let ChatGPT do the work. </p><p>I also assume that you understand the fundamental different of distributed computing (why it exists) and how Spark is different from Hadoop (writing to disk vs processing in memory). This article is a bit more deep dive on the architecture of Spark itself. For each topic, I would go over the details and highlight the key TLDR by bolding it.</p><p>Well, enough said, let&#8217;s dig into the flow how I spend 3 hours on this topic.</p><h1>The Input</h1><p>Think about the input similar to your</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">pandas.read_csv()</code></pre></div><p>Similarly, Spark does the same thing - but at distributed scale. It can read data from Parquet files, CSVs, JSON files, Kafka streams, databases via JDBC, cloud object storage (such as S3 or GCS), and lakehouse formats like Delta Lake and Iceberg. Regardless of the source, Spark loads the data into a DataFrame with a unified API. Here are a few examples:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">-- local files
df = spark.read.csv("customers.csv", header=True)

-- s3 bucket
spark.read.csv("s3://bucket/data.csv")

-- kakfa stream
spark.readStream \
    .format("kafka") \
    .option("subscribe", "orders") \
    .load()
</code></pre></div><p>Quite similar to Pandas right, I guess there&#8217;s nothing too crazy here. One thing that I wanted to note is that <strong>Parquet on disk</strong><span> is compressed (SNAPPY, dictionary encoding). </span><strong>In-memory</strong><span> DataFrame when reading it could be ~8-10x larger.</span></p><p><em><strong><span>Key idea: Spark can read from mutiple sources and create a Dataframe (similar to what pandas does)</span></strong></em></p><h1>The Input partition</h1><p>Again, we are talking about the <strong>INPUT</strong> partition. When I first learn Spark, I was easily confuse about when the articles talk about &#8220;partitions&#8221;. Turn out there&#8217;s <strong>input partitions </strong>and <strong>shuffle partitions. </strong>These are 2 really different concepts. </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">                         +-------------+
                        /|  partition  |
                       / +-------------+
                      /
+---------+          /   +-------------+
|         |---------+---&gt;|  partition  |
|  input  |          \   +-------------+
|         |           \
+---------+            \ +-------------+
 parquet                \|  partition  |
                         +-------------+
                          \
                           \+-------------+
                            |  partition  |
                            +-------------+</code></pre></div><p>When Spark reads data into a DataFrame, it <strong>splits the data into partitions. </strong> You don&#8217;t need to know what these partitions does yet! Just remember that:</p><p><em><strong>Spark never processes the DataFrame as one giant object. It always breaks it into multiple input partitions</strong></em></p><p>You can actually control how many input partitions are created by configuring <code>spark.sql.files.maxPartitionBytes</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">spark.conf.set("spark.sql.files.maxPartitionBytes", 256 * 1024 * 1024) # 256MB

# default is 128MB</code></pre></div><p>So for example, let&#8217;s say we have a 2GB dataset and we set the maxPartitionBytes, we will have </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">2,048 MiB / 128 MiB = 16 input partitions # if max size is 128
2,048 MiB / 256 MiB = 8 input partitions # if max size is 256</code></pre></div><p>Nice, what happen if it&#8217;s 3000MB? Spark will round up and maximize the size of each input partition.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">3,000 / 256 = 11.71875

=&gt; 12 input partitions

Partitions 1&#8211;11: ~256 MiB
Partition 12:     ~184 MiB</code></pre></div><p>There will always be trade of between high vs low number of input partitions</p><ul><li><p><strong>High:</strong> fewer tasks but more memory per task and higher spill risk (don&#8217;t worry we will talk about this later)</p></li><li><p><strong>Low:</strong> Scheduling overhead - More tasks mean more scheduling (we will also talk about this later)</p></li></ul><h1>Jobs, Stages, and Tasks</h1><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">        +------------+
        |    Jobs    |
        +------------+
              |
              v
        +------------+
        |   Stages   |
        +------------+
              |
              v
        +------------+
        |   Tasks    |  &lt;--- smallest
        +------------+       executable
                             step</code></pre></div><p>When you execute an action (show(), collect(), write(), count()), Spark creates a job. This job will be submited to the Spark master (dw about this).</p><p>Then the job will be split into stages, and each stages will be split into tasks. <strong>Tasks is a smallest executable step in Spark</strong></p><p><strong>Order: Job &#8594; Stages &#8594; Tasks</strong></p><h1>The workers</h1><p>I also have a hard time understanding this concepts at first, but turns out it&#8217;s not that complicated. The worker are just a bunch of resources that&#8217;s <strong>INDEPENDENT</strong> from the spark cluster itself. </p><p>Spark cluster can utilize some (or all of these resources) but they exist independently from the cluster itself. For example: Imagine two EC2 instances, each with 8 vCPUs and 16 GB of RAM. The instances exist before any Spark application starts and remain running after it finishes. They may also run other workloads. </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">+----------------------+  +----------------------+
| EC2 worker 1         |  | EC2 worker 2         |
| Available resources  |  | Available resources  |
+----------------------+  +----------------------+</code></pre></div><p><em><strong>Do not confuse workers with executors</strong></em></p><p>The EC2 instances are the underlying worker machines. Executors are temporary processes created for a particular Spark application. </p><h1>The Spark Submit</h1><p><code>spark-submit</code> sends your application code and configuration to the cluster. It is the command that <strong>starts a Spark application</strong>, not the component that processes the data.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">spark-submit \
  --master spark://master-host:7077 \
  app.py</code></pre></div><p><code>spark-submit</code><strong> submits your application and configuration to the cluster manager.</strong> In Spark Standalone, that cluster manager is the Spark Master. The driver then requests executors and coordinates execution of your code across them.</p><p>For other cluster manager, configuration might differ</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Spark Standalone -&gt; Spark Master
Amazon EMR       -&gt; YARN ResourceManager
Kubernetes       -&gt; Kubernetes API</code></pre></div><h1>The Spark Cluster (Spark Master, Driver, Executor)</h1><p>A Spark cluster usually contain of 3 components:</p><ul><li><p>Spark Master: resource manager</p></li><li><p>Driver: brain of one Spark application</p></li><li><p>Executor: do the work</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Submit job
    |
    v
+------------------+       requests resources        +------------------+
|                  | ------------------------------&gt; | Executor JVM #1  |
|  Spark Master    |                                 +------------------+
|                  | ------------------------------&gt; +------------------+
+------------------+                                 | Executor JVM #2  |
                                                     +------------------+
          |
          | starts / manages application
          v
+------------------+
| Driver JVM       |
|                  |
| Schedules tasks  | ------------------------------&gt; Executors
+------------------+</code></pre></div><h2>1. Spark master</h2><pre><code><code>Spark Master = cluster resource manager</code></code></pre><p>It knows which <strong>worker machines</strong> have available CPU and memory. It allocates those resources to Spark applications.</p><p>The name <strong>Spark Master</strong> specifically applies to Spark Standalone. On EMR, this role is usually handled by <strong>YARN ResourceManager</strong> instead. </p><h2>2. Driver (JVM process)</h2><pre><code><code>Driver = brain of one Spark application</code></code></pre><p>The driver is a JVM process. In cluster mode it usually runs on a worker node. In client mode it runs on the machine that submitted the application.</p><ul><li><p>Runs your <code>main</code> Python or Scala code</p></li><li><p>Converts operations into jobs, stages, and tasks</p></li><li><p>Requests executors</p></li><li><p>Sends tasks to executors</p></li><li><p>Collects task results and status</p></li></ul><p>There is normally one driver for each Spark application.</p><h2>3. Executors (JVM process)</h2><pre><code><code>Executor = worker process for one Spark application</code></code></pre><p>An executor is a JVM process launched on a worker node for a specific Spark application.</p><ul><li><p>Run tasks</p></li><li><p>Process partitions</p></li><li><p>Cache intermediate data</p></li><li><p>Send results and status back to the driver</p></li><li><p>An application can have many executors, and a worker machine can run multiple executor processes. Executors belong to a particular Spark application and disappear when that application ends.</p></li></ul><h1>Ways to run a Spark application</h1><p>Since we have just cover the 3 main component of SparkStandalone (the Spark Master, Driver, and Executors) let&#8217;s take a look at 3 different ways to run </p><h3>1. Local mode</h3><p>The driver and executors run on the same machine. No Spark cluster is used.</p><pre><code><code>One machine
+--------------------------+
| Driver                    |
| Executors / worker threads|
+--------------------------+</code></code></pre><p>Example:</p><pre><code><code>spark-submit --master local[*] app.py</code></code></pre><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Your Laptop
&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;

spark-submit
      |
      |
      &#9660;

Spark Driver JVM
&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
|
&#9500;&#9472;&#9472; SparkContext
&#9500;&#9472;&#9472; DAG Scheduler
&#9500;&#9472;&#9472; Task Scheduler
|
&#9500;&#9472;&#9472; Executor (inside same JVM)
|      |
|      &#9500;&#9472;&#9472; Thread 1 &#8594; Task
|      &#9500;&#9472;&#9472; Thread 2 &#8594; Task
|      &#9500;&#9472;&#9472; Thread 3 &#8594; Task
|      &#9492;&#9472;&#9472; Thread 4 &#8594; Task
|
&#9492;&#9472;&#9472; Memory / Cache


(no cluster manager)
(no workers)
(no remote executors)</code></pre></div><h3>2. Client mode </h3><p>The driver runs on the machine where <code>spark-submit</code> is executed. The executors run on the worker nodes in the cluster.</p><pre><code><code>Client machine                 Spark cluster
+-------------+               +-------------+
| Driver      | ------------&gt; | Executors   |
+-------------+               +-------------+</code></code></pre><p>The client machine must remain connected while the application is running.</p><h3>3. Cluster mode</h3><p>The driver runs inside the Spark cluster, usually on one of the worker nodes. The machine that runs <code>spark-submit</code> can disconnect after submitting the application.</p><pre><code><code>Submission machine             Spark cluster
+-------------+               +----------------------+
| spark-submit| ------------&gt; | Driver + Executors   |
+-------------+               +----------------------+</code></code></pre><h1>The Executors (Cores and memory)</h1><p>Let&#8217;s say we have 2 8CPU cores, 16GB RAM VM. The Spark application requests the following executor configuration from the <strong>cluster manager.</strong></p><ul><li><p><code>spark.executor.instances=2 &#8594; </code>2 executor JVMs across the cluster.</p></li><li><p><code>spark.executor.cores=4 &#8594;</code> 4 cores per executor</p></li><li><p><code>spark.executor.memory=8g</code> <code>&#8594;</code> 8 GB memory allocate to each executor</p></li><li><p><code>spark.cores.max=8 &#8594;</code><span> caps total cores (standalone Spark is greedy by default).</span></p></li></ul><p>Here would be the architecture diagram</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">                         Spark Standalone cluster

+----------------------------------+    +----------------------------------+
| Worker VM 1                      |    | Worker VM 2                      |
| 8 CPU cores, 16 GB RAM           |    | 8 CPU cores, 16 GB RAM           |
|                                  |    |                                  |
| +------------------------------+ |    | +------------------------------+ |
| | Executor JVM 1               | |    | | Executor JVM 2               | |
| | 4 CPU cores                  | |    | | 4 CPU cores                  | |
| | 8 GB memory                  | |    | | 8 GB memory                  | |
| +------------------------------+ |    | +------------------------------+ |
|                                  |    |                                  |
| Spare: 4 CPU cores, 8 GB RAM     |    | Spare: 4 CPU cores, 8 GB RAM     |
+----------------------------------+    +----------------------------------+</code></pre></div><p>We can also set</p><ul><li><p><code>spark.task.cpus=1</code><span> (default, not in code) &#8594; Number of cores per task. Usually 1 core will handle 1 tasks </span></p></li></ul><p><em><strong>1 input partitions &#8594; 1 task &#8594; handles by 1 executor</strong></em></p><p>Therefore in the example above, we will have 8 concurrent tasks, each task handle a partition in a stage:</p><ul><li><p>2 executor * 4 cores = 8 concurrent tasks (assuming 1 task is handled by 1 core)</p></li></ul><h1>Memory Per Executor</h1><h3>Memory overhead</h3><p>So let&#8217;s say you request 8gb of memory for a executor</p><pre><code><code>spark.executor.memory = 8g</code></code></pre><p>This means:</p><blockquote><p>Each executor JVM gets <strong>8 GB of JVM heap memory</strong>.</p></blockquote><p>It does <strong>not</strong> mean the whole container/process only uses 8 GB. There is also memory overhead. </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Executor Container Memory
================================================

spark.executor.memory
(JVM Heap)
8 GB

+ 

spark.executor.memoryOverhead
(Off-heap / non-JVM)
~0.1 &#215; executor memory
(minimum 384 MB)

================================================

Total memory requested from worker
&#8776; 8.8 GB</code></pre></div><h3>Outside the JVM Heap</h3><p>Outside JVM heap, there are memory buffer includes:</p><ul><li><p>Python worker process (PySpark)</p></li><li><p>Arrow</p></li><li><p>Native libraries</p></li><li><p>JVM native memory</p></li><li><p>Thread stacks</p></li><li><p>Network buffers</p></li></ul><h3>Inside the JVM Heap</h3><p>Inside the &#8220;8gb&#8221; itself, Spark divides the heap into two major regions - <strong>Spark Memory </strong>and <strong>User memory</strong> </p><ul><li><p><strong>Spark memory</strong></p><ul><li><p>Execution Memory</p><ul><li><p>HashAggregate</p></li><li><p>Sort</p></li><li><p>Join</p></li><li><p>Shuffle</p></li></ul></li><li><p>Storage Memory</p><ul><li><p>Cached DataFrames/RDDs</p></li><li><p>Broadcast variables</p></li></ul></li></ul></li><li><p><strong>User memory</strong></p><ul><li><p>Java/Python objects</p></li><li><p>UDF variables</p></li><li><p>Temporary collections (<code>ArrayList</code>, <code>HashMap</code>, etc.)</p></li><li><p>Driver/executor metadata</p></li><li><p>Third-party libraries</p></li></ul></li></ul><pre><code><code>JVM Heap (8 GB)
================================

Spark Memory (60%)
Execution + Storage

&#8776; 4.8 GB


User Memory (40%)
User objects, metadata, UDFs, etc.

&#8776; 3.2 GB</code></code></pre><p>The ratio:</p><pre><code><code>spark.memory.fraction = 0.6</code></code></pre><p>controls the Spark region.</p><h3>Borrowing </h3><p>Spark Memory (~60% of the heap) is a <strong>shared pool</strong> split into:</p><ul><li><p><strong>Execution Memory</strong> &#8594; HashAggregate, joins, sort, shuffle</p></li><li><p><strong>Storage Memory</strong> &#8594; <code>cache()</code>, <code>persist()</code>, broadcast</p></li></ul><p><em><strong>There fore, Execution can borrow unused Storage memory and Storage can use free memory when Execution isn&#8217;t using it.</strong></em></p><h1>Outro</h1><p>Today I spent several hours learning Spark, hope this help everyone</p><p></p>]]></content:encoded></item><item><title><![CDATA[Leetcode Series: Analyze Organization Hierarchy ]]></title><description><![CDATA[Not gonna lie Recursive CTEs always threw me off]]></description><link>https://markphammm.substack.com/p/leetcode-series-analyze-organization</link><guid isPermaLink="false">https://markphammm.substack.com/p/leetcode-series-analyze-organization</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Mon, 20 Jul 2026 21:20:07 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21wbM1%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>TLDR:</h1><p>Recursive CTEs is hard, and I&#8217;m trying my best to explain it to you.</p><h1>Question:</h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21wbM1%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wbM1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png 424w, https://substackcdn.com/image/fetch/$s_!wbM1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png 848w, https://substackcdn.com/image/fetch/$s_!wbM1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png 1272w, https://substackcdn.com/image/fetch/$s_!wbM1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21wbM1%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png" width="1008" height="504" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:504,&quot;width&quot;:1008,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;New Leetcode Feature in PearAI - Supercharge Your Interview Prep With AI -  PearAI&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="New Leetcode Feature in PearAI - Supercharge Your Interview Prep With AI -  PearAI" title="New Leetcode Feature in PearAI - Supercharge Your Interview Prep With AI -  PearAI" srcset="https://substackcdn.com/image/fetch/$s_!wbM1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png 424w, https://substackcdn.com/image/fetch/$s_!wbM1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png 848w, https://substackcdn.com/image/fetch/$s_!wbM1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png 1272w, https://substackcdn.com/image/fetch/$s_!wbM1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6806ac6b-5da9-4bb9-bad1-2661dc1469ff_1008x504.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Here&#8217;s the <strong>Leetcode</strong> <a href="iframe.php?url=https%3A%2F%2Fleetcode.com%2Fproblems%2Fanalyze-organization-hierarchy%2Fdescription%2F">question</a>: Unfortunately you will need a premium account to access it.</p><p>Write a solution to analyze the organizational hierarchy and answer the following:</p><ol><li><p><strong>Hierarchy Levels:</strong> For each employee, determine their level in the organization (CEO is level <code>1</code>, employees reporting directly to the CEO are level <code>2</code>, and so on).</p></li><li><p><strong>Team Size:</strong> For each employee who is a manager, count the total number of employees under them (direct and indirect reports).</p></li><li><p><strong>Salary Budget:</strong> For each manager, calculate the total salary budget they control (sum of salaries of all employees under them, including indirect reports, plus their own salary).</p></li></ol><p>Return <em>the result table ordered by the result ordered by <strong>level</strong> in <strong>ascending</strong> order, then by <strong>budget</strong> in <strong>descending</strong> order, and finally by <strong>employee_name</strong> in <strong>ascending</strong> order.</em></p><p><em>The result format is in the following example.</em></p><pre><code>+-------------+---------------+------------+--------+-------------+
| employee_id | employee_name | manager_id | salary | department  |
+-------------+---------------+------------+--------+-------------+
| 1           | Alice         | null       | 12000  | Executive   |
| 2           | Bob           | 1          | 10000  | Sales       |
| 3           | Charlie       | 1          | 10000  | Engineering |
| 4           | David         | 2          | 7500   | Sales       |
| 5           | Eva           | 2          | 7500   | Sales       |
| 6           | Frank         | 3          | 9000   | Engineering |
| 7           | Grace         | 3          | 8500   | Engineering |
| 8           | Hank          | 4          | 6000   | Sales       |
| 9           | Ivy           | 6          | 7000   | Engineering |
| 10          | Judy          | 6          | 7000   | Engineering |
+-------------+---------------+------------+--------+-------------+</code></pre><p><strong>Output:</strong></p><pre><code>+-------------+---------------+-------+-----------+--------+
| employee_id | employee_name | level | team_size | budget |
+-------------+---------------+-------+-----------+--------+
| 1           | Alice         | 1     | 9         | 84500  |
| 3           | Charlie       | 2     | 4         | 41500  |
| 2           | Bob           | 2     | 3         | 31000  |
| 6           | Frank         | 3     | 2         | 23000  |
| 4           | David         | 3     | 1         | 13500  |
| 7           | Grace         | 3     | 0         | 8500   |
| 5           | Eva           | 3     | 0         | 7500   |
| 9           | Ivy           | 4     | 0         | 7000   |
| 10          | Judy          | 4     | 0         | 7000   |
| 8           | Hank          | 4     | 0         | 6000   |
+-------------+---------------+-------+-----------+--------+</code></pre><p><strong>Explanation:</strong></p><ul><li><p><strong>Organization Structure:</strong></p><ul><li><p>Alice (ID: 1) is the CEO (level 1) with no manager</p></li><li><p>Bob (ID: 2) and Charlie (ID: 3) report directly to Alice (level 2)</p></li><li><p>David (ID: 4), Eva (ID: 5) report to Bob, while Frank (ID: 6) and Grace (ID: 7) report to Charlie (level 3)</p></li><li><p>Hank (ID: 8) reports to David, and Ivy (ID: 9) and Judy (ID: 10) report to Frank (level 4)</p></li></ul></li><li><p><strong>Level Calculation:</strong></p><ul><li><p>The CEO (Alice) is at level 1</p></li><li><p>Each subsequent level of management adds 1 to the level</p></li></ul></li><li><p><strong>Team Size Calculation:</strong></p><ul><li><p>Alice has 9 employees under her (the entire company except herself)</p></li><li><p>Bob has 3 employees (David, Eva, and Hank)</p></li><li><p>Charlie has 4 employees (Frank, Grace, Ivy, and Judy)</p></li><li><p>David has 1 employee (Hank)</p></li><li><p>Frank has 2 employees (Ivy and Judy)</p></li><li><p>Eva, Grace, Hank, Ivy, and Judy have no direct reports (team_size = 0)</p></li></ul></li><li><p><strong>Budget Calculation:</strong></p><ul><li><p>Alice&#8217;s budget: Her salary (12000) + all employees&#8217; salaries (72500) = 84500</p></li><li><p>Charlie&#8217;s budget: His salary (10000) + Frank&#8217;s budget (23000) + Grace&#8217;s salary (8500) = 41500</p></li><li><p>Bob&#8217;s budget: His salary (10000) + David&#8217;s budget (13500) + Eva&#8217;s salary (7500) = 31000</p></li><li><p>Frank&#8217;s budget: His salary (9000) + Ivy&#8217;s salary (7000) + Judy&#8217;s salary (7000) = 23000</p></li><li><p>David&#8217;s budget: His salary (7500) + Hank&#8217;s salary (6000) = 13500</p></li><li><p>Employees with no direct reports have budgets equal to their own salary</p></li></ul></li></ul><p><strong>Note:</strong></p><ul><li><p>The result is ordered first by level in ascending order</p></li><li><p>Within the same level, employees are ordered by budget in descending order then by name in ascending order</p></li></ul><h1>Solution </h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JpRi%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JpRi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png 424w, https://substackcdn.com/image/fetch/$s_!JpRi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png 848w, https://substackcdn.com/image/fetch/$s_!JpRi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png 1272w, https://substackcdn.com/image/fetch/$s_!JpRi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JpRi%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png" width="590" height="435" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:435,&quot;width&quot;:590,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:181622,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/207826833?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!JpRi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png 424w, https://substackcdn.com/image/fetch/$s_!JpRi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png 848w, https://substackcdn.com/image/fetch/$s_!JpRi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png 1272w, https://substackcdn.com/image/fetch/$s_!JpRi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcbc200b-5818-4061-9703-ddf795ef70c3_590x435.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When I look at this type of question, my obvious initial thoughts would be to use <code>recursive cte</code>. But the question is how? Then I realized another way to think of this is tree traversal. But we will be traversing differently in 2 different ctes</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;c800db07-8847-40b5-85c1-3b11a7429f71&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">                        Alice
                      /       \
                  Bob         Charlie
                 /   \        /     \
            David   Eva   Frank    Grace
              |             /  \
            Hank         Ivy   Judy</code></pre></div><h2>Part 1: Generating <code>levels using bfs </code></h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;2514f288-d389-4f65-b604-a903f0197bc7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| Level | Employees                |
| ----: | ------------------------ |
|     0 | Alice                    |
|     1 | Bob, Charlie             |
|     2 | David, Eva, Frank, Grace |
|     3 | Hank, Ivy, Judy          |</code></pre></div><p>Okay so let&#8217;s simplify this question, <strong>ignore the later 2 columns and only care about levels,</strong> how can we generate a level column?</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| employee_id | employee_name | level |
| ----------- | ------------- | ----- |
| 1           | Alice         | 1     |
| 2           | Bob           | 2     |
| 3           | Charlie       | 2     |
| 4           | David         | 3     |
...
and so on</code></pre></div><p>Naturally we would start at the root of the tree</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">select
    *,
    0 as level
from employees 
where manager_id is null</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| employee_id | employee_name | manager_id | salary | department | level |
| ----------- | ------------- | ---------- | ------ | ---------- | ----- |
| 1           | Alice         | null       | 12000  | Executive  | 0     |</code></pre></div><p>Now it&#8217;s the harder part, we wants to find all the direct reports from this &#8220;current&#8221; manager. The manager would be the &#8220;root&#8221; and direct reports would be the leaf</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">select
    leaf.*, 
    1 as level 
from employees root
left join employees leaf 
on root.employee_id = leaf.manager_id
where root.manager_id is null
order by 1</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| employee_id | employee_name | manager_id | salary | department  | level |
| ----------- | ------------- | ---------- | ------ | ----------- | ----- |
| 2           | Bob           | 1          | 10000  | Sales       | 1     |
| 3           | Charlie       | 1          | 10000  | Engineering | 1     |</code></pre></div><p>Nice! let&#8217;s say we wants to keep doing this until we were able to traverse around all the leaf of the tree, how would we do that? We will need a technique call <code>recursive ctes</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">with recursive calc_level as (

    select 
        employee_id,
        employee_name,  
        1 as level
    from employees root
    where manager_id is null 

    union all 

    select
        leaf.employee_id,
        leaf.employee_name, 
        level + 1 as level 
    from employees leaf -- find all the leaf belong to the current root
    join calc_level root  
    on root.employee_id = leaf.manager_id
)
select * from calc_level</code></pre></div><p>Let&#8217;s slow it down, this is, in my humble opinion, a really complex concept. The key is that the SQL engine <strong>does not literally rerun the query against the entire </strong><code>calc_level</code><strong> table every time</strong>. Internally, it maintains a <strong>working table</strong>.</p><p>Think of it like this:</p><ul><li><p><strong>Result table</strong> = everything we&#8217;ve found so far.</p></li><li><p><strong>Working table</strong> = only the rows found in the previous iteration.</p></li></ul><p>The algorithm is essentially:</p><pre><code><code>result  = anchor query
working = anchor query

while working is not empty:
    new_rows = recursive_query(working)
    add new_rows to result
    working = new_rows</code></code></pre><p>A good way of understanding this concepts is to generate a series from 1-n using SQL, it would be something like this: </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">with recursive numbers as (
    select 
        0 as number
    
    union all 
    
    select 
        number + 1 
    from numbers 
    where number &lt; 10
)

select * from numbers</code></pre></div><h3>1. Step 1: Anchor base line for level 1</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;42b09229-3eca-4f56-8d1a-e22131ae8142&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT employee_id, employee_name, 1 AS level
FROM employees
WHERE manager_id IS NULL;</code></pre></div><p>Result:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| id | name  | level |
| -- | ----- | ----- |
| 1  | Alice | 1     |</code></pre></div><p>Now</p><pre><code><code>Result table
------------
Alice

Working table
-------------
Alice</code></code></pre><div><hr></div><h3>2. Step 2: The recursive queries - create level 2</h3><p>The recursive query is</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;c74e3267-997d-4c14-baca-d344eb664946&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    leaf.employee_id,
    leaf.employee_name,
    root.level + 1
FROM employees leaf
JOIN calc_level root
ON root.employee_id = leaf.manager_id;</code></pre></div><p>Conceptually, the engine substitutes the <strong>working table</strong> for <code>calc_level</code>.</p><p>So it&#8217;s really doing</p><pre><code><code>employees
JOIN
Working table (Alice)
-- Find all direct report of Alice</code></code></pre><p>which is</p><pre><code><code>Alice
   |
employees.manager_id = 1</code></code></pre><p>It finds</p><pre><code><code>Bob
Charlie</code></code></pre><p>Now</p><pre><code><code>Result
------
Alice -- level 1
Bob -- level 2
Charlie -- level 2

Working
-------
Bob -- level 2
Charlie -- level 2</code></code></pre><p>Notice Alice is <strong>not</strong> in the working table anymore.</p><h3>5. Step 3: Keep going with the same logic on working table</h3><p>Now at the end the engine tries</p><pre><code><code>employees
JOIN
(Hank, Ivy, Judy) 
-- find all employee where manager id is Hank, Ivy, Judy</code></code></pre><p>None of them manage anyone.</p><p>The working table becomes empty.</p><pre><code><code>Working
-------
(empty)</code></code></pre><p>The recursion stops.</p><h3>Question: Why doesn&#8217;t it keep expanding Alice forever?</h3><p>Because <strong>Alice is no longer in the working table after iteration 1</strong>. If the engine kept joining against the entire accumulated result every iteration, it would keep finding Bob and Charlie again:</p><pre><code><code>Iteration 2
Alice -&gt; Bob

Iteration 3
Alice -&gt; Bob   &lt;-- duplicate
Bob -&gt; David

Iteration 4
Alice -&gt; Bob   &lt;-- duplicate again
Bob -&gt; David   &lt;-- duplicate again
...</code></code></pre><p>That would either produce duplicates forever, which create chaos and run-time error.</p><h2>Finalize part 1: Generating <code>levels</code></h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">with recursive calc_level as (

    select 
        employee_id,
        employee_name,  
        1 as level
    from employees root
    where manager_id is null 

    union all 

    select
        leaf.employee_id,
        leaf.employee_name, 
        level + 1 as level 
    from employees leaf -- find all the leaf belong to the current root
    join calc_level root  
    on root.employee_id = leaf.manager_id
)
select * from calc_level
</code></pre></div><p>This would give you exactly what we wanted</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| employee_id | employee_name | level |
| ----------- | ------------- | ----- |
| 1           | Alice         | 1     |
| 2           | Bob           | 2     |
| 3           | Charlie       | 2     |
| 4           | David         | 3     |
| 5           | Eva           | 3     |
| 6           | Frank         | 3     |
| 7           | Grace         | 3     |
| 8           | Hank          | 4     |
| 9           | Ivy           | 4     |
| 10          | Judy          | 4     |</code></pre></div><h1>Part 2: Generate tree and all possible &#8220;Subtree&#8221;</h1><p>Now, in order to find team size and total, we would need to have a table of all possible subtree, then aggregate it to the employee_id level that is in order to calculate this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| employee_id | employee_name | team_size | budget |
| ----------- | ------------- | --------: | -----: |
| 1           | Alice         |         9 |  84500 |
| 3           | Charlie       |         4 |  41500 |
| 2           | Bob           |         3 |  31000 |
| 6           | Frank         |         2 |  23000 |
| 4           | David         |         1 |  13500 |
| 7           | Grace         |         0 |   8500 |
| 5           | Eva           |         0 |   7500 |
| 9           | Ivy           |         0 |   7000 |
| 10          | Judy          |         0 |   7000 |
| 8           | Hank          |         0 |   6000 |
</code></pre></div><p>We need to be able to generate this: </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Manager 1 (Alice)

                        Alice
                      /       \
                  Bob         Charlie
                 /   \        /      \
            David   Eva   Frank     Grace
              |             /  \
            Hank         Ivy   Judy


Manager 2 (Bob)

                  Bob
                /     \
            David     Eva
              |
            Hank


Manager 3 (Charlie)

                 Charlie
                /       \
            Frank      Grace
            /    \
         Ivy    Judy


Manager 4 (David)

            David
              |
            Hank


Manager 5 (Eva)

             Eva


Manager 6 (Frank)

            Frank
           /     \
        Ivy      Judy


Manager 7 (Grace)

            Grace


Manager 8 (Hank)

            Hank


Manager 9 (Ivy)

             Ivy


Manager 10 (Judy)

            Judy</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">
| manager_id | subordinate_id | salary |
| ---------- | -------------- | ------ |
| 1          | 1              | 12000  |
| 1          | 2              | 10000  |
| 1          | 3              | 10000  |
| 1          | 4              | 7500   |
| 1          | 5              | 7500   |
| 1          | 6              | 9000   |
| 1          | 7              | 8500   |
| 1          | 8              | 6000   |
| 1          | 9              | 7000   |
| 1          | 10             | 7000   |
...
and so on for all manager_id</code></pre></div><p>After that, we would be able to aggregate  it into the <strong>manager_id</strong> granularity</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;eedd6565-56c3-4271-89e6-29c8ecad8b12&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">with recursive calc_all_direct_report as (
    select 
        employee_id as manager_id, 
        employee_id, 
        salary
    from employees 
    -- where manager_id is null 

    union all 

    select 
         root.manager_id, 
         leaf.employee_id,
         leaf.salary
    from employees leaf 
    join calc_all_direct_report root on 
    root.employee_id = leaf.manager_id
)
</code></pre></div><p>We would use the same technique. Here we would anchor root.manager_id to traverse and find all subtree. The traversal for manager_id = 1 is exactly similar to the one before, so let&#8217;s try and see an example with manager_id = 2.</p><p>Original tree:</p><pre><code><code>                  Bob
                /     \
            David     Eva
              |
            Hank</code></code></pre><h3>1. Anchor starts Bob</h3><p>From:</p><pre><code><code>SELECT employee_id AS manager_id, employee_id
FROM employees</code></code></pre><p>You get:</p><pre><code><code>manager_id = 2
employee_id = 2</code></code></pre><p>Meaning:</p><pre><code><code>Bob is the root of this subtree</code></code></pre><h3>2. Find Bob&#8217;s direct reports</h3><p>Join:</p><pre><code><code>root.employee_id = leaf.manager_id</code></code></pre><p>Bob manages:</p><pre><code><code>David (id=4)
Eva   (id=5)</code></code></pre><p>Add:</p><pre><code><code>manager_id = 2
employee_id = 4

manager_id = 2
employee_id = 5</code></code></pre><h3>3. Expand David and Eva</h3><p>Now current employees are:</p><pre><code><code>David
Eva</code></code></pre><p>David manages:</p><pre><code><code>Hank (id=8)</code></code></pre><p>Add:</p><pre><code><code>manager_id = 2
employee_id = 8</code></code></pre><p>Important: <strong>still manager_id = 2</strong> because we are still building Bob&#8217;s subtree.</p><p>Final output for Bob:</p><pre><code><code>        Bob (2)
       /      \
  David (4)  Eva (5)
      |
   Hank (8)</code></code></pre><p>Table:</p><pre><code><code>manager_id | employee_id
-----------+------------
2          | 2
2          | 4
2          | 5
2          | 8</code></code></pre><p>The key idea is </p><pre><code><code>manager_id = 2  &lt;-- stays fixed (the tree owner)
employee_id     &lt;-- moves down the tree</code></code></pre><p>From here, you would get all possible subtree in one table and we can roll up to the manager_id level</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| manager_id | employee_id | salary |
| ---------- | ----------- | ------ |
| 1          | 1           | 12000  |
| 1          | 10          | 7000   |
| 1          | 9           | 7000   |
| 1          | 8           | 6000   |
| 1          | 7           | 8500   |
| 1          | 6           | 9000   |
| 1          | 5           | 7500   |
| 1          | 4           | 7500   |
| 1          | 2           | 10000  |
| 1          | 3           | 10000  |
| 2          | 2           | 10000  |
| 2          | 8           | 6000   |
| 2          | 5           | 7500   |
| 2          | 4           | 7500   |
| 3          | 6           | 9000   |</code></pre></div><h1>Putting it all togher</h1><p>The hardest part is done, now we have 2 ctes: <code>calc_level</code> and <code>calc_all_direct_report</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| employee_id | employee_name | level |
| ----------- | ------------- | ----- |
| 1           | Alice         | 1     |
| 2           | Bob           | 2     |
| 3           | Charlie       | 2     |
| 4           | David         | 3     |
...</code></pre></div><p>and </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| manager_id | employee_id | salary |
| ---------- | ----------- | ------ |
| 1          | 1           | 12000  |
| 1          | 10          | 7000   |
| 1          | 9           | 7000   |
| 1          | 8           | 6000   |
| 1          | 7           | 8500   |
| 1          | 6           | 9000   |
| 1          | 5           | 7500   |
| 1          | 4           | 7500   |
| 1          | 2           | 10000  |
| 1          | 3           | 10000  |
| 2          | 2           | 10000  |
| 2          | 8           | 6000   |
| 2          | 5           | 7500   |
| 2          | 4           | 7500   |
| 3          | 6           | 9000   |
...</code></pre></div><p>Bring it all together  we have </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">with recursive calc_level as (

    select 
        employee_id,
        employee_name,  
        1 as level
    from employees root
    where manager_id is null 

    union all 

    select
        leaf.employee_id,
        leaf.employee_name, 
        level + 1 as level 
    from employees leaf -- find all the leaf belong to the current root
    join calc_level root  
    on root.employee_id = leaf.manager_id
), 

calc_all_direct_report as (
    select 
        employee_id as manager_id, 
        employee_id, 
        salary
    from employees 
    -- where manager_id is null 

    union all 

    select 
         root.manager_id, 
         leaf.employee_id,
         leaf.salary
    from employees leaf 
    join calc_all_direct_report root on 
    root.employee_id = leaf.manager_id
), 


direct_report_roll_up as (
    select 
        manager_id, 
        count(employee_id) - 1 team_size, 
        sum(salary) budget
    from calc_all_direct_report
    group by 1 
    order by 1
)

select 
    employee_id, 
    employee_name, 
    level, 
    team_size, 
    budget
from calc_level
join direct_report_roll_up 
on calc_level.employee_id = direct_report_roll_up.manager_id
order by 3, 5 desc, 2</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21qoxZ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qoxZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png 424w, https://substackcdn.com/image/fetch/$s_!qoxZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png 848w, https://substackcdn.com/image/fetch/$s_!qoxZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png 1272w, https://substackcdn.com/image/fetch/$s_!qoxZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21qoxZ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png" width="739" height="450" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:450,&quot;width&quot;:739,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:39415,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/207826833?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qoxZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png 424w, https://substackcdn.com/image/fetch/$s_!qoxZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png 848w, https://substackcdn.com/image/fetch/$s_!qoxZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png 1272w, https://substackcdn.com/image/fetch/$s_!qoxZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7cea84-a269-4eb7-989b-8c8ee0438103_739x450.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p>]]></content:encoded></item><item><title><![CDATA[I tell people I'm an AE, but wtf does that mean?]]></title><description><![CDATA[And no it's not Account Executive for god sake]]></description><link>https://markphammm.substack.com/p/i-tell-people-im-an-ae-so-wtf-does</link><guid isPermaLink="false">https://markphammm.substack.com/p/i-tell-people-im-an-ae-so-wtf-does</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Sat, 20 Jun 2026 19:30:35 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%219bbB%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>From the very beginning of my blogging journey, I was focused on sharing knowledge, learning, and helping people. I tried to keep the best version of myself on Substack, watching my words, avoiding hot takes. And everything I shared ended up being about data, until I realized I don&#8217;t have shit to write about besides how nerdy I am.</p><p>That&#8217;s not the real me. That&#8217;s not how I talk, and it&#8217;s not what I actually want to talk about. After weeks of grinding through Spark, all I want to say is: fuck it. I don&#8217;t want to write about data the polite way anymore. I want these blogs to actually sound like me, so, sorry in advance for the language, but this is how every fucking post from now on is going to read. More personal. Still data. Just with way more swearing and way more hot takes.</p><p>Lately I keep getting asked what I actually do as an AE. How many leads did you get? Are you on the phone all day? Nope, none of that, because I&#8217;m an Analytics Engineer, not some handsome guy in sales.</p><p>So today I'm going to try to explain my job like you're five.</p><h2>Data Team</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%219bbB%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9bbB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png 424w, https://substackcdn.com/image/fetch/$s_!9bbB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png 848w, https://substackcdn.com/image/fetch/$s_!9bbB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png 1272w, https://substackcdn.com/image/fetch/$s_!9bbB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%219bbB%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png" width="1456" height="1456" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1456,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Onebridge&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Onebridge" title="Onebridge" srcset="https://substackcdn.com/image/fetch/$s_!9bbB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png 424w, https://substackcdn.com/image/fetch/$s_!9bbB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png 848w, https://substackcdn.com/image/fetch/$s_!9bbB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png 1272w, https://substackcdn.com/image/fetch/$s_!9bbB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F538bc704-30fa-49e9-b365-38c0f85bef8f_2000x2000.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Analytics Engineer is a member of the data team, not Account Executive. So no, I&#8217;m not closing deals, I&#8217;m not hitting quota, and I&#8217;m definitely not the guy who emails you &#8220;just circling back!&#8221; four times. If you came here for sales tips, wrong blog, sorry.</p><p><strong>TLDR: the data team is to help people make better decisions.</strong> That&#8217;s it. That&#8217;s the whole reason we exist. Some VP wants to know if a campaign is bleeding money, some PM wants to know if the new feature is actually getting used, and our job is to put a real number in front of them instead of a vibe. Decisions, not dashboards for the sake of dashboards.</p><p><strong>And Analytics Engineer, along with Data Engineers, are the ones responsible for creating the dataset for the downstream consumers.</strong> Downstream meaning everyone who eats what we cook: Data Analysts, Data Scientists, and a long parade of stakeholders who all swear their question is &#8220;quick.&#8221; It&#8217;s never quick. The dataset is the product. If the dataset is garbage, every chart, every model, every &#8220;data driven&#8221; decision on top of it is also garbage, just garbage with a nicer font. Garbage in, garbage out, except now it&#8217;s in a Tableau dashboard so it looks official.</p><p>So when people ask what I do, the honest version is: I&#8217;m the person making sure the number you&#8217;re about to bet a budget on isn&#8217;t a lie.</p><h2>Data Warehouse</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21inPQ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fb5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!inPQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png 424w, https://substackcdn.com/image/fetch/$s_!inPQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png 848w, https://substackcdn.com/image/fetch/$s_!inPQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png 1272w, https://substackcdn.com/image/fetch/$s_!inPQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21inPQ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fb5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png" width="1456" height="983" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:983,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;What Is a Data Warehouse? Definition and Use in Data Mining&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="What Is a Data Warehouse? Definition and Use in Data Mining" title="What Is a Data Warehouse? Definition and Use in Data Mining" srcset="https://substackcdn.com/image/fetch/$s_!inPQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png 424w, https://substackcdn.com/image/fetch/$s_!inPQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png 848w, https://substackcdn.com/image/fetch/$s_!inPQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png 1272w, https://substackcdn.com/image/fetch/$s_!inPQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5210687-7f37-4bdb-8362-e61c17b9e231_1500x1013.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Think simply, this is where people store data. A warehouse is where you store your inventory, and in this case the inventory is data. Boxes of it, stacked to the ceiling, some of it useful and some of it nobody&#8217;s touched since 2021 but we&#8217;re too scared to delete it.</p><p>But it&#8217;s more than just a storage closet. The warehouse is the source of truth. When two people show up to a meeting with two different numbers for the same thing, the warehouse is supposed to be the thing that settles the fight. It&#8217;s where the data lives, the one place everyone agrees to point at and say &#8220;okay, that&#8217;s the real number.&#8221; No more &#8220;well my spreadsheet says...&#8221; Your spreadsheet is wrong. The warehouse is right. That&#8217;s the deal.</p><p>In our case the warehouse is Redshift, but honestly it doesn&#8217;t matter if it&#8217;s Redshift, Snowflake, BigQuery, whatever. They all do the same job, they just argue about pricing.</p><p>And going back to the food thing: the warehouse is the kitchen. It&#8217;s where the raw ingredients get dropped off, where I do all the prep and cooking, and where the finished dishes sit ready to go out. Everything happens in here. If the kitchen&#8217;s a mess, the food&#8217;s a mess, and everybody downstream is eating off the floor.</p><h2>Traditional Data Roles</h2><ul><li><p>DE <strong>(Producer)</strong></p></li><li><p>DA/DS &amp; many more <strong>(Consumer)</strong></p></li></ul><p>Picture the data warehouse as a kitchen. The DA/DS are the customers out front, hungry, waiting for food. The DE is the one who does everything in the back: goes hunting for the ingredients (get data from the source), drives the truck back (load it into the kitchen), and cooks it (transform). One person, three jobs. Seems like a lot, right?</p><p><strong>It is a lot. And for years that was just the gig. One guy hunting, driving, and cooking. The food came out fine, but the kitchen was on fire every single day.</strong></p><p>For a small place it actually works. Five ingredients, one pipeline, one guy who knows where all the bodies are buried. He&#8217;s a hero and the food&#8217;s good.</p><p>Then the company grows. Now it&#8217;s not five sources, it&#8217;s fifty. Salesforce, Stripe, Google Ads, Meta, and some vendor API still returning XML in the year of our lord 2026. Your one hero is now hunting fifty animals, driving fifty trucks, cooking fifty dishes, and the second a single pipeline dies at 2am the whole kitchen goes dark. One person physically cannot do this. The old way breaks.</p><p>Quick detour on why it was built this way in the first place.</p><h2>ETL</h2><p><strong>The old way had a name: ETL. Extract, Transform, Load, in that order.</strong> You pulled the data out, cooked it on some server outside the warehouse, and only loaded the finished dish once it was done. Why cook before loading? Because storage and compute used to cost a fortune. Dumping raw garbage into the warehouse was like renting a mansion to store your trash, so you trimmed everything down on the way in. And the only person who could build and babysit that whole machine was the DE. That&#8217;s why he was god. The cooking was welded to the pipeline, and the pipeline was one fragile, holy artifact nobody else could touch.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21LXwe%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LXwe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg 424w, https://substackcdn.com/image/fetch/$s_!LXwe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg 848w, https://substackcdn.com/image/fetch/$s_!LXwe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!LXwe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21LXwe%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg" width="1216" height="698" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:698,&quot;width&quot;:1216,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;ETL: Core concepts, use sases, and FAQs | dbt Labs&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="ETL: Core concepts, use sases, and FAQs | dbt Labs" title="ETL: Core concepts, use sases, and FAQs | dbt Labs" srcset="https://substackcdn.com/image/fetch/$s_!LXwe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg 424w, https://substackcdn.com/image/fetch/$s_!LXwe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg 848w, https://substackcdn.com/image/fetch/$s_!LXwe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!LXwe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a921d61-2986-40f4-ae43-53e87fdd0a80_1216x698.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>ELT - Cloud Data Warehouse</h2><p>Then cloud warehouses showed up and changed the math. Redshift, Snowflake, BigQuery. Storage got cheap as dirt and compute scaled on demand. Suddenly there was zero reason to cook before loading. Just dump it all in raw, the warehouse can take it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21YUCM%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YUCM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png 424w, https://substackcdn.com/image/fetch/$s_!YUCM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png 848w, https://substackcdn.com/image/fetch/$s_!YUCM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png 1272w, https://substackcdn.com/image/fetch/$s_!YUCM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21YUCM%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Inside Modern Data Warehouses: Snowflake, BigQuery, Redshift Explained | by  Satyam Sahu | T3CH | Medium&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Inside Modern Data Warehouses: Snowflake, BigQuery, Redshift Explained | by  Satyam Sahu | T3CH | Medium" title="Inside Modern Data Warehouses: Snowflake, BigQuery, Redshift Explained | by  Satyam Sahu | T3CH | Medium" srcset="https://substackcdn.com/image/fetch/$s_!YUCM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png 424w, https://substackcdn.com/image/fetch/$s_!YUCM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png 848w, https://substackcdn.com/image/fetch/$s_!YUCM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png 1272w, https://substackcdn.com/image/fetch/$s_!YUCM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce7ec505-5714-44ab-9b83-09cce6c85177_2000x1091.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p style="text-align: center;"><a href="iframe.php?url=https%3A%2F%2Fmedium.com%2Fh7w%2Finside-modern-data-warehouses-snowflake-bigquery-redshift-explained-469c93b31c03">sources</a></p><p>So somebody flipped two letters and the entire industry changed. ETL became ELT. Extract, Load, Transform. Get the raw ingredients into the kitchen first, cook later, inside the warehouse, with plain SQL.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21AMlO%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AMlO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg 424w, https://substackcdn.com/image/fetch/$s_!AMlO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg 848w, https://substackcdn.com/image/fetch/$s_!AMlO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!AMlO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21AMlO%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg" width="1456" height="638" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:638,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Understanding ELT: extract, load, transform | dbt Labs&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Understanding ELT: extract, load, transform | dbt Labs" title="Understanding ELT: extract, load, transform | dbt Labs" srcset="https://substackcdn.com/image/fetch/$s_!AMlO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg 424w, https://substackcdn.com/image/fetch/$s_!AMlO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg 848w, https://substackcdn.com/image/fetch/$s_!AMlO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!AMlO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9036ac96-7055-490e-b29d-058eb74d992b_1706x748.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And that flip is the whole reason my job exists.</p><p>Once the T moved to the end and lived inside the warehouse, it split clean off from the rest. <strong>Extract and Load got so easy a button does it now (hi Fivetran, hi Airbyte).</strong> But Transform got bigger and uglier, because now every raw source on earth is sitting in your kitchen and somebody has to turn that mountain of raw chicken into an actual menu. Not the DE, he&#8217;s busy keeping the trucks running. Not the DA/DS, they&#8217;re out front with the customers.</p><p>It&#8217;s a new role. The person who only cooks.</p><p>The Analytics Engineer.</p><h2>That&#8217;s me. That&#8217;s the Analytics Engineer.</h2><p>The DE backs the truck up and dumps raw ingredients in the walk-in, then walks off the second the data lands in the warehouse. <strong>They don&#8217;t give a fuck if it&#8217;s a clean table or five levels of nested JSON. Not their problem anymore.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21fzk4%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fzk4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png 424w, https://substackcdn.com/image/fetch/$s_!fzk4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png 848w, https://substackcdn.com/image/fetch/$s_!fzk4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png 1272w, https://substackcdn.com/image/fetch/$s_!fzk4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21fzk4%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png" width="1334" height="944" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:944,&quot;width&quot;:1334,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;6 Reasons Why Every Business Needs Analytics Engineering | Aimpoint Digital&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="6 Reasons Why Every Business Needs Analytics Engineering | Aimpoint Digital" title="6 Reasons Why Every Business Needs Analytics Engineering | Aimpoint Digital" srcset="https://substackcdn.com/image/fetch/$s_!fzk4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png 424w, https://substackcdn.com/image/fetch/$s_!fzk4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png 848w, https://substackcdn.com/image/fetch/$s_!fzk4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png 1272w, https://substackcdn.com/image/fetch/$s_!fzk4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa37ce7d5-bbfa-4b01-907a-17ca02509027_1334x944.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Then there&#8217;s the AE, the line cook who turns that pile into something a human can actually put in their mouth. Raw events, ugly JSON, five tables that all kind of mean &#8220;user&#8221; but not really. I clean it, I prep it, I combine it, and I label the damn containers so the next person isn&#8217;t standing there guessing whether that&#8217;s revenue or a refund. By the time the DA/DS walk into the kitchen, the dishes are plated and the menu makes sense. They decide what to serve and who to serve it to. I&#8217;m the one making sure none of it gives the customer food poisoning.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Cu2D%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Cu2D!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Cu2D!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Cu2D!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Cu2D!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Cu2D%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg" width="898" height="704" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:704,&quot;width&quot;:898,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;What is dbt? | dbt Developer Hub&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="What is dbt? | dbt Developer Hub" title="What is dbt? | dbt Developer Hub" srcset="https://substackcdn.com/image/fetch/$s_!Cu2D!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Cu2D!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Cu2D!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Cu2D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f9e5aeb-c461-4d58-b07f-52704a16bf64_898x704.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The thing that turned this from &#8220;guy who writes SQL in a corner&#8221; into an actual job title is dbt (Thanks god dbt was created). dbt is basically the recipe book plus the health inspector. Every transform is a recipe: version controlled, tested, documented. So when someone asks &#8220;wait, why is this number different?&#8221; I can point at the exact recipe instead of shrugging. This work always existed, it just used to live in 4000 lines of cursed stored procedures that one guy understood, and that guy quit in 2019.</p><p>But here&#8217;s the part I actually like. I&#8217;m not just the cook. At a start up like Insurify I&#8217;m the spanner (Netflix also call their AEs spanners), the tool you grab when you don&#8217;t know what else to grab. Some days I&#8217;m building the dashboard myself instead of handing it off. Some days a pipeline&#8217;s on fire and I&#8217;m doing the DE&#8217;s job, hunting and driving the truck because somebody has to. Some days I&#8217;m sitting with a stakeholder figuring out what the question even is. The lines are blurry as hell and honestly that&#8217;s the whole appeal. I&#8217;m not locked in one station. I float wherever the kitchen needs me, and that flexibility is what makes this job actually fun instead of just SQL all day. So that&#8217;s the five year old version: </p><ul><li><p>DE go hunting (extract), getting foods into the kitchen (load)</p></li><li><p>AE cook (Transform)</p></li><li><p>DA/DS run the dining room. </p></li></ul><p>None of us are calling you about your car insurance.</p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[All things Spark - TLDR (Part 5 - Joins and Aggregations)]]></title><description><![CDATA[Having a hard time learning Spark? This is my TLDR]]></description><link>https://markphammm.substack.com/p/all-things-spark-tldr-part-5-joins</link><guid isPermaLink="false">https://markphammm.substack.com/p/all-things-spark-tldr-part-5-joins</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Fri, 24 Apr 2026 19:08:19 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%212fOu%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Aggregation Strategies</h2><ul><li><p>When Spark performs an aggregation (like <code>SUM</code> or <code>AVG</code>), it doesn&#8217;t send every raw record across the network.</p></li><li><p>Instead it runs a <strong>partial aggregation</strong> locally on each partition first (Map phase), writing out summarized results as shuffle files. The reduce tasks then read those files and perform a <strong>final aggregation</strong> to produce the result.</p></li></ul><pre><code><code>Partition 1: [a,a,b,b,b] &#8594; partial: {a:2, b:3}
Partition 2: [a,b,b,b]   &#8594; partial: {a:1, b:3}
                    &#8595;  shuffle by key
Final merge: {a:3, b:6}
</code></code></pre><ul><li><p>This significantly reduces network I/O &#8212; executors exchange summaries rather than raw rows. That&#8217;s also why you&#8217;ll see the aggregation operator appear <strong>twice</strong> in the physical plan: once for partial and once for final.</p></li><li><p>Spark has 2 aggregation strategy</p><ul><li><p><strong>HashAggregate</strong> builds an in-memory hash table keyed by the group key. The value is an aggregation buffer updated in-place (e.g. running SUM). No sorting needed &#8212; fast and preferred.</p></li><li><p><strong>SortAggregate</strong> sorts the input data by the group key first, then iterates over it linearly to accumulate groups. Slower because of the sort pass.</p></li></ul></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%214hp4%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4hp4!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 424w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 848w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 1272w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%214hp4%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_lossy%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif" width="1280" height="532" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:532,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Apache Spark Aggregation Methods: Hash-based Vs. Sort-based&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Apache Spark Aggregation Methods: Hash-based Vs. Sort-based" title="Apache Spark Aggregation Methods: Hash-based Vs. Sort-based" srcset="https://substackcdn.com/image/fetch/$s_!4hp4!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 424w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 848w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 1272w, https://substackcdn.com/image/fetch/$s_!4hp4!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a0f5b03-abaa-4130-ad98-c5a79b14cc8e_1280x532.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>HashAggregate</h2><ul><li><p><strong>HashAggregate</strong> builds an in-memory hash table keyed by the group key. The value is an aggregation buffer updated in-place (e.g. running SUM). No sorting needed &#8212; fast and preferred.</p></li><li><p>For <code>HashAggregate</code> to work efficiently, Spark uses the <strong>UnsafeRow</strong> format &#8212; an off-heap binary representation that bypasses the JVM garbage collector (part of Project Tungsten).</p></li><li><p>This format works best with <strong>fixed-size types</strong> that can be updated in-place in the hash table: integers, longs, doubles, dates, booleans.</p></li></ul><h2>Sort aggregation</h2><ul><li><p><strong>SortAggregate</strong> sorts the input data by the group key first, then iterates over it linearly to accumulate groups. Slower because of the sort pass.</p></li><li><p><strong>Variable-length types like String are not easily mutable in a fixed-size memory slot.</strong> When any aggregation buffer holds a String (or ArrayType, MapType), Spark falls back to SortAggregate.</p></li></ul><h2>Joins Strategies</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%212fOu%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2fOu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png 424w, https://substackcdn.com/image/fetch/$s_!2fOu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png 848w, https://substackcdn.com/image/fetch/$s_!2fOu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png 1272w, https://substackcdn.com/image/fetch/$s_!2fOu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%212fOu%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png" width="1400" height="576" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:576,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Join Strategies in Spark. When working with large-scale data&#8230; | by Saurabh  Mahajan | Medium&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Join Strategies in Spark. When working with large-scale data&#8230; | by Saurabh  Mahajan | Medium" title="Join Strategies in Spark. When working with large-scale data&#8230; | by Saurabh  Mahajan | Medium" srcset="https://substackcdn.com/image/fetch/$s_!2fOu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png 424w, https://substackcdn.com/image/fetch/$s_!2fOu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png 848w, https://substackcdn.com/image/fetch/$s_!2fOu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png 1272w, https://substackcdn.com/image/fetch/$s_!2fOu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa90e2fd4-ac48-4140-9b94-cd411da37b17_1400x576.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Spark supports several physical join strategies &#8212; <strong>Sort Merge Join (SMJ)</strong>, <strong>Shuffle Hash Join (SHJ),</strong> and <strong>Broadcast Hash Join (BHJ)</strong>. The optimizer picks one based on table sizes and configuration</p><ul><li><p><strong>Sort merge join:</strong></p><ol><li><p>Step 1: Scan and Shuffle (AQE coalesce shuffling partition here)</p></li><li><p>Step 2: Sort</p></li><li><p>Step 3: Merge</p></li><li><p>Step 4: Aggregation and Output</p></li></ol></li><li><p><strong>Shuffle hash join</strong></p><ul><li><p>Step 1: Scan and Shuffle (AQE coalesce shuffling partition here)</p></li><li><p>Step 2: Build</p></li><li><p>Step 3: Probe</p></li><li><p>Step 4: Aggregation and Output</p></li></ul></li><li><p><strong>Broadcast hash join</strong></p><ul><li><p>Step 1: Scan and Broadcast</p></li><li><p>Step 2: Build (Build left)</p></li><li><p>Step 3: Probe</p></li><li><p>Step 4: Aggregation and Output</p></li></ul></li><li><p><strong>Bucket join</strong></p><ul><li><p>Step 1: Write bucket</p></li><li><p>Step 2: Scan</p></li><li><p>Step 3: Merge</p></li></ul></li><li><p><strong>Broadcast Nested Loop Join</strong></p></li></ul><h2>Joins Strategies: Sort Merge Join</h2><p>SMJ is Spark&#8217;s default strategy for joining two large tables. It is not unique to Spark &#8212; it has long existed in the database world. The execution happens in three phases.</p><h3>Phases</h3><ol><li><p><strong>Phase 1: Shuffle</strong></p><ul><li><p>Before the join can happen, rows with the same join key from both tables must end up on the same physical partition.</p></li><li><p>Spark performs an <code>Exchange</code> (shuffle) to redistribute data across the cluster by join key</p></li><li><p>If AQE is enabled, an <code>AQEShuffleRead</code> node follows, coalescing small partitions to keep the join efficient.</p></li></ul></li><li><p><strong>Phase 2: Sort: O(n log n) + O(m log m)</strong></p><ul><li><p>Once data is partitioned by key, each partition is sorted locally. Both branches of the join tree show an explicit <code>Sort</code> operator. This is what makes the merge phase possible.</p></li></ul></li><li><p><strong>Phase 3: Merge (O (m+n))</strong></p><ul><li><p>With both sides partitioned identically and sorted, the join itself is a linear scan through both sorted streams simultaneously:</p><ul><li><p>Compare the keys at the current pointer of each stream</p></li><li><p>If keys match &#8594; produce a joined row</p></li><li><p>If left key is smaller &#8594; advance left pointer</p></li><li><p>If right key is smaller &#8594; advance right pointer</p></li></ul></li></ul></li></ol><h2>Join Strategies: Shuffle Hash Join</h2><p>SMJ is the preferred join strategy today &#8212; but the crown used to belong to Shuffle Hash Join (SHJ). No <code>Sort</code> nodes &#8212; that&#8217;s the key difference from SMJ.</p><ul><li><p>SHJ requires the <strong>build side of every partition</strong> to fit entirely in memory. If a partition is too large (e.g. due to data skew), the executor throws an <code>OutOfMemoryError</code>. SMJ, by contrast, can safely spill to disk &#8212; making it more reliable</p></li></ul><h3>Phases</h3><ol><li><p>Phase 1: Shuffle</p><ul><li><p>Similar to SMJ, Before the join can happen, rows with the same join key from both tables must end up on the same physical partition.</p></li></ul></li><li><p>Phase 2: Build</p><ul><li><p>Once data is partitioned, Spark identifies the <strong>smaller</strong> of the two datasets within each partition. It builds an <strong>in-memory hash table</strong> from that smaller side, using the join key as the hash key.</p></li></ul></li><li><p>Phase 3: Probe</p><ul><li><p>Spark reads through the larger dataset. For each row, it hashes the join key and looks up a match in the hash table. If there is a match, the rows are combined.</p></li></ul></li></ol><h3>Enabling</h3><p>Two configs are required:</p><pre><code><code>spark.sql.join.preferSortMergeJoin = false
spark.sql.adaptive.maxShuffledHashJoinLocalMapThreshold = 256mb
</code></code></pre><p><code>maxShuffledHashJoinLocalMapThreshold</code> sets the maximum per-partition size allowed to build a hash table. Default is <code>0</code> &#8212; SHJ is never used. Setting it to <code>256mb</code> means partitions up to 256 MB can use SHJ.</p><h2>Join Strategies: Broadcast Hash Join</h2><p>Broadcast Hash Join (BHJ) is the fastest join strategy &#8212; it eliminates the shuffle entirely.</p><p>For SMJ and SHJ, both tables go through a shuffle to co-locate matching keys. BHJ takes a different approach: if one table is small enough to fit entirely in memory, Spark sends it to <strong>all executors</strong>.</p><p>Each executor then builds a local hash table from that broadcast copy and handles the join entirely with its own partition of the large table &#8212; no network exchange needed.</p><ol><li><p>Phase 1: Broadcast</p><ul><li><p>The left side (<code>orders</code>, the smaller table) is scanned and <strong>broadcast</strong> to all executors</p></li><li><p>he right side (<code>lineitem</code>) is simply scanned locally -- no shuffle, no broadcast</p></li></ul></li><li><p>Phase 2: Build</p><ul><li><p>Each executor receives the broadcast <code>orders</code> table and builds an in-memory hash table from it -- identical to Shuffle Hash Join&#8217;s build phase, except <strong>every executor builds the same hash table</strong> from the same complete data</p></li></ul></li><li><p>Phase 3: Probe</p><ul><li><p>Spark reads through the larger dataset. For each row, it hashes the join key and looks up a match in the hash table. If there is a match, the rows are combined.</p></li></ul></li></ol><h2>Join Strategies: Bucket Join</h2><p>In SMJ and SHJ, Spark shuffles data at <strong>join time</strong> to co-locate matching keys. A bucket join shifts that shuffle to <strong>write time</strong> instead.</p><p>Bucketing distributes data across a fixed number of buckets based on the hash of a column value. If both tables are bucketed by the same key with the same number of buckets, rows with matching keys already live in the same bucket &#8212; Spark can skip the shuffle entirely at join time.</p><h3>Requirements for a bucket join to work</h3><ol><li><p>Both tables bucketed on the <strong>same column</strong> with the <strong>same bucket count</strong></p></li><li><p>Metadata registered in the <strong>Hive metastore</strong> via <code>saveAsTable()</code></p></li><li><p>SparkSession must have <code>enableHiveSupport()</code> active</p></li><li><p>Read via <code>spark.table()</code>, not <code>spark.read.parquet()</code></p></li></ol><h3>Why bucketBy() requires saveAsTable()</h3><p><code>bucketBy()</code> only works with <code>saveAsTable()</code> &#8212; it does <strong>not</strong> work with <code>write.parquet()</code>.</p><p>When you join bucketed tables later, Spark needs to know which column was used for bucketing and how many buckets there are. That metadata must be stored somewhere persistent &#8212; the <strong>Hive metastore</strong>.</p><ul><li><p><code>saveAsTable("my_table")</code> &#8212; writes the data AND registers the bucketing metadata in the metastore</p></li><li><p><code>write.parquet(path)</code> &#8212; writes data only, no metastore, bucketing info is lost</p></li></ul><h3>How to write a bucketed table</h3><pre><code><code>df.write \\
    .bucketBy(8, "o_orderkey") \\
    .sortBy("o_orderkey") \\
    .format("parquet") \\
    .saveAsTable("orders_bucketed")
</code></code></pre><p>Bucketing shifts that work to <strong>write time</strong>. Data is distributed across buckets by hashing the join key &#8212; rows with the same key always land in the same bucket number. When both tables are bucketed by the same key with the same bucket count, Spark knows bucket 1 of <code>lineitem</code> contains the same keys as bucket 1 of <code>orders</code>. No runtime shuffle needed.</p>]]></content:encoded></item><item><title><![CDATA[All things Spark - TLDR (Part 4 - Dynamic Allocation, Catalyst Optimizer, and Caching)]]></title><description><![CDATA[Having a hard time learning Spark? This is my TLDR]]></description><link>https://markphammm.substack.com/p/all-things-spark-tldr-part-4-dynamic</link><guid isPermaLink="false">https://markphammm.substack.com/p/all-things-spark-tldr-part-4-dynamic</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Mon, 20 Apr 2026 17:48:07 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21F_fL%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Dynamic Allocation</h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21ncsh%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ncsh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ncsh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ncsh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ncsh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21ncsh%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg" width="1400" height="1209" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1209,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;I spent 4 hours learning Apache Spark Resource Allocation | by Vu Trinh |  Data Engineer Things&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="I spent 4 hours learning Apache Spark Resource Allocation | by Vu Trinh |  Data Engineer Things" title="I spent 4 hours learning Apache Spark Resource Allocation | by Vu Trinh |  Data Engineer Things" srcset="https://substackcdn.com/image/fetch/$s_!ncsh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ncsh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ncsh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ncsh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e753748-1036-42ca-9614-332cb8dcb96e_1400x1209.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p style="text-align: center;"><strong>Image from Vu Trinh&#8217;s article</strong></p><ul><li><p>Static vs dynamic allocation: Spark provides two ways to allocate resources for an application.</p><ol><li><p><strong>Static allocation</strong> (default) &#8212; each application gets a fixed set of executors reserved for the duration of the SparkContext. Resources are held even when idle.</p></li><li><p><strong>Dynamic allocation</strong> &#8212; the application can return executors to the cluster when no longer needed and request them back when demand rises.</p></li></ol></li></ul><pre><code><code>spark.dynamicAllocation.enabled = true</code></code></pre><ul><li><p><strong>Request policy</strong></p><ul><li><p>When pending tasks have been waiting for <code>spark.dynamicAllocation.schedulerBacklogTimeout</code> seconds, Spark requests more executors. If the backlog persists, it requests again every <code>spark.dynamicAllocation.sustainedSchedulerBacklogTimeout</code> seconds.</p></li><li><p>Requests are made in <strong>exponential rounds</strong> &#8212; 1 executor, then 2, 4, 8, etc.</p></li><li><p>Scale more at first &#8594; more rapidly</p></li></ul></li><li><p><strong>Remove policy</strong></p><ul><li><p>An executor is removed when it has been idle for <code>spark.dynamicAllocation.executorIdleTimeout</code> seconds (default 60s).</p></li></ul></li></ul><h1>Dynamic Allocation config</h1><pre><code><code>spark.dynamicAllocation.enabled                       = true
spark.dynamicAllocation.minExecutors                  = 1
spark.dynamicAllocation.maxExecutors                  = 4
spark.dynamicAllocation.executorIdleTimeout           = 60s
spark.dynamicAllocation.schedulerBacklogTimeout       = 1s
spark.dynamicAllocation.sustainedSchedulerBacklogTimeout = 1s
</code></code></pre><ul><li><p><code>spark.dynamicAllocation.enabled = true</code> Turns on dynamic executor scaling. Spark can add or remove executors based on workload.</p></li><li><p><code>spark.dynamicAllocation.minExecutors = 1</code> Spark will keep at least 1 executor alive, even when the cluster is mostly idle.</p></li><li><p><code>spark.dynamicAllocation.maxExecutors = 4</code> Spark can scale up to at most 4 executors when more parallel work shows up.</p></li><li><p><code>spark.dynamicAllocation.executorIdleTimeout = 60s</code> If an executor sits idle for 60 seconds, Spark can remove it to save resources.</p></li><li><p><code>spark.dynamicAllocation.schedulerBacklogTimeout = 1s</code> If tasks are waiting in the scheduler backlog for 1 second, Spark starts requesting more executors.</p></li><li><p><code>spark.dynamicAllocation.sustainedSchedulerBacklogTimeout = 1s</code> If that backlog keeps continuing for another 1 second window, Spark keeps asking for more executors again.</p></li></ul><h1>Catalyst Optimizer</h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21F_fL%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!F_fL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg 424w, https://substackcdn.com/image/fetch/$s_!F_fL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg 848w, https://substackcdn.com/image/fetch/$s_!F_fL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!F_fL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21F_fL%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg" width="1050" height="591" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:591,&quot;width&quot;:1050,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Spark's Catalyst Optimizer: How it Boosts Performance in DataFrames | by  Swayamprava Panda | Medium&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Spark's Catalyst Optimizer: How it Boosts Performance in DataFrames | by  Swayamprava Panda | Medium" title="Spark's Catalyst Optimizer: How it Boosts Performance in DataFrames | by  Swayamprava Panda | Medium" srcset="https://substackcdn.com/image/fetch/$s_!F_fL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg 424w, https://substackcdn.com/image/fetch/$s_!F_fL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg 848w, https://substackcdn.com/image/fetch/$s_!F_fL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!F_fL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba192205-1149-460b-abd1-47e2e7441743_1050x591.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Catalyst is Spark&#8217;s query optimizer, designed around functional programming constructs in Scala. It provides a general library for representing query plans as trees and applying rules to transform them. Catalyst supports both <strong>rule-based</strong> and <strong>cost-based</strong> optimization:</p><ul><li><p><strong>Rule-Based Optimization (RBO)</strong> &#8212; applies a fixed set of predefined rules based on query structure and schema, without considering actual data distribution.</p></li><li><p><strong>Cost-Based Optimization (CBO)</strong> &#8212; uses statistics about the data (table sizes, data distribution) to estimate the cost of multiple candidate plans and pick the cheapest one. Requires accurate statistics to be available.</p></li></ul><h1>Catalyst Optimizer process</h1><ol><li><p><strong>Analysis</strong> &#8212; Spark starts with an unresolved logical plan (column names and table references are not yet validated). It uses Catalyst rules and the <strong>Catalog</strong> &#8212; the central metadata store for databases, tables, and functions &#8212; to resolve attributes, check column names, and infer types.</p></li><li><p><strong>Logical Optimization</strong> &#8212; once the plan is resolved, Spark applies rule-based rewrites: predicate pushdown, projection pruning, null propagation, constant folding, and more.</p></li><li><p><strong>Physical Planning</strong> &#8212; Spark takes the optimized logical plan and generates one or more physical plans (e.g. hash join vs sort-merge join). It selects the final plan using a cost model.</p></li><li><p><strong>Code Generation</strong> &#8212; Spark generates Java bytecode for execution using Scala&#8217;s quasiquotes, which allow programmatic construction of ASTs compiled into bytecode at runtime. This is especially effective for CPU-bound, in-memory operations.</p></li></ol><h3>Projection pruning</h3><ul><li><p>Only columns actually used in the query are read from the file. If your Parquet file has 50 columns but you only <code>select</code> 3, Spark reads only those 3.</p></li></ul><h3>Predicate pushdown</h3><ul><li><p>Spark moves <code>filter</code> operations as early as possible &#8212; ideally down to the file reader so unneeded rows are never loaded into memory.</p></li><li><p>In the physical plan you will see <code>PushedFilters: [IsNotNull(status), EqualTo(status,OPEN)]</code> on the scan node, not after the scan.</p></li></ul><h1>Caching</h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21mqo-%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ffc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mqo-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png 424w, https://substackcdn.com/image/fetch/$s_!mqo-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png 848w, https://substackcdn.com/image/fetch/$s_!mqo-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png 1272w, https://substackcdn.com/image/fetch/$s_!mqo-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21mqo-%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Ffc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png" width="1456" height="778" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:778,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;A Common Mistake That Many Spark Programmers Commit and Never Notice&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A Common Mistake That Many Spark Programmers Commit and Never Notice" title="A Common Mistake That Many Spark Programmers Commit and Never Notice" srcset="https://substackcdn.com/image/fetch/$s_!mqo-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png 424w, https://substackcdn.com/image/fetch/$s_!mqo-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png 848w, https://substackcdn.com/image/fetch/$s_!mqo-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png 1272w, https://substackcdn.com/image/fetch/$s_!mqo-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc5927c2-9d17-46c0-93f9-de72d48e1683_2576x1376.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p>Spark recomputes a DataFrame from scratch every time an action is called on it. If you use the same DataFrame for multiple actions &#8594; caching stores the result after the first action so subsequent actions reuse it.</p></li><li><p>Caching also gives you visibility into the <strong>real in-memory size</strong> of your data. Parquet files are compressed on disk (typically with Snappy). When Spark reads them, it decompresses and deserializes the data into memory &#8212; the in-memory size is always larger than the on-disk size.</p></li></ul><pre><code><code>df = spark.read.parquet("big_file/")  # not loaded yet
df.count()    # reads file, computes count, result discarded
df.count()    # reads file AGAIN &#8212; same work repeated
</code></code></pre><ul><li><p>With caching:</p></li></ul><pre><code><code>df.cache()
df.count()    # reads file, computes, stores in memory
df.count()    # reuses cached copy &#8212; no file I/O
</code></code></pre><h2><code>cache()</code> vs <code>persist()</code></h2><pre><code><code>df.cache()
# equivalent to:
df.persist(StorageLevel.MEMORY_AND_DISK)
</code></code></pre><p><code>cache()</code> is a shortcut. <code>persist()</code> lets you choose the storage level.</p><ul><li><p>Important: caching is lazy: Calling <code>cache()</code> or <code>persist()</code> does NOT immediately store anything. The data is cached the first time an <strong>action</strong> touches it.</p></li></ul><pre><code><code>df.cache()      # registers intent, nothing stored yet
df.count()      # this action triggers materialization &#8594; now cached
df.filter(...)  # uses cached data
</code></code></pre><ul><li><p>When to cache</p><ul><li><p>You call multiple actions on the same DataFrame</p></li><li><p>The DataFrame is expensive to recompute (large file scan + transformations)</p></li><li><p>The DataFrame fits (at least partially) in memory</p></li></ul></li><li><p>When NOT to cache</p><ul><li><p>You only use the DataFrame once</p></li><li><p>The DataFrame is larger than available memory (thrashing)</p></li><li><p>The DataFrame is derived from a streaming source</p></li></ul></li></ul>]]></content:encoded></item><item><title><![CDATA[All things Spark - TLDR (Part 3 - Deploys, Spark Submit and Executors)]]></title><description><![CDATA[Having a hard time learning Spark? This is my TLDR]]></description><link>https://markphammm.substack.com/p/all-things-spark-tldr-part-3-deploys</link><guid isPermaLink="false">https://markphammm.substack.com/p/all-things-spark-tldr-part-3-deploys</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Sun, 19 Apr 2026 21:30:28 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JYR9%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Deploy Modes</h2><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!u8QP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8052a02-4d23-4969-af24-f303dde9f1db_1759x432.png 424w, https://substackcdn.com/image/fetch/$s_!u8QP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8052a02-4d23-4969-af24-f303dde9f1db_1759x432.png 848w, https://substackcdn.com/image/fetch/$s_!u8QP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8052a02-4d23-4969-af24-f303dde9f1db_1759x432.png 1272w, https://substackcdn.com/image/fetch/$s_!u8QP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8052a02-4d23-4969-af24-f303dde9f1db_1759x432.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21u8QP%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe8052a02-4d23-4969-af24-f303dde9f1db_1759x432.png" width="1456" height="358" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e8052a02-4d23-4969-af24-f303dde9f1db_1759x432.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:358,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Understanding Apache Spark Deployment Modes: Client Mode, Cluster Mode, and  Local Mode | by Meet Patel | Stackademic&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Understanding Apache Spark Deployment Modes: Client Mode, Cluster Mode, and  Local Mode | by Meet Patel | Stackademic" title="Understanding Apache Spark Deployment Modes: Client Mode, Cluster Mode, and  Local Mode | by Meet Patel | Stackademic" srcset="https://substackcdn.com/image/fetch/$s_!u8QP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8052a02-4d23-4969-af24-f303dde9f1db_1759x432.png 424w, https://substackcdn.com/image/fetch/$s_!u8QP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8052a02-4d23-4969-af24-f303dde9f1db_1759x432.png 848w, https://substackcdn.com/image/fetch/$s_!u8QP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8052a02-4d23-4969-af24-f303dde9f1db_1759x432.png 1272w, https://substackcdn.com/image/fetch/$s_!u8QP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8052a02-4d23-4969-af24-f303dde9f1db_1759x432.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></div></figure></div><ul><li><p><strong>Local mode</strong> &#8212; the entire Spark application runs on a single machine, achieving parallelism through multiple threads. No cluster required. </p></li><li><p><strong>Client mode</strong> &#8212; the driver remains on the client machine that submitted the application. The client machine must maintain the driver process throughout the application&#8217;s execution. Used for interactive sessions and debugging.</p></li><li><p><strong>Cluster mode</strong> &#8212; the driver process is launched on a worker node alongside the executor processes. The cluster manager handles all processes. The submitting machine can disconnect after submission. Used for production batch jobs.</p></li></ul><h2>Spark Submit</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21lvVZ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lvVZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png 424w, https://substackcdn.com/image/fetch/$s_!lvVZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png 848w, https://substackcdn.com/image/fetch/$s_!lvVZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png 1272w, https://substackcdn.com/image/fetch/$s_!lvVZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21lvVZ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png" width="761" height="516" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:516,&quot;width&quot;:761,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Running Spark on Kubernetes - Spark 4.1.1 Documentation&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Running Spark on Kubernetes - Spark 4.1.1 Documentation" title="Running Spark on Kubernetes - Spark 4.1.1 Documentation" srcset="https://substackcdn.com/image/fetch/$s_!lvVZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png 424w, https://substackcdn.com/image/fetch/$s_!lvVZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png 848w, https://substackcdn.com/image/fetch/$s_!lvVZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png 1272w, https://substackcdn.com/image/fetch/$s_!lvVZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0c83af5-563c-4a1a-97ac-b086bb21642e_761x516.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>spark-submit</code> is the command-line tool for launching Spark applications on a cluster. It packages your script, sends it to the cluster manager, and starts the Driver and Executors.</p><ol><li><p><code>spark-submit</code> connects to the <strong>Master</strong> node via the cluster URL.</p></li><li><p>The Master finds available <strong>Worker</strong> nodes with enough resources.</p></li><li><p>The Worker launches <strong>Executor</strong> processes &#8212; JVM processes that will run your tasks.</p></li><li><p>Your <strong>Driver</strong> (running in client mode on the machine that called <code>spark-submit</code>) coordinates the job, sends tasks to executors, and collects results.</p></li><li><p>When the job finishes, executors are released and the Driver exits.</p></li></ol><h2>Spark Submit Basic syntax</h2><pre><code><code>spark-submit \\
  --master &lt;master-url&gt; \\
  --deploy-mode &lt;client|cluster&gt; \\
  --name &lt;app-name&gt; \\
  --conf key=value \\
  path/to/script.py
</code></code></pre><h3>Key flags</h3><ul><li><p><code>--master</code> </p><ul><li><p>Description: Cluster manager URL </p></li><li><p>Example: <code>spark://spark-master:7077</code></p></li></ul></li><li><p><code>--deploy-mode</code> </p><ul><li><p>Description: Where Driver runs </p></li><li><p>Example: <code>client</code> or <code>cluster</code></p></li></ul></li><li><p><code>--name</code> Description: </p><ul><li><p>App name (appears in UI &amp; History Server) </p></li><li><p>Example: <code>my-etl-job</code></p></li></ul></li><li><p><code>--executor-memory</code> </p><ul><li><p>Description: Memory per executor </p></li><li><p>Example: <code>4g</code></p></li></ul></li><li><p><code>--executor-cores</code> </p><ul><li><p>Description: Cores per executor </p></li><li><p>Example: <code>2</code></p></li></ul></li><li><p><code>--num-executors</code> </p><ul><li><p>Description: Number of executors </p></li><li><p>Example: <code>2</code></p></li></ul></li><li><p><code>--conf</code> </p><ul><li><p>Description: Any Spark config </p></li><li><p>Example: <code>spark.sql.shuffle.partitions=8</code></p></li></ul></li></ul><h2><strong>Executor Configuration &amp; Static Allocation</strong></h2><p>An <strong>executor</strong> is a JVM process on a worker node. Each executor:</p><ul><li><p>Holds a fixed number of CPU cores (tasks run in parallel = number of cores)</p></li><li><p>Has a fixed amount of memory (heap + overhead)</p></li><li><p>Lives for the duration of the application (static allocation)</p></li></ul><h3>Key configs</h3><pre><code><code>spark.executor.instances   # how many executors to launch - eg: 2
spark.executor.cores       # CPU cores per executor: eg: 2
spark.executor.memory      # JVM heap per executor (e.g. "4g")
</code></code></pre><h2>Executor memory - on heap</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JYR9%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JYR9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png 424w, https://substackcdn.com/image/fetch/$s_!JYR9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png 848w, https://substackcdn.com/image/fetch/$s_!JYR9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png 1272w, https://substackcdn.com/image/fetch/$s_!JYR9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JYR9%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png" width="1034" height="464" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:464,&quot;width&quot;:1034,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;How Does Apache Spark Manage Executor Memory? | by Kerrache Massipssa |  Medium&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="How Does Apache Spark Manage Executor Memory? | by Kerrache Massipssa |  Medium" title="How Does Apache Spark Manage Executor Memory? | by Kerrache Massipssa |  Medium" srcset="https://substackcdn.com/image/fetch/$s_!JYR9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png 424w, https://substackcdn.com/image/fetch/$s_!JYR9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png 848w, https://substackcdn.com/image/fetch/$s_!JYR9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png 1272w, https://substackcdn.com/image/fetch/$s_!JYR9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20db748f-2aff-4db0-bd0a-96303422252a_1034x464.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>executor.memory</code><strong> (on-heap) is split into 3 buckets:</strong> reserved (300MB fixed), unified (~60%), and user (~40%).</p><ul><li><p><code>reserved memory</code> (300MB fixed)</p></li><li><p><code>unified memory</code> is the main working area: shared between execution (compute) and storage (cache) - define by <code>spark.memory.fraction</code></p><ul><li><p><strong>Execution (joins, shuffles) has priority:</strong> it can take memory from storage if needed.</p><ul><li><p><code>1-spark.memory.storageFraction</code></p></li></ul></li><li><p><strong>Storage (cached data) is flexible:</strong> uses free space but gets evicted under pressure.</p><ul><li><p><code>spark.memory.storageFraction</code></p></li></ul></li></ul></li><li><p><code>user memory</code><strong> is unmanaged:</strong> large custom objects here can still cause OOM. (<code>1- spark.memory.fraction</code></p></li></ul><pre><code><code>On-heap total        : 4096 MB
  Reserved           :  300 MB  (fixed)
  Remaining          : 3796 MB
  User memory        : 3796 &#215; 0.4 &#8776; 1518 MB
  Unified memory     : 3796 &#215; 0.6 &#8776; 2278 MB
    Storage (initial): 2278 &#215; 0.5 &#8776; 1139 MB
    Execution        : 2278 &#215; 0.5 &#8776; 1139 MB
</code></code></pre><h2>Executor memory - off heap</h2><ul><li><p>Spark off-heap memory stores Spark&#8217;s internal working data outside the JVM heap to reduce garbage collection pauses and Java object overhead.</p></li><li><p>It is mainly used for execution tasks like shuffle, sort, joins, and aggregation, plus serialized cached data used by Spark SQL/DataFrames.</p></li><li><p>It can improve performance, but it still counts against real executor/container memory, so you must size memory overhead correctly.</p></li></ul><h2><strong>Scheduling Modes</strong></h2><ul><li><p><strong>FIFO (default):</strong> Jobs are queued in order of submission. The first job gets priority on all available resources. If it doesn&#8217;t consume everything, later jobs can start immediately &#8212; but if it uses all resources, later jobs wait until it finishes.</p></li><li><p><strong>Fair:</strong> Spark assigns tasks between jobs in a <strong>round-robin</strong> fashion, ensuring equal resource sharing. A short job submitted while a long job is running can start receiving resources immediately without waiting for the long job to finish.</p></li><li><p><strong>Pools</strong></p><ul><li><p>You can group jobs into named <strong>pools</strong> and control how resources are distributed between them. Each pool has three configurable properties:</p><ol><li><p><strong>Scheduling mode</strong> (default: FIFO) &#8212; controls scheduling behavior <em>within</em> the pool. Can be FIFO or FAIR.</p></li><li><p><strong>Weight</strong> (default: 1) &#8212; controls the pool&#8217;s share of cluster resources relative to other pools. A pool with <code>weight=2</code> gets double the resources of a pool with <code>weight=1</code>.</p></li><li><p><strong>minShare</strong> (default: 0) &#8212; a minimum number of CPU cores the scheduler always tries to satisfy for the pool before redistributing extra resources by weight.</p></li></ol></li></ul></li></ul><pre><code><code>&lt;!-- fairscheduler.xml --&gt; # example config
&lt;allocations&gt;
  &lt;pool name="high_priority"&gt;
    &lt;weight&gt;2&lt;/weight&gt;
    &lt;minShare&gt;2&lt;/minShare&gt;
  &lt;/pool&gt;
  &lt;pool name="default"&gt;
    &lt;weight&gt;1&lt;/weight&gt;
    &lt;minShare&gt;0&lt;/minShare&gt;
  &lt;/pool&gt;
&lt;/allocations&gt;
</code></code></pre><ul><li><p>When to use</p><ul><li><p>Sequential batch ETL - FIFO</p></li><li><p>Interactive queries (notebooks) - Fair</p></li><li><p>Mixed priority jobs - Fair + pools</p></li></ul></li></ul>]]></content:encoded></item><item><title><![CDATA[All things Spark - TLDR (Part 2 - RDD)]]></title><description><![CDATA[Having a hard time learning Spark? This is my TLDR]]></description><link>https://markphammm.substack.com/p/all-things-spark-tldr-part-2-rdd</link><guid isPermaLink="false">https://markphammm.substack.com/p/all-things-spark-tldr-part-2-rdd</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Sat, 18 Apr 2026 19:14:28 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vLX3%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here&#8217;s my TLDR on reading several <a href="iframe.php?url=https%3A%2F%2Fvutr.substack.com%2F">Vu Trinh&#8217;s</a> articles. </p><h2>RDD</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%210vLe%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Feb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0vLe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png 424w, https://substackcdn.com/image/fetch/$s_!0vLe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png 848w, https://substackcdn.com/image/fetch/$s_!0vLe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png 1272w, https://substackcdn.com/image/fetch/$s_!0vLe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%210vLe%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Feb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png" width="1280" height="611" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:611,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;What is an RDD in Apache Spark?&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="What is an RDD in Apache Spark?" title="What is an RDD in Apache Spark?" srcset="https://substackcdn.com/image/fetch/$s_!0vLe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png 424w, https://substackcdn.com/image/fetch/$s_!0vLe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png 848w, https://substackcdn.com/image/fetch/$s_!0vLe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png 1272w, https://substackcdn.com/image/fetch/$s_!0vLe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb0be311-e8fc-42a4-a60d-3a571767377f_1280x611.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p>RDD stands for <strong>Resilient Distributed Dataset</strong>. It is Spark&#8217;s foundational data abstraction &#8212; the thing every higher-level API (DataFrames, Datasets) compiles down to under the hood.</p></li><li><p>RDDs are lazy &#8212; nothing runs until you call an <strong>action. </strong>Remember this! this is important</p></li></ul><h2>RDD Characteristics</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vLX3%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vLX3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp 424w, https://substackcdn.com/image/fetch/$s_!vLX3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp 848w, https://substackcdn.com/image/fetch/$s_!vLX3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp 1272w, https://substackcdn.com/image/fetch/$s_!vLX3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vLX3%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp" width="1280" height="960" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:960,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;How to Answer Spark Interview Questions on RDDs, DataFrames &amp; Datasets&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="How to Answer Spark Interview Questions on RDDs, DataFrames &amp; Datasets" title="How to Answer Spark Interview Questions on RDDs, DataFrames &amp; Datasets" srcset="https://substackcdn.com/image/fetch/$s_!vLX3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp 424w, https://substackcdn.com/image/fetch/$s_!vLX3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp 848w, https://substackcdn.com/image/fetch/$s_!vLX3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp 1272w, https://substackcdn.com/image/fetch/$s_!vLX3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ce1e21-5758-4669-b2ab-0be078dd89aa_1280x960.webp 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p><strong>Immutable:</strong> once created, its data never changes, new transformation = new RDD</p><ol><li><p><strong>Concurrent processing</strong> - immutability keeps data consistent across multiple tasks and threads without complex locks or race conditions.</p></li><li><p><strong>Lineage and fault tolerance</strong> - each transformation records how it was derived. If a partition is lost, Spark recomputes it by replaying the lineage. Mutable data would make this impossible.</p></li><li><p><strong>Functional programming</strong> - transformations become pure functions: same input always produces the same output.</p></li></ol></li><li><p><strong>Partitioned:</strong> the data is split into chunks called partitions (can be process by different executor)</p></li><li><p><strong>In-memory by default:</strong> keeps data in memory for as long as possible (avoiding disk IO)</p></li><li><p>RDDs are lazyyyyy, this help Spark refine the plan before action. </p></li></ul><h2>RDD properties</h2><ul><li><p><strong>List of partitions</strong>: How the data is split for parallel processing</p></li><li><p><strong>Computation function</strong>: How to compute the data for each partition</p></li><li><p><strong>Dependencies</strong>: Which parent RDD(s) this was derived from</p></li><li><p><strong>Partitioner (optional)</strong>: For key-value RDDs: how keys are mapped to partitions</p></li><li><p><strong>Preferred locations (optional)</strong>: Where partitions should ideally be computed (e.g., HDFS block locations)</p></li></ul><h2><strong>RDD Operation (Transformation vs Actions)</strong></h2><ul><li><p><strong>Transformations &#8212; lazy:</strong></p><ul><li><p>Transformations define <em>how</em> data should be changed, but they do not run immediately</p></li><li><p>Because RDDs are immutable, every transformation produces a <strong>new</strong> RDD.</p></li><li><p>Examples: <code>map(f)</code> , <code>filter(f)</code> , <code>flatMap(f)</code></p></li></ul></li><li><p><strong>Actions &#8212; eager</strong></p><ul><li><p>Actions tell Spark to actually execute the accumulated transformations and return a result.</p></li><li><p>Examples: <code>count()</code> , <code>collect()</code> , <code>take(n)</code> , <code>reduce(f)</code> , <code>saveAsTextFile(path)</code></p></li></ul></li></ul><h2><strong>Spark execution anatomy</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%213QsF%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3QsF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png 424w, https://substackcdn.com/image/fetch/$s_!3QsF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png 848w, https://substackcdn.com/image/fetch/$s_!3QsF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png 1272w, https://substackcdn.com/image/fetch/$s_!3QsF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%213QsF%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png" width="1074" height="492" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:492,&quot;width&quot;:1074,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Everything you need to know about Spark Structured Streaming&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Everything you need to know about Spark Structured Streaming" title="Everything you need to know about Spark Structured Streaming" srcset="https://substackcdn.com/image/fetch/$s_!3QsF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png 424w, https://substackcdn.com/image/fetch/$s_!3QsF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png 848w, https://substackcdn.com/image/fetch/$s_!3QsF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png 1272w, https://substackcdn.com/image/fetch/$s_!3QsF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0aa8b3a1-ff25-470b-b13f-b87ae53d4275_1074x492.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p><strong>Job</strong> &#8212; a series of transformations triggered by an action. It represents the entire workflow from start to finish.</p></li><li><p><strong>Stage</strong> &#8212; a segment of a job that executes without shuffling data. A job is split into multiple stages whenever a transformation requires redistributing data across partitions (a shuffle).</p></li><li><p><strong>Task</strong> &#8212; the smallest unit of execution. Each stage is divided into tasks, <strong>one per partition,</strong> running in parallel across executors.</p><ul><li><p>A partition is Spark&#8217;s unit of parallelism</p></li><li><p>An RDD is divided into partitions</p></li><li><p>Each partition is processed independently by a different executor (or thread in local mode).</p></li></ul></li><li><p><strong>DAG</strong> &#8212; Spark builds a Directed Acyclic Graph of stages from RDD dependencies, scheduling them in topological order.</p></li></ul><h2>Controlling partitions</h2><ul><li><p><code>numSlices=n</code> can determine the number of partition you have</p></li><li><p>Repartitioning after creation</p><ul><li><p><code>repartition(n)</code>: Increase or decrease &#8212; causes a full shuffle</p></li><li><p><code>coalesce(n)</code>: Decrease only &#8212; avoids a shuffle by merging local partitions</p></li></ul></li><li><p>rule of thumb</p><ul><li><p>Too few partitions &#8594; some executors sit idle, work is uneven</p></li><li><p>Too many partitions &#8594; scheduling overhead dominates, tasks finish too fast to be worth the overhead</p></li><li><p>A good rule of thumb: 2&#8211;4 partitions per CPU core in your cluster</p></li></ul></li></ul><h2><strong>Narrow vs Wide Transformations</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21pycD%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pycD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png 424w, https://substackcdn.com/image/fetch/$s_!pycD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png 848w, https://substackcdn.com/image/fetch/$s_!pycD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png 1272w, https://substackcdn.com/image/fetch/$s_!pycD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21pycD%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png" width="633" height="386" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:386,&quot;width&quot;:633,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;RDD Transformations and Actions. In previous article on Spark&#8230; | by Piyush  Jain | Medium&quot;,&quot;title&quot;:&quot;RDD Transformations and Actions. In previous article on Spark&#8230; | by Piyush  Jain | Medium&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="RDD Transformations and Actions. In previous article on Spark&#8230; | by Piyush  Jain | Medium" title="RDD Transformations and Actions. In previous article on Spark&#8230; | by Piyush  Jain | Medium" srcset="https://substackcdn.com/image/fetch/$s_!pycD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png 424w, https://substackcdn.com/image/fetch/$s_!pycD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png 848w, https://substackcdn.com/image/fetch/$s_!pycD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png 1272w, https://substackcdn.com/image/fetch/$s_!pycD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b7f2395-26a2-4d34-9408-5298cb02faee_633x386.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p>A <strong>shuffle</strong> is when Spark needs to redistribute data across partitions - moving records between executors over the network. It is the most expensive operation in Spark because it involves disk writes and network I/O.</p></li><li><p><strong>Narrow dependencies:</strong></p><ul><li><p>A transformation has a narrow dependency when each output partition depends on at most one input partition**. No data needs to move between executors.**</p></li><li><p>These transformations stay in the same <strong>stage</strong> &#8212; Spark can pipeline them together.</p></li><li><p>Examples: <code>map</code>, <code>filter</code>, <code>flatMap</code>, <code>coalesce</code></p></li></ul></li><li><p><strong>Wide dependencies:</strong></p><ul><li><p>A transformation has a wide dependency when each output partition may depend on multiple input partitions. Data must be shuffled across the network.</p></li><li><p>Wide transformations create a <strong>stage boundary</strong> in Spark&#8217;s execution plan &#8212; the current stage must complete before the next one can begin.</p></li><li><p>Examples: <code>groupByKey</code>, <code>reduceByKey</code>, <code>join</code>, <code>distinct</code>, <code>repartition</code></p></li></ul></li></ul><h2><strong>Stage boundaries</strong></h2><ul><li><p>Stage boundaries happen when there&#8217;s a wide transformation, the current stage must complete before the next one can begin.</p></li></ul><pre><code><code>rdd2 = rdd.map(...)          # narrow &#8212; same stage
rdd3 = rdd2.filter(...)      # narrow &#8212; same stage
rdd4 = rdd3.groupByKey()     # WIDE &#8212; new stage begins here (shuffle)
rdd5 = rdd4.map(...)         # narrow &#8212; same stage as rdd4
</code></code></pre><ul><li><p>When a wide transformation (shuffle) appears, you&#8217;ll see extra indentation in the debug string &#8212; that marks a stage boundary:</p></li></ul><pre><code><code>(1) PythonRDD[...] []           &#8592; stage 2 starts here
 |  ShuffledRDD[...] []
 +-(1) PythonRDD[...] []        &#8592; stage 1
    |  ParallelCollectionRDD[]
</code></code></pre><h2><strong>Lineage and Fault Tolerance</strong></h2><ul><li><p>Spark does not replicate data across nodes the way Hadoop MapReduce does. Instead, it tracks <strong>lineage</strong> &#8212; a record of every transformation applied to produce each RDD.</p></li><li><p>If a partition is lost (e.g., an executor dies), Spark can reconstruct it by replaying only the transformations for that partition, starting from the original data source. No data replication needed, no writing intermediate results to disk.</p></li></ul>]]></content:encoded></item><item><title><![CDATA[All things Spark - TLDR (Part 1)]]></title><description><![CDATA[Having a hard time learning Spark? This is my TLDR]]></description><link>https://markphammm.substack.com/p/all-things-spark-tldr-part-1</link><guid isPermaLink="false">https://markphammm.substack.com/p/all-things-spark-tldr-part-1</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Fri, 17 Apr 2026 20:59:09 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21M70e%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%213DKg%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3DKg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png 424w, https://substackcdn.com/image/fetch/$s_!3DKg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png 848w, https://substackcdn.com/image/fetch/$s_!3DKg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png 1272w, https://substackcdn.com/image/fetch/$s_!3DKg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%213DKg%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png" width="960" height="499" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:499,&quot;width&quot;:960,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;File:Apache Spark logo.svg - Wikimedia Commons&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="File:Apache Spark logo.svg - Wikimedia Commons" title="File:Apache Spark logo.svg - Wikimedia Commons" srcset="https://substackcdn.com/image/fetch/$s_!3DKg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png 424w, https://substackcdn.com/image/fetch/$s_!3DKg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png 848w, https://substackcdn.com/image/fetch/$s_!3DKg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png 1272w, https://substackcdn.com/image/fetch/$s_!3DKg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1a2f9d6-3443-4297-a517-4c9a6d431ee9_960x499.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Computer Architecture Basics</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JNQ_%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fcbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JNQ_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png 424w, https://substackcdn.com/image/fetch/$s_!JNQ_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png 848w, https://substackcdn.com/image/fetch/$s_!JNQ_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png 1272w, https://substackcdn.com/image/fetch/$s_!JNQ_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JNQ_%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fcbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png" width="568" height="610" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:610,&quot;width&quot;:568,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Memory Consistency Models: A Tutorial &#8212; James Bornholt&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Memory Consistency Models: A Tutorial &#8212; James Bornholt" title="Memory Consistency Models: A Tutorial &#8212; James Bornholt" srcset="https://substackcdn.com/image/fetch/$s_!JNQ_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png 424w, https://substackcdn.com/image/fetch/$s_!JNQ_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png 848w, https://substackcdn.com/image/fetch/$s_!JNQ_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png 1272w, https://substackcdn.com/image/fetch/$s_!JNQ_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbf0ec26-a5a5-43e9-9be1-fe22166dcde5_568x610.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p><strong>Core</strong> = independent processing unit in a CPU. More cores = more things at once.</p></li><li><p><strong>Process</strong> = a running program with its own isolated memory (e.g., a Spark executor is a JVM process).</p></li><li><p><strong>Thread</strong> = lightweight unit of work inside a process. Threads share the process&#8217;s RAM.</p></li><li><p><strong>Multithreading</strong> = multiple threads running in one process on different cores. This is how executors run tasks.</p></li><li><p><strong>JVM</strong> = Java Virtual Machine, the <strong>process</strong> that runs Spark code. Even PySpark sends instructions to a JVM via Py4J.</p></li><li><p><strong>RAM</strong> = fast, volatile memory for active data (~100 ns access). <strong>Disk</strong> = slow, persistent storage (~0.1 ms SSD).</p></li><li><p><strong>OOM</strong> = Out Of Memory. Spark spills to disk first; if that&#8217;s not enough, the executor crashes.</p></li></ul><h2><strong>Spark Cluster Architecture - Cluster Manager &amp; Executor</strong></h2><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21M70e%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!M70e!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png 424w, https://substackcdn.com/image/fetch/$s_!M70e!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png 848w, https://substackcdn.com/image/fetch/$s_!M70e!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png 1272w, https://substackcdn.com/image/fetch/$s_!M70e!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21M70e%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png" width="1400" height="788" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:788,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Apache Spark Architecture. This article is an introduction to&#8230; | by Hiral  Yadav | Medium&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Apache Spark Architecture. This article is an introduction to&#8230; | by Hiral  Yadav | Medium" title="Apache Spark Architecture. This article is an introduction to&#8230; | by Hiral  Yadav | Medium" srcset="https://substackcdn.com/image/fetch/$s_!M70e!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png 424w, https://substackcdn.com/image/fetch/$s_!M70e!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png 848w, https://substackcdn.com/image/fetch/$s_!M70e!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png 1272w, https://substackcdn.com/image/fetch/$s_!M70e!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c7e1213-60c9-4381-975e-3dec0700b8c3_1400x788.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p>The <strong>Spark cluster</strong> is JVM processes (driver + executors) that run on top of those machines.</p></li><li><p>Including Cluster Manager and Executor</p></li><li><p>The <strong>cluster manager</strong> (standalone master at <code>:7077</code>) brokers resources between them.</p></li><li><p><strong>Ports</strong>: <code>:8080</code> Master Web UI, <code>:7077</code> Master RPC, <code>:18080</code> History Server.</p></li><li><p><strong>History Server</strong> = read-only UI that persists after jobs finish. Reads event logs from shared <code>spark-events</code> volume.</p></li></ul><h2><strong>Resource Request (</strong><code>instances</code><strong> , </strong><code>cores</code><strong>, </strong><code>memory</code><strong> ,</strong> <code>maxPartitionBytes</code> )</h2><ul><li><p>A Spark Job divided into Stages</p><ul><li><p>A Stage divided into Tasks</p></li></ul></li></ul><ul><li><p>Machines <strong>offer</strong> resources (eg: workers: 4 cores, 4g each). Spark app <strong>requests</strong> a subset.</p></li><li><p><code>spark.executor.instances=2</code> &#8594; 2 executor JVMs across the cluster.</p></li><li><p><code>spark.executor.cores=2</code> &#8594; 2 cores per executor &#8594; 2 tasks in parallel per executor.</p></li><li><p><code>spark.executor.memory="2g"</code> &#8594; JVM heap per executor. Usable = <code>(2048-300) &#215; 0.6 = 1048 MB</code>.</p></li><li><p><code>spark.cores.max=4</code> &#8594; caps total cores (standalone Spark is greedy by default).</p></li><li><p><code>spark.sql.files.maxPartitionBytes=128MB</code> &#8594; partition size &#8594; determines number of tasks. eg set by <code>128 * 1024 * 1024</code></p></li><li><p><code>spark.task.cpus=1</code> (default, not in code) &#8594; cores per task. Tasks per executor = <code>executor.cores &#247; task.cpus</code>.</p></li></ul><h2><strong>Partitions</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%212HpA%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2HpA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png 424w, https://substackcdn.com/image/fetch/$s_!2HpA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png 848w, https://substackcdn.com/image/fetch/$s_!2HpA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png 1272w, https://substackcdn.com/image/fetch/$s_!2HpA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%212HpA%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png" width="1400" height="994" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:994,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Spark Partitions Explained: Why Partitioning Matters for Performance | by  The Data Forge | Medium&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Spark Partitions Explained: Why Partitioning Matters for Performance | by  The Data Forge | Medium" title="Spark Partitions Explained: Why Partitioning Matters for Performance | by  The Data Forge | Medium" srcset="https://substackcdn.com/image/fetch/$s_!2HpA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png 424w, https://substackcdn.com/image/fetch/$s_!2HpA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png 848w, https://substackcdn.com/image/fetch/$s_!2HpA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png 1272w, https://substackcdn.com/image/fetch/$s_!2HpA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F996469ba-d7b3-4124-91c7-9634c3fc9468_1400x994.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p>A <strong>partition</strong> = a chunk of data processed by 1 task. </p></li><li><p>Execution hierarchy: <strong>Application &#8594; Job &#8594; Stage &#8594; Task</strong>.</p></li><li><p>Set by <code>spark.sql.files.maxPartitionBytes=128MB</code> &#8594; partition size &#8594; determines number of tasks. eg set by <code>128 * 1024 * 1024</code></p></li><li><p><strong>Shuffle boundaries</strong> (groupBy, join, repartition) create new stages.</p><ul><li><p>Larger partitions = fewer tasks but more memory per task and higher spill risk.</p></li><li><p>Smaller partitions = more tasks but lighter workload per task.</p></li></ul></li></ul><h2>Caching</h2><ul><li><p><strong>Parquet on disk</strong> is compressed (SNAPPY, dictionary encoding). <strong>In-memory</strong> DataFrame is ~8-10x larger.</p></li><li><p><strong>Caching</strong> (<code>df.cache()</code> + <code>df.count()</code>) materializes data in RAM. Visible in History Server Storage tab.</p></li><li><p>If cached data exceeds executor memory, partitions spill to disk.</p></li></ul><h2><strong>Execution Plan</strong></h2><ul><li><p>Spark uses <strong>two-phase aggregation</strong>: partial aggregate (map-side) &#8594; shuffle &#8594; final aggregate (reduce-side).</p><ul><li><p><strong>HashAggregate</strong> = fast (in-memory hash map), used for numeric/fixed-size types.</p></li><li><p><strong>SortAggregate</strong> = slower (requires sorting), used for variable-size types like Strings.</p></li></ul></li><li><p><strong>Exchange (shuffle)</strong> = redistribute data by key across executors. Controlled by <code>spark.sql.shuffle.partitions</code> (default 200).</p></li><li><p><strong>AQE</strong> (Adaptive Query Execution) = runtime optimization that coalesces small shuffle partitions.</p></li></ul><h2>Hash Aggregate (Better)</h2><ul><li><p>Builds a hash table where each key maps to an aggregation buffer that gets updated in-place.</p></li><li><p>No sorting needed &#8212; just hash the key and update the buffer</p></li><li><p>Requires a hash table in memory (can be large)</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21PnKo%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PnKo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png 424w, https://substackcdn.com/image/fetch/$s_!PnKo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png 848w, https://substackcdn.com/image/fetch/$s_!PnKo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png 1272w, https://substackcdn.com/image/fetch/$s_!PnKo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21PnKo%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png" width="1456" height="1125" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1125,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PnKo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png 424w, https://substackcdn.com/image/fetch/$s_!PnKo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png 848w, https://substackcdn.com/image/fetch/$s_!PnKo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png 1272w, https://substackcdn.com/image/fetch/$s_!PnKo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9082d0-3a60-408d-9f83-d8e6f2443ed8_1522x1176.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Sort Aggregate (Fall back)</h2><ul><li><p>Sorts the data by the grouping key first, then iterates over it. When the key changes, the previous group is complete.</p></li><li><p>Requires sorting the data first (expensive)</p></li><li><p>Memory-efficient: only holds one group in memory at a time</p></li><li><p>When does Spark fall back on SortAggregate?</p><ul><li><p><strong>Case 1: Hash table doesn&#8217;t fit in memory:</strong> If the number of unique keys is very large, the hash table grows too big for the executor&#8217;s memory.</p></li><li><p><strong>Case 2: Non-fixed-size data types in the aggregation buffer</strong>: For HashAggregate, Spark uses <strong>UnsafeRow</strong> format &#8212; a compact, off-heap memory representation that bypasses JVM garbage collection (part of <strong>Project Tungsten</strong>). This format works by allocating fixed-size memory slots for each aggregation buffer. If the column doesn&#8217;t have fixed-size memory, it doesn&#8217;t work. </p></li></ul></li></ul>]]></content:encoded></item><item><title><![CDATA[The Productivity Stack I Actually Use as an Analytics Engineer]]></title><description><![CDATA[The shortcuts, aliases, and terminal tools that help me move faster and reduce friction at work]]></description><link>https://markphammm.substack.com/p/the-productivity-stack-i-actually</link><guid isPermaLink="false">https://markphammm.substack.com/p/the-productivity-stack-i-actually</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Tue, 17 Mar 2026 16:12:46 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%216_hZ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Intro</h1><p>I&#8217;m an Analytics Engineer at Insurify, and I&#8217;m borderline obsessed with tech and productivity tools.</p><p>Not in the fake &#8220;morning routine, optimized water bottle, 17-tab Notion dashboard&#8221; way. I mean the kind of obsession that comes from doing real work every day and getting annoyed by tiny bits of friction that keep slowing you down. A shortcut you have to remember manually. A formatter you forget to run. A command you type over and over. A workflow that should take one keystroke but somehow takes five.</p><p>Over time, I&#8217;ve built a stack of small systems that make my day faster, cleaner, and less mentally taxing. Better keybindings in <strong>Cursor</strong> and <strong>VS Code</strong>. <strong>Shell aliases</strong> that remove repetitive commands. CLI tools that actually make the terminal enjoyable to use. Little workflow upgrades that seem minor on their own, but add up hard over weeks and months.</p><p>This is a look into that setup: the shortcuts, configs, and tools I actually use, why I use them, and where they save me time. Nothing theoretical. Just practical stuff that makes shipping code and moving through projects less annoying.</p><p>If you care about developer ergonomics, terminal workflows, and cutting useless friction out of your day, this is for you.</p><h1><code>Keybinding.json</code></h1><p>Pushing code only to realize you haven&#8217;t run <code>sqlfluff</code>/ <code>black</code> is annoying. The <code>keybinding.json</code> file solves that problem. The <code>keybinding.json</code> file is a configuration file used in VS Code/Cursor to customize keyboard shortcuts. It allows you to override default keyboard bindings or create new ones to enhance your development workflow. Example such as:</p><ul><li><p>Terminal integration shortcuts (<code>Cmd+1</code>) &#8212; switch around easily from panel to terminal</p></li><li><p>File formatting for Python (using Black) and SQL (using SQLFluff)</p></li><li><p>Composer mode activation (<code>cmd+i</code>)</p></li><li><p>Combined save and format operations (<code>cmd+shift+s</code>)</p></li></ul><pre><code><code>[
    {
        "key": "cmd+i",
        "command": "composerMode.agent"
    },
    {
        "key": "cmd+1",
        "command": "workbench.action.terminal.focus"
    },
    {
        "key": "cmd+1",
        "command": "workbench.action.focusActiveEditorGroup",
        "when": "terminalFocus"
    },
    {
        "key": "cmd+shift+c",
        "command": "copyRelativeFilePath",
        "when": "editorFocus"
    },
    {
        "key": "cmd+shift+f",
        "command": "workbench.action.tasks.runTask",
        "args": "SQLFluff Fix Current File",
        "when": "editorTextFocus &amp;&amp; resourceExtname == .sql"
    },
    {
        "key": "cmd+shift+l",
        "command": "workbench.action.tasks.runTask",
        "args": "SQLFluff Lint Current File",
        "when": "editorTextFocus &amp;&amp; resourceExtname == .sql"
    },
    {
        "key": "cmd+shift+b",
        "command": "workbench.action.terminal.sendSequence",
        "args": {
            "text": "black ${file}\\n"
        },
        "when": "editorTextFocus &amp;&amp; resourceExtname == .py"
    },
    {
        "key": "cmd+s",
        "command": "workbench.action.files.save"
    },
    {
        "key": "cmd+shift+s",
        "command": "runCommands",
        "args": {
            "commands": [
                {
                    "command": "workbench.action.terminal.sendSequence",
                    "args": {
                        "text": "sqff ${file}\\nsqfl ${file}\\n"
                    }
                },
                {
                    "command": "workbench.action.files.save"
                }
            ]
        },
        "when": "editorTextFocus &amp;&amp; resourceExtname == .sql"
    },
    {
        "key": "cmd+shift+s",
        "command": "runCommands",
        "args": {
            "commands": [
                {
                    "command": "workbench.action.terminal.sendSequence",
                    "args": {
                        "text": "black ${file}\\n"
                    }
                },
                {
                    "command": "workbench.action.files.save"
                }
            ]
        },
        "when": "editorTextFocus &amp;&amp; resourceExtname == .py"
    },
    {
        "key": "cmd+shift+e",
        "command": "workbench.action.terminal.sendSequence",
        "args": {
            "text": "python /Users/user/insurify/dbt/tests/completeness/model_yaml_matching_local.py\\n"
        }
    },
    {
        "key": "ctrl+z",
        "command": "workbench.action.terminal.sendSequence",
        "args": {
            "text": "\\u001f"
        } // sends Ctrl+/ (Undo in readline)
    },
    {
        "key": "ctrl+shift+z",
        "command": "workbench.action.terminal.sendSequence",
        "args": {
            "text": "\\u001f"
        } // also map Redo to the same undo-sequence
    }
]
</code></code></pre><h2>Example usage</h2><p>In my development workflow, one of the most time-saving keyboard shortcuts I&#8217;ve configured is <code>cmd+shift+s</code>. This powerful shortcut automates code formatting and saving in a single keystroke.</p><p>The shortcut is configured to perform different actions based on the file type:</p><ul><li><p><strong>For Python files</strong> (.py): It runs Black formatter and then saves the file</p></li><li><p><strong>For SQL files</strong> (.sql): It runs SQLFluff formatter and then saves the file</p></li></ul><p>This automation eliminates the repetitive task of manually formatting code before saving, ensuring consistent code style across projects.</p><h1><code>Alias</code></h1><p>Aliases in a <code>.zshrc</code> file are custom shortcuts for longer commands, making your terminal experience more efficient and personalized. They follow the syntax <code>alias shortcut="original command"</code>. For example, with an alias like <code>alias ll="ls -la"</code>, typing <code>ll</code> will execute the longer <code>ls -la</code> command.</p><p>These aliases save significant time by reducing keystrokes for repetitive tasks and standardizing workflows. You can add new aliases at any time by editing your <code>.zshrc</code> file and running <code>refresh</code> (which itself is an alias for <code>source ~/.zshrc</code>).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21eEPI%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eEPI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png 424w, https://substackcdn.com/image/fetch/$s_!eEPI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png 848w, https://substackcdn.com/image/fetch/$s_!eEPI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png 1272w, https://substackcdn.com/image/fetch/$s_!eEPI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21eEPI%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png" width="893" height="597" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/25899d2d-b412-404e-9f6b-017b310877ed_893x597.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:597,&quot;width&quot;:893,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:187324,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/191266646?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eEPI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png 424w, https://substackcdn.com/image/fetch/$s_!eEPI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png 848w, https://substackcdn.com/image/fetch/$s_!eEPI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png 1272w, https://substackcdn.com/image/fetch/$s_!eEPI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25899d2d-b412-404e-9f6b-017b310877ed_893x597.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code><code># docker alias
#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 
alias docker_clean="docker system prune -a &amp;&amp; docker volume prune &amp;&amp; docker image prune -a"
alias docker_start_mwaa="docker start 0f74edd6386b &amp;&amp; docker start c3855cb70002"
alias docker_stop_all="docker stop $(docker ps -q)" 
alias docker_remove_all_images="docker rmi $(docker images -q)"

#-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

# dbt alias

## dbt run 
dbt_run() {
  dbt run -s "$@" -f
}

dbt_run_operation() {
  dbt run-operation "$1" --args "${2:-{}}"
}
alias dbt_run_defer="dbt run --defer --state prod_dbt_artifacts -s"
alias dbt_run_defer_favor_state="dbt run --defer --state prod_dbt_artifacts --favor-state -s"
alias dbt_run_changed="dbt run --defer --state prod_dbt_artifacts --select state:modified+"

## dbt test 
alias dbt_test="dbt test -s"
alias dbt_test_defer="dbt test --defer --state prod_dbt_artifacts -s"
alias dbt_test_changed="dbt test --defer --state prod_dbt_artifacts --select state:modified+"

## dbt build 
alias dbt_build_changed="dbt build --defer --state prod_dbt_artifacts --select state:modified+"
alias dbt_build_defer="dbt build --defer --state prod_dbt_artifacts -s"
alias dbt_build_defer_favor_state="dbt build --defer --state prod_dbt_artifacts --favor-state -s"
alias dbt_build_all="dbt build --defer --state prod_dbt_artifacts --select + --resource-type=model"

## dbt clone
alias dbt_clone="dbt clone --state prod_dbt_artifacts -f --resource-type=model -s"

## dbt seed
alias dbt_seed="dbt seed -s"

## dbt parse
alias dbt_parse_fix="dbt parse --no-partial-parse"

## Run freshness checks
alias dbt_freshness="dbt source freshness"

## get_ manifest
alias get_dbt_manifest="cd ~/insurify/dbt/ &amp;&amp; curl -L -o json path" 

find_dir() {
  rg --files | xargs -n1 dirname | grep -i "$1" | sort -u
}

#-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
# sql fluff alias
alias sqfl="sqlfluff lint"
alias sqff="sqlfluff fix"

#-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
# .zshrc alias
alias refresh="source ~/.zshrc"
export PATH="$PATH:/Users/minh.pham/.local/bin"

#-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
# .git alias

git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)&lt;%an&gt;%Creset' --abbrev-commit"
alias git_delete_merged_branches='git checkout -q master &amp;&amp; git for-each-ref refs/heads/ "--format=%(refname:short)" | while read branch; do mergeBase=$(git merge-base master $branch) &amp;&amp; [[ $(git cherry master $(git commit-tree $(git rev-parse "$branch^{tree}") -p $mergeBase -m _)) == "-"* ]] &amp;&amp; git branch -D $branch; done'
alias git_branch_clean="git branch --merged | grep -v master | xargs git branch -d "
gitall() {
    git add .
    if [ "$1" != "" ] # or better, if [ -n "$1" ]
    then
        git commit -m "$1"
    else
        git commit
    fi
    git push
}

# Use eza instead of ls
alias ls="eza --group-directories-first"
alias ll="eza -l --group-directories-first"
alias la="eza -la --group-directories-first"
</code></code></pre><h1><code>CLI Tools</code></h1><p>For me, CLI tools are where productivity gains become real. I use tools like Warp, zoxide, eza, Oh My Zsh, and lazygit because they make the terminal faster, cleaner, and way less painful to work in all day. They cut down repetitive commands, make navigation easier, and turn common tasks like moving between projects, checking files, or handling Git into something that feels almost automatic. None of them are necessary, but once they&#8217;re part of your workflow, it&#8217;s hard to go back.</p><h3><code>Warp</code>: AI CLI Tools</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21W-YO%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!W-YO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png 424w, https://substackcdn.com/image/fetch/$s_!W-YO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png 848w, https://substackcdn.com/image/fetch/$s_!W-YO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png 1272w, https://substackcdn.com/image/fetch/$s_!W-YO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21W-YO%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png" width="1448" height="828" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:828,&quot;width&quot;:1448,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;GitHub - warpdotdev/Warp: Warp is the agentic development environment,  built for coding with multiple AI agents. &#183; GitHub&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="GitHub - warpdotdev/Warp: Warp is the agentic development environment,  built for coding with multiple AI agents. &#183; GitHub" title="GitHub - warpdotdev/Warp: Warp is the agentic development environment,  built for coding with multiple AI agents. &#183; GitHub" srcset="https://substackcdn.com/image/fetch/$s_!W-YO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png 424w, https://substackcdn.com/image/fetch/$s_!W-YO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png 848w, https://substackcdn.com/image/fetch/$s_!W-YO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png 1272w, https://substackcdn.com/image/fetch/$s_!W-YO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9650f3-d7b0-4ad7-a10b-0e88625546ec_1448x828.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Warp is a modern terminal with AI capabilities that enhances your command-line experience.</p><p><strong>Installation:</strong></p><pre><code><code># macOS (using Homebrew)
brew install --cask warp</code></code></pre><p><strong>Features:</strong></p><ul><li><p>AI command suggestions</p></li><li><p>Command history search</p></li><li><p>Improved terminal UI with blocks and workspaces</p></li></ul><h3><code>Zoxide</code></h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%211n1R%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1n1R!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg 424w, https://substackcdn.com/image/fetch/$s_!1n1R!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg 848w, https://substackcdn.com/image/fetch/$s_!1n1R!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!1n1R!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%211n1R%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;zoxide &#8212; better `cd` command&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="zoxide &#8212; better `cd` command" title="zoxide &#8212; better `cd` command" srcset="https://substackcdn.com/image/fetch/$s_!1n1R!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg 424w, https://substackcdn.com/image/fetch/$s_!1n1R!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg 848w, https://substackcdn.com/image/fetch/$s_!1n1R!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!1n1R!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f5f095a-e65a-4378-abc1-23ad57d9c102_1280x720.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Zoxide is a smarter cd command that helps you navigate directories faster by tracking your most used directories.</p><p><strong>Installation:</strong></p><pre><code><code># macOS (using Homebrew)
brew install zoxide

# Add to shell (already configured in your .zshrc)
eval "$(zoxide init zsh --cmd cd)"</code></code></pre><p><strong>Usage:</strong></p><ul><li><p>Use <code>cd</code> as normal (it&#8217;s configured to replace cd in your .zshrc)</p></li><li><p>Jump to frequent directories by typing part of the path: <code>cd proj</code></p></li><li><p>So imagine you&#8217;re in the dbt repo, you wanna quickly open up airflow, just &#8220;cd airflow&#8221; even though airflow doesn&#8217;t exist in the current directory, zoxide will find it</p></li></ul><h3><code>Eza</code></h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21ftLM%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ftLM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ftLM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ftLM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ftLM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21ftLM%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg" width="982" height="528" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:528,&quot;width&quot;:982,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;eza: Simplifying File Listing with Improved 'ls' Functionality&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="eza: Simplifying File Listing with Improved 'ls' Functionality" title="eza: Simplifying File Listing with Improved 'ls' Functionality" srcset="https://substackcdn.com/image/fetch/$s_!ftLM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ftLM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ftLM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ftLM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74020345-07e9-444a-9aa6-62ac123635e6_982x528.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Eza is a modern replacement for the <code>ls</code> command with improved colors and features.</p><p><strong>Installation:</strong></p><pre><code><code># macOS (using Homebrew)
brew install eza</code></code></pre><p><strong>Usage:</strong></p><ul><li><p>Already configured as aliases in your .zshrc</p></li><li><p><code>ls</code>: Lists files with directories first</p></li><li><p><code>ll</code>: Long listing format</p></li><li><p><code>la</code>: Shows all files including hidden ones</p></li></ul><h3><code>Oh My Zsh</code></h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Kz2z%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Kz2z!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Kz2z!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Kz2z!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Kz2z!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Kz2z%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg" width="1456" height="838" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:838,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;How to Install Oh My Zsh on Mac&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="How to Install Oh My Zsh on Mac" title="How to Install Oh My Zsh on Mac" srcset="https://substackcdn.com/image/fetch/$s_!Kz2z!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Kz2z!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Kz2z!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Kz2z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7eb37bd6-8568-4486-864a-3e42098dec3f_1894x1090.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Oh My Zsh is a framework for managing your Zsh configuration with themes and plugins.</p><p><strong>Installation:</strong></p><pre><code><code>sh -c "$(curl -fsSL &lt;https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh&gt;)"
</code></code></pre><p><strong>Usage:</strong></p><ul><li><p>Already configured in your <code>.zshrc</code> with <code>Powerlevel10k</code> theme</p></li><li><p>Add plugins by editing the plugins array in <code>.zshrc</code></p></li></ul><pre><code><code># Path to your Oh My Zsh installation.
export ZSH="$HOME/.oh-my-zsh"

ZSH_THEME="jonathan"

source $ZSH/oh-my-zsh.sh</code></code></pre><h3><strong>Jonathan Theme</strong></h3><p><strong>Configuration:</strong></p><p>Your <code>.zshrc</code> is configured with:</p><pre><code>ZSH_THEME=&#8221;jonathan&#8221;</code></pre><p>If you want to switch themes later, just update the <code>ZSH_THEME</code> value in your <code>.zshrc</code> and run:</p><pre><code>refresh</code></pre><p>If you want, I can also rewrite the full <strong>CLI Tools</strong> section so it stays consistent with the fact that you use Jonathan theme instead of Powerlevel10k.</p><h3><code>Lazygit</code></h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%216_hZ%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6_hZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png 424w, https://substackcdn.com/image/fetch/$s_!6_hZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png 848w, https://substackcdn.com/image/fetch/$s_!6_hZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png 1272w, https://substackcdn.com/image/fetch/$s_!6_hZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%216_hZ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png" width="1456" height="946" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:946,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:748471,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/191266646?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6_hZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png 424w, https://substackcdn.com/image/fetch/$s_!6_hZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png 848w, https://substackcdn.com/image/fetch/$s_!6_hZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png 1272w, https://substackcdn.com/image/fetch/$s_!6_hZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7cf3aaa-4b8d-4e5e-898e-bad05a23790a_3024x1964.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>Lazygit</code> is a simple terminal UI for git commands, making git operations more visual and intuitive.</p><p><strong>Installation:</strong></p><pre><code><code># macOS (using Homebrew)
brew install lazygit</code></code></pre><p><strong>Usage:</strong></p><ul><li><p>Run <code>lazygit</code> in any git repository</p></li><li><p>Navigate with arrow keys, perform actions with keyboard shortcuts</p></li><li><p>Press <code>?</code> for help</p></li></ul><h3><code>Lzop</code></h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21iWpH%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iWpH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png 424w, https://substackcdn.com/image/fetch/$s_!iWpH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png 848w, https://substackcdn.com/image/fetch/$s_!iWpH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png 1272w, https://substackcdn.com/image/fetch/$s_!iWpH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21iWpH%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png" width="716" height="394" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:394,&quot;width&quot;:716,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;lzop Linux Command - GeeksforGeeks&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="lzop Linux Command - GeeksforGeeks" title="lzop Linux Command - GeeksforGeeks" srcset="https://substackcdn.com/image/fetch/$s_!iWpH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png 424w, https://substackcdn.com/image/fetch/$s_!iWpH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png 848w, https://substackcdn.com/image/fetch/$s_!iWpH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png 1272w, https://substackcdn.com/image/fetch/$s_!iWpH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e95de97-3971-41b8-99e7-8c42001921f0_716x394.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>Lzop</code> is a file compressor similar to gzip but optimized for speed.</p><p><strong>Installation:</strong></p><pre><code><code># macOS (using Homebrew)
brew install lzop</code></code></pre><p><strong>Usage:</strong></p><ul><li><p>Compress: <code>lzop filename</code></p></li><li><p>Decompress: <code>lzop -d filename.lzo</code></p></li><li><p>View compressed file: <code>lzop -c -d filename.lzo</code></p></li></ul><h1>Keyboard Shortcuts</h1><p>Keyboard shortcuts have probably been the biggest game changer in my workflow because they let me move around without friction. Instead of constantly reaching for the mouse or breaking focus, I can jump between files, the terminal, the editor, and search almost instantly. That speed sounds small, but it adds up fast when you&#8217;re coding all day. Once the most common actions are mapped to muscle memory, everything feels smoother, faster, and a lot less interruptive.</p><h2>Cursor/VS Code Shortcuts</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21urxB%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!urxB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png 424w, https://substackcdn.com/image/fetch/$s_!urxB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png 848w, https://substackcdn.com/image/fetch/$s_!urxB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png 1272w, https://substackcdn.com/image/fetch/$s_!urxB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21urxB%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png" width="1456" height="947" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:947,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:531881,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/191266646?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!urxB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png 424w, https://substackcdn.com/image/fetch/$s_!urxB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png 848w, https://substackcdn.com/image/fetch/$s_!urxB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png 1272w, https://substackcdn.com/image/fetch/$s_!urxB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc15169c5-673d-4e3f-8886-c2a271ed3d9f_3012x1960.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p><strong>Command Palette:</strong> <code>cmd+shift+p</code> - Access all commands and settings</p></li><li><p><strong>Quick Open:</strong> <code>cmd+p</code> - Quickly open files in your project</p></li><li><p><strong>Toggle Sidebar:</strong> <code>cmd+b</code> - Hide or show the sidebar</p></li><li><p><strong>Split Editor:</strong> <code>cmd+\\</code> - Split the current editor into two panes</p></li><li><p><strong>Copy File Name:</strong> <code>cmd+alt+f</code> - Copy the current file name</p></li><li><p><strong>Settings:</strong> <code>cmd+,</code> - Open settings</p></li><li><p><strong>Find &amp; Replace:</strong> <code>cmd+shift+h</code> - Find and replace across entire directory</p></li><li><p><strong>Select Line:</strong> <code>cmd+l</code> - Highlight the current line</p></li><li><p><strong>Comment/Uncomment:</strong> <code>cmd+/</code> - Toggle comments for selected lines</p></li><li><p><strong>Terminal:</strong> `ctrl+`` - Open integrated terminal</p></li></ul><h2>Terminal</h2><ul><li><p><strong>Delete Word:</strong> <code>ctrl+w</code> - Delete the word before the cursor</p></li><li><p><strong>Clear Line:</strong> <code>ctrl+u</code> - Clear the entire line before the cursor</p></li><li><p><strong>Clear Screen:</strong> <code>ctrl+l</code> - Clear the terminal screen</p></li><li><p><strong>End of Line:</strong> <code>ctrl+e</code> - Move cursor to end of line</p></li><li><p><strong>Beginning of Line:</strong> <code>ctrl+a</code> - Move cursor to beginning of line</p></li><li><p><strong>Search History:</strong> <code>ctrl+r</code> - Search through command history</p></li><li><p><strong>Move Cursor Back:</strong> <code>ctrl+b</code> - Move cursor one character back</p></li><li><p><strong>Move Cursor Forward:</strong> <code>ctrl+f</code> - Move cursor one character forward</p></li><li><p><strong>Stop Current Process:</strong> <code>ctrl+c</code> - Terminate the current running command</p></li><li><p><strong>Beginning of Word:</strong> <code>option+b</code> (or <code>alt+b</code>) - Move cursor to beginning of current word</p></li><li><p><strong>End of Word:</strong> <code>alt+f</code> - Move cursor to end of the current word</p></li><li><p><strong>Precise Cursor Movement:</strong> <code>hold option</code> + <code>click</code> - Move directly to clicked position within command line</p></li><li><p><strong>Tab Completion:</strong> <code>tab</code> - Complete commands and filenames</p></li><li><p><strong>Command History:</strong> <code>up</code> - Navigate through previous commands</p></li></ul><h1>Other useful commadn</h1><ul><li><p><strong>List Processes:</strong> <code>ps -ax</code> - Show all active processes</p></li><li><p><strong>Git Branch Cleanup:</strong> <code>git fetch --prune --all</code> - Remove deleted branches from git checkout autocomplete</p></li><li><p><strong>Pipe Command:</strong> <code>|</code> - Send output of first command as input to second command (e.g., <code>ps -ax | grep vpn</code>)</p></li></ul><h1>Outro</h1><p>That&#8217;s pretty much my current setup. None of these tools or shortcuts are revolutionary on their own, but together they make my workflow faster, smoother, and a lot less annoying. Hope this helps, and maybe gives you a few ideas for cleaning up your own setup.</p>]]></content:encoded></item><item><title><![CDATA[I Don’t Know What Type of SQL to Learn!]]></title><description><![CDATA[Because a query is a query &#8211; no matter the flavor.]]></description><link>https://markphammm.substack.com/p/i-dont-know-what-type-of-sql-to-learn</link><guid isPermaLink="false">https://markphammm.substack.com/p/i-dont-know-what-type-of-sql-to-learn</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Sun, 30 Nov 2025 20:31:38 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Somz%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Somz%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Somz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Somz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Somz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Somz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Somz%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg" width="728" height="540.2597402597403" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:400,&quot;width&quot;:539,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;SQL Server 2019 | Microsoft&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SQL Server 2019 | Microsoft" title="SQL Server 2019 | Microsoft" srcset="https://substackcdn.com/image/fetch/$s_!Somz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Somz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Somz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Somz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736a2c3f-97c4-4a20-9ff3-d51d29a6d1ee_539x400.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Today I want to tackle a question I hear <strong>all the time</strong>: <em>&#8220;I don&#8217;t know what type of SQL to learn. There are so many &#8211; MySQL, PostgreSQL, Oracle, SQL Server!&#8221;</em> </p><blockquote><p>Bro, just <strong>learn ANSI SQL</strong>. </p></blockquote><p>Period. Specifically, start with <strong>ANSI SQL</strong>, the standard. Why? Because once you know the fundamentals of SQL, you can work with <em>any</em> database engine out there. Let&#8217;s break down why standard SQL is the way to go, in a language we can all understand (and maybe with a joke or two along the way).</p><h2>The SQL Dialect Dilemma (Too Many Flavors!)</h2><p>When I first dipped my toes into data, I was bewildered by the buffet of &#8220;SQLs&#8221; on the menu. Oracle SQL, MySQL, T-SQL, PL/SQL, PostgreSQL&#8230; it felt like I had to pick the <em>one right</em> database religion. &#128565; <em>Spoiler:</em> I didn&#8217;t. Instead, I learned <strong>SQL basics</strong> &#8211; <em>SELECT, JOIN, WHERE, GROUP BY</em> &#8211; the stuff that&#8217;s the same everywhere. And guess what? It didn&#8217;t matter that my first database was MySQL and later I used Redshift at work. The <em>core language</em> was the same.</p><p>Think of SQL dialects like ice cream flavors. Sure, chocolate chip vs. rocky road have slight differences (one&#8217;s got chocolate bits, the other marshmallows), but they&#8217;re all ice cream at heart. As one wise commenter put it, <em>SQL databases are &#8220;different flavors of the exact same thing &#8211; think vanilla ice cream with different toppings.&#8221;</em> If you know one flavor, you can pick up another easily. In other words: <strong>SQL is SQL.</strong> Don&#8217;t overthink the flavor; focus on the ice cream. &#127848;</p><h2>What Exactly <em>Is</em> ANSI SQL?</h2><p><strong>ANSI SQL</strong> (sometimes called &#8220;Standard SQL&#8221;) is the official standard language for relational databases, defined way back by the <em>American National Standards Institute</em>. It&#8217;s basically the agreed-upon core of SQL that most databases implement. Think of ANSI SQL as the grammar rules of the SQL language. Just like English has grammar that most writers follow (even if some have unique slang), databases follow ANSI SQL with their own twists.</p><p>No database implements the standard <strong>100% perfectly</strong> (vendors love their proprietary features). Reasons vary: they add new features not in the standard yet, or they just want to lock you into their ecosystem. Examples of dialects (i.e. those twists on standard SQL) include:</p><ul><li><p><strong>T-SQL (Transact-SQL)</strong> &#8211; Microsoft SQL Server&#8217;s flavor, with extra goodies for stored procedures.</p></li><li><p><strong>PL/SQL (Procedural SQL)</strong> &#8211; Oracle&#8217;s flavor, integrates procedural programming.</p></li><li><p><strong>PL/pgSQL</strong> &#8211; PostgreSQL&#8217;s procedural extension of SQL.</p></li><li><p><strong>MySQL&#8217;s SQL</strong> &#8211; pretty close to standard, with a few quirks (e.g. <code>LIMIT</code> clause for top-N results, which other databases might handle with different syntax).</p></li><li><p><strong>Redshift/BigQuery/Snowflake - </strong>cloud data-warehouse syntax are really close with ANSI-SQL, with a touch of extra function like <code>QUALIFY</code> or Semi-structured data functions (<code>PARSE_JSON, TO_VARIANT</code>)</p></li></ul><p>Each of these has <em>some</em> non-standard extensions or functions. But here&#8217;s the kicker: the foundation &#8211; creating tables, inserting data, querying with SELECT/JOIN &#8211; <strong>all of that is standardized</strong>. If you learn ANSI SQL first, you&#8217;ve covered 80-90% of what you&#8217;ll use day-to-day in any SQL database. The remaining 10-20% are like dialect-specific idioms you can pick up as needed.</p><h2>All SQL Dialects = Same Core, Small Tweaks</h2><p>Let&#8217;s address the fear: <em>&#8220;What if I invest time in SQL X and later need SQL Y? Will my knowledge be useless?&#8221;</em> Absolutely not! The core concepts carry over. In my career, I&#8217;ve bounced between MySQL, PostgreSQL, Redshift, and DuckDB (and yes, those are <em>very</em> different under the hood &#8211; from big cloud warehouses to tiny in-process DBs). Yet I never felt like I had to <em>re-learn SQL</em> from scratch for each. I might have to adjust to a different function name or date format function, but a <code>SELECT</code> is a <code>SELECT</code> everywhere.</p><p>In fact, <strong>SQL skills are highly transferable across systems</strong>. The syntax and commands you use in ANSI SQL are widely recognized across MySQL, SQL Server, Oracle, PostgreSQL &#8211; you name it. It&#8217;s like knowing how to drive: once you can drive a sedan, you can drive an SUV or a pickup with a bit of practice. The buttons might be in different places, but the steering wheel and pedals work the same. &#128663;</p><p>To give a concrete example: at my company (Insurify), we recently considered migrating our data warehouse from Amazon Redshift to Snowflake. Big change under the hood, but our team wasn&#8217;t terrified. Why? Because our analysts already speak fluent SQL. The <em>flavor</em> might shift (Redshift has some PostgreSQL-ish traits; Snowflake is ANSI SQL with its own functions), but our analytical queries and skills carry over with minimal changes. We spend our energy tuning performance or learning new features, not relearning how to write <code>SELECT</code> statements.</p><h2>Why Starting with Standard SQL Makes Sense</h2><p>If you&#8217;re just beginning, here&#8217;s why I firmly believe you should start with ANSI/standard SQL:</p><ul><li><p><strong>Widely Applicable Skills:</strong> You&#8217;ll build a foundation that works on any relational database. Your knowledge isn&#8217;t tied to one vendor. Tomorrow if you switch from, say, SQLite to Oracle, your skills come with you.</p></li><li><p><strong>Core Concepts First:</strong> Focusing on standard SQL forces you to master the basics &#8211; <em>querying data (DQL)</em>, <em>updating/inserting (DML)</em>, <em>creating tables (DDL)</em>. These are the bread-and-butter of any database work. Advanced, platform-specific tricks can come later.</p></li><li><p><strong>Easier to Learn Dialect Nuances:</strong> Once you know the core, learning a dialect&#8217;s quirks (like T-SQL&#8217;s <code>@@IDENTITY</code> or Oracle&#8217;s <code>DECODE()</code> function) is way easier. It&#8217;s just learning a new idiom on top of a language you already speak.</p></li><li><p><strong>Transferable in Jobs and Interviews:</strong> Employers rarely care <em>which</em> SQL dialect you know; they care that you can use SQL to get answers. Mastering standard SQL showcases logical thinking and data savvy. It&#8217;s an &#8220;industry standard&#8221; skill that proves you can adapt to whatever database the job uses. In technical interviews too, you&#8217;ll often be asked to write SQL without focusing on a specific dialect &#8211; they expect you to use standard SQL syntax.</p></li></ul><p>Now, does this mean <strong>vendor-specific knowledge is useless</strong>? Not at all. Eventually, if you work with a database like Oracle or SQL Server, you&#8217;ll learn their extensions (like Oracle&#8217;s analytic functions or SQL Server&#8217;s T-SQL tricks). But that&#8217;s the icing, not the cake. <em>Get the cake right (ANSI SQL), then worry about the icing later.</em></p><h2>My Two Cents on Getting Started</h2><ol><li><p><strong>Pick One Database to Practice:</strong> I recommend an easy one like <strong>PostgreSQL</strong> or <strong>MySQL</strong> for beginners &#8211; both are free and widely used. Even <strong>DuckDB</strong> or <strong>SQLite</strong> are fantastic for learning; they&#8217;re lightweight and require almost no setup (you can query a CSV in seconds). The key is to start writing queries. Don&#8217;t agonize over choosing &#8220;the perfect database.&#8221; As one experienced developer noted about their career using Access, SQL Server, Teradata, and BigQuery: <em>&#8220;They each have their own nuances, but if you know one, it&#8217;s pretty easy to pick up the others&#8230; focus on getting good with SQL, then apply that to different DBMS&#8221;</em>. In short: just start somewhere, anywhere.</p></li><li><p><strong>Learn by Doing:</strong> Spin up a small sample database and practice the basics. Create a table, insert some data, write SELECTs with various filters and joins. There&#8217;s no substitute for getting your hands dirty. You&#8217;ll quickly see that <code>SELECT name FROM customers WHERE city=&#8217;Boston&#8217;</code> works the same on <em>any</em> SQL-compliant database.</p></li><li><p><strong>Solve some Leetcode: </strong>Practice by solving Leetcode question, divide up questions into different topics and solve them gradually</p></li><li><p><strong>Don&#8217;t Get Lost in the Details:</strong> It&#8217;s easy to fall into a rabbit hole of differences (Yes, MS SQL Server uses <code>TOP 10</code> instead of <code>LIMIT 10</code>, Oracle uses <code>FETCH FIRST 10 ROWS ONLY</code> &#8211; whoop-de-doo). These are minor syntax tweaks. Google them when you encounter them. Focus your energy on understanding <strong>what</strong> your query should do &#8211; the <em>logic</em>, not memorizing every dialect&#8217;s keywords.</p></li><li><p><strong>Use Resources That Teach Standard SQL:</strong> Many online tutorials (SQLBolt, Mode tutorials, Khan Academy, etc.) teach SQL in a vendor-neutral way or let you choose which dialect to practice on. Start with those. Once you&#8217;re comfortable, you can explore specifics of, say, PL/SQL if your job requires Oracle, but you&#8217;ll find it much easier then.</p></li></ol><h2>Conclusion: SQL is SQL &#8211; Just Start</h2><p>At the end of the day, <strong>don&#8217;t let the multitude of SQL flavors paralyze you</strong>. The biggest threat to your success isn&#8217;t picking the &#8220;wrong&#8221; database to learn on &#8211; it&#8217;s never starting at all. SQL is a foundational skill in the data world, and the fundamentals are universal. Learn the standard syntax and you&#8217;ll gain confidence knowing you can step into <em>any</em> SQL environment and find your way. As I often say, <em>a good data engineer or analyst is defined by how well they understand the data and the queries &#8211; not by memorizing obscure dialect syntax.</em></p><p>So grab a scoop of that sweet, standard ANSI SQL. &#127846; Once you&#8217;ve savored it, you can try other flavors as needed &#8211; but you&#8217;ll realize they all taste familiar. Happy querying, and until next time, keep it simple and standard!</p><p><em><strong>&#8211; Mark</strong></em></p>]]></content:encoded></item><item><title><![CDATA[Understanding pip, Poetry, and uv]]></title><description><![CDATA[Because managing Python dependencies shouldn&#8217;t feel like herding cats.]]></description><link>https://markphammm.substack.com/p/understanding-pip-poetry-and-uv</link><guid isPermaLink="false">https://markphammm.substack.com/p/understanding-pip-poetry-and-uv</guid><dc:creator><![CDATA[Mark Pham]]></dc:creator><pubDate>Wed, 12 Nov 2025 05:07:18 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21sGtE%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21sGtE%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sGtE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png 424w, https://substackcdn.com/image/fetch/$s_!sGtE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png 848w, https://substackcdn.com/image/fetch/$s_!sGtE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png 1272w, https://substackcdn.com/image/fetch/$s_!sGtE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21sGtE%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png" width="1015" height="627" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:627,&quot;width&quot;:1015,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;uv overtakes Poetry | Wagtail CMS&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="uv overtakes Poetry | Wagtail CMS" title="uv overtakes Poetry | Wagtail CMS" srcset="https://substackcdn.com/image/fetch/$s_!sGtE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png 424w, https://substackcdn.com/image/fetch/$s_!sGtE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png 848w, https://substackcdn.com/image/fetch/$s_!sGtE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png 1272w, https://substackcdn.com/image/fetch/$s_!sGtE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dc80e61-778c-42b4-a734-1eaf08d8cef3_1015x627.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Introduction</h2><p>If you&#8217;ve worked with <code>Python</code> for a minute, you&#8217;ve met <code>pip</code> &#8211; the tried-and-true package installer that&#8217;s been around forever. But in recent years, new tools like <code>Poetry </code>and <code>uv</code> have arrived on the scene, each promising to make Python project management a smoother ride. I remember the confusion when I first heard about Poetry (&#8220;why not just use pip?&#8221;) and then again when the buzz around uv started (&#8220;do we really need another tool?&#8221;). In this article, I&#8217;ll break down these three approaches to Python dependencies and environments, in plain English and with a dose of personal experience. By the end, you&#8217;ll know <em>when to stick with pip</em>, <em>when to embrace Poetry</em>, and <em>why the new &#8220;uv&#8221; might just blow your socks off</em>.</p><h2>The Project Scenario: a simple data app</h2><p>To make this comparison concrete, let&#8217;s consider a tiny project I&#8217;ll call <code>my_data_app</code>. It&#8217;s a simple data analysis app that uses <strong>Pandas</strong> for data manipulation and <strong>DuckDB</strong> for running SQL queries on the data. (DuckDB is like a lightweight in-process database that plays nicely with Pandas DataFrames &#8211; perfect for local analytics.) We&#8217;ll also use <strong>Pytest</strong> for writing a couple of tests.</p><p>So our project needs three main packages: <code>pandas</code>, <code>duckdb</code>, and <code>pytest</code>. The code itself might look something like this (just to set the stage, don&#8217;t worry about the details too much):</p><pre><code><code># src/main.py (for example)
import pandas as pd
import duckdb

def load_data():
    # Create a sample DataFrame
    df = pd.DataFrame({&#8221;id&#8221;: [1, 2, 3, 4], &#8220;value&#8221;: [10, 20, 30, 40]})
    return df

def query_data(df: pd.DataFrame):
    con = duckdb.connect()  # in-memory DuckDB instance
    con.register(&#8221;mytable&#8221;, df)           # make the DataFrame queryable
    result = con.execute(&#8221;&#8220;&#8221;
        SELECT id, value, value * 2 AS value2
        FROM mytable
        WHERE value &gt;= 20
        ORDER BY id DESC;
    &#8220;&#8221;&#8220;).df()
    return result

if __name__ == &#8220;__main__&#8221;:
    data = load_data()
    print(&#8221;Input:\\n&#8221;, data)
    output = query_data(data)
    print(&#8221;Query result:\\n&#8221;, output)

</code></code></pre><p>This little script creates a DataFrame, then uses DuckDB&#8217;s SQL engine to filter and augment the data. Nothing too fancy &#8211; but enough to illustrate how pip, Poetry, and uv handle project setup differently. Let&#8217;s dive into each tool using this project as an example.</p><h2>1. pip &#8211; The OG Approach</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21e_wH%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!e_wH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png 424w, https://substackcdn.com/image/fetch/$s_!e_wH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png 848w, https://substackcdn.com/image/fetch/$s_!e_wH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png 1272w, https://substackcdn.com/image/fetch/$s_!e_wH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21e_wH%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fe6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png" width="1456" height="582" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:582,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;How to Create a Pip Package for Python&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="How to Create a Pip Package for Python" title="How to Create a Pip Package for Python" srcset="https://substackcdn.com/image/fetch/$s_!e_wH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png 424w, https://substackcdn.com/image/fetch/$s_!e_wH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png 848w, https://substackcdn.com/image/fetch/$s_!e_wH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png 1272w, https://substackcdn.com/image/fetch/$s_!e_wH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c9de56-6820-48b7-b9d8-6f1a30bcf406_2400x960.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>pip</code> is the OG of Python package management. It&#8217;s the <strong>standard installer</strong> that comes with Python (via ensurepip) and it&#8217;s extremely straightforward: you want a package, you run <code>pip install &lt;package&gt;</code> and it fetches it from PyPI. Done. No bells or whistles. And sometimes, that&#8217;s all you need.</p><p>Using pip for our <code>my_data_app</code> example might go something like this: we&#8217;d first <strong>create a virtual environment</strong> for the project, activate it, then install our packages:</p><pre><code><code># 1. Create a virtual environment (isolated Python installation)
python3 -m venv .venv

# 2. Activate the virtual environment
source .venv/bin/activate    # (On Windows, use &#8220;.venv\\\\Scripts\\\\activate&#8221;)

# 3. Install the required packages inside this venv
pip install pandas duckdb
pip install pytest

# 4. (Optional) Freeze the exact versions to a requirements file
pip freeze &gt; requirements.txt

</code></code></pre><p>A quick rundown for newcomers: the <strong>virtual environment</strong> step is critical. A virtual environment is like a sandboxed Python installation just for this project &#8211; it has its own interpreter and its own <code>site-packages</code> directory where installed libraries go. When you activate it, your shell&#8217;s <code>PATH</code> is modified so that <code>python</code> and <code>pip</code> refer to the ones inside <code>.venv</code>. This keeps your project&#8217;s libraries isolated from your system Python, preventing version conflicts (and &#8220;it works on my machine&#8221; headaches). You can see that the <code>.venv</code> folder gets created in our project directory; it contains a <code>bin</code> (or <code>Scripts</code> on Windows) folder with the Python executable and a <code>lib</code> folder with all the packages we installed. When we&#8217;re done, we can deactivate the venv and go back to our system&#8217;s Python.</p><p>After installing <code>pandas</code>, <code>duckdb</code>, and <code>pytest</code> with pip, I used <code>pip freeze &gt; requirements.txt</code> to generate a <strong>requirements file</strong>. This <code>requirements.txt</code> is just a snapshot of all the packages in our environment (including dependencies of the ones we installed) with pinned versions. It might look like:</p><pre><code><code>duckdb==1.4.1
numpy==1.26.0      # numpy is a dependency of pandas and duckdb
pandas==2.3.3
pytest==8.4.2
python-dateutil==2.9.0.post0   # pulled in by pandas
pytz==2023.3               # pulled in by pandas
</code></code></pre><p>The idea is that if someone else wants to replicate this environment, they can use the same Python version, create their own <code>venv</code>, and run <code>pip install -r requirements.txt</code> to get <em>those exact versions</em>. In practice, this works, <strong>though pip doesn&#8217;t actively &#8220;lock&#8221; sub-dependency versions unless you froze them.</strong> It&#8217;s a bit manual: whenever you add or update a package, you&#8217;re supposed to update the requirements file (often by freezing again). There&#8217;s no concept of separate &#8220;dev&#8221; vs &#8220;prod&#8221; dependencies in a plain requirements.txt &#8211; you might maintain multiple files (like <code>requirements-dev.txt</code>) if you want to differentiate.</p><p><strong>Pros of pip:</strong> It&#8217;s simple and ubiquitous. Everyone using Python has pip, and every Python tutorial starts with <code>pip install ...</code>. There&#8217;s almost nothing new to learn. For small projects or quick scripts, pip + venv gets the job done with minimal fuss.</p><p><strong>Cons of pip:</strong> By itself, pip is pretty bare-bones for larger projects. You have to remember to set up and activate the venv (pip won&#8217;t do it for you). You have to manage that requirements.txt manually. There&#8217;s no built-in resolution of conflicting requirements beyond pip&#8217;s own dependency solver (which operates one <code>pip install</code> at a time), and no lock file to guarantee that two installs a week apart get the exact same set of sub-dependencies. In short, pip on its own is lacking some of the &#8220;project management&#8221; features that bigger tools provide.</p><p><em>(Side note: pip has gotten some improvements over the years &#8211; for example, </em><code>pip install</code><em> can now use a </em><code>pyproject.toml</code><em> for build requirements, and there are third-party tools like </em><code>pip-tools</code><em> to help with lock files &#8211; but in the context of this article, pip refers to the basic usage as above.)</em></p><h2>2. Poetry &#8211; The All-in-One Project Tool</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Ski0%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Feabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ski0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png 424w, https://substackcdn.com/image/fetch/$s_!Ski0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png 848w, https://substackcdn.com/image/fetch/$s_!Ski0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png 1272w, https://substackcdn.com/image/fetch/$s_!Ski0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Ski0%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Feabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png" width="1456" height="762" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:762,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Developing Python Projects with poetry&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Developing Python Projects with poetry" title="Developing Python Projects with poetry" srcset="https://substackcdn.com/image/fetch/$s_!Ski0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png 424w, https://substackcdn.com/image/fetch/$s_!Ski0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png 848w, https://substackcdn.com/image/fetch/$s_!Ski0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png 1272w, https://substackcdn.com/image/fetch/$s_!Ski0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feabb5ae1-49cc-4f05-b5ed-c4c76920e78b_2000x1047.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>After wrestling with pip and manual <code>venvs </code>for a while, I discovered <strong>Poetry</strong>, and it felt like leveling up. Poetry is often described as a <strong>Python project manager</strong> or a higher-level dependency tool. It still uses pip under the hood for installing, but it wraps a lot of functionality to provide a more streamlined experience for building and maintaining projects. In essence, Poetry handles <strong>dependency resolution, lock file generation, virtual environment management, and packaging</strong> all in one. It turns your project into something that&#8217;s easier to share and reproduce by anyone.</p><p>How would we use Poetry for <code>my_data_app</code>? First, you&#8217;d install Poetry itself (for example via <code>pipx install poetry</code>, so that you have a <code>poetry</code> command available globally). Then in your project folder, instead of manually making a venv and writing a requirements file, you do:</p><pre><code><code>poetry init

</code></code></pre><p>This command walks you through creating a <code>pyproject.toml</code> file, which is the central configuration for your project. You&#8217;d give your project a name, a version, a description, and then list its dependencies (and their version constraints). You can also specify your Python version. For our example, the <strong>pyproject.toml</strong> created by Poetry might include:</p><pre><code><code>[tool.poetry]
name = &#8220;my_data_app&#8221;
version = &#8220;0.1.0&#8221;
description = &#8220;A simple data app using pandas and DuckDB&#8221;
authors = [&#8221;Your Name &lt;you@example.com&gt;&#8221;]

[tool.poetry.dependencies]
python = &#8220;^3.11&#8221;       # means Python 3.11.x
pandas = &#8220;^2.0&#8221;        # means pandas 2.x
duckdb = &#8220;^1.4&#8221;        # means duckdb 1.4.x

[tool.poetry.group.dev.dependencies]
pytest = &#8220;^8.0&#8221;        # dev dependency (not needed in production)

[build-system]
requires = [&#8221;poetry-core&#8221;]
build-backend = &#8220;poetry.core.masonry.api&#8221;
</code></code></pre><p>Don&#8217;t worry about the exact TOML syntax &#8211; the key point is that Poetry has now recorded <em>what our project is and what it needs</em>. Notice it has separate sections for main dependencies vs dev dependencies (like <code>pytest</code>). This is already nicer than a single requirements.txt because we have clarity on what&#8217;s needed for running the app versus what&#8217;s only for development/testing. Poetry will also create a <strong>lock file</strong> called <code>poetry.lock</code> once we install everything.</p><p>Speaking of installing: after <code>poetry init</code>, we run:</p><pre><code><code>poetry install
</code></code></pre><p>And just like that, Poetry will do a few things:</p><ul><li><p>Resolve all the dependency versions that fit the criteria (e.g., find the latest <code>pandas</code> 2.x, <code>duckdb</code> 1.4.x, etc. that are mutually compatible).</p></li><li><p>Create a new virtual environment for the project (unless one already exists). By default, Poetry will make a venv in a cache directory (usually under <code>~/.cache/pypoetry/virtualenvs/</code> with a name like <code>my_data_app-abcdefgh-py3.11</code>), but you can configure it to create the venv in your project folder if you prefer.</p></li><li><p>Install the exact versions of packages it resolved, into that virtual environment.</p></li><li><p>Generate the <code>poetry.lock</code> file pinning those exact versions (including sub-dependencies).</p></li></ul><p>From then on, anyone who checks out your project can just run <code>poetry install</code> and they&#8217;ll get the <em>same versions</em> thanks to the lock file. Poetry&#8217;s lock file ensures <strong>reproducible installs</strong> &#8211; a cornerstone for consistent environments. As the Poetry documentation puts it, the lock file &#8220;locks dependency versions, ensuring reproducible environments&#8221;<a href="iframe.php?url=https%3A%2F%2Frealpython.com%2Fdependency-management-python-poetry%2F%23%3A%7E%3Atext%3D%2Cdependency%2520versions%252C%2520ensuring%2520reproducible%2520environments">realpython.com</a><a href="iframe.php?url=https%3A%2F%2Frealpython.com%2Fdependency-management-python-poetry%2F%23%3A%7E%3Atext%3DPoetry%2520is%2520a%2520tool%2520for%2Cconsistent%2520environment%2520across%2520different%2520machines">realpython.com</a>. It should be committed to your version control.</p><p>Using Poetry day-to-day is pretty nice. You don&#8217;t even need to explicitly activate the <code>venv</code> it creates. If you want to run something in the project&#8217;s environment, you can do <code>poetry run &lt;command&gt;</code>  (for example, <code>poetry run python src/main.py</code> or <code>poetry run pytest</code>). That automatically uses the project&#8217;s venv. If you prefer an interactive session, <code>poetry shell</code> will spawn a new shell with the venv activated (so your prompt shows <code>(my_data_app-py3.11)</code> or similar). Essentially, Poetry handles the venv for you; you just have to remember to use <code>poetry run</code> or <code>poetry shell</code> when working on the project.</p><p>Want to add a new package? <code>poetry add flask</code> (for a normal dependency) or <code>poetry add --group dev black</code> (to add, say, Black as a dev-only tool). This updates the pyproject and lock file in one go. Need to update packages? <code>poetry update</code> will refresh everything within the allowed version constraints you set. And when it&#8217;s time to ship your code, Poetry can even build distributable artifacts (wheels/sdists) with <code>poetry build</code>, and publish to PyPI with <code>poetry publish</code> &#8211; all using the info from pyproject.toml. It&#8217;s a one-stop shop for project management.</p><p><strong>Pros of Poetry:</strong> It brings order and structure. You get a <strong>lock file</strong> for guaranteed repeatability, <strong>environment isolation</strong> without the manual steps, easy separation of dev vs prod requirements, and built-in support for packaging and publishing. It&#8217;s fantastic for applications and services where you want to be confident that what runs in production is exactly what you tested locally. The <code>pyproject.toml</code> is also becoming a standard place to configure Python projects (PEP 518 and PEP 621), so Poetry is aligned with modern Python packaging standards (albeit using its own <code>[tool.poetry]</code> sections for specifics). Many teams (including mine) have adopted Poetry to avoid the &#8220;it works on my machine&#8221; syndrome and dependency hell. As one tutorial succinctly says, &#8220;Poetry is a dependency manager that ensures consistent package versions and simplifies Python project setup&#8221;<a href="iframe.php?url=https%3A%2F%2Frealpython.com%2Fdependency-management-python-poetry%2F%23%3A%7E%3Atext%3D%2Cpoetry%2520update">realpython.com</a>.</p><p><strong>Cons of Poetry:</strong> There&#8217;s a bit of a learning curve coming from pip. It&#8217;s an additional tool you have to install and get used to. While not complicated, it abstracts a lot &#8211; which can occasionally obscure what&#8217;s going on (for example, if something&#8217;s wrong with your <code>venv</code>, Poetry manages it behind the scenes). The dependency resolver, while usually fine, <strong>can be slower than pip for complex setups.</strong> And in some edge cases, Poetry&#8217;s stricter structure can be confusing &#8211; like needing to name your packages and structure your project in a certain way. It&#8217;s also a fairly heavy tool (written in Python, with a bunch of its own dependencies), whereas pip is part of Python itself.</p><p><strong>A little gotcha (and fix):</strong> The first time I used Poetry on a simple script project, I ran <code>poetry install</code> and got a warning: <strong>&#8220;The current project could not be installed: No file/folder found for package &#8230;&#8221;</strong>. Huh? Everything looked fine to me. What I learned is that Poetry was trying to install <em>my</em> project as a package (because by default it assumes you&#8217;re making a library or app that should be importable). It couldn&#8217;t find a proper Python package folder that matches the project name, hence the warning. There are a couple of ways to fix this:</p><ul><li><p><strong>If you&#8217;re just using Poetry for dependency management and don&#8217;t actually need to </strong><code>import</code><strong> your own project as a module,</strong> you can <strong>disable the project packaging</strong>. Add <code>package-mode = false</code> under <code>[tool.poetry]</code> in pyproject.toml, which tells Poetry &#8220;don&#8217;t bother trying to install this project as a library.&#8221; <a href="iframe.php?url=https%3A%2F%2Fantoine-does-ai.com%2FBlog%2FWhat-did-I-learn%2FBeware-of-poetry-package-named-differently-from-project-structure%23%3A%7E%3Atext%3D1%2Cneed%2520to%2520install%2520the%2520project">antoine-does-ai.com</a>.</p></li><li><p>Alternatively, you can run <code>poetry install --no-root</code>, <strong>which installs the dependencies but skips installing the project itself.</strong></p></li><li><p>The more &#8220;proper&#8221; fix (if you <em>do</em> want your project to be an installable package) is to structure your project with a package directory. For example, if your project is named <code>my_data_app</code>, create a <code>my_data_app/</code> folder in <code>src/</code> with an <code>__init__.py</code> inside. Or explicitly tell Poetry where your packages are in pyproject.toml. For instance:</p></li></ul><pre><code><code>[tool.poetry]
name = &#8220;my_data_app&#8221;
version = &#8220;0.1.0&#8221;
packages = [{ include = &#8220;my_data_app&#8221;, from = &#8220;src&#8221; }]</code></code></pre><ul><li><p>This way, Poetry knows your code lives under <code>src/my_data_app/</code>. If those names don&#8217;t align, you&#8217;ll get the &#8220;no file/folder for package&#8221; error<a href="iframe.php?url=https%3A%2F%2Fantoine-does-ai.com%2FBlog%2FWhat-did-I-learn%2FBeware-of-poetry-package-named-differently-from-project-structure%23%3A%7E%3Atext%3DAttempting%2520to%2520install%2520the%2520project%2Cresult%2520in%2520the%2520following%2520error">antoine-does-ai.com</a><a href="iframe.php?url=https%3A%2F%2Fantoine-does-ai.com%2FBlog%2FWhat-did-I-learn%2FBeware-of-poetry-package-named-differently-from-project-structure%23%3A%7E%3Atext%3D2%2Cwant%2520to%2520install%2520your%2520package">antoine-does-ai.com</a>.</p></li></ul><p><strong>Poetry&#8217;s strictness with project structure can be annoying for simple one-off projects,</strong> but it comes from a good place: <strong>treating your project like a proper package</strong> can pay off in the long run (you can easily install it elsewhere, or publish it). That said, if I&#8217;m just hacking together a quick analysis script, I&#8217;ll sometimes use Poetry&#8217;s <code>--no-root</code> option or set <code>package-mode=false</code> to keep things simple.</p><h2>3. uv &#8211; The New Speed Demon</h2><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VaIJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png 424w, https://substackcdn.com/image/fetch/$s_!VaIJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png 848w, https://substackcdn.com/image/fetch/$s_!VaIJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png 1272w, https://substackcdn.com/image/fetch/$s_!VaIJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21VaIJ%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png" width="1456" height="315" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:315,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:81265,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://markphammm.substack.com/i/178660548?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VaIJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png 424w, https://substackcdn.com/image/fetch/$s_!VaIJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png 848w, https://substackcdn.com/image/fetch/$s_!VaIJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png 1272w, https://substackcdn.com/image/fetch/$s_!VaIJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67582066-7ea9-4d3b-98f1-170032404e5a_1950x422.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>Now let&#8217;s talk about <strong>uv</strong> &#8211; the newest tool of the bunch. If Poetry felt like a level-up, uv feels like an experiment from the future that&#8217;s quickly becoming production-ready. It&#8217;s built by the folks at Astral (the same team behind the ridiculously fast Ruff linter), and the big selling point is <strong>speed</strong>. In fact, uv is written in Rust, and its goal is to be <em>an extremely fast, all-in-one Python package and project manager (</em><a href="iframe.php?url=https%3A%2F%2Fastral.sh%2Fblog%2Fuv%23%3A%7E%3Atext%3DTL%253BDR%253A%2520uv%2520is%2520an%2520extremely%2Ctools%2560%2520workflows">astral.sh</a>). Think of uv as trying to combine the best of pip, virtualenv/venv, and Poetry into one unified tool &#8211; and doing it much faster. The Astral team even described uv as a step toward a &#8220;Cargo for Python&#8221; (Cargo is Rust&#8217;s package manager), meaning one tool to manage environments, dependencies, and builds.</p><p>When they first released uv (early 2024), they benchmarked it and the numbers were eye-popping: uv was reported to be <strong>8&#8211;10&#215; faster than pip/pip-tools for resolving dependencies, and about 80&#8211;115&#215; faster with a warm cache</strong>. Yes, you read that right &#8211; up to eighty times faster in some scenarios. How? Basically by using a highly optimized dependency resolver and installer (Rust shines at this), caching wheels aggressively, and even using some filesystem tricks like hardlinks to avoid redundant copies. It&#8217;s also distributed as a single binary, so there&#8217;s no interpreter startup overhead and no need to have pip already working. It&#8217;s fair to say uv&#8217;s performance focus is <em>obsessive</em>. (As a side effect, uv&#8217;s name might evoke <strong>ultraviolet</strong> &#8211; fitting for something this fast, and indeed they hinted that &#8220;uv&#8221; can stand for ultraviolet or universal, while conveniently being a short name available on PyPI (<a href="iframe.php?url=https%3A%2F%2Flwn.net%2FArticles%2F986663%2F%23%3A%7E%3Atext%3D%2Cshort%2520and%2520easy%2520to%2520type">lwn.net</a>)</p><p>Using uv on our example project will feel a bit like a hybrid of pip and Poetry workflows. First, you&#8217;d install uv itself &#8211; for example via the installer script </p><pre><code><code>curl -sSL &lt;https://astral.sh/uv/install.sh&gt; | sh</code> </code></pre><p>or with <code>Homebrew</code>, or even <code>pipx</code>. Once you have the <code>uv</code> command, here&#8217;s how you&#8217;d set up <code>my_data_app</code>:</p><ol><li><p><strong>Create a virtual environment</strong> for the project using uv&#8217;s own command:</p></li></ol><pre><code><code>uv venv .venv
</code></code></pre><ol><li><p>This is analogous to <code>python -m venv .venv</code>, but it&#8217;s actually even faster (the uv devs built their own venv creator that&#8217;s about 80&#215; faster than Python&#8217;s, by doing it in Rust). By default, <code>uv venv .venv</code> will pick the current Python version to create an environment in the <code>.venv</code> folder. If you wanted a specific Python, you could do <code>uv venv .venv --python=3.11</code> for example.</p></li><li><p><strong>Declare your project&#8217;s dependencies</strong> in a <code>pyproject.toml</code>. Unlike Poetry, uv embraces the <strong>PEP 621 standard</strong> for project metadata. So instead of <code>[tool.poetry]</code> sections, you use the generic <code>[project]</code> section that other tools (like Flit or Hatch) also use. For example, our <code>pyproject.toml</code> for uv might look like:</p></li></ol><pre><code><code>[project]
name = &#8220;my_data_app&#8221;
version = &#8220;0.1.0&#8221;
description = &#8220;A simple data app using pandas and DuckDB&#8221;
requires-python = &#8220;&gt;=3.11&#8221;
dependencies = [
    &#8220;pandas&gt;=2.0,&lt;3.0&#8221;,
    &#8220;duckdb&gt;=1.4,&lt;2.0&#8221;
]

[project.optional-dependencies]
dev = [ &#8220;pytest&gt;=8.0,&lt;9.0&#8221; ]

[build-system]
requires = [&#8221;poetry-core&gt;=1.5.0&#8221;]
build-backend = &#8220;poetry.core.masonry.api&#8221;

[tool.uv.dependency-groups.dev]
include = [&#8221;dev&#8221;]

</code></code></pre><ol><li><p>A lot of that is boilerplate (and uv can actually auto-generate this via commands like <code>uv init</code>). The key parts: under <code>[project]</code> we listed our main dependencies and Python version. We also defined an &#8220;optional dependency group&#8221; named &#8220;dev&#8221; that includes pytest &#8211; this is how we specify dev requirements in a standard way. The <code>[tool.uv.dependency-groups.dev]</code> section is a uv-specific hint telling uv that when we say &#8220;install dev dependencies,&#8221; it should look at the &#8220;dev&#8221; group. Essentially, uv recognizes that dev group and can install it on demand.</p></li><li><p><strong>Install (sync) the environment</strong>: Once the pyproject is set up, you run:</p></li></ol><pre><code><code>uv sync</code></code></pre><ol><li><p>This is uv&#8217;s equivalent of <code>pip install</code> or <code>poetry install</code>. It will resolve versions for all your dependencies (very quickly, thanks to Rust) and then install them into the <code>.venv</code> we created. It also writes out a <strong>lock file</strong> called <code>uv.lock</code>. The lock file serves the same purpose as Poetry&#8217;s lock: capturing exact versions and hashes for reproducibility. The format is a bit different (TOML-based, with sections for each package including the exact URL and hash of the wheel or source distribution). One interesting thing about <strong>uv.lock</strong> is that it&#8217;s platform-aware &#8211; it can include multiple wheels for different platforms. For example, the lock entry for DuckDB might list a wheel for macOS/ARM, one for macOS/x86, one for manylinux, etc., along with their hashes<a href="iframe.php?url=https%3A%2F%2Fastral.sh%2Fblog%2Fuv%23%3A%7E%3Atext%3D1%2Cand">astral.sh</a><a href="iframe.php?url=https%3A%2F%2Fastral.sh%2Fblog%2Fuv%23%3A%7E%3Atext%3Duv%2520is%25208%2Cdownloading">astral.sh</a>. This means the same lock file can be used on different OSes, and uv will pick the appropriate wheel. Pretty cool for cross-platform teams.</p><p>After <code>uv sync</code>, you&#8217;ll have your packages installed and a lock file created. If a teammate pulls the repo, they just run <code>uv sync</code> and uv will recreate the venv and install everything exactly as specified in uv.lock. (In CI, you&#8217;d likely run <code>uv sync --frozen</code> or <code>uv sync --locked</code> to ensure it fails if the lock file isn&#8217;t honored, similar to <code>poetry install --no-update</code> behavior.)</p></li><li><p><strong>Using the environment</strong>: You have options. You can activate the <code>.venv</code> like a normal venv (<code>source .venv/bin/activate</code>) and then run Python. Or, you can use <code>uv run</code> to run commands inside the venv without explicitly activating it. For example, <code>uv run python src/main.py</code> would run our script using the venv&#8217;s interpreter. <code>uv run pytest</code> would run pytest from the venv. This is very much like <code>poetry run</code>. So uv, while not forcing you to, encourages a similar approach of not manually activating environments if you don&#8217;t want to.</p></li></ol><p>Day-to-day, uv can also manage your dependencies after the initial setup. If you want to add a package: <code>uv add flask</code> (and add <code>--group dev</code> if it&#8217;s a dev-only dependencies). That updates <code>pyproject.toml </code>and <code>uv.lock</code> together. Need to upgrade something? Edit <code>pyproject</code> or use <code>uv lock --upgrade package_name</code> to bump it, then <code>uv sync</code>. Under the hood, uv can function as a drop-in for some pip commands too: for example <code>uv pip install -U somepkg</code> will install and update the lock file accordingly, and <code>uv pip freeze</code> shows installed packages. In fact, uv&#8217;s CLI has a whole <code>uv pip ...</code> namespace to mirror pip and pip-tools commands for familiarity. But the recommended way is to use its higher-level project commands.</p><p><strong>Pros of uv:</strong> Speed, speed, speed. </p><p>Did I mention speed? When I first tried uv on a moderately complex project, I was floored by how fast the environment got built. It&#8217;s especially noticeable in continuous integration environments or when you&#8217;re frequently creating fresh venvs. Another pro is <strong>consolidation</strong>: uv aims to replace the trio of <code>python -m venv</code>, pip, and pip-tools with a single unified tool. That means less context switching and fewer tools to install. It also has neat features like managing multiple Python versions (there&#8217;s a <code>uv python install</code> command to install Python interpreters, similar to pyenv). The use of standard pyproject <code>[project]</code> metadata means you&#8217;re not locked into uv; that config can be understood by other tools as well. And because uv is still evolving, it&#8217;s quickly incorporating community feedback &#8211; it&#8217;s an exciting space to watch.</p><p><strong>Cons of uv:</strong> The flip side of being new is that it&#8217;s not yet as battle-tested as pip or Poetry. The uv project is moving fast (no pun intended) and while it&#8217;s reached a stable release, you might encounter minor bugs or missing features around the edges. Also, not everyone on your team may be familiar with it yet &#8211; convincing colleagues to adopt uv might require some evangelism, whereas Poetry has become quite mainstream in Python circles. Another potential con is that because uv does <em>so much</em>, it can feel a bit magical or heavy &#8211; but honestly, Poetry set the precedent there, and uv at least is extremely efficient in doing its work. I&#8217;d say the main reason you <em>wouldn&#8217;t</em> switch to uv (yet) is inertia or caution: pip/Poetry are working fine for you, so why risk a new workflow? That&#8217;s perfectly valid. But if you&#8217;re the curious type (or you maintain CI pipelines where every second saved is gold), uv is definitely worth a look.</p><h2>4. Choosing the Right Tool (pip vs Poetry vs uv)</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21sKrU%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sKrU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg 424w, https://substackcdn.com/image/fetch/$s_!sKrU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg 848w, https://substackcdn.com/image/fetch/$s_!sKrU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!sKrU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21sKrU%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg" width="1400" height="788" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:788,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;From Pip to UV: A Modern Take on Python Project Management (Part 1) | by  Harshit Kandoi | Towards AI&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="From Pip to UV: A Modern Take on Python Project Management (Part 1) | by  Harshit Kandoi | Towards AI" title="From Pip to UV: A Modern Take on Python Project Management (Part 1) | by  Harshit Kandoi | Towards AI" srcset="https://substackcdn.com/image/fetch/$s_!sKrU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg 424w, https://substackcdn.com/image/fetch/$s_!sKrU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg 848w, https://substackcdn.com/image/fetch/$s_!sKrU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!sKrU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7658f152-115c-4663-833b-3a501ed015e6_1400x788.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>So, with three different ways to manage a Python project, how do you choose? It really comes down to the scope and needs of your project:</p><ul><li><p><strong>Use pip</strong> (with a manual venv and requirements.txt) <strong>for simplicity</strong>: If this is a small script, a one-off analysis, or just a quick experiment, pip is often the path of least resistance. It&#8217;s already there, it&#8217;s quick to get going, and you won&#8217;t confuse future readers (or yourself) with additional config files. Just remember to create a virtual environment to avoid polluting your system Python! Pip is great when you don&#8217;t have a ton of dependencies or when you&#8217;re okay managing them by hand. For example, a short data script that only uses <code>requests</code> and <code>pandas</code> &#8211; you can pip install those, maybe pin the versions if needed, and be on your way.</p></li><li><p><strong>Use Poetry</strong> when you&#8217;re building a <strong>structured application, library, or anything long-lived</strong>. If your project is going to be deployed, or needs consistent environments across multiple machines, or will be worked on by a team, Poetry gives you that reliability. The lock file ensures that the &#8220;works on my machine&#8221; is reproducible, the pyproject.toml makes dependencies and metadata explicit, and having dev vs prod dependencies separate is a huge plus for larger projects. Also, if you think you might eventually publish the project (open-source library, internal package, etc.), Poetry sets you up with the packaging basics out of the box. In our <code>my_data_app</code> case, if this were an actual service or a tool I planned to maintain, I&#8217;d likely choose Poetry to manage it, so that any collaborator can clone the repo and just run <code>poetry install</code> to get started.</p></li><li><p><strong>Try uv</strong> if you <strong>value speed or are managing very large projects/monorepos</strong>, or if you&#8217;re just eager to use the latest-and-greatest in Python tooling. Especially in continuous integration scenarios, uv can drastically cut down install times. If your project has dozens of packages, or if you find yourself frequently creating new virtualenvs, uv&#8217;s performance pays off. It&#8217;s also a good fit if you like the idea of having one tool that does it all (no need for pip, pipenv, virtualenv, etc., in theory). That said, since uv is relatively new, I&#8217;d say it&#8217;s great for personal projects or forward-looking teams. Conservative or heavily-regulated environments might wait a bit longer for uv to mature. I&#8217;ve started using uv for some of my side projects and have been impressed &#8211; it hasn&#8217;t completely replaced Poetry for me yet, but I wouldn&#8217;t be surprised if it becomes the standard in the next year or two given its momentum (the GitHub repo already has tens of thousands of stars, which is a good sign of community interest).</p></li></ul><p>To summarize the differences, here&#8217;s a quick comparison of pip, Poetry, and uv for key aspects:</p><ul><li><p><strong>Config File</strong>: Pip uses a simple <code>requirements.txt</code> (a flat list of packages==versions). Poetry uses <code>pyproject.toml</code> with its own <code>[tool.poetry]</code> tables (plus a <code>poetry.lock</code>). uv also uses <code>pyproject.toml</code>, but in a <strong>PEP 621</strong> standard way (the <code>[project]</code> table), plus its <code>uv.lock</code>. In practice, Poetry&#8217;s config might not be directly usable by other tools, whereas uv&#8217;s config is more universal.</p></li><li><p><strong>Lockfile</strong>: Pip doesn&#8217;t have a native lock file (you can treat a fully pinned requirements.txt as one, but there&#8217;s no resolver ensuring consistency beyond manual freezing). Poetry&#8217;s <code>poetry.lock</code> is a detailed lockfile listing exact versions of everything, ensuring reproducible builds. uv&#8217;s <code>uv.lock</code> similarly pins exact versions (and hashes) but also includes multi-platform info and is optimized for speed. Both Poetry and uv expect you to commit the lockfile to version control for team use.</p></li><li><p><strong>Virtual Environment Management</strong>: With pip alone, you manage venvs manually (using venv or virtualenv). Poetry will <strong>auto-create and manage</strong> a venv for your project &#8211; you typically let it handle activation via <code>poetry run</code> or <code>poetry shell</code>. uv is somewhat in-between: you explicitly create a venv with <code>uv venv</code>, but then uv will automatically use that environment for any <code>uv run</code> or <code>uv sync</code> commands (so it feels seamless once set up). If you prefer the old activate/deactivate dance, you can still do that with any of these tools; none of them prevent you from activating the venv if you want to.</p></li><li><p><strong>Ecosystem and Extras</strong>: Pip is minimal but universal. Poetry has a rich ecosystem (plugins, a robust CLI, active community) and has been around longer than uv. uv is cutting-edge and integrates other Astral tools (for example, it works nicely with Ruff, etc.), and it might introduce new features as it grows (like easier Python version management, etc.). If you need to publish packages or manage complex constraints, Poetry currently has more documentation and community knowledge base. uv, however, is catching up fast and aims to cover those use cases too (its developers are actually maintainers of another tool called Rye, and they&#8217;re folding lessons from that into uv&#8217;s design).</p></li></ul><p>Finally, let me put it in personal terms: for a <strong>throwaway script or tiny automation</strong>, I still just use pip in a venv, because it&#8217;s one or two installs and done. For any project that I anticipate maintaining or sharing, <strong>Poetry is my default choice</strong> &#8211; it gives me peace of mind that everything is pinned and that setting up on a new machine is trivial. For new projects where I&#8217;m feeling adventurous (or if I&#8217;m really annoyed at how long pip install is taking in CI), I&#8217;ve been giving <strong>uv</strong> a shot, and it&#8217;s impressive. In a year&#8217;s time, who knows &#8211; I might be using uv for everything and reminiscing about the days of waiting 30 seconds for pip to resolve dependencies. &#128516;</p><h2><strong>Outro:</strong> </h2><p>There&#8217;s no one-size-fits-all, but the good news is Python&#8217;s tooling has come a long way. We&#8217;re no longer stuck with just &#8220;install globally and pray nothing conflicts.&#8221; Whether you choose pip, Poetry, or uv, you&#8217;re adopting practices that isolate and pin your environment, which is a huge win for reliability. Each tool has its niche: pip for quick-and-dirty tasks, Poetry for robust project management, and uv for speed and forward-thinking workflows. Try them out on different projects and see what feels right. And hey, you don&#8217;t have to pick a permanent allegiance &#8211; it&#8217;s totally fine to use pip on one project and Poetry or uv on another, based on needs. The real goal is to make dependency management <strong>one less headache</strong> in our development life. Happy coding, and may your environments be clean and your dependencies conflict-free!</p>]]></content:encoded></item></channel></rss>