<?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[Canadian Data Guy Unfiltered]]></title><description><![CDATA[The engineer who writes documentation-grade deep dives with production code you can run today]]></description><link>https://www.canadiandataguy.com</link><image><url>https://substackcdn.com/image/fetch/$s_!n3Eg!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30cc7753-f8fb-4300-ac7f-1806e112a06a_1024x1024.png</url><title>Canadian Data Guy Unfiltered</title><link>https://www.canadiandataguy.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 03 Apr 2026 20:32:46 GMT</lastBuildDate><atom:link href="https://www.canadiandataguy.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Canadian Data Guy]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[canadiandataguy@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[canadiandataguy@substack.com]]></itunes:email><itunes:name><![CDATA[Canadian Data Guy]]></itunes:name></itunes:owner><itunes:author><![CDATA[Canadian Data Guy]]></itunes:author><googleplay:owner><![CDATA[canadiandataguy@substack.com]]></googleplay:owner><googleplay:email><![CDATA[canadiandataguy@substack.com]]></googleplay:email><googleplay:author><![CDATA[Canadian Data Guy]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Inside Delta Lake’s Idempotency Magic: The Secret to Exactly-Once Spark]]></title><description><![CDATA[Learn how txnAppId and epochId work together to create a bulletproof distributed two-phase commit. Achieve true exactly-once semantics for your production pipelines]]></description><link>https://www.canadiandataguy.com/p/inside-delta-lakes-idempotency-magic</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/inside-delta-lakes-idempotency-magic</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Tue, 27 Jan 2026 01:51:30 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/185914250/1dd56360f1e2700fb6a8d8bd66e89de4.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<h2><strong>Introduction</strong></h2><p>When a Spark Structured Streaming job fails mid-flight, how does it know where to resume? What prevents duplicate writes to your Delta tables? This article explores the elegant mechanisms that make Spark Structured Streaming fault-tolerant and exactly-once.</p><blockquote><p><strong>Key Insight:</strong></p><p>The checkpoint directory and Delta Lake&#8217;s transaction log work together to ensure correctness even when clusters die between writing data and recording completion.</p></blockquote><h2><strong>Checkpoint Directory Structure</strong></h2><p>When you start a streaming query, Spark creates a checkpoint directory with the following structure:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xbRm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xbRm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.png 424w, https://substackcdn.com/image/fetch/$s_!xbRm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.png 848w, https://substackcdn.com/image/fetch/$s_!xbRm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.png 1272w, https://substackcdn.com/image/fetch/$s_!xbRm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xbRm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.png" width="1348" height="746" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:746,&quot;width&quot;:1348,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&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="" title="" srcset="https://substackcdn.com/image/fetch/$s_!xbRm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.png 424w, https://substackcdn.com/image/fetch/$s_!xbRm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.png 848w, https://substackcdn.com/image/fetch/$s_!xbRm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.png 1272w, https://substackcdn.com/image/fetch/$s_!xbRm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479b398d-6327-49d6-a5dd-31effd84cd09_1348x746.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"><svg role="img" 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"><g><title></title><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"><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"><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><blockquote><p><strong>Critical Timing:</strong></p><p><strong>offsets/N</strong> is written <em>before</em> processing batch N starts.<br><strong>commits/N</strong> is written <em>after</em> batch N completes successfully.</p></blockquote><h2><strong>The Normal Happy Path</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SVoD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SVoD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.png 424w, https://substackcdn.com/image/fetch/$s_!SVoD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.png 848w, https://substackcdn.com/image/fetch/$s_!SVoD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.png 1272w, https://substackcdn.com/image/fetch/$s_!SVoD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SVoD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.png" width="1338" height="946" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:946,&quot;width&quot;:1338,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&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="" title="" srcset="https://substackcdn.com/image/fetch/$s_!SVoD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.png 424w, https://substackcdn.com/image/fetch/$s_!SVoD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.png 848w, https://substackcdn.com/image/fetch/$s_!SVoD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.png 1272w, https://substackcdn.com/image/fetch/$s_!SVoD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da910fd-5a2d-4d7c-b1af-eab648e182de_1338x946.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"><svg role="img" 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"><g><title></title><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"><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"><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>The Critical Failure Scenario</strong></h2><p>Here&#8217;s where things get interesting. What happens when the cluster dies <em>after</em> writing to Delta but <em>before</em> writing the commit file?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!U46n!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!U46n!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png 424w, https://substackcdn.com/image/fetch/$s_!U46n!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png 848w, https://substackcdn.com/image/fetch/$s_!U46n!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png 1272w, https://substackcdn.com/image/fetch/$s_!U46n!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!U46n!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png" width="1248" height="658" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:658,&quot;width&quot;:1248,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&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="" title="" srcset="https://substackcdn.com/image/fetch/$s_!U46n!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png 424w, https://substackcdn.com/image/fetch/$s_!U46n!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png 848w, https://substackcdn.com/image/fetch/$s_!U46n!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png 1272w, https://substackcdn.com/image/fetch/$s_!U46n!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf4281b-3f14-4a50-a1fc-6ad72c3fcaf0_1248x658.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><blockquote><p><strong>The Problem</strong></p><p>Batch N+1 was successfully written to Delta Lake, but the commit file was never created. On restart, Spark will see:</p><ul><li><p><code>offsets/N+1</code> exists</p></li><li><p><code>commits/N+1</code> does not exist</p></li><li><p>Data is already in the Delta table</p></li></ul><p><strong>Question: Won&#8217;t re-running batch N+1 create duplicates? &#129300;</strong></p></blockquote><h2><strong>Delta Lake&#8217;s Idempotency Magic</strong></h2><p>This is where Delta Lake&#8217;s transaction log saves the day. Delta records two critical pieces of metadata with every streaming write:</p><blockquote><p><strong>Note:</strong> The terms <code>epochId</code> and <code>batchId</code> refer almost to the same thing - the monotonically increasing micro-batch number. I am trying to find more details to figure out the difference</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LDSm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LDSm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png 424w, https://substackcdn.com/image/fetch/$s_!LDSm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png 848w, https://substackcdn.com/image/fetch/$s_!LDSm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png 1272w, https://substackcdn.com/image/fetch/$s_!LDSm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LDSm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png" width="1072" height="874" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:874,&quot;width&quot;:1072,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&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="" title="" srcset="https://substackcdn.com/image/fetch/$s_!LDSm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png 424w, https://substackcdn.com/image/fetch/$s_!LDSm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png 848w, https://substackcdn.com/image/fetch/$s_!LDSm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png 1272w, https://substackcdn.com/image/fetch/$s_!LDSm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ff817b-1a1a-46cc-8af9-edf86e1aa2ad_1072x874.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>txnAppId (Query ID)</strong> The unique streaming query identifier from <code>metadata/ .</code>This ensures different queries don&#8217;t interfere with each other</p><p><strong>txnVersion (Epoch ID)</strong> The micro-batch number (0, 1, 2, 3...) Monotonically increasing; each batch gets its own ID</p><h4><strong>The Solution</strong></h4><blockquote><p>When Spark retries batch N+1, Delta Lake checks its transaction log:</p><ul><li><p>Has transaction (queryId: &#8220;abc-def-123-456&#8221;, epochId: N+1) been committed?</p></li><li><p><strong>If YES</strong>: Skip the duplicate write, create <code>commits/N+1</code></p></li><li><p><strong>If NO</strong>: Proceed with the write, then create <code>commits/N+1</code></p></li></ul></blockquote><h2><strong>Complete Recovery Flow</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mvWj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mvWj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png 424w, https://substackcdn.com/image/fetch/$s_!mvWj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png 848w, https://substackcdn.com/image/fetch/$s_!mvWj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png 1272w, https://substackcdn.com/image/fetch/$s_!mvWj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mvWj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png" width="1348" height="1014" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1014,&quot;width&quot;:1348,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&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="" title="" srcset="https://substackcdn.com/image/fetch/$s_!mvWj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png 424w, https://substackcdn.com/image/fetch/$s_!mvWj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png 848w, https://substackcdn.com/image/fetch/$s_!mvWj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png 1272w, https://substackcdn.com/image/fetch/$s_!mvWj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd478451-ed00-42c4-a022-2c6f5126351d_1348x1014.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><blockquote><p><strong>Key Insight:</strong> The checkpoint and Delta transaction log work together as a distributed two-phase commit. The checkpoint tracks <em>intent</em>, while Delta&#8217;s log tracks <em>completion</em>. Both must agree for the system to move forward.</p></blockquote><h2><strong>Offset Semantics / ( inclusive, exclusive]</strong></h2><p>When reading from Kafka, understanding offset boundaries is crucial for reasoning about what each batch consumes.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AAZS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AAZS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png 424w, https://substackcdn.com/image/fetch/$s_!AAZS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png 848w, https://substackcdn.com/image/fetch/$s_!AAZS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png 1272w, https://substackcdn.com/image/fetch/$s_!AAZS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AAZS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png" width="1092" height="358" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:358,&quot;width&quot;:1092,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&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="" title="" srcset="https://substackcdn.com/image/fetch/$s_!AAZS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png 424w, https://substackcdn.com/image/fetch/$s_!AAZS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png 848w, https://substackcdn.com/image/fetch/$s_!AAZS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png 1272w, https://substackcdn.com/image/fetch/$s_!AAZS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3be2a7ec-9176-42d2-8ab1-8810759c86ed_1092x358.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Start Offset: Inclusive-</strong> If <code>start = 100</code>, offset 100 <strong>is</strong> included in the batch</p><p><strong>End Offset: Exclusive-</strong> If <code>end = 200</code>, offset 200 <strong>is not</strong> included in the batch</p><p><strong>Next Batch:</strong> Batch N+1 would start at offset <code>200</code>(the previous end becomes the next start)</p><h2><strong>TLDR: Recovery Rules</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nkib!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nkib!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png 424w, https://substackcdn.com/image/fetch/$s_!nkib!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png 848w, https://substackcdn.com/image/fetch/$s_!nkib!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png 1272w, https://substackcdn.com/image/fetch/$s_!nkib!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nkib!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png" width="1396" height="668" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:668,&quot;width&quot;:1396,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&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="" title="" srcset="https://substackcdn.com/image/fetch/$s_!nkib!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png 424w, https://substackcdn.com/image/fetch/$s_!nkib!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png 848w, https://substackcdn.com/image/fetch/$s_!nkib!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png 1272w, https://substackcdn.com/image/fetch/$s_!nkib!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9531f0-c1aa-467c-9e1e-a069faa63222_1396x668.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Where to Find the IDs</strong></h3><p><strong>Streaming Query ID</strong></p><ul><li><p>&#128193;Checkpoint <code>metadata/</code> file</p></li><li><p>&#128421;&#65039;Spark Streaming UI (while query runs)</p></li><li><p>&#128211;Notebook outputs showing query status</p></li></ul><p>Batch/Epoch IDs</p><ul><li><p>&#128202;Tracked per micro-batch (0, 1, 2, ...)</p></li><li><p>&#127991;&#65039;Used by Delta to prevent duplicate commits (same as epochId)</p></li><li><p>&#128220;Visible in Delta transaction log</p></li></ul><h2><strong>When Things Go Wrong: A Real-World Accident</strong></h2><p>I encountered a scenario where duplicate records appeared in my Delta Lake table despite Structured Streaming&#8217;s exactly-once guarantees. The quickest way to identify the scope of the problem was using Delta&#8217;s <code>_metadata</code> column to pinpoint which Parquet files contained duplicates. By tracing these files back through the Delta transaction log versions, I discovered the root cause: <strong>the same epochId appeared in multiple transactions with different queryId values</strong>. This broke Delta Lake&#8217;s idempotency mechanism, which relies on the unique combination of <strong>(queryId, epochId)</strong> to detect and skip duplicate writes.</p><h3><strong>Root Cause</strong></h3><p>The checkpoint directory was accidentally overwritten or corrupted, causing Spark to reinitialize with a new queryId while replaying already-processed batches. Since Delta only saw new queryId values, it treated these as legitimate new transactions rather than duplicates, resulting in data duplication.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qQ7X!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qQ7X!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png 424w, https://substackcdn.com/image/fetch/$s_!qQ7X!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png 848w, https://substackcdn.com/image/fetch/$s_!qQ7X!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png 1272w, https://substackcdn.com/image/fetch/$s_!qQ7X!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qQ7X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png" width="1456" height="550" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:550,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&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="" title="" srcset="https://substackcdn.com/image/fetch/$s_!qQ7X!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png 424w, https://substackcdn.com/image/fetch/$s_!qQ7X!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png 848w, https://substackcdn.com/image/fetch/$s_!qQ7X!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png 1272w, https://substackcdn.com/image/fetch/$s_!qQ7X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1b441b-ae9a-4b25-88c7-50bd86205564_2730x1032.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Key Lessons:</strong></h3><p>This incident brought to light the critical importance of:</p><ol><li><p><strong>Enable comprehensive logging</strong> Implement S3 Server Access Logging or AWS CloudTrail to audit all checkpoint location and detect unauthorized changes</p></li><li><p><strong>Implement strict access control</strong> on checkpoint directories to prevent accidental modifications or deletions using UC Volumes</p></li><li><p><strong>Treat checkpoint directories as critical infrastructure</strong> requiring the same level of protection and operational discipline as your data itself</p></li></ol><blockquote><p><strong>Critical Takeaway:</strong> While Spark and Delta Lake provide strong exactly-once semantics, the checkpoint directory is a critical piece of infrastructure that requires the same level of protection and monitoring as your data itself.</p></blockquote><h2><strong>Summary: The Complete Picture</strong></h2><ol><li><p><strong>Checkpoint structure:</strong> <code>offsets/</code>tracks what to process, <code>commits/</code> tracks what&#8217;s been completed</p></li><li><p><strong>Timing matters:</strong> Offsets written before processing, commits written after success</p></li><li><p><strong>Delta Lake&#8217;s role:</strong> Transaction log with (query ID, epoch ID) prevents duplicates</p></li><li><p><strong>Safe replay:</strong> If a batch is replayed, Delta checks for prior commit and skips if found</p></li><li><p><strong>Exactly-once guarantee:</strong> Together, checkpoint + Delta transaction log ensure no data loss or duplication</p></li></ol><h2><strong>Related Deep Dives:</strong></h2><p>If you found this troubleshooting walkthrough helpful, I have a couple of other related posts that dive deeper into Delta Lake forensics and data management:</p><ul><li><p><strong>Forensic Analysis:</strong> I&#8217;ve written a detailed guide on how to trace exactly which Delta version number, Parquet file, and commit produced specific records, including sample code for the investigation process. If there&#8217;s interest, I&#8217;m happy to write a dedicated breakdown on this methodology&#8212;just leave a comment below!</p></li><li><p><strong>Handling Data Deletion:</strong> When you need to delete data from Delta Lake tables, understanding the impact on downstream streaming consumers is critical. I&#8217;ve covered this scenario in depth, including patterns for safe deletion and stream recovery. Check it out here: <a href="https://www.databricksters.com/p/how-to-actually-delete-data-in-spark">How to Actually Delete Data in Spark</a></p></li><li><p><strong>&#128250; Deep Dive into Stateful Stream Processing in Structured Streaming</strong></p><p>This talk covers the internals of stateful stream processing, checkpoint mechanisms, and recovery patterns in production environments. It provides invaluable insights into how Spark manages state and handles failures at scale.</p></li></ul><p>Let me know in the comments if you&#8217;d like to see more operational war stories and troubleshooting techniques!</p>]]></content:encoded></item><item><title><![CDATA[How to Choose Between Liquid Clustering and Partitioning with Z-Order in Databricks]]></title><description><![CDATA[The views expressed in this blog are my own and do not represent official guidance from Databricks]]></description><link>https://www.canadiandataguy.com/p/optimizing-delta-lake-tables-liquid</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/optimizing-delta-lake-tables-liquid</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Thu, 15 Jan 2026 19:01:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!j_gb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><em>This is one of the most-read posts on the website, so we decided to give it a well-deserved 2026 update. Thank you to <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Geethu&quot;,&quot;id&quot;:309314985,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!_nBv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff741d3a9-46f8-4807-a7b8-796f5656358e_144x144.png&quot;,&quot;uuid&quot;:&quot;e676bfe4-2610-48f0-8c1d-fd704b0978d3&quot;}" data-component-name="MentionToDOM"></span> for co-authoring on this revision and for raising the technical bar of the article.</em></p></blockquote><p><strong>Delta Lake</strong>, an open source storage format, offers two primary methods for organizing data: <strong>liquid clustering</strong> and <strong>partitioning with Z-order</strong>. This blog post will help you navigate the decision-making process between these two approaches. Clustering in Delta Lake enhances query performance by organizing data based on frequently accessed columns, similar to indexing in relational databases. The key difference is that clustering physically sorts the data within the table rather than creating separate index structures.</p><h2><strong>Understanding the Basics: Liquid Clustering vs. Partitioned Z-Order Tables</strong></h2><h4><strong>Liquid Clustering</strong></h4><p>Liquid clustering is a newer algorithm for Delta Lake tables, offering several advantages:</p><ul><li><p><strong>Flexibility</strong>: You can change clustering columns at any time.</p></li><li><p><strong>Optimization for Unpartitioned Tables</strong>: It works well without partitioning.</p></li><li><p><strong>Efficiency</strong>: It doesn&#8217;t re-cluster previously clustered files unless explicitly instructed.</p></li></ul><p>Liquid clustering relies on <strong>optimistic concurrency control (OCC)</strong> to handle conflicts when multiple writes occur to the same table.</p><h4><strong>Partitioned Z-Order Tables</strong></h4><p>Partitioning combined with Z-ordering is a traditional approach that:</p><ul><li><p><strong>Control</strong>: Allows greater control over data organization.</p></li><li><p><strong>Parallel Writes</strong>: Supports parallel writes more effectively.</p></li><li><p><strong>Fine-Grained Optimization</strong>: Enables optimization of specific partitions.</p></li></ul><p>However, data engineers must be aware of querying patterns upfront to choose an appropriate partition column.</p><h2>Decision Tree</h2><p><strong>Built in Jan 2026, this decision tree will be continuously updated as technology evolves. As new enhancements emerge, my understanding will grow, and this resource will be refined accordingly. This is a complex topic, but I will do my best to provide at least an intuitive grasp to help you develop a clearer understanding.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!j_gb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!j_gb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png 424w, https://substackcdn.com/image/fetch/$s_!j_gb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png 848w, https://substackcdn.com/image/fetch/$s_!j_gb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png 1272w, https://substackcdn.com/image/fetch/$s_!j_gb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!j_gb!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:1156,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4437352,&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://www.canadiandataguy.com/i/149768489?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!j_gb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png 424w, https://substackcdn.com/image/fetch/$s_!j_gb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png 848w, https://substackcdn.com/image/fetch/$s_!j_gb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png 1272w, https://substackcdn.com/image/fetch/$s_!j_gb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4435c9c7-4db6-47af-8ff9-b3d2767273d7_8192x6505.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><h2>Factors to Consider When Choosing</h2><h4>Table Size</h4><ul><li><p><strong>Small tables (&lt; 10 TB)</strong>: If you need fast lookups on exactly two columns, Liquid Clustering on those columns typically delivers comparable performance with simpler maintenance. If your workload involves highly selective lookups across three or more columns, Partition + Z-order may perform better, assuming the partition key has low cardinality. That said, Liquid Clustering can still work for multi-column lookups and is often worth benchmarking with tuned clustering keys.</p></li><li><p><strong>Medium tables (10 TB -500TB)</strong>: For medium-sized tables, the key decision factor is partition cardinality. If partitioning results in fewer than ~5,000 distinct values (for example, ~1,100 partitions for 3 years of daily data), Partition + Z-order can work well when queries include the partition column. If the number of distinct values exceeds ~5,000, Liquid Clustering is generally preferred to avoid over-partitioning. In practice, benchmark both approaches with representative queries to validate performance.</p></li><li><p><strong>Large tables (&gt; 500 TB)</strong>: You should reach out to your Databricks representative and have a discussion.</p></li></ul><h5><em><strong>Note: Liquid is being actively improved so the guidance could change </strong></em></h5><h4><strong>Data Ingestion Pattern</strong></h4><p>How data is written - batch or streaming - can influence which data organization strategy is most appropriate.</p><ul><li><p><strong>Batch Ingestion : </strong>For batch workloads, Liquid Clustering remains a strong default choice. Batch writes naturally organize data efficiently. In the latest Databricks Runtime versions, eager clustering can be enabled to make the data well-clustered as it is written, so queries see an optimized view right away.</p></li><li><p><strong>Streaming Ingestion : </strong>For streaming workloads, the choice depends on your main priority.</p><ul><li><p><strong>Low Latency:</strong> If getting data into the table quickly is most important, Liquid Clustering is preferred without eager clustering. This reduces shuffle overhead during ingestion. Data may not be fully optimized immediately, but query performance can improve later using Predictive I/O.</p></li><li><p><strong>Fast Downstream Lookups:</strong> If queries need to be fast as soon as data arrives, Liquid Clustering with eager clustering is recommended. This ensures data is well-clustered on write, and follow-up OPTIMIZE can further improve query performance.</p></li></ul></li></ul><h4>Query Patterns</h4><ul><li><p>If users consistently include the partition column in their queries, partitioning can be very effective.</p></li><li><p>Liquid clustering may be more suitable for more flexible query patterns where users may not always include the partition column.</p></li></ul><h4>Data Distribution</h4><ul><li><p>If you have uneven partition sizes, the liquid will be better.</p></li><li><p>Date-based data (e.g., clickstream data) often benefits from partitioning.</p></li><li><p>For data without a clear partitioning strategy, liquid clustering may be better.</p></li></ul><h4>Partition Column Selection</h4><p>When choosing a partition column:</p><ul><li><p>Select immutable columns (e.g., click date, sale date)</p></li><li><p>Avoid high-cardinality columns like timestamps</p></li><li><p>For timestamp data, create a derived date column for partitioning</p></li></ul><ul><li><p>Aim for fewer than 10,000 distinct partition values.</p></li><li><p>Each partition should contain at least ~1-10 GB of data.</p></li></ul><h2>Real-World Example: Amazon Clickstream Data</h2><p>Let's consider a real-world scenario using Amazon's clickstream data:</p><ul><li><p>The table stores 3 years of data for 10 countries</p></li><li><p>Partitioning by click date results in approximately 1,000 partitions (365 * 3)</p></li><li><p>10 countries * 1,000 date partitions = 10,000 total partitions</p></li></ul><p>This setup is within the recommended partition count (&lt; 10,000) and provides good control over the data. Here's how we might structure this table:</p><ol><li><p>Partition by <code>click_date, country</code></p></li><li><p>Z-order by  <code>merchant_id</code>, and <code>advertiser_id</code></p></li></ol><h4>Optimizing the Partitioned Table</h4><p>To maintain optimal performance, you can run a daily optimization job on the newest partition:</p><pre><code><code>OPTIMIZE table_name
WHERE click_date = 'ANY_DATE' and country = 'CANADA'
ZORDER BY ( merchant_id, advertiser_id)
</code></code></pre><p>This approach ensures good performance for date-range queries and lookups on Z-ordered columns.</p><h2>Optimistic Concurrency Control</h2><p>Delta Lake uses optimistic concurrency control to manage parallel writes. Here's how it works:</p><ol><li><p>Writers check the current version of the Delta table (e.g., version 100).</p></li><li><p>They attempt to write a new JSON file (e.g., 101.json).</p></li><li><p>Only one writer can succeed in creating this file.</p></li><li><p>The "losing" writer checks if there are conflicts with what was previously written.</p></li><li><p>If no conflicts, it creates the next version (e.g., 102.json).</p></li></ol><p>This approach works well for appends but can be challenging for updates, especially when multiple writers are trying to modify the same files.</p><h3></h3><h2>Potential Pitfalls and Best Practices</h2><p>Here are some key considerations and common mistakes to avoid:</p><ul><li><p>Do not add <strong>Co-related columns</strong> to liquid: If two columns are highly correlated, you only need to include one of them as a clustering key. Example, if you have click_date, click_timestamp then only cluster by click_timestamps</p></li><li><p><strong>Skip meaningless keys:</strong>  When it comes to clustering, try to avoid using meaningless keys such as UUIDs, which are inheritable and unsortable strings. If possible, refrain from using them in both liquid and z-order clustering. However, I understand that sometimes customers require quick lookups on these UUID columns. In those cases, you may include them.</p></li><li><p><strong>Over-Partitioning</strong>: A common mistake is creating too many partitions. While partitioning helps with performance, too many partitions can result in overhead. A good rule of thumb is to keep partition counts under 10,000. For example, if you're storing three years of daily click data, partitioning by <code>click_date</code> would result in around 1,000 partitions for three years&#8212;well within the 10,000-partition guideline. Example: Avoid partitioning on high cardinality columns (e.g., timestamps). This would result in too many partitions, leading to performance degradation. Instead, partition on a date column and ensure it has enough data per partition.</p></li><li><p>Enable <strong>Predictive Optimization </strong> on your Databricks workspace to automatically manage maintenance for Unity Catalog&#8211;managed tables. PO identifies tables that can benefit from operations such as <code>OPTIMIZE</code>, <code>VACUUM</code>, and <code>ANALYZE</code>, and schedules these jobs using serverless compute. This eliminates the need to manually schedule <code>OPTIMIZE</code> for compaction or clustering, as the platform triggers operations based on usage patterns, table statistics, and overall table health.</p><ul><li><p>For partitioned tables, PO applies compaction and layout improvements within each partition. </p></li><li><p>For Liquid Clustered tables, PO integrates with <code>CLUSTER BY AUTO</code>, automatically selecting clustering keys and scheduling incremental clustering jobs. This reduces manual tuning and ensures that the table layout evolves with changing query patterns, keeping queries efficient without intervention.</p></li></ul></li><li><p><strong>Schedule Optimization (If Required) </strong>: With Predictive Optimization (PO) enabled, most maintenance tasks are handled automatically. You only need to manually run <code>OPTIMIZE</code> in the following cases:</p><ul><li><p>For Zordered tables, Note that <code>OPTIMIZE</code> does not automatically apply ZORDER, so manual <code>OPTIMIZE</code> runs are still required if Z-ordering is needed.</p></li><li><p>For Liquid Clustered tables, manual <code>OPTIMIZE</code> is only needed if queries require faster response times immediately after data arrival, or additional optimization is necessary to improve query performance.</p></li></ul></li></ul><h2>Conclusion</h2><p>Choosing between liquid clustering and partitioned Z-order tables depends on various factors including table size, write patterns, and query requirements.  Always consider your specific use case and be prepared to test both approaches to determine the best fit for your data and query patterns. The right choice will significantly impact your query performance and overall data management efficiency.</p><h1><strong>Keep This Post Discoverable: Your Engagement Counts!</strong></h1><p>Your engagement with this blog post is crucial! Without claps, comments, or shares, this valuable content might become lost in the vast sea of online information. Search engines like Google rely on user engagement to determine the relevance and importance of web pages. If you found this information helpful, please take a moment to clap, comment, or share. Your action not only helps others discover this content but also ensures that you&#8217;ll be able to find it again in the future when you need it. Don&#8217;t let this resource disappear from search results &#8212; show your support and help keep quality content accessible!</p><h3>References</h3><ul><li><p><a href="https://docs.databricks.com/aws/en/delta/clustering">https://docs.databricks.com/aws/en/delta/clustering</a><strong><a href="https://docs.databricks.com/aws/en/delta/clustering"> </a></strong></p><div id="youtube2-yZmrpXJg-G8" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;yZmrpXJg-G8&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/yZmrpXJg-G8?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div></li><li><p>https://www.databricks.com/blog/2018/07/31/processing-petabytes-of-data-in-seconds-with-databricks-delta.html</p></li><li><p>https://docs.databricks.com/en/delta/clustering.html</p></li><li><p>https://www.databricks.com/blog/announcing-general-availability-liquid-clustering</p></li><li><p></p><div id="youtube2-tEP7Nb-8JRg" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;tEP7Nb-8JRg&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/tEP7Nb-8JRg?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><div id="youtube2-LgLf0xgsaes" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;LgLf0xgsaes&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/LgLf0xgsaes?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><div id="youtube2-CwJeKANlSLo" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;CwJeKANlSLo&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/CwJeKANlSLo?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><div id="youtube2-A1aR1A8OwOU" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;A1aR1A8OwOU&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/A1aR1A8OwOU?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div></li></ul>]]></content:encoded></item><item><title><![CDATA[Unlocking Sub-Second Latency with Databricks ]]></title><description><![CDATA[Watch now | How Spark Real Time Mode Achieving Millisecond Latency with a Simple Trigger Switch]]></description><link>https://www.canadiandataguy.com/p/unlocking-sub-second-latency-with</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/unlocking-sub-second-latency-with</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Wed, 14 Jan 2026 23:16:43 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/184601973/e496110f1b4801747332d854d079a2a7.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>I spent a whole month trying to write the &#8220;perfect&#8221; single blog on Spark Structured Streaming Real-Time Mode&#8230; and then I accepted reality: it&#8217;s too much to cram into one post without turning it into a textbook. So this is a <strong>series</strong>.</p><p>In this first post, I&#8217;m not building a crypto demo. I&#8217;m building a pattern you can reuse for things that actually move the needle: fraud detection, IoT sensor monitoring, real-time offers, security signals&#8212;<strong>anything where you need to respond to events ASAP.</strong></p><p>The goal is simple:</p><blockquote><p><strong>When an event looks suspicious or invalid, flag it immediately and route it differently.</strong></p></blockquote><p>That &#8220;suspicious or invalid&#8221; could be:</p><ul><li><p><strong>Fraud detection:</strong> a transaction looks off &#8594; trigger a downstream</p></li><li><p><strong>IoT:</strong> a sensor reading is impossible &#8594; trigger an action</p></li><li><p><strong>Security:</strong> payload contains secrets/PII patterns &#8594; quarantine in real time</p></li><li><p><strong>Offers/personalization:</strong> Respond to specific events</p></li></ul><p>For the dataset, I&#8217;m using Ethereum blocks because they&#8217;re high volume and behave like real production traffic. But the point isn&#8217;t crypto. The point is the operational pattern: <strong>real-time guardrails</strong>.</p><p>Concretely, I&#8217;m doing two checks on every block event:</p><ul><li><p><strong>Payload hygiene:</strong> flag suspicious strings in <code>extra_data</code> (think accidental secrets/PII-style patterns)</p></li><li><p><strong>Data quality:</strong> <code>gas_used &gt; gas_limit</code> (This should not happen&#8212;if it does, something is wrong)</p></li></ul><p>If any check trips, the event gets tagged <strong>QUARANTINE</strong>. Otherwise <strong>ALLOW</strong>. The output is just an enriched Kafka event that downstream consumers can act on immediately.</p><div class="pullquote"><p>Also, I used <strong><a href="https://www.redpanda.com/sign-up">Redpanda</a></strong> to run Kafka because they make it ridiculously easy to spin up a cluster, and new signups get <strong>$100 in credits for 14 days</strong>. Not sponsored.<br>Redpanda, if you&#8217;re reading this: give me more credits. I have too many experiments.</p></div><p>If you know me, you know I always test things at scale; if it does not scale, then I don&#8217;t write about it.  I uploaded the full Ethereum chain into Kafka&#8212;about <strong>95 GB</strong> into <strong>4 partitions</strong>&#8212;roughly <strong>23 million messages</strong>. If you want my notebook that dumps data into Redpanda, drop a comment and I&#8217;ll share it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZXzA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZXzA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png 424w, https://substackcdn.com/image/fetch/$s_!ZXzA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png 848w, https://substackcdn.com/image/fetch/$s_!ZXzA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png 1272w, https://substackcdn.com/image/fetch/$s_!ZXzA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZXzA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png" width="1431" height="550" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:550,&quot;width&quot;:1431,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:79752,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.canadiandataguy.com/i/183844527?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.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_!ZXzA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png 424w, https://substackcdn.com/image/fetch/$s_!ZXzA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png 848w, https://substackcdn.com/image/fetch/$s_!ZXzA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png 1272w, https://substackcdn.com/image/fetch/$s_!ZXzA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93abf208-2868-4dd5-af9b-8f842f7c55f5_1431x550.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Why am I doing this?</h2><p>Because I&#8217;m tired of hearing:</p><blockquote><p>&#8220;Spark isn&#8217;t fast enough like Flink, so we need a whole new stack for this one use case.&#8221;</p></blockquote><p>After 10+ years in data engineering, one lesson keeps paying rent: <strong>maintainability beats shiny tools</strong> more often than people want to admit. Even if the new tool is 20% faster and I have the energy to learn it, that does not automatically mean my whole team should learn it too.</p><p>I&#8217;ve benchmarked Spark streaming enough to be confident about this: i<a href="https://towardsdev.com/need-for-speed-benchmarking-the-best-tools-for-kafka-to-delta-ingestion-e1969121ed2e">f you can tolerate ~1&#8211;2 seconds</a>, Spark micro-batch will happily land data into Delta all day. I should redo that benchmark&#8212;last time it cost me <strong>$1,300</strong> (Confluent waived it, bless them). I&#8217;m here for sub-second latency, not <strong>sub-second &#8220;your card has been charged&#8221; notifications</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TqeN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TqeN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png 424w, https://substackcdn.com/image/fetch/$s_!TqeN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png 848w, https://substackcdn.com/image/fetch/$s_!TqeN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png 1272w, https://substackcdn.com/image/fetch/$s_!TqeN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TqeN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png" width="623" height="431" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:431,&quot;width&quot;:623,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43726,&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://www.canadiandataguy.com/i/183844527?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.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_!TqeN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png 424w, https://substackcdn.com/image/fetch/$s_!TqeN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png 848w, https://substackcdn.com/image/fetch/$s_!TqeN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png 1272w, https://substackcdn.com/image/fetch/$s_!TqeN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36408a16-1015-4fc1-b004-8dd32e9669f5_623x431.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Now Spark is stepping into the <strong>sub-second</strong> territory with <strong>Real-Time Mode</strong>&#8212;a new trigger type designed for operational workloads that need immediate response, with end-to-end latency advertised as low as <strong>5 ms</strong> (Public Preview, DBR 16.4 LTS+). <a href="https://docs.databricks.com/aws/en/structured-streaming/real-time">Databricks Documentation</a></p><p>I don&#8217;t buy marketing. So I tested it.</p><h2>What we&#8217;re building: Operational Guardrail Stream </h2><p>Every incoming event gets evaluated immediately and we emit an enriched event downstream with:</p><ul><li><p>a <strong>decision</strong>: <code>ALLOW</code> vs <code>QUARANTINE</code></p></li><li><p><strong>reasons</strong>: why we flagged it (data quality, payload hygiene, etc.)</p></li></ul><p>This is the operational pattern that shows up everywhere:</p><ul><li><p>&#8220;Do I quarantine it?&#8221;</p></li><li><p>&#8220;Do I enrich it so downstream can react instantly?&#8221;</p></li></ul><p>In my dataset, the &#8220;event&#8221; is an Ethereum block. In your world, it could be a transaction, sensor reading, auth log, API call&#8212;same idea.</p><h4>The dataset and assumptions</h4><p>Source topic: <code>ethereum-blocks-ordered-global</code><br>Target topic: <code>topic-with-4-partitions</code></p><p>Assumptions:</p><ul><li><p>Kafka <code>value</code> is JSON</p></li><li><p>We parse it into a hardcoded schema so we have typed columns like <code>gas_used</code>, <code>gas_limit</code>, <code>timestamp</code>, etc.</p></li><li><p>We also keep <code>kafka_ts</code> (Kafka append timestamp) because for operational monitoring, arrival time matters.</p></li></ul><h2>What makes an event &#8220;bad&#8221; in this post</h2><p>I&#8217;m keeping the rules intentionally simple and high-signal:</p><h4>Rule 1: Payload hygiene check</h4><p>Scan <code>extra_data</code> for obvious &#8220;this shouldn&#8217;t be here&#8221; patterns.</p><p>In the blog code, I show basic examples (email/JWT/AWS key shapes). Replace these with your real rules (PII patterns, internal IDs, API keys, etc.).</p><p>The point isn&#8217;t regex perfection. The point is: <strong>real-time guardrails belong in the pipeline, not in a postmortem.</strong></p><h4>Rule 2: Bad data check</h4><p><code>gas_used &gt; gas_limit</code></p><p>This should not happen. If it happens, either:</p><ul><li><p>the data is corrupted,</p></li><li><p>the producer is wrong,</p></li><li><p>you&#8217;re parsing incorrectly,</p></li><li><p>or something upstream is broken.</p></li></ul><p>Operationally, that&#8217;s exactly what we want: <em>flag it immediately.</em></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lr-O!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lr-O!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png 424w, https://substackcdn.com/image/fetch/$s_!lr-O!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png 848w, https://substackcdn.com/image/fetch/$s_!lr-O!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!lr-O!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lr-O!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png" width="1200" height="670.054945054945" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:813,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:6239929,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.canadiandataguy.com/i/183844527?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!lr-O!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png 424w, https://substackcdn.com/image/fetch/$s_!lr-O!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png 848w, https://substackcdn.com/image/fetch/$s_!lr-O!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!lr-O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dfa35e8-d8a0-4708-87e2-b663dba387cc_2752x1536.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><hr></div><h2>Real-Time Mode: the tiny bit you need to know</h2><p>Real-Time Mode is enabled by using the real-time trigger and runs under update mode. In PySpark you specify an interval like <code>"5 minutes"</code>.</p><p>Two important &#8220;don&#8217;t skip this&#8221; notes:</p><ol><li><p><strong>Cluster config matters</strong> (Databricks documents the required job cluster settings and the RTM enablement flag).</p></li><li><p><strong>Output mode must be </strong><code>update</code> with RTM triggers.</p></li></ol><p>That&#8217;s all I&#8217;m going to say here, because this post is about the operational pattern. I&#8217;ll do a deeper &#8220;RTM setup checklist&#8221; in the next post.</p><h2>The code: Real-time guardrail (Kafka &#8594; Spark RTM &#8594; Kafka)</h2><p>This is a single-pass pipeline:</p><ul><li><p>Connect to Kafka</p></li><li><p>Parse kafka input</p></li><li><p>Compute <code>decision</code>, <code>reasons</code></p></li><li><p>write JSON back to Kafka <strong>as strings</strong> (no binary needed)</p></li></ul><h4>Imports &amp; Configuration</h4><pre><code><code>import json
import re
import uuid
from pyspark.sql import functions as F
from pyspark.sql.types import (
    StructType, StructField, StringType, LongType, 
    DoubleType, TimestampType, DateType
)

# -------------------------------------------------------------------------
# 1. CONFIGURATION
# -------------------------------------------------------------------------

# --- Kafka Connection Details ---
# Ideally, fetch these from secrets (e.g., dbutils.secrets.get)
BOOTSTRAP_SERVERS = "d5deqhbrcoacstishppg.any.us-west-2.mpx.prd.cloud.redpanda.com:9092"
SASL_MECHANISM = "SCRAM-SHA-256"
RP_USERNAME = "redpanda"
RP_PASSWORD = ""

# --- Kafka Options ---
RP_KAFKA_OPTIONS = {
    "kafka.bootstrap.servers": BOOTSTRAP_SERVERS,
    "kafka.security.protocol": "SASL_SSL",
    "kafka.sasl.mechanism": SASL_MECHANISM,
    "kafka.sasl.jaas.config": (
        'kafkashaded.org.apache.kafka.common.security.scram.ScramLoginModule required '
        f'username="{RP_USERNAME}" password="{RP_PASSWORD}";'
    ),
    "kafka.ssl.endpoint.identification.algorithm": "https",
}

# --- Job Settings ---
INPUT_TOPIC = "ethereum-blocks-ordered-global"
OUTPUT_TOPIC = "topic-with-4-partitions"
CHECKPOINT_LOCATION = f"/tmp/chk_rtm_stateless_guardrail_{uuid.uuid4()}"

# Set shuffle partitions for purposes (default 200 which is too high in low latency use cases)
spark.conf.set("spark.sql.shuffle.partitions", "8")</code></code></pre><h4>Connect to Kafka</h4><pre><code><code># --- Step A: Read from Kafka ---
df_raw = (
    spark.readStream
    .format("kafka")
    .options(**RP_KAFKA_OPTIONS)
    .option("subscribe", INPUT_TOPIC)
    .option("startingOffsets", "earliest")
    .option("failOnDataLoss", "false")
    .load()
)

#display(df_raw)</code></code></pre><div class="pullquote"><p><strong>Special note:</strong> <code>display()</code> It is a special function that <strong>initiates the streaming query for you,</strong> allowing you to preview the live output. It kicks off the stream so you can see rows flowing without wiring up a full sink. You don&#8217;t even need to specify a checkpoint just to preview results. It&#8217;s perfect for quick debugging&#8212;just don&#8217;t confuse it with a production pipeline.</p></div><h4>Parse JSON Payload </h4><pre><code><code># -------------------------------------------------------------------------
# 2. SCHEMA DEFINITION
# -------------------------------------------------------------------------

block_schema = StructType([
    StructField("hash", StringType(), True),
    StructField("miner", StringType(), True),
    StructField("nonce", StringType(), True),
    StructField("number", LongType(), True),
    StructField("size", LongType(), True),
    StructField("timestamp", TimestampType(), True),
    StructField("total_difficulty", DoubleType(), True),
    StructField("base_fee_per_gas", LongType(), True),
    StructField("gas_limit", LongType(), True),
    StructField("gas_used", LongType(), True),
    StructField("extra_data", StringType(), True),
    StructField("logs_bloom", StringType(), True),
    StructField("parent_hash", StringType(), True),
    StructField("state_root", StringType(), True),
    StructField("receipts_root", StringType(), True),
    StructField("transactions_root", StringType(), True),
    StructField("sha3_uncles", StringType(), True),
    StructField("transaction_count", LongType(), True),
    StructField("date", DateType(), True),
    StructField("last_modified", TimestampType(), True),
])

# --- Step B: Parse JSON Payload ---
# We cast the binary 'value' to string, parse it, and flatten the struct
df_parsed = (
    df_raw
    .select(
        F.col("timestamp").alias("kafka_ts"),
        F.col("key").cast("string").alias("kafka_key"),
        F.col("value").cast("string").alias("value_str")
    )
    .withColumn("parsed", F.from_json(F.col("value_str"), block_schema))
    .where(F.col("parsed").isNotNull())  # Filter out malformed JSON
    .select(
        "kafka_ts", 
        "kafka_key",
        F.col("parsed.*")
    )
)</code></code></pre><h4>Compute <code>decision</code>, <code>reasons/ Your Custom Logic / Rules</code></h4><pre><code><code># -------------------------------------------------------------------------
# 3. UDF DEFINITIONS
# -------------------------------------------------------------------------

# Pre-compile regex patterns for efficiency
EMAIL_RE = re.compile(r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}", re.IGNORECASE)
JWT_RE   = re.compile(r"eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+")
AWS_RE   = re.compile(r"AKIA[0-9A-Z]{16}")

@F.udf("string")
def extra_data_reason_udf(extra_data: str) -&gt; str:
    """
    Scans 'extra_data' field for sensitive or suspicious patterns.
    Returns a reason code if a match is found, otherwise None.
    """
    if extra_data is None:
        return None
    if EMAIL_RE.search(extra_data):
        return "EXTRA_DATA_EMAIL"
    if JWT_RE.search(extra_data):
        return "EXTRA_DATA_JWT"
    if AWS_RE.search(extra_data):
        return "EXTRA_DATA_AWS_KEY"
    return None


# --- Step C: Enrich &amp; Apply Business Rules ---

# Rule 1: Gas Used cannot exceed Gas Limit
condition_bad_gas = (
    F.col("gas_used").isNotNull() &amp;
    F.col("gas_limit").isNotNull() &amp;
    (F.col("gas_used") &gt; F.col("gas_limit"))
)

# Rule 2: Check for suspicious patterns in extra_data
col_extra_reason = extra_data_reason_udf(F.col("extra_data"))

df_enriched = (
    df_parsed
    .withColumn("bad_gas", condition_bad_gas)
    .withColumn("extra_reason", col_extra_reason)
    # Collect all failure reasons into an array
    .withColumn(
        "reasons",
        F.expr("""
            filter(
                array(
                    case when bad_gas then 'BAD_GAS_USED_GT_LIMIT' end,
                    extra_reason
                ),
                x -&gt; x is not null
            )
        """)
    )
    # Determine final decision: Quarantine if any reasons exist
    .withColumn("is_quarantined", F.size(F.col("reasons")) &gt; 0)
    .withColumn(
        "decision",
        F.when(F.col("is_quarantined"), F.lit("QUARANTINE"))
         .otherwise(F.lit("ALLOW"))
    )
    # Prepare final output structure for Kafka (Key, Value JSON)
    .select(
        F.col("kafka_key").cast("binary").alias("key"),
        F.to_json(F.struct(
            F.col("kafka_ts"),
            F.col("number"),
            F.col("hash"),
            F.col("miner"),
            F.col("timestamp").alias("block_ts"),
            F.col("gas_used"),
            F.col("gas_limit"),
            F.col("decision"),
            F.col("is_quarantined"),
            F.col("reasons"),
            F.col("extra_data")
        )).alias("value")
    )
)</code></code></pre><h4>Write Back To Kafka</h4><pre><code><code># --- Step D: Write to Kafka (Real-Time Mode) ---
# The key highlight here is trigger(realTime="...")
query = (
    df_enriched.writeStream
    .format("kafka")
    .options(**RP_KAFKA_OPTIONS)
    .option("topic", OUTPUT_TOPIC)
    .option("checkpointLocation", CHECKPOINT_LOCATION)
    .option("queryName", f"rtm-stateless-guardrail-{OUTPUT_TOPIC}")
    .outputMode("update")
    # -----------------------------------------------------------------
    # REAL TIME MODE: Asynchronous checkpointing for lower latency
    # -----------------------------------------------------------------
    .trigger(realTime="1 minutes")
    .start()
)</code></code></pre><h2><strong>The Best Part? It&#8217;s Just the Flip of a Switch</strong></h2><p>Perhaps the most surprising aspect of Real-Time Mode is its remarkable ease of adoption for developers already familiar with Structured Streaming. Enabling this powerful new capability does not require a complex migration or a rewrite of existing code.</p><p>Instead, users can unlock millisecond-level latency by simply changing the trigger configuration in their existing query.</p><p>This seamless user experience is a critical feature. It means teams can prototype and productionize operational workloads without the massive overhead of learning, deploying, and managing an entirely separate technology stack. This drastically accelerates innovation and reduces the risk associated with adopting new real-time use cases.</p><h1>References</h1><p><a href="https://www.youtube.com/watch?v=sgUIcWwE8aQ&amp;t=473s"> Real-Time Mode Technical Deep Dive: How We Built Sub-300 Millisecond Streaming Into Apache Spark&#8482;</a></p><p><a href="https://www.youtube.com/watch?v=zGJvbV80FdU&amp;t=1376s">Delivering Sub-Second Latency for Operational Workloads on Databricks</a></p><p><a href="https://people.eecs.berkeley.edu/~matei/papers/2018/sigmod_structured_streaming.pdf">Structured Streaming Paper</a></p>]]></content:encoded></item><item><title><![CDATA[I Knew the Answer. I Just Couldn’t Remember It.]]></title><description><![CDATA[How you can turn your notes into a personal Knowledge Agent &#8212; no code required]]></description><link>https://www.canadiandataguy.com/p/i-knew-the-answer-i-just-couldnt</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/i-knew-the-answer-i-just-couldnt</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Sat, 10 Jan 2026 16:31:05 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/184054134/15d24b541e6184f863d912df45b397f8.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>I don&#8217;t use ChatGPT or Google to answer most technical questions people ask me. Here&#8217;s why: most people think AI advantage comes from picking the right model. But models are becoming commodities. The real advantage is whether your AI has <em>your</em> knowledge.</p><blockquote><p><em>You don&#8217;t forget things. You forget where your brain stored them.</em></p></blockquote><p>This kept happening: someone would ask, &#8220;How did you speed up merges when writing to N tables at once ?&#8221; And I&#8217;d think&#8212;I know this. I&#8217;ve seen this. I probably even wrote about it. But I couldn&#8217;t recall where it is, and then spent 10 minutes finding the right piece.</p><p>The problem wasn&#8217;t a lack of knowledge. The problem was <strong>fragmentation</strong>. My knowledge lived in bookmarks, blogs, notes, documents, and videos. The information existed. Retrieval was broken.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VRbt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VRbt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.png 424w, https://substackcdn.com/image/fetch/$s_!VRbt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.png 848w, https://substackcdn.com/image/fetch/$s_!VRbt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.png 1272w, https://substackcdn.com/image/fetch/$s_!VRbt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VRbt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.png" width="1456" height="703" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:703,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5153288,&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://www.canadiandataguy.com/i/184054134?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.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_!VRbt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.png 424w, https://substackcdn.com/image/fetch/$s_!VRbt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.png 848w, https://substackcdn.com/image/fetch/$s_!VRbt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.png 1272w, https://substackcdn.com/image/fetch/$s_!VRbt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbe168a2-5693-429e-8e92-bcf928adeff3_3266x1576.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"><svg role="img" 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"><g><title></title><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"><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"><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>My Old Stack: Why It Failed</strong></h2><p>For years I was paying for Notion. I was paying for Notion AI. And I was paying for a separate diagramming tool because I build a lot of decision trees and system diagrams.</p><p>Then I realized something uncomfortable: I was paying just to store my own thinking. Everything lived in different platforms. And I still had the same problem&#8212;when someone asked a question, I knew I had the answer somewhere. I just didn&#8217;t know where.</p><p>That&#8217;s when I knew I didn&#8217;t need more tools. <strong>I needed a recall layer.</strong></p><h2><strong>Discovering Obsidian</strong></h2><p>Then a colleague showed me Obsidian. If you don&#8217;t know it, it&#8217;s a free, local, markdown-based note tool. Out of curiosity, I looked up who builds it.</p><p>This surprised me: Obsidian is built and maintained by a team of less than 20 people&#8212;not a massive tech company. Which is amazing, because the tool feels extremely polished.</p><h3><strong>What I loved immediately:</strong></h3><ul><li><p>&#9670;<strong>Local:</strong> My notes are just Markdown files on my computer</p></li><li><p>&#9670;<strong>Markdown:</strong> Plain text, universally readable, version-controllable</p></li><li><p>&#9670;<strong>Mine:</strong> No lock-in. I own my data completely.</p></li></ul><h2><strong>The Simple / No Code Solution </strong></h2><p>Around the same time, I already knew Cursor could index everything inside a workspace. Then the idea clicked:</p><p><strong>What if Obsidian and Cursor pointed to the same folder?</strong></p><p>Obsidian manages my notes. Cursor indexes them. Suddenly Cursor stopped being just an IDE and became my <strong>personal knowledge agent</strong>. No training. No fine-tuning. Just my corpus.</p><div class="pullquote"><p>Hack: You can use Cursor to do the dirty work of organizing and cleaning your notes. </p></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YN33!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YN33!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png 424w, https://substackcdn.com/image/fetch/$s_!YN33!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png 848w, https://substackcdn.com/image/fetch/$s_!YN33!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png 1272w, https://substackcdn.com/image/fetch/$s_!YN33!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YN33!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png" width="1456" height="721" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:721,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3843637,&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://www.canadiandataguy.com/i/184054134?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.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_!YN33!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png 424w, https://substackcdn.com/image/fetch/$s_!YN33!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png 848w, https://substackcdn.com/image/fetch/$s_!YN33!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png 1272w, https://substackcdn.com/image/fetch/$s_!YN33!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ecdbc07-4c32-4bec-bebc-8b6df769222c_3516x1740.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>How I Feed the Agent</strong></h2><p>Then I started feeding it properly. All my daily work notes go into Obsidian. All my blogs go into Obsidian. I set up RSS feeds for sources I trust&#8212;Databricks blogs, Canadian Data Guy, and a few others.</p><p>And one extra trick: I manually downloaded transcripts from YouTube videos I really liked and stored them as notes. Now, this vault is a living, evolving record of my thoughts.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1K15!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1K15!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png 424w, https://substackcdn.com/image/fetch/$s_!1K15!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png 848w, https://substackcdn.com/image/fetch/$s_!1K15!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png 1272w, https://substackcdn.com/image/fetch/$s_!1K15!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1K15!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png" width="1456" height="894" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:894,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4540359,&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://www.canadiandataguy.com/i/184054134?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.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_!1K15!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png 424w, https://substackcdn.com/image/fetch/$s_!1K15!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png 848w, https://substackcdn.com/image/fetch/$s_!1K15!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png 1272w, https://substackcdn.com/image/fetch/$s_!1K15!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ed9e441-ce51-4cea-99e8-afd19fd0679b_3028x1860.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><h3><strong>Recommended Obsidian Plugins</strong></h3><ul><li><p><code>RSS Reader</code>&#8212; Auto-import articles from trusted sources</p></li><li><p><code>Templater</code>&#8212; Standardize note structures</p></li><li><p><code>Daily Notes</code>&#8212; Automatic daily journal creation</p></li></ul><h2><strong>Creative Things I Use the Cursor For</strong></h2><p>Cursor isn&#8217;t just helping me retrieve notes. This is where it gets really interesting. Because now that it understands my entire vault, I don&#8217;t just ask it questions&#8212;I use it to <strong>work on my knowledge with me</strong>.</p><h3><strong>Chaos &#8594; Structure</strong></h3><p>Sometimes I have a single document that&#8217;s just messy&#8212;bullet points, half sentences, random links. I highlight the file and say: &#8220;Turn these incomplete notes into a clean, structured explanation.&#8221; Cursor organizes it into sections and connects it to things I&#8217;ve written before.</p><h3><strong>Multi-Note Synthesis</strong></h3><p>I select three or four old notes from different months and say: &#8220;These are related. Combine them into one coherent technical explanation.&#8221; Cursor doesn&#8217;t just rewrite&#8212;it synthesizes. It connects things I forgot were even related.</p><h3><strong>Example Prompts</strong></h3><pre><code>&gt; &#8220;Look at this document and turn these incomplete notes into a clean explanation&#8221;
&gt; &#8220;These 4 files are related. Combine them into one coherent technical doc&#8221;
&gt; &#8220;How have my approaches to streaming cost optimization evolved over time?&#8221;
&gt; &#8220;What would I say about Delta Lake checkpoints based on my past notes?&#8221;
&gt; &#8220;Reflect my thinking back to me - what blind spots do you see?&#8221;</code></pre><h2><strong>What It&#8217;s Good At (And Not)</strong></h2><p>This is not a research engine. If someone asks me something completely outside my domain, I don&#8217;t pretend my agent knows. That&#8217;s a Google or ChatGPT question.</p><p>This solves a different problem. About 80% of the questions I get are: <em>&#8220;I know you&#8217;ve seen this before&#8230;&#8221;</em> That&#8217;s exactly what this is built for.</p><h3><strong>&#9889; Key Insight</strong></h3><p>The answers don&#8217;t sound like the internet. They sound like <em>me</em>. Because they&#8217;re grounded in my notes. This is the difference between generic AI and a personal knowledge agent.</p><h2><strong>Why This Matters</strong></h2><p>Not all knowledge belongs in public models. Your real thinking is messy. Evolving. Sometimes confidential.</p><p>Your agent needs to evolve with you. My notes change every day. So my agent changes every day.</p><blockquote><p><em>This doesn&#8217;t make me smarter. It makes me harder to forget.</em></p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rhlz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rhlz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png 424w, https://substackcdn.com/image/fetch/$s_!rhlz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png 848w, https://substackcdn.com/image/fetch/$s_!rhlz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png 1272w, https://substackcdn.com/image/fetch/$s_!rhlz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rhlz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png" width="1456" height="786" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:786,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4735747,&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://www.canadiandataguy.com/i/184054134?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.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_!rhlz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png 424w, https://substackcdn.com/image/fetch/$s_!rhlz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png 848w, https://substackcdn.com/image/fetch/$s_!rhlz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png 1272w, https://substackcdn.com/image/fetch/$s_!rhlz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F271c1b23-b96a-48a9-9fee-c4a7eebcad47_3180x1716.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Going Deeper: Production Agents</strong></h2><p>This setup solves most of my daily problems. It helps me every single day.</p><p>But if you want something much more concrete&#8212;something deeper, something you might even share with others&#8212;then you should look at building agents with <strong><a href="https://www.databricksters.com/p/your-low-code-shortcut-to-production?r=5ehbt&amp;utm_campaign=post&amp;utm_medium=web">AgentBricks on Databricks</a></strong><a href="https://www.databricksters.com/p/your-low-code-shortcut-to-production?r=5ehbt&amp;utm_campaign=post&amp;utm_medium=web">.</a></p><p>That gives you a much more powerful, production-grade way to build agents. This personal setup doesn&#8217;t replace that. This is the precursor.</p><h5>Start here. Build your own local recall engine.</h5><p>And when you&#8217;re ready to go deeper, AgentBricks helps you scale it to other humans and agents.</p><div class="embedded-post-wrap" data-attrs="{&quot;id&quot;:179205898,&quot;url&quot;:&quot;https://www.databricksters.com/p/your-low-code-shortcut-to-production&quot;,&quot;publication_id&quot;:3757239,&quot;publication_name&quot;:&quot;Databricksters&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!zPJJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff49ecae-7c56-403c-9389-61b28de6a50f_1280x1280.png&quot;,&quot;title&quot;:&quot;Your Low-Code Shortcut to Production-Grade Agent on Databricks&quot;,&quot;truncated_body_text&quot;:&quot;The code base is available at https://github.com/jiteshsoni/BrickBrain, but you likely don&#8217;t need it. You can simply use the Databricks UI to create your agent.&quot;,&quot;date&quot;:&quot;2025-11-18T16:02:42.715Z&quot;,&quot;like_count&quot;:3,&quot;comment_count&quot;:0,&quot;bylines&quot;:[{&quot;id&quot;:9073721,&quot;name&quot;:&quot;Canadian Data Guy&quot;,&quot;handle&quot;:&quot;canadiandataguy&quot;,&quot;previous_name&quot;:&quot;Jitesh Soni&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1441c9b8-d40b-4ac7-b91f-4260f55db017_2586x2586.jpeg&quot;,&quot;bio&quot;:null,&quot;profile_set_up_at&quot;:&quot;2025-04-13T04:00:41.958Z&quot;,&quot;reader_installed_at&quot;:&quot;2025-04-13T08:05:17.626Z&quot;,&quot;publicationUsers&quot;:[{&quot;id&quot;:2962604,&quot;user_id&quot;:9073721,&quot;publication_id&quot;:2913897,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:true,&quot;publication&quot;:{&quot;id&quot;:2913897,&quot;name&quot;:&quot;Canadian Data Guy Unfiltered&quot;,&quot;subdomain&quot;:&quot;canadiandataguy&quot;,&quot;custom_domain&quot;:&quot;www.canadiandataguy.com&quot;,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;Simplifying complex data concepts for everyone, without the buzzwords&#8212;elevating your game in your data journey!&quot;,&quot;logo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/30cc7753-f8fb-4300-ac7f-1806e112a06a_1024x1024.png&quot;,&quot;author_id&quot;:9073721,&quot;primary_user_id&quot;:9073721,&quot;theme_var_background_pop&quot;:&quot;#FF6719&quot;,&quot;created_at&quot;:&quot;2024-08-20T19:42:52.628Z&quot;,&quot;email_from_name&quot;:&quot;Canadian Data Guy&quot;,&quot;copyright&quot;:&quot;Canadian Data Guy&quot;,&quot;founding_plan_name&quot;:&quot;Founding Member&quot;,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;disabled&quot;,&quot;language&quot;:null,&quot;explicit&quot;:false,&quot;homepage_type&quot;:&quot;magaziney&quot;,&quot;is_personal_mode&quot;:false}},{&quot;id&quot;:3830653,&quot;user_id&quot;:9073721,&quot;publication_id&quot;:3757239,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:false,&quot;publication&quot;:{&quot;id&quot;:3757239,&quot;name&quot;:&quot;Databricksters&quot;,&quot;subdomain&quot;:&quot;databricksters&quot;,&quot;custom_domain&quot;:&quot;www.databricksters.com&quot;,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;Sharing field learnings so you don't have to&quot;,&quot;logo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ff49ecae-7c56-403c-9389-61b28de6a50f_1280x1280.png&quot;,&quot;author_id&quot;:9073721,&quot;primary_user_id&quot;:62428265,&quot;theme_var_background_pop&quot;:&quot;#FF6719&quot;,&quot;created_at&quot;:&quot;2025-01-14T18:12:17.311Z&quot;,&quot;email_from_name&quot;:&quot;Databricksters&quot;,&quot;copyright&quot;:&quot;Soni&quot;,&quot;founding_plan_name&quot;:null,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;disabled&quot;,&quot;language&quot;:null,&quot;explicit&quot;:false,&quot;homepage_type&quot;:&quot;magaziney&quot;,&quot;is_personal_mode&quot;:false}}],&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null,&quot;status&quot;:{&quot;bestsellerTier&quot;:null,&quot;subscriberTier&quot;:null,&quot;leaderboard&quot;:null,&quot;vip&quot;:false,&quot;badge&quot;:null,&quot;paidPublicationIds&quot;:[],&quot;subscriber&quot;:null}},{&quot;id&quot;:11708054,&quot;name&quot;:&quot;Veena&quot;,&quot;handle&quot;:&quot;veenaramesh&quot;,&quot;previous_name&quot;:&quot;jason funderburker&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2e215c6a-7de4-4e56-afe8-42f61c24d204_322x322.png&quot;,&quot;bio&quot;:&quot;Veena Ramesh | machine learning and AI @ Databricks and amateur artist. &quot;,&quot;profile_set_up_at&quot;:&quot;2022-03-27T17:41:05.161Z&quot;,&quot;reader_installed_at&quot;:&quot;2022-03-30T01:20:38.078Z&quot;,&quot;publicationUsers&quot;:[{&quot;id&quot;:3346453,&quot;user_id&quot;:11708054,&quot;publication_id&quot;:3284996,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:true,&quot;publication&quot;:{&quot;id&quot;:3284996,&quot;name&quot;:&quot;welcome to our swamp. &quot;,&quot;subdomain&quot;:&quot;toadmind&quot;,&quot;custom_domain&quot;:null,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;TOADMIND is a substack dedicated to our fantasy world. &quot;,&quot;logo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bc19c30c-d655-49ad-ace8-b4278bfcf3e1_828x828.png&quot;,&quot;author_id&quot;:11708054,&quot;primary_user_id&quot;:11708054,&quot;theme_var_background_pop&quot;:&quot;#FF6719&quot;,&quot;created_at&quot;:&quot;2024-11-04T19:26:06.370Z&quot;,&quot;email_from_name&quot;:null,&quot;copyright&quot;:&quot;jason funderburker&quot;,&quot;founding_plan_name&quot;:null,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;disabled&quot;,&quot;language&quot;:null,&quot;explicit&quot;:false,&quot;homepage_type&quot;:&quot;newspaper&quot;,&quot;is_personal_mode&quot;:false}},{&quot;id&quot;:3834873,&quot;user_id&quot;:11708054,&quot;publication_id&quot;:3757239,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:false,&quot;publication&quot;:{&quot;id&quot;:3757239,&quot;name&quot;:&quot;Databricksters&quot;,&quot;subdomain&quot;:&quot;databricksters&quot;,&quot;custom_domain&quot;:&quot;www.databricksters.com&quot;,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;Sharing field learnings so you don't have to&quot;,&quot;logo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ff49ecae-7c56-403c-9389-61b28de6a50f_1280x1280.png&quot;,&quot;author_id&quot;:9073721,&quot;primary_user_id&quot;:62428265,&quot;theme_var_background_pop&quot;:&quot;#FF6719&quot;,&quot;created_at&quot;:&quot;2025-01-14T18:12:17.311Z&quot;,&quot;email_from_name&quot;:&quot;Databricksters&quot;,&quot;copyright&quot;:&quot;Soni&quot;,&quot;founding_plan_name&quot;:null,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;disabled&quot;,&quot;language&quot;:null,&quot;explicit&quot;:false,&quot;homepage_type&quot;:&quot;magaziney&quot;,&quot;is_personal_mode&quot;:false}}],&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null,&quot;status&quot;:{&quot;bestsellerTier&quot;:null,&quot;subscriberTier&quot;:null,&quot;leaderboard&quot;:null,&quot;vip&quot;:false,&quot;badge&quot;:null,&quot;paidPublicationIds&quot;:[],&quot;subscriber&quot;:null}}],&quot;utm_campaign&quot;:null,&quot;belowTheFold&quot;:true,&quot;type&quot;:&quot;podcast&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="EmbeddedPostToDOM"><a class="embedded-post" native="true" href="https://www.databricksters.com/p/your-low-code-shortcut-to-production?utm_source=substack&amp;utm_campaign=post_embed&amp;utm_medium=web"><div class="embedded-post-header"><img class="embedded-post-publication-logo" src="https://substackcdn.com/image/fetch/$s_!zPJJ!,w_56,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff49ecae-7c56-403c-9389-61b28de6a50f_1280x1280.png" loading="lazy"><span class="embedded-post-publication-name">Databricksters</span></div><div class="embedded-post-title-wrapper"><div class="embedded-post-title-icon"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M3 18V12C3 9.61305 3.94821 7.32387 5.63604 5.63604C7.32387 3.94821 9.61305 3 12 3C14.3869 3 16.6761 3.94821 18.364 5.63604C20.0518 7.32387 21 9.61305 21 12V18" stroke-linecap="round" stroke-linejoin="round"></path>
  <path d="M21 19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21H18C17.4696 21 16.9609 20.7893 16.5858 20.4142C16.2107 20.0391 16 19.5304 16 19V16C16 15.4696 16.2107 14.9609 16.5858 14.5858C16.9609 14.2107 17.4696 14 18 14H21V19ZM3 19C3 19.5304 3.21071 20.0391 3.58579 20.4142C3.96086 20.7893 4.46957 21 5 21H6C6.53043 21 7.03914 20.7893 7.41421 20.4142C7.78929 20.0391 8 19.5304 8 19V16C8 15.4696 7.78929 14.9609 7.41421 14.5858C7.03914 14.2107 6.53043 14 6 14H3V19Z" stroke-linecap="round" stroke-linejoin="round"></path>
</svg></div><div class="embedded-post-title">Your Low-Code Shortcut to Production-Grade Agent on Databricks</div></div><div class="embedded-post-body">The code base is available at https://github.com/jiteshsoni/BrickBrain, but you likely don&#8217;t need it. You can simply use the Databricks UI to create your agent&#8230;</div><div class="embedded-post-cta-wrapper"><div class="embedded-post-cta-icon"><svg width="32" height="32" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <path classname="inner-triangle" d="M10 8L16 12L10 16V8Z" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg></div><span class="embedded-post-cta">Listen now</span></div><div class="embedded-post-meta">5 months ago &#183; 3 likes &#183; Canadian Data Guy and Veena</div></a></div>]]></content:encoded></item><item><title><![CDATA[4 Surprising Truths That Will Change How You Think About Spark Streaming]]></title><description><![CDATA[Spark gives you Real-Time without the complexity and pain]]></description><link>https://www.canadiandataguy.com/p/4-surprising-truths-that-will-change</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/4-surprising-truths-that-will-change</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Mon, 15 Dec 2025 15:13:29 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/snJs2DlzA0o" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>TL;DR</h2><ul><li><p>Spark now competes with Flink on real&#8209;time: Real&#8209;Time mode achieves double&#8209;digit millisecond latency; think 20 ms </p></li><li><p>One engine, one API: Batch, near&#8209;real&#8209;time, and true real&#8209;time in the same Spark paradigm.</p></li><li><p>Simplicity at scale: Checkpointing, fault tolerance, exactly&#8209;once semantics built in.</p></li><li><p>Real&#8209;time without friction: No second system or new programming model required.</p></li></ul><h2>Four Counter&#8209;Intuitive Truths</h2><ol><li><p><strong>Real-time without a new paradigm</strong></p><p>You don&#8217;t need a separate engine + a separate mental model. Same Spark APIs, same ecosystem, same team skillset.</p></li><li><p><strong>Real-time to hourly, depending on the business need</strong></p><p>Streaming doesn&#8217;t mean 24/7.  Spark Streaming is incremental, not perpetual. Choose the schedule your business needs&#8212;continuous, 15&#8209;minute, hourly, or weekly. triggerAvailableNow kicks off, processes all new data in a single efficient run, and exits. You get batch&#8209;style cost control with streaming&#8209;grade correctness.</p></li><li><p><strong>Checkpointing changes the game operationally</strong></p><p>Build batch with the streaming paradigm. Design batch pipelines as streaming from day one to avoid rewrites when SLAs tighten. Going from daily to every four hours can be a small code change, not an architectural overhaul. And you drop brittle input parameters (e.g., process_date): Spark tracks progress so engineers focus on logic, not bookkeeping. Streaming can be cheaper than batch Late or frequently updated data punishes batch. Checkpointing persists a &#8220;bookmark&#8221; so Spark processes only net&#8209;new changes and skips what it has already seen. </p><p><em>&#8220;checkpointing says you don&#8217;t worry about what&#8217;s net new I&#8217;ll identify what&#8217;s net new and only process that&#8221;</em></p></li><li><p><strong>Latency is a business decision, not an ego metric</strong><br>The learning curve is flatter than you think Spark&#8217;s unified API means you reuse the same DataFrame/Dataset logic for batch and streaming. The shift is incremental: same engine, same abstractions, different triggers and sinks. Teams extend what they know instead of adopting a second framework.</p></li></ol><h2>Conclusion </h2><p>If this blog has reshaped how you think about Spark Streaming, the YouTube session demonstrates <strong>how it actually works in practice</strong>.</p><p>In the video, I go beyond concepts and walk through:</p><ul><li><p>How Spark Structured Streaming achieves <strong>double-digit millisecond latency</strong> in real deployments</p></li><li><p>Kafka &#8594; Delta ingestion patterns that teams run in production</p></li><li><p>How checkpointing simplifies operations and reduces cost compared to batch reprocessing</p></li><li><p>When to use continuous mode vs triggerAvailableNow vs micro-batch &#8212; and why this is a <strong>business decision</strong>, not a technical flex</p></li></ul><p>This isn&#8217;t a theoretical take. It&#8217;s based on <strong>shipping streaming workloads to production week after week at Databricks</strong>, dealing with real SLAs, real failures, and real cost pressure.</p><p>Watch the full walkthrough here:<br>&#128073; </p><div id="youtube2-snJs2DlzA0o" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;snJs2DlzA0o&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/snJs2DlzA0o?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Why I Materialize Delta History for Debugging]]></title><description><![CDATA[Just a Quick Tip]]></description><link>https://www.canadiandataguy.com/p/why-i-materialize-delta-history-for</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/why-i-materialize-delta-history-for</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Thu, 27 Nov 2025 22:36:52 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!IxOb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I&#8217;m debugging a Delta table with millions of commits &#8212; especially tables with <strong>heavy ingestion</strong>, lots of parquet files &#8212; I often need to trace a specific record back to:</p><ul><li><p>which <strong>commit</strong> wrote it</p></li><li><p>which <strong>wrote this record (Job id, Job Run Id)</strong></p></li><li><p>which <strong>operation</strong> triggered that write</p><p></p></li></ul><p><code>DESCRIBE HISTORY</code> gives you this metadata, but on large tables it can be slow, and running it repeatedly while investigating a bug quickly becomes painful.</p><p></p><p>The practical workaround is to <strong>dump the entire history once</strong> into a physical table.<br>From there, you can filter, join, and slice it instantly &#8212; without re-scanning the entire Delta log on every query.</p><h3><strong>One-Time Dump of Delta Table History</strong></h3><pre><code><code>CREATE TABLE IF NOT EXISTS databricks_support.default.describe_history__your_table_name AS
SELECT *
FROM (
    DESCRIBE HISTORY your_catalog_name.your_database_name._your_table_name
);
</code></code></pre><p>For deep debugging (record &#8594; parquet file &#8594; commit lineage), this table becomes a fast, queryable audit log.<br>In practice, this works best when run from a <strong>notebook</strong>, where long-running metadata operations are less fragile.</p><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IxOb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IxOb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!IxOb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!IxOb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!IxOb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IxOb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ebc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1441462,&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://www.canadiandataguy.com/i/180138751?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.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_!IxOb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!IxOb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!IxOb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!IxOb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc11881-0c9f-4741-bdf4-28cb1c59fe00_1024x1024.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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 also have a script that can identify which row is written in which Parquet file by which commit; drop me a comment if you need it.</p><p></p>]]></content:encoded></item><item><title><![CDATA[Stop Waiting for Connectors: Stream ANYTHING into Spark (It's 4 Functions)]]></title><description><![CDATA[Listen now | How to ingest data from any source into Apache Spark &#8212; demystified with real-world example of BlockChain Ingestion]]></description><link>https://www.canadiandataguy.com/p/stop-waiting-for-connectors-stream</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/stop-waiting-for-connectors-stream</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Mon, 03 Nov 2025 17:24:45 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/177861173/2ee733487ca1a5ca414a57c4cede2c92.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<h3>&#128161; What You&#8217;ll Learn</h3><p>By the end of this guide, you&#8217;ll understand that building a custom Spark streaming source isn&#8217;t rocket science. It&#8217;s actually a well-defined conversation between Spark and your code, with just <strong>5 key methods</strong> to implement. We&#8217;ll use a real Ethereum blockchain streaming example to show you exactly how it works.</p><h2>The Problem: You Have Data, Spark Wants It</h2><p>You&#8217;ve got data streaming in from somewhere unique &#8212; maybe it&#8217;s IoT sensors, a blockchain, a custom message queue, or an internal database. You want to process it with Spark&#8217;s powerful distributed engine, but there&#8217;s no pre-built connector. What do you do?</p><p>The good news: <strong>You can build your own custom source</strong>. The even better news: <strong>It&#8217;s simpler than you think</strong>.</p><div class="pullquote"><p><strong>Real-World Use Case:</strong> In this guide, we&#8217;ll walk through streaming Ethereum blockchain data into Spark. The same principles apply to any data source &#8212; from proprietary APIs to custom databases. The pattern is universal.</p></div><h2>The Secret: It&#8217;s Just a Conversation</h2><p>Think of building a custom Spark streaming source as a conversation between two specialists:</p><p><strong>The Two Characters in Our Story</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qNYK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qNYK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png 424w, https://substackcdn.com/image/fetch/$s_!qNYK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png 848w, https://substackcdn.com/image/fetch/$s_!qNYK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png 1272w, https://substackcdn.com/image/fetch/$s_!qNYK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qNYK!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png" width="1200" height="561.2167300380228" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:369,&quot;width&quot;:789,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:187528,&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://www.canadiandataguy.com/i/177539932?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!qNYK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png 424w, https://substackcdn.com/image/fetch/$s_!qNYK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png 848w, https://substackcdn.com/image/fetch/$s_!qNYK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png 1272w, https://substackcdn.com/image/fetch/$s_!qNYK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16ac2a20-a2f0-4a5a-9210-7c50b282584e_789x369.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Spark&#8217;s job</strong> (the Project Manager) is to handle all the complex distributed computing stuff: checkpointing, fault tolerance, distributing work across a cluster, and guaranteeing exactly-once processing semantics.</p><p><strong>Your code&#8217;s job</strong> (the Data Specialist) is much simpler: answer Spark&#8217;s questions about where your data is, how to access it, and how to break it into chunks that can be processed in parallel.</p><div class="pullquote"><p><strong>&#127919; Key Insight:</strong> You don&#8217;t need to understand distributed systems, fault tolerance algorithms, or checkpoint mechanisms. You just need to implement 5 simple methods that answer Spark&#8217;s questions about your data source.</p></div><h2>The 5 Questions Spark Will Ask You</h2><p>Spark&#8217;s conversation with your code follows a predictable pattern. It asks 5 questions, and you provide straightforward answers. Let&#8217;s look at each one:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LND2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LND2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png 424w, https://substackcdn.com/image/fetch/$s_!LND2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png 848w, https://substackcdn.com/image/fetch/$s_!LND2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png 1272w, https://substackcdn.com/image/fetch/$s_!LND2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LND2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png" width="828" height="662" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:662,&quot;width&quot;:828,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:130242,&quot;alt&quot;:&quot;1initialOffset() &#8212; &#8220;Where do we start?&#8221;Spark asks: &#8220;This is a brand new query. Where should I begin reading?&#8221;You answer: {&#8221;offset&#8221;: 1000} &#8212; &#8220;Start at block 1000&#8221;2latestOffset() &#8212; &#8220;What&#8217;s the newest data?&#8221;Spark asks: &#8220;What&#8217;s the most recent data available right now?&#8221;You answer: {&#8221;offset&#8221;: 1100} &#8212; &#8220;Latest block is 1100&#8221;3partitions() &#8212; &#8220;How do we split this work?&#8221;Spark asks: &#8220;We need to process blocks 1000-1100. Break this into parallel chunks&#8221;You answer: [Partition(1000-1025), Partition(1025-1050), ...] &#8212; &#8220;4 chunks of 25 blocks&#8221;4read() &#8212; &#8220;Fetch the data!&#8221;Spark tells each worker: &#8220;Here&#8217;s your chunk. Go get the actual data&#8221;You fetch: Loop through blocks 1000-1025, fetch each one, yield Row objects5commit() &#8212; &#8220;All done!&#8221; [optional]checkpoint/commit/{N} file created. Optional method for cleanup tasks.&quot;,&quot;title&quot;:&quot;1initialOffset() &#8212; &#8220;Where do we start?&#8221;Spark asks: &#8220;This is a brand new query. Where should I begin reading?&#8221;You answer: {&#8221;offset&#8221;: 1000} &#8212; &#8220;Start at block 1000&#8221;2latestOffset() &#8212; &#8220;What&#8217;s the newest data?&#8221;Spark asks: &#8220;What&#8217;s the most recent data available right now?&#8221;You answer: {&#8221;offset&#8221;: 1100} &#8212; &#8220;Latest block is 1100&#8221;3partitions() &#8212; &#8220;How do we split this work?&#8221;Spark asks: &#8220;We need to process blocks 1000-1100. Break this into parallel chunks&#8221;You answer: [Partition(1000-1025), Partition(1025-1050), ...] &#8212; &#8220;4 chunks of 25 blocks&#8221;4read() &#8212; &#8220;Fetch the data!&#8221;Spark tells each worker: &#8220;Here&#8217;s your chunk. Go get the actual data&#8221;You fetch: Loop through blocks 1000-1025, fetch each one, yield Row objects5commit() &#8212; &#8220;All done!&#8221; [optional]checkpoint/commit/{N} file created. Optional method for cleanup tasks.&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.canadiandataguy.com/i/177539932?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="1initialOffset() &#8212; &#8220;Where do we start?&#8221;Spark asks: &#8220;This is a brand new query. Where should I begin reading?&#8221;You answer: {&#8221;offset&#8221;: 1000} &#8212; &#8220;Start at block 1000&#8221;2latestOffset() &#8212; &#8220;What&#8217;s the newest data?&#8221;Spark asks: &#8220;What&#8217;s the most recent data available right now?&#8221;You answer: {&#8221;offset&#8221;: 1100} &#8212; &#8220;Latest block is 1100&#8221;3partitions() &#8212; &#8220;How do we split this work?&#8221;Spark asks: &#8220;We need to process blocks 1000-1100. Break this into parallel chunks&#8221;You answer: [Partition(1000-1025), Partition(1025-1050), ...] &#8212; &#8220;4 chunks of 25 blocks&#8221;4read() &#8212; &#8220;Fetch the data!&#8221;Spark tells each worker: &#8220;Here&#8217;s your chunk. Go get the actual data&#8221;You fetch: Loop through blocks 1000-1025, fetch each one, yield Row objects5commit() &#8212; &#8220;All done!&#8221; [optional]checkpoint/commit/{N} file created. Optional method for cleanup tasks." title="1initialOffset() &#8212; &#8220;Where do we start?&#8221;Spark asks: &#8220;This is a brand new query. Where should I begin reading?&#8221;You answer: {&#8221;offset&#8221;: 1000} &#8212; &#8220;Start at block 1000&#8221;2latestOffset() &#8212; &#8220;What&#8217;s the newest data?&#8221;Spark asks: &#8220;What&#8217;s the most recent data available right now?&#8221;You answer: {&#8221;offset&#8221;: 1100} &#8212; &#8220;Latest block is 1100&#8221;3partitions() &#8212; &#8220;How do we split this work?&#8221;Spark asks: &#8220;We need to process blocks 1000-1100. Break this into parallel chunks&#8221;You answer: [Partition(1000-1025), Partition(1025-1050), ...] &#8212; &#8220;4 chunks of 25 blocks&#8221;4read() &#8212; &#8220;Fetch the data!&#8221;Spark tells each worker: &#8220;Here&#8217;s your chunk. Go get the actual data&#8221;You fetch: Loop through blocks 1000-1025, fetch each one, yield Row objects5commit() &#8212; &#8220;All done!&#8221; [optional]checkpoint/commit/{N} file created. Optional method for cleanup tasks." srcset="https://substackcdn.com/image/fetch/$s_!LND2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png 424w, https://substackcdn.com/image/fetch/$s_!LND2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png 848w, https://substackcdn.com/image/fetch/$s_!LND2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png 1272w, https://substackcdn.com/image/fetch/$s_!LND2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F403de673-2c13-4cba-bd3a-5fabd03f3d14_828x662.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Let&#8217;s See Real Code: Streaming Ethereum Blocks</h2><p>Theory is great, but let&#8217;s look at actual implementation. Here&#8217;s how these 5 methods work in practice for streaming Ethereum blockchain data:</p><h3><strong>1.</strong> initialOffset() &#8212; Setting the Starting Point</h3><pre><code><code>def initialOffset(self) -&gt; dict:
    &#8220;&#8221;&#8220;
    Called ONCE when starting a brand new query.
    Return where to begin reading.
    &#8220;&#8221;&#8220;
    start_block = self.options.get(&#8221;start_block&#8221;, 0)
    return {&#8221;offset&#8221;: int(start_block)}
</code></code></pre><p>That&#8217;s it! Just return a dictionary with your starting position. Spark saves this and uses it as the baseline for the entire query lifecycle.</p><h3><strong>2.</strong> latestOffset() &#8212; Checking What&#8217;s Available</h3><pre><code><code>def latestOffset(self) -&gt; dict:
    &#8220;&#8221;&#8220;
    Called at the START of every batch.
    Connect to your source and return the newest available data.
    &#8220;&#8221;&#8220;
    latest_block = self.w3.eth.block_number
    return {&#8221;offset&#8221;: int(latest_block)}
</code></code></pre><p>This method connects to your data source (in this case, an Ethereum node) and asks &#8220;what&#8217;s the latest?&#8221; The answer defines the upper bound for the current batch.</p><blockquote><p><strong>&#9888;&#65039; Python API Limitation:</strong> In PySpark, <code>latestOffset()</code> must return the absolute latest data point. If you&#8217;re backfilling from very old data, your first batch could be huge. The Scala API offers more fine-grained control here, but for most real-time use cases, the Python API works perfectly.<br><br><strong>&#128221; Note:</strong> This limitation is actively being addressed - there&#8217;s currently a pull request in progress to fix this in Spark.</p></blockquote><h3><strong>3.</strong> partitions() &#8212; Dividing the Work</h3><pre><code><code>def partitions(self, start: dict, end: dict) -&gt; list:
    &#8220;&#8221;&#8220;
    Spark gives you a range (start &#8594; end).
    You break it into smaller chunks for parallel processing.
    &#8220;&#8221;&#8220;
    start_block = start[&#8221;offset&#8221;]
    end_block = end[&#8221;offset&#8221;]  # This is EXCLUSIVE (not included)
    
    num_partitions = self.spark.conf.get(&#8221;spark.sql.shuffle.partitions&#8221;, &#8220;4&#8221;)
    blocks_per_partition = (end_block - start_block) // int(num_partitions)
    
    partitions = []
    for i in range(int(num_partitions)):
        partition_start = start_block + (i * blocks_per_partition)
        partition_end = partition_start + blocks_per_partition
        if i == int(num_partitions) - 1:  # Last partition gets any remainder
            partition_end = end_block
            
        partitions.append(BlockRangePartition(partition_start, partition_end))
    
    return partitions
</code></code></pre><p><strong>How Partitioning Works</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nC1l!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nC1l!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.png 424w, https://substackcdn.com/image/fetch/$s_!nC1l!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.png 848w, https://substackcdn.com/image/fetch/$s_!nC1l!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.png 1272w, https://substackcdn.com/image/fetch/$s_!nC1l!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nC1l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.png" width="778" height="344" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fa0236f5-7080-4243-be23-b427bd18fd86_778x344.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:344,&quot;width&quot;:778,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43967,&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://www.canadiandataguy.com/i/177539932?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.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_!nC1l!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.png 424w, https://substackcdn.com/image/fetch/$s_!nC1l!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.png 848w, https://substackcdn.com/image/fetch/$s_!nC1l!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.png 1272w, https://substackcdn.com/image/fetch/$s_!nC1l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa0236f5-7080-4243-be23-b427bd18fd86_778x344.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>s</p><blockquote><p><strong>&#128273; Critical Detail:</strong> Notice that the end block (1100) is <strong>exclusive</strong>. This means partition ranges are [1000, 1025), [1025, 1050), etc. Block 1100 is NOT processed&#8212;it becomes the start of the next batch. This [start, end) pattern is how Spark guarantees no data is ever processed twice.</p></blockquote><h3><strong>4</strong> read() &#8212; Actually Fetching the Data</h3><pre><code><code>def read(self, partition: BlockRangePartition):
    &#8220;&#8221;&#8220;
    This runs on EXECUTOR nodes (distributed across the cluster).
    Each executor gets one partition and must fetch its assigned data.
    
    Must be DETERMINISTIC - same input = same output, every time.
    This allows Spark to safely retry failed tasks.
    &#8220;&#8221;&#8220;
    for block_number in range(partition.start_block, partition.end_block):
        # Connect to Ethereum and fetch this specific block
        block = self.w3.eth.get_block(block_number, full_transactions=True)
        
        # Convert to Spark Row format
        yield Row(
            block_number=block.number,
            block_hash=block.hash.hex(),
            timestamp=block.timestamp,
            transaction_count=len(block.transactions),
            # ... more fields ...
        )
</code></code></pre><p>This is where the real work happens! Each executor in your cluster runs this method for its assigned partition, fetching the actual data.</p><div class="pullquote"><p><strong>&#128170; The Power of Parallelism:</strong> If you have 10 executors and create 100 partitions, all 10 executors work simultaneously. Each one processes its chunk, and as executors finish, Spark automatically assigns them new partitions. This is how Spark achieves massive throughput.</p></div><h3><strong>5</strong> commit() &#8212; Cleanup (Usually Empty)</h3><pre><code><code>def commit(self, end: dict):
    &#8220;&#8221;&#8220;
    Called AFTER all partitions successfully complete.
    The checkpoint/commit/{N} file gets created at this point.
    This method is optional - mainly used for cleanup tasks.
    &#8220;&#8221;&#8220;
    pass  # Usually empty unless you need cleanup
</code></code></pre><p>In most cases, this method is empty. The checkpoint/commit/{N} file gets created automatically. You only need to implement this if you have cleanup tasks to perform after a batch completes.</p><h2>The Complete Flow: Visual Walkthrough</h2><p>Now let&#8217;s see how these methods work together in a complete streaming query:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rHAT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rHAT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png 424w, https://substackcdn.com/image/fetch/$s_!rHAT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png 848w, https://substackcdn.com/image/fetch/$s_!rHAT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png 1272w, https://substackcdn.com/image/fetch/$s_!rHAT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rHAT!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png" width="1200" height="901.3392857142857" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/aabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6b14386f-dc2b-4334-8310-c8aab15f34c7_896x673.png&quot;,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:673,&quot;width&quot;:896,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:128772,&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://www.canadiandataguy.com/i/177539932?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b14386f-dc2b-4334-8310-c8aab15f34c7_896x673.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!rHAT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png 424w, https://substackcdn.com/image/fetch/$s_!rHAT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png 848w, https://substackcdn.com/image/fetch/$s_!rHAT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png 1272w, https://substackcdn.com/image/fetch/$s_!rHAT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faabc703d-d9e3-4c55-b900-a70ed91e0bb1_896x673.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Why This Design Is Brilliant</h2><h4>&#128737;&#65039; Fault Tolerance</h4><p>If an executor fails while reading blocks 1025-1050, Spark simply restarts that task on another machine. Because <code>read()</code> is deterministic, it fetches exactly the same data again. The user never knows a failure occurred.</p><h4>&#9889; Exactly-Once Semantics</h4><p>The [start, end) exclusive range pattern means no block is ever processed twice. Block 1100 is the start of the next batch, not the end of the previous one. Combined with checkpointing, this guarantees exactly-once processing.</p><h4>&#128640; Massive Parallelism</h4><p>By implementing <code>partitions()</code>, you tell Spark how to break work into chunks. Spark handles distributing those chunks to hundreds or thousands of executors. You get massive scale &#8220;for free.&#8221;</p><h4>&#129513; Separation of Concerns</h4><p>You focus on <em>your data source&#8217;s logic</em>. Spark handles scheduling, distribution, checkpointing, fault recovery, and coordination. Clean boundaries make complex systems manageable.</p><h2>What About Edge Cases?</h2><h3>Handling Source Failures</h3><p>What if Ethereum node goes down during <code>read()</code>?</p><pre><code><code>def read(self, partition: BlockRangePartition):
    max_retries = 3
    for block_number in range(partition.start_block, partition.end_block):
        for attempt in range(max_retries):
            try:
                block = self.w3.eth.get_block(block_number, full_transactions=True)
                yield Row(...)
                break  # Success!
            except Exception as e:
                if attempt == max_retries - 1:
                    raise  # Let Spark handle the failure
                time.sleep(2 ** attempt)  # Exponential backoff
</code></code></pre><p>If retries don&#8217;t work, the exception bubbles up, Spark marks the task as failed, and restarts it on another executor. Eventually the source recovers and processing continues from the checkpoint.</p><h3>Dealing with Large Batches</h3><p>What if <code>latestOffset()</code> returns a huge number?</p><blockquote><p><strong>The Golden Rule:</strong> Your processing rate should be greater than your input rate. Ideally, aim for <strong>10x faster processing than data arrival</strong>. This is the key design principle.<br><br>If you&#8217;re processing data faster than it&#8217;s arriving, Spark will naturally catch up with any backfill over the next few batches. You don&#8217;t need to worry about temporarily large batch sizes.<br><br><strong>About spark.sql.shuffle.partitions:</strong> You can adjust this, but don&#8217;t set it to an extremely high number. A reasonable partition count is sufficient as long as your processing rate exceeds your input rate.</p></blockquote><h3>Ensuring Determinism in read()</h3><p>The golden rule: <strong>Same partition input must produce same output</strong>.</p><p>Bad (non-deterministic):</p><pre><code><code># &#10060; DON&#8217;T DO THIS
def read(self, partition):
    current_time = time.time()  # Different each time!
    yield Row(timestamp=current_time, ...)
</code></code></pre><p>Good (deterministic):</p><pre><code><code># &#9989; DO THIS
def read(self, partition):
    block = self.w3.eth.get_block(partition.block_number)
    yield Row(timestamp=block.timestamp, ...)  # Block timestamp is consistent
</code></code></pre><h2>The Complete Picture: Architecture</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!73zt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!73zt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png 424w, https://substackcdn.com/image/fetch/$s_!73zt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png 848w, https://substackcdn.com/image/fetch/$s_!73zt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png 1272w, https://substackcdn.com/image/fetch/$s_!73zt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!73zt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png" width="826" height="541" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:541,&quot;width&quot;:826,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:104672,&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://www.canadiandataguy.com/i/177539932?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.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_!73zt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png 424w, https://substackcdn.com/image/fetch/$s_!73zt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png 848w, https://substackcdn.com/image/fetch/$s_!73zt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png 1272w, https://substackcdn.com/image/fetch/$s_!73zt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F813ddac6-37ae-4fc8-892e-b7935028e236_826x541.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>&#127919; You&#8217;re Ready to Build Your Own!</h2><blockquote><p>You now understand the complete lifecycle of a custom Spark streaming source. It&#8217;s not magic&#8212;it&#8217;s a well-designed conversation between Spark and your code.</p><p>Just implement 5 methods, and Spark handles the rest: fault tolerance, distribution, checkpointing, and exactly-once semantics.</p></blockquote><h2>Quick Reference: The 5 Methods</h2><p><strong>Your Implementation Checklist</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sR-g!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sR-g!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png 424w, https://substackcdn.com/image/fetch/$s_!sR-g!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png 848w, https://substackcdn.com/image/fetch/$s_!sR-g!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png 1272w, https://substackcdn.com/image/fetch/$s_!sR-g!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sR-g!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png" width="869" height="575" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:575,&quot;width&quot;:869,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:77932,&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://www.canadiandataguy.com/i/177539932?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.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_!sR-g!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png 424w, https://substackcdn.com/image/fetch/$s_!sR-g!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png 848w, https://substackcdn.com/image/fetch/$s_!sR-g!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png 1272w, https://substackcdn.com/image/fetch/$s_!sR-g!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffea27c0b-cd3e-41f3-8c2b-e3bcb2c3f690_869x575.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Final Thoughts: Why This Matters</h2><p>The beauty of this architecture is its universality. Whether you&#8217;re streaming from Ethereum, MongoDB, a proprietary API, or carrier pigeons &#128038;, the pattern is the same:</p><ol><li><p><strong>Define where to start</strong> (<code>initialOffset</code>)</p></li><li><p><strong>Check what&#8217;s new</strong> (<code>latestOffset</code>)</p></li><li><p><strong>Break work into chunks</strong> (<code>partitions</code>)</p></li><li><p><strong>Fetch the data</strong> (<code>read</code>)</p></li><li><p><strong>Confirm completion</strong> (<code>commit</code>)</p></li></ol><p>Spark handles everything else&#8212;checkpointing, distribution, scheduling, fault recovery. You just focus on the specifics of your data source.</p><h3>&#128640; Take Action</h3><div class="pullquote"><p>The barrier to entry is lower than you thought. Pick a data source you&#8217;re working with, implement these 5 methods, and you&#8217;ll have a production-ready Spark streaming source in an afternoon.</p><p><strong>Start small:</strong> Get <code>initialOffset()</code> and <code>latestOffset()</code> working first. Then add <code>partitions()</code> and <code>read()</code>. Test with a single partition before scaling up. You&#8217;ve got this! &#128170;</p></div><p><strong>Now go build something amazing with Spark Streaming. The data world is your oyster. &#127754;</strong></p><h2><a href="https://github.com/jiteshsoni/ethereum-streaming-pipeline/blob/6e06cdea573780ba09a33a334f7f07539721b85e/ethereum_block_stream_chainstack.py">Download the code</a></h2>]]></content:encoded></item><item><title><![CDATA[How to write your first Spark application with Stream-Stream Joins with working code]]></title><description><![CDATA[A Practical, Hands-On Guide to Joining Real-Time Data Streams in Spark Structured Streaming]]></description><link>https://www.canadiandataguy.com/p/how-to-write-your-first-spark-application-c23</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/how-to-write-your-first-spark-application-c23</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Wed, 15 Oct 2025 17:39:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!lVsP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you been waiting to try Streaming but cannot take the plunge?</p><p>In a single blog, we will teach you whatever needs to be understood about Streaming Joins. We will give you a working code which you can use for your next Streaming Pipeline.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>The steps involved:</p><ol><li><p>Create a fake dataset at scale</p></li><li><p>Set a baseline using traditional SQL</p></li><li><p>Define Temporary Streaming Views</p></li><li><p>Inner Joins with optional Watermarking</p></li><li><p>Left Joins with Watermarking</p></li><li><p>The cold start edge case: withEventTimeOrder</p></li><li><p>Cleanup</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lVsP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lVsP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!lVsP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!lVsP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!lVsP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lVsP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1505856,&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://www.canadiandataguy.com/i/176255602?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.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_!lVsP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!lVsP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!lVsP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!lVsP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54172092-ba0b-49db-b7ae-033b9bef0640_1024x1024.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"><svg role="img" 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"><g><title></title><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"><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"><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>What is Stream-Stream Join?</strong></h2><p>Stream-stream join is a widely used operation in stream processing where two or more data streams are joined based on some common attributes or keys. It is essential in several use cases, such as real-time analytics, fraud detection, and IoT data processing.</p><h3><strong>Concept of Stream-Stream Join</strong></h3><p>Stream-stream join combines two or more streams based on a common attribute or key. The join operation is performed on an ongoing basis, with each new data item from the stream triggering a join operation. In stream-stream join, each data item in the stream is treated as an event, and it is matched with the corresponding event from the other stream based on matching criteria. This matching criterion could be a common attribute or key in both streams.</p><p>When it comes to joining data streams, there are a few key challenges that must be addressed to ensure successful results. One of the biggest hurdles is the fact that, at any given moment, neither stream has a complete view of the dataset. This can make it difficult to find matches between inputs and generate accurate join results.</p><p>To overcome this challenge, it&#8217;s important to buffer past input as a streaming state for both input streams. This allows for every future input to be matched with past input, which can help to generate more accurate join results. Additionally, this buffering process can help to automatically handle late or out-of-order data, which can be common in streaming environments.</p><p>To further optimize the join process, it&#8217;s also important to use watermarks to limit the state. This can help to ensure that only the most relevant data is being used to generate join results, which can help to improve accuracy and reduce processing times.</p><h3><strong>Types of Stream-Stream Join</strong></h3><p>Depending on the nature of the join and the matching criteria, there are several types of stream-stream join operations. Some of the popular types of stream-stream join are:</p><p><strong>Inner Join</strong><br>In inner join, only those events are returned where there is a match in both the input streams. This type of join is useful when combining the data from two streams with a common key or attribute.</p><p><strong>Outer Join</strong><br>In outer join, all events from both the input streams are included in the joined stream, whether or not there is a match between them. This type of join is useful when we need to combine data from two streams, and there may be missing or incomplete data in either stream.</p><p><strong>Left Join</strong><br>In left join, all events from the left input stream are included in the joined stream, and only the matching events from the right input stream are included. This type of join is useful when we need to combine data from two streams and keep all the data from the left stream, even if there is no matching data in the right stream.</p><h2><strong>1. The Setup: Create a fake dataset at scale</strong></h2><p>Most people do not have 2 streams just hanging around for one to experiment with Stream Steam Joins. Thus I used Faker to mock 2 different streams which we will use for this example.</p><p>The name of the library being used is Faker and faker_vehicle to create Datasets.</p><pre><code>!pip install faker_vehicle
!pip install faker</code></pre><p>Imports</p><pre><code>from faker import Faker
from faker_vehicle import VehicleProvider
from pyspark.sql import functions as F
import uuid
from utils import logger</code></pre><p>Parameters</p><pre><code># define schema name and where should the table be stored
schema_name = &#8220;test_streaming_joins&#8221;
schema_storage_location = &#8220;/tmp/CHOOSE_A_PERMANENT_LOCATION/&#8221;</code></pre><p><strong>Create the Target Schema/Database</strong><br>Create a Schema and set location. This way, all tables would inherit the base location.</p><pre><code>create_schema_sql = f&#8221;&#8221;&#8221;
 CREATE SCHEMA IF NOT EXISTS {schema_name}
 COMMENT &#8216;This is {schema_name} schema&#8217;
 LOCATION &#8216;{schema_storage_location}&#8217;
 WITH DBPROPERTIES ( Owner=&#8217;Jitesh&#8217;);
 &#8220;&#8221;&#8221;
print(f&#8221;create_schema_sql: {create_schema_sql}&#8221;)
spark.sql(create_schema_sql)</code></pre><p>Use Faker to define functions to help generate fake column values</p><pre><code>fake = Faker()
fake.add_provider(VehicleProvider)</code></pre><pre><code>event_id = F.udf(lambda: str(uuid.uuid4()))
vehicle_year_make_model = F.udf(fake.vehicle_year_make_model)
vehicle_year_make_model_cat = F.udf(fake.vehicle_year_make_model_cat)
vehicle_make_model = F.udf(fake.vehicle_make_model)
vehicle_make = F.udf(fake.vehicle_make)
vehicle_model = F.udf(fake.vehicle_model)
vehicle_year = F.udf(fake.vehicle_year)
vehicle_category = F.udf(fake.vehicle_category)
vehicle_object = F.udf(fake.vehicle_object)</code></pre><pre><code>latitude = F.udf(fake.latitude)
longitude = F.udf(fake.longitude)
location_on_land = F.udf(fake.location_on_land)
local_latlng = F.udf(fake.local_latlng)
zipcode = F.udf(fake.zipcode)</code></pre><p>Generate Streaming source data at your desired rate</p><pre><code>def generated_vehicle_and_geo_df (rowsPerSecond:int , numPartitions :int ):
    return (
        spark.readStream.format(&#8221;rate&#8221;)
        .option(&#8221;numPartitions&#8221;, numPartitions)
        .option(&#8221;rowsPerSecond&#8221;, rowsPerSecond)
        .load()
        .withColumn(&#8221;event_id&#8221;, event_id())
        .withColumn(&#8221;vehicle_year_make_model&#8221;, vehicle_year_make_model())
        .withColumn(&#8221;vehicle_year_make_model_cat&#8221;, vehicle_year_make_model_cat())
        .withColumn(&#8221;vehicle_make_model&#8221;, vehicle_make_model())
        .withColumn(&#8221;vehicle_make&#8221;, vehicle_make())
        .withColumn(&#8221;vehicle_year&#8221;, vehicle_year())
        .withColumn(&#8221;vehicle_category&#8221;, vehicle_category())
        .withColumn(&#8221;vehicle_object&#8221;, vehicle_object())
        .withColumn(&#8221;latitude&#8221;, latitude())
        .withColumn(&#8221;longitude&#8221;, longitude())
        .withColumn(&#8221;location_on_land&#8221;, location_on_land())
        .withColumn(&#8221;local_latlng&#8221;, local_latlng())
        .withColumn(&#8221;zipcode&#8221;, zipcode())
        )

# You can uncomment the below display command to check if the code in this cell works
#display(generated_vehicle_and_geo_df)</code></pre><pre><code># You can uncomment the below display command to check if the code in this cell works
#display(generated_vehicle_and_geo_df)</code></pre><p>Now let&#8217;s generate the base source table and let&#8217;s call it Vehicle_Geo</p><pre><code>def stream_write_to_vehicle_geo_table(rowsPerSecond: int = 1000, numPartitions: int = 10):
    table_name_vehicle_geo= &#8220;vehicle_geo&#8221;
    (
        generated_vehicle_and_geo_df(rowsPerSecond, numPartitions)
            .writeStream
            .queryName(f&#8221;write_to_delta_table: {table_name_vehicle_geo}&#8221;)
            .option(&#8221;checkpointLocation&#8221;, f&#8221;{schema_storage_location}/{table_name_vehicle_geo}/_checkpoint&#8221;)
            .format(&#8221;delta&#8221;)
            .toTable(f&#8221;{schema_name}.{table_name_vehicle_geo}&#8221;)
    )
stream_write_to_vehicle_geo_table(rowsPerSecond = 1000, numPartitions = 10)</code></pre><p>Let the above code run for a few iterations, and you can play with rowsPerSecond and numPartitions to control how much data you would like to generate. Once you have generated enough data, kill the above stream and get a base line for row count.</p><pre><code>spark.read.table(f&#8221;{schema_name}.{table_name_vehicle_geo}&#8221;).count()</code></pre><pre><code>display(
    spark.sql(f&#8221;&#8220;&#8221;
    SELECT * 
    FROM {schema_name}.{table_name_vehicle_geo}
&#8220;&#8221;&#8220;)
)</code></pre><p>Let&#8217;s also get a min &amp; max of the timestamp column as we would be leveraging it for watermarking.</p><pre><code>display(
    spark.sql(f&#8221;&#8220;&#8221;
    SELECT 
         min(timestamp)
        ,max(timestamp)
        ,current_timestamp()
    FROM {schema_name}.{table_name_vehicle_geo}
&#8220;&#8221;&#8220;)
)</code></pre><h3><strong>Next, we will break this Delta table into 2 different tables</strong></h3><p>Because for Stream-Stream Joins we need 2 different streams. We will use Delta To Delta Streaming here to create these tables.</p><ol><li><p><strong>a ) Table: Vehicle</strong></p></li></ol><pre><code>vehicle_df = (
        spark.readStream.format(&#8221;delta&#8221;).option(&#8221;maxFilesPerTrigger&#8221;,&#8221;100&#8221;).table(f&#8221;{schema_name}.vehicle_geo&#8221;)
        .selectExpr(
            &#8220;event_id&#8221;
            ,&#8221;timestamp as vehicle_timestamp&#8221;
            ,&#8221;vehicle_year_make_model&#8221;
            ,&#8221;vehicle_year_make_model_cat&#8221;
            ,&#8221;vehicle_make_model&#8221;
            ,&#8221;vehicle_make&#8221;
            ,&#8221;vehicle_year&#8221;
            ,&#8221;vehicle_category&#8221;
            ,&#8221;vehicle_object&#8221;
            )
    )
#display(vehicle_df)
def stream_write_to_vehicle_table():
    table_name_vehicle = &#8220;vehicle&#8221;
    (   vehicle_df
        .writeStream
        #.trigger(availableNow=True)
        .queryName(f&#8221;write_to_delta_table: {table_name_vehicle}&#8221;)
        .option(&#8221;checkpointLocation&#8221;, f&#8221;{schema_storage_location}/{table_name_vehicle}/_checkpoint&#8221;)
        .format(&#8221;delta&#8221;)
        .toTable(f&#8221;{schema_name}.{table_name_vehicle}&#8221;)
    )

stream_write_to_vehicle_table()   </code></pre><ol><li><p><strong>b) Table: Geo</strong></p></li></ol><p>We have added a filter when we write to this table. This would be useful when we emulate the left join scenario. Filter: <code>where(&#8221;value like &#8216;1%&#8217; &#8220;)</code></p><pre><code>geo_df = (
    spark.readStream.format(&#8221;delta&#8221;).option(&#8221;maxFilesPerTrigger&#8221;,&#8221;100&#8221;).table(f&#8221;{schema_name}.vehicle_geo&#8221;)
        .selectExpr(
            &#8220;event_id&#8221;
            ,&#8221;value&#8221;
            ,&#8221;timestamp as geo_timestamp&#8221;
            ,&#8221;latitude&#8221;
            ,&#8221;longitude&#8221;
            ,&#8221;location_on_land&#8221;
            ,&#8221;local_latlng&#8221;
            ,&#8221;cast( zipcode as integer) as zipcode&#8221;
        ).where(&#8221;value like &#8216;1%&#8217; &#8220;) 
    )
#geo_df.printSchema()
#display(geo_df)

def stream_write_to_geo_table():
    table_name_geo = &#8220;geo&#8221;
    (   geo_df
        .writeStream
        #.trigger(availableNow=True)
        .queryName(f&#8221;write_to_delta_table: {table_name_geo}&#8221;)
        .option(&#8221;checkpointLocation&#8221;, f&#8221;{schema_storage_location}/{table_name_geo}/_checkpoint&#8221;)
        .format(&#8221;delta&#8221;)
        .toTable(f&#8221;{schema_name}.{table_name_geo}&#8221;)
    )
    
stream_write_to_geo_table()    </code></pre><h2><strong>2. Set a baseline using traditional SQL</strong></h2><p>Before we do the actual streaming joins. Let&#8217;s do a regular join and figure out the expected row count.</p><p><strong>Get row count from Inner Join</strong></p><pre><code>sql_query_batch_inner_join = f&#8217;&#8216;&#8217;
        SELECT count(vehicle.event_id) as row_count_for_inner_join
        FROM {schema_name}.{table_name_vehicle} vehicle
        JOIN {schema_name}.{table_name_geo} geo
        ON vehicle.event_id = geo.event_id
    AND vehicle_timestamp &gt;= geo_timestamp  - INTERVAL 5 MINUTES        
        &#8216;&#8217;&#8216;
print(f&#8217;&#8216;&#8217; Run SQL Query: 
          {sql_query_batch_inner_join}       
       &#8216;&#8217;&#8216;)
display( spark.sql(sql_query_batch_inner_join) )</code></pre><p><strong>Get row count from Inner Join</strong></p><pre><code>sql_query_batch_left_join = f&#8217;&#8216;&#8217;
        SELECT count(vehicle.event_id) as row_count_for_left_join
        FROM {schema_name}.{table_name_vehicle} vehicle
        LEFT JOIN {schema_name}.{table_name_geo} geo
        ON vehicle.event_id = geo.event_id
            -- Assume there is a business logic that timestamp cannot be more than 15 minutes off
    AND vehicle_timestamp &gt;= geo_timestamp  - INTERVAL 5 MINUTES
        &#8216;&#8217;&#8216;
print(f&#8217;&#8216;&#8217; Run SQL Query: 
          {sql_query_batch_left_join}       
       &#8216;&#8217;&#8216;)
display( spark.sql(sql_query_batch_left_join) )</code></pre><h2><strong>Summary so far:</strong></h2><ol><li><p>We created a Source Delta Table: vehicle_geo</p></li><li><p>We took the previous table and divided its column into two tables: Vehicle and Geo</p></li><li><p>Vehicle row count matches with vehicle_geo, and it has a subset of those columns</p></li><li><p>The Geo row count is lesser than Vehicle because we added a filter when we wrote to the Geo table</p></li><li><p>We ran 2 SQL to identify what the row count should be after we do stream-stream join</p></li></ol><h2><strong>3. Define Temporary Streaming Views</strong></h2><p>Some people prefer to write the logic in SQL. Thus, we are creating streaming views which could be manipulated with SQL. The below code block will help create a view and set a watermark on the stream.</p><pre><code>def stream_from_delta_and_create_view (schema_name: str, table_name:str, column_to_watermark_on:str, how_late_can_the_data_be: str = &#8220;2 minutes&#8221; , maxFilesPerTrigger: int = 100):
    view_name = f&#8221;_streaming_vw_{schema_name}_{table_name}&#8221;
    print(f&#8221;Table {schema_name}.{table_name} is now streaming under a temporoary view called {view_name}&#8221;)
    (
        spark.readStream.format(&#8221;delta&#8221;)
        .option(&#8221;maxFilesPerTrigger&#8221;, f&#8221;{maxFilesPerTrigger}&#8221;)
        .option(&#8221;withEventTimeOrder&#8221;, &#8220;true&#8221;)
        .table(f&#8221;{schema_name}.{table_name}&#8221;)
        .withWatermark(f&#8221;{column_to_watermark_on}&#8221;,how_late_can_the_data_be)
        .createOrReplaceTempView(view_name)
    )
</code></pre><p><strong>3. a Create Vehicle Stream</strong></p><h2>Get CanadianDataGuy.com&#8217;s stories in your inbox</h2><p>Join Medium for free to get updates from this writer.</p><p>Subscribe</p><p>Let&#8217;s create a Vehicle Stream and set its watermark as 1mins</p><pre><code>stream_from_delta_and_create_view(schema_name =schema_name, table_name = &#8216;vehicle&#8217;, column_to_watermark_on =&#8221;vehicle_timestamp&#8221;, how_late_can_the_data_be = &#8220;1 minutes&#8221; )</code></pre><p>Let&#8217;s visualize the stream.</p><pre><code>display(
    spark.sql(f&#8217;&#8216;&#8217;
        SELECT *
        FROM _streaming_vw_test_streaming_joins_vehicle
    &#8216;&#8217;&#8216;)
)</code></pre><p>You can also do an aggregation on the stream. It&#8217;s out of the scope of this blog, but I wanted to show you how you can do it</p><pre><code>display(
    spark.sql(f&#8217;&#8216;&#8217;
        SELECT 
            vehicle_make
            ,count(1) as row_count
        FROM _streaming_vw_test_streaming_joins_vehicle
        GROUP BY vehicle_make
        ORDER BY vehicle_make
    &#8216;&#8217;&#8216;)
)</code></pre><p><strong>3. b Create Geo Stream</strong></p><p>Let&#8217;s create a Geo Stream and set its watermark as 2 mins</p><pre><code>stream_from_delta_and_create_view(schema_name =schema_name, table_name = &#8216;geo&#8217;, column_to_watermark_on =&#8221;geo_timestamp&#8221;, how_late_can_the_data_be = &#8220;2 minutes&#8221; )</code></pre><p>Have a look at what the data looks like</p><pre><code>display(
    spark.sql(f&#8217;&#8216;&#8217;
        SELECT *
        FROM _streaming_vw_test_streaming_joins_geo
    &#8216;&#8217;&#8216;)
)</code></pre><h2><strong>4. Inner Joins with optional Watermarking</strong></h2><p>While inner joins on any kind of columns and with any kind of conditions are possible in streaming environments, it&#8217;s important to be aware of the potential for unbounded state growth. As new input arrives, it can potentially match with any input from the past, leading to a rapidly increasing streaming state size.</p><p>To avoid this issue, it&#8217;s essential to define additional join conditions that prevent indefinitely old inputs from matching with future inputs. By doing so, it&#8217;s possible to clear old inputs from the state, which can help to prevent unbounded state growth and ensure more efficient processing.</p><p>There are a variety of techniques that can be used to define these additional join conditions. For example, you might limit the scope of the join by only matching on a subset of columns, or you might set a time-based constraint that prevents old inputs from being considered after a certain period of time has elapsed.</p><p>Ultimately, the key to managing streaming state size and ensuring efficient join processing is to consider the unique requirements of your specific use case carefully and to leverage the right techniques and tools to optimize your join conditions accordingly. <strong>Although watermarking could be optional, I would highly recommend you set a watermark on both streams.</strong></p><pre><code>sql_for_stream_stream_inner_join = f&#8221;&#8220;&#8221;
    SELECT 
        vehicle.*
        ,geo.latitude
        ,geo.longitude
        ,geo.zipcode
    FROM _streaming_vw_test_streaming_joins_vehicle vehicle
    JOIN _streaming_vw_test_streaming_joins_geo geo
    ON vehicle.event_id = geo.event_id
    -- Assume there is a business logic that timestamp cannot be more than X minutes off
    AND vehicle_timestamp BETWEEN geo_timestamp  - INTERVAL 5 MINUTES AND geo_timestamp
&#8220;&#8221;&#8220;
#display(spark.sql(sql_for_stream_stream_inner_join))</code></pre><pre><code>table_name_stream_stream_innner_join =&#8217;stream_stream_innner_join&#8217;

(   spark.sql(sql_for_inner_join)
    .writeStream
    #.trigger(availableNow=True)
        .queryName(f&#8221;write_to_delta_table: {table_name_stream_stream_innner_join}&#8221;)
        .option(&#8221;checkpointLocation&#8221;, f&#8221;{schema_storage_location}/{table_name_stream_stream_innner_join}/_checkpoint&#8221;)
        .format(&#8221;delta&#8221;)
        .toTable(f&#8221;{schema_name}.{table_name_stream_stream_innner_join}&#8221;)
)</code></pre><p>If the stream has finished then in the next step. You should find that the row count should match up with the regular batch SQL Job</p><pre><code>spark.read.table(f&#8221;{schema_name}.{table_name_stream_stream_innner_join}&#8221;).count()</code></pre><h3><strong>How was the watermark computed in this scenario?</strong></h3><p>When we defined streaming views for Vehicle and Geo, we set them as 1 min and 2 min, respectively.</p><p>If you look at the join condition we mentioned :</p><pre><code>AND vehicle_timestamp &gt;= geo_timestamp - INTERVAL 5 minutes</code></pre><p>5 min + 2 min = 7 min.</p><p>Spark Streaming would automatically calculate this 7 min number and the state would be cleared after that.</p><h2><strong>5. Left Joins with Watermarking</strong></h2><p>While the watermark + event-time constraints is optional for inner joins, for outer joins they must be specified. This is because for generating the NULL results in outer join, the engine must know when an input row is not going to match with anything in future. Hence, the watermark + event-time constraints must be specified for generating correct results.</p><h3><strong>5.a How Left Joins works differently than an Inner Join</strong></h3><p>One important factor is that the outer NULL results will be generated with a delay that depends on the specified watermark delay and the time range condition. This delay is necessary to ensure that there were no matches, and that there will be no matches in the future.</p><p>In the current implementation of the micro-batch engine, watermarks are advanced at the end of each micro-batch, and the next micro-batch uses the updated watermark to clean up the state and output outer results. However, this means that the generation of outer results may be delayed if there is no new data being received in the stream. If either of the two input streams being joined does not receive data for a while, the outer output (in both left and right cases) may be delayed.</p><pre><code>sql_for_stream_stream_left_join = f&#8221;&#8220;&#8221;
    SELECT 
        vehicle.*
        ,geo.latitude
        ,geo.longitude
        ,geo.zipcode
    FROM _streaming_vw_test_streaming_joins_vehicle vehicle
    LEFT JOIN _streaming_vw_test_streaming_joins_geo geo
    ON vehicle.event_id = geo.event_id
        AND vehicle_timestamp BETWEEN geo_timestamp  - INTERVAL 5 MINUTES AND geo_timestamp
&#8220;&#8221;&#8220;
#display(spark.sql(sql_for_stream_stream_left_join))

table_name_stream_stream_left_join =&#8217;stream_stream_left_join&#8217;

(   spark.sql(sql_for_stream_stream_left_join)
    .writeStream
    #.trigger(availableNow=True)
        .queryName(f&#8221;write_to_delta_table: {table_name_stream_stream_left_join}&#8221;)
        .option(&#8221;checkpointLocation&#8221;, f&#8221;{schema_storage_location}/{table_name_stream_stream_left_join}/_checkpoint&#8221;)
        .format(&#8221;delta&#8221;)
        .toTable(f&#8221;{schema_name}.{table_name_stream_stream_left_join}&#8221;)
)</code></pre><p>If the stream has finished, then in the next step. You should find that the row count should match up with the regular batch SQL Job.</p><pre><code>spark.read.table(f&#8221;{schema_name}.{table_name_stream_stream_left_join}&#8221;).count()</code></pre><blockquote><p><em><strong>You will find that some records that could not match are not being released, which is expected. </strong>The outer NULL results will be generated with a delay that depends on the specified watermark delay and the time range condition. This is because the engine has to wait for that long to ensure there were no matches and there will be no more matches in future.</em></p><p><em><strong>**Watermark will advance once new data is pushed to it**</strong></em></p></blockquote><p>Thus let&#8217;s generate some more fate data to the base table: <strong>vehicle_geo. </strong>This time we are sending a much lower volume of 10 records per second. Let the below command run for at least one batch and then kill it.</p><pre><code>stream_write_to_vehicle_geo_table(rowsPerSecond = 10, numPartitions = 10)</code></pre><h3><strong>5. b What to observe:</strong></h3><ol><li><p>Soon you should see the watermark moves ahead and the number of records in &#8216;Aggregation State&#8217; goes down.</p></li><li><p>If you click on the running stream and click the raw data tab and look for &#8220;watermark&#8221;. You will see it has advanced</p></li><li><p>Once 0 records per second are being processed, that means your stream has caught up, and now your row count should match up with the traditional SQL left join</p></li></ol><pre><code>spark.read.table(f&#8221;{schema_name}.{table_name_stream_stream_left_join}&#8221;).count()</code></pre><h2><strong>6. The cold start edge case: withEventTimeOrder</strong></h2><blockquote><p><em>&#8220;When using a Delta table as a stream source, the query first processes all of the data present in the table. The Delta table at this version is called the initial snapshot. By default, the Delta table&#8217;s data files are processed based on which file was last modified. However, the last modification time does not necessarily represent the record event time order.</em></p><p><em>In a stateful streaming query with a defined watermark, processing files by modification time can result in records being processed in the wrong order. This could lead to records dropping as late events by the watermark.</em></p><p><em>You can avoid the data drop issue by enabling the following option:</em></p><p><em>withEventTimeOrder: Whether the initial snapshot should be processed with event time order.</em></p></blockquote><p><strong>If you use startingVersion then withEventTimeOrder attribute is ignored.</strong></p><p>In our scenario, I pushed this inside Step 3 when we created the temporary streaming views.</p><pre><code>spark.readStream.format(&#8221;delta&#8221;)
        .option(&#8221;maxFilesPerTrigger&#8221;, f&#8221;{maxFilesPerTrigger}&#8221;)
        .option(&#8221;withEventTimeOrder&#8221;, &#8220;true&#8221;)
        .table(f&#8221;{schema_name}.{table_name}&#8221;)</code></pre><h2><strong>7. Cleanup</strong></h2><p>Drop all tables in the database and delete all the checkpoints</p><pre><code>spark.sql(
    f&#8221;&#8220;&#8221;
    drop schema if exists {schema_name} CASCADE
&#8220;&#8221;&#8220;
)


dbutils.fs.rm(schema_storage_location, True)</code></pre><p>If you have reached so far, you now have a working pipeline and a solid example which you can use going forward.</p><h2><strong>Download the code</strong></h2><p><a href="https://github.com/jiteshsoni/material_for_public_consumption/blob/main/notebooks/spark_stream_stream_join.py">https://github.com/jiteshsoni/material_for_public_consumption/blob/main/notebooks/spark_stream_stream_join.py</a></p><h3><strong>References:</strong></h3><ol><li><p><a href="https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#stream-stream-joins">https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#stream-stream-joins</a></p></li><li></li></ol><div id="youtube2-hyZU_bw1-ow" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;hyZU_bw1-ow&quot;,&quot;startTime&quot;:&quot;1181&quot;,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/hyZU_bw1-ow?start=1181&amp;rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><ol><li></li></ol><div id="youtube2-1cBDGsSbwRA" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;1cBDGsSbwRA&quot;,&quot;startTime&quot;:&quot;1500s&quot;,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/1cBDGsSbwRA?start=1500s&amp;rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><ol><li><p><a href="https://www.databricks.com/blog/2022/08/22/feature-deep-dive-watermarking-apache-spark-structured-streaming.html">https://www.databricks.com/blog/2022/08/22/feature-deep-dive-watermarking-apache-spark-structured-streaming.html</a></p></li><li><p><a href="https://docs.databricks.com/structured-streaming/delta-lake.html#process-initial-snapshot-without-data-being-dropped">https://docs.databricks.com/structured-streaming/delta-lake.html#process-initial-snapshot-without-data-being-dropped</a></p></li></ol><h2><strong>Footnote:</strong></h2><p>Thank you for taking the time to read this article. If you found it helpful or enjoyable, please consider clapping to show appreciation and help others discover it. Don&#8217;t forget to follow me for more insightful content, and visit my website <strong><a href="https://canadiandataguy.com/">CanadianDataGuy.com</a></strong> for additional resources and information. Your support and feedback are essential to me, and I appreciate your engagement with my work.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Build an Ethereum ETL Pipeline for Free Using Databricks Free Edition]]></title><description><![CDATA[Build a zero-infrastructure streaming pipeline: Step-by-step Ethereum data ingestion, schema evolution, and Delta storage]]></description><link>https://www.canadiandataguy.com/p/build-an-ethereum-etl-pipeline-for</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/build-an-ethereum-etl-pipeline-for</guid><dc:creator><![CDATA[Yogita Nesargi]]></dc:creator><pubDate>Tue, 23 Sep 2025 04:29:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jJwt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The Ethereum blockchain generates one of the richest transactional datasets in the world. Yet analyzing this data directly from a node proves impractical &#8212; blocks and transactions arrive as deeply nested structures, making efficient querying nearly impossible without transformation.</p><p>Best of all? You can build this entire pipeline without spending a dollar. Databricks Free Edition is a no-cost version of Databricks designed for students, educators, hobbyists, and anyone interested in learning or experimenting with data and AI <a href="https://docs.databricks.com/aws/en/getting-started/free-edition">Databricks</a>. Simply <a href="https://www.databricks.com/learn/free-edition">sign up for Databricks Free Edition</a> &#8212; no credit card required &#8212; and you'll get a workspace with serverless compute ready to go.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>In this technical walkthrough, we'll build a streaming ETL pipeline in Databricks that:</p><ul><li><p>Ingests raw Ethereum blocks from AWS's public dataset</p></li><li><p>Extracts and flattens transaction data</p></li><li><p>Stores everything in queryable Delta Lake tables for analytics</p></li></ul><p>=</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jJwt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jJwt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif 424w, https://substackcdn.com/image/fetch/$s_!jJwt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif 848w, https://substackcdn.com/image/fetch/$s_!jJwt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif 1272w, https://substackcdn.com/image/fetch/$s_!jJwt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jJwt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif" width="606" height="444" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:444,&quot;width&quot;:606,&quot;resizeWidth&quot;:606,&quot;bytes&quot;:1748110,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.canadiandataguy.com/i/174119673?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!jJwt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif 424w, https://substackcdn.com/image/fetch/$s_!jJwt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif 848w, https://substackcdn.com/image/fetch/$s_!jJwt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.gif 1272w, https://substackcdn.com/image/fetch/$s_!jJwt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f6bc0f9-0955-4774-88a2-cd3cfd421008_606x444.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"><svg role="img" 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"><g><title></title><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"><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"><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>Why Databricks + AutoLoader for Blockchain Data?</h2><p>Databricks Autoloader provides a Structured Streaming source called <code>cloudFiles</code> that automatically processes new files as they arrive, with the option of also processing existing files <a href="https://learn.microsoft.com/en-us/azure/databricks/ingestion/cloud-object-storage/auto-loader/">Microsoft Learn</a><a href="https://docs.databricks.com/aws/en/ingestion/cloud-object-storage/auto-loader/">Databricks</a>. This makes it ideal for blockchain data because:</p><ul><li><p><strong>Scalable storage</strong>: Blockchain data grows continuously &#8212; Delta Lake handles petabyte-scale datasets effortlessly</p></li><li><p><strong>Schema enforcement</strong>: Flatten complex nested Ethereum data into clean, queryable tables with automatic schema evolution</p></li><li><p><strong>Streaming ingestion</strong>: Process blocks and transactions in near real-time as they arrive</p></li><li><p><strong>SQL + ML integration</strong>: Run ad-hoc queries or feed data directly into ML models for fraud detection, token analytics, or NFT tracking</p></li></ul><h2>Step 1: Load Historical Ethereum Data from AWS S3</h2><p>AWS provides free access to blockchain datasets through the aws-public-blockchain S3 bucket, with data optimized for analytics by being transformed into compressed Parquet files, partitioned by date for efficient querying <a href="https://aws.amazon.com/blogs/web3/access-bitcoin-and-ethereum-open-datasets-for-cross-chain-analytics/">AWS</a><a href="https://registry.opendata.aws/aws-public-blockchain/">AWS Open Data Registry</a>.</p><p>Instead of setting up an Ethereum node, we'll directly pull historical block data that's already preprocessed into Parquet format &#8212; completely free.</p><p>Here's what we'll accomplish:</p><ol><li><p>Connect anonymously to AWS S3</p></li><li><p>List available Ethereum block files (stored as Parquet)</p></li><li><p>Download selected files into a Unity Catalog Volume in Databricks</p></li><li><p>Verify successful data landing</p></li></ol><p>This foundational step prepares our workspace for efficient block processing using Spark and Delta Lake.</p><pre><code># Databricks notebook source
# === SIMPLE PARAMETERIZATION (ONLY NECESSARY VARIABLES) ===
# Parameterize only what needs to be variable for reusability
dbutils.widgets.text("catalog_name", "blockchain", "Catalog Name")
dbutils.widgets.text("schema_name", "ethereum", "Schema Name")
dbutils.widgets.text("num_files", "20", "Number of Files to Download")

# === CONFIGURATION ===
# Get widget values
CATALOG = dbutils.widgets.get("catalog_name")
SCHEMA = dbutils.widgets.get("schema_name")
NUM_FILES = int(dbutils.widgets.get("num_files"))

# Hard-coded values (no need to parameterize constants)
AWS_BUCKET = "aws-public-blockchain"
S3_PREFIX = "v1.0/eth/blocks/"

# Unity Catalog volume paths for data organization
DATA_VOLUME = f"/Volumes/{CATALOG}/{SCHEMA}/ethereum"
CHECKPOINT_VOLUME = f"/Volumes/{CATALOG}/{SCHEMA}/ethereum_checkpoints"
SCHEMA_VOLUME = f"/Volumes/{CATALOG}/{SCHEMA}/ethereum_schemas"

print(f"&#128295; Using Catalog: {CATALOG}, Schema: {SCHEMA}")
print(f"&#128230; Downloading {NUM_FILES} files from s3://{AWS_BUCKET}/{S3_PREFIX}")

# === UNITY CATALOG SETUP ===
stmts = [
    f"CREATE CATALOG IF NOT EXISTS {CATALOG}",
    f"CREATE SCHEMA IF NOT EXISTS {CATALOG}.{SCHEMA}",
    f"CREATE VOLUME IF NOT EXISTS {CATALOG}.{SCHEMA}.ethereum",
    f"CREATE VOLUME IF NOT EXISTS {CATALOG}.{SCHEMA}.ethereum_checkpoints",
    f"CREATE VOLUME IF NOT EXISTS {CATALOG}.{SCHEMA}.ethereum_schemas",
]

for i, s in enumerate(stmts, 1):
    print(f"[{i}/{len(stmts)}] {s}")
    try:
        spark.sql(s)
        print("  &#9989; Success")
    except Exception as e:
        print(f"  &#10060; Error: {e}")

print(f"\nCreated/verified UC objects. Paths available:")
print(f"  Data: {DATA_VOLUME}")
print(f"  Checkpoints: {CHECKPOINT_VOLUME}")
print(f"  Schemas: {SCHEMA_VOLUME}")

# === DATA DOWNLOAD ===
import os
import boto3
from botocore import UNSIGNED
from botocore.client import Config

print(f"\n&#128229; Downloading to: {DATA_VOLUME}")
os.makedirs(DATA_VOLUME, exist_ok=True)

# Configure anonymous S3 client (no AWS credentials needed!)
s3 = boto3.client("s3", config=Config(signature_version=UNSIGNED))

# Collect parquet files from S3
keys = []
token = None

while len(keys) &lt; NUM_FILES:
    params = {
        "Bucket": AWS_BUCKET, 
        "Prefix": S3_PREFIX, 
        "MaxKeys": min(1000, NUM_FILES - len(keys))
    }
    if token:
        params["ContinuationToken"] = token
    
    resp = s3.list_objects_v2(**params)
    
    for obj in resp.get("Contents", []) or []:
        if obj["Key"].endswith(".parquet"):
            keys.append(obj["Key"])
            if len(keys) &gt;= NUM_FILES:
                break
    
    if not resp.get("IsTruncated"):
        break
    token = resp.get("NextContinuationToken")

if not keys:
    raise RuntimeError(f"No parquet files found under s3://{AWS_BUCKET}/{S3_PREFIX}")

# Download files with progress tracking
for i, key in enumerate(keys, 1):
    rel_path = key.replace("v1.0/eth/", "")
    dest_path = os.path.join(DATA_VOLUME, rel_path)
    os.makedirs(os.path.dirname(dest_path), exist_ok=True)
    
    print(f"[{i}/{len(keys)}] {os.path.basename(key)} ...", end=" ", flush=True)
    s3.download_file(AWS_BUCKET, key, dest_path)
    print("&#10003;")

print("&#9989; Download complete!")</code></pre><div><hr></div><h2>Step 2: Stream Raw Blocks with Autoloader</h2><p>With Ethereum blockchain data now in our Unity Catalog volume, we can continuously process new blocks as they arrive. In production, you'd use web3.py to poll the Ethereum network and save new blocks as Parquet files. For now, we'll stream the historical Parquet files we downloaded.</p><p>Autoloader's cloudFiles source automatically processes new files as they arrive <a href="https://learn.microsoft.com/en-us/azure/databricks/ingestion/cloud-object-storage/auto-loader/">Microsoft</a>, making it perfect for blockchain data ingestion.</p><h3>How Autoloader Works</h3><p>Configuration options specific to the cloudFiles source are prefixed with cloudFiles so that they are in a separate namespace from other Structured Streaming source options <a href="https://docs.databricks.com/aws/en/ingestion/cloud-object-storage/auto-loader/options">Databricks</a>. Key features include:</p><ul><li><p><strong>Automatic file discovery</strong>: Watches folders continuously and picks up new files</p></li><li><p><strong>Schema evolution</strong>: Auto Loader can detect schema drifts, notify you when schema changes happen, and rescue data that would have been otherwise ignored or lost <a href="https://learn.microsoft.com/en-us/azure/databricks/ingestion/cloud-object-storage/auto-loader/">What is Auto Loader? - Azure Databricks | Microsoft Learn</a></p></li><li><p><strong>Exactly-once processing</strong>: Maintains state in checkpoint location to ensure no data loss or duplication</p></li><li><p>Schema Hints: Provides control if you want to specially handle a few column without handling each and every column which is tedious</p></li></ul><pre><code># === STREAMING READER ===
reader = (
    spark.readStream.format("cloudFiles")
        .option("cloudFiles.format", "parquet")  # Specify Parquet file format
        .option("cloudFiles.schemaLocation", SCHEMA_VOLUME)  # Schema tracking
        .option("cloudFiles.schemaEvolutionMode", "addNewColumns")  # Handle new fields
        .option("cloudFiles.schemaHints", "number BIGINT, baseFeePerGas BIGINT")  # Type hints
        .load(f"dbfs:{DATA_VOLUME}/blocks/")
)

# Display streaming data for monitoring
display(reader)</code></pre><h2>Step 3: Create Delta Tables for Blockchain Data</h2><p>Next, we extract or add fields from our streaming data and write them into a Delta table. This provides a foundation for efficient queries and downstream transformations.</p><pre><code># Extract and transform block-level fields if needed
blocks_df = reader.select(
   "*",
   "_metadata"
)

# Write to Delta using Structured Streaming
blocks_query = (
    blocks_df.writeStream
        .format("delta")  # Delta Lake sink for ACID transactions
        .outputMode("append")  # Append new blocks as they arrive
        .option("checkpointLocation", f"{CHECKPOINT_VOLUME}/blocks/")  # State management
        .trigger(availableNow=True)  # Process all available data
        .table(f"{CATALOG}.{SCHEMA}.blocks")  # Save as managed Delta table
)

</code></pre><h2>Trigger Types: Why <code>trigger(availableNow)</code> Matters</h2><p>The available now trigger option consumes all available records as an incremental batch with the ability to configure batch size with options such as maxBytesPerTrigger <a href="https://learn.microsoft.com/en-us/azure/databricks/structured-streaming/triggers">Microsoft Learn</a><a href="https://docs.databricks.com/aws/en/structured-streaming/triggers">Databricks</a>. Understanding trigger options is crucial for optimizing your blockchain pipeline:</p><h4>Available Trigger Types</h4><p>Databricks Structured Streaming provides multiple triggers to control micro-batch execution:</p><h4><code>trigger(once=True)</code> (Deprecated)</h4><ul><li><p>Runs the query a single time, processing all currently available data, then stops</p></li><li><p>Ideal for one-time backfills or finite datasets</p></li><li><p>In Databricks Runtime 11.3 LTS and above, the Trigger.Once setting is deprecated. Databricks recommends you use Trigger.AvailableNow for all incremental batch processing workloads <a href="https://learn.microsoft.com/en-us/azure/databricks/structured-streaming/triggers">Microsoft Learn</a><a href="https://docs.databricks.com/aws/en/structured-streaming/triggers">Databricks</a></p></li></ul><h4><code>trigger(availableNow=True)</code> (Recommended for Blockchain Pipelines)</h4><ul><li><p>Processes all currently available data immediately</p></li><li><p>Continues processing new data as it arrives</p></li><li><p>With Trigger.AvailableNow, file discovery happens asynchronously with data processing and data can be processed across multiple micro-batches with rate limiting <a href="https://learn.microsoft.com/en-us/azure/databricks/ingestion/cloud-object-storage/auto-loader/production">Configure Auto Loader for production workloads - Azure Databricks | Microsoft Learn</a></p></li><li><p>Ensures historical and new blocks are captured seamlessly</p></li><li><p>Handles schema evolution safely without dropping fields</p></li><li><p>Prevents missing transactions that appear during ingestion</p></li></ul><h4><code>trigger(processingTime='10 seconds')</code></h4><ul><li><p>Processes data at fixed time intervals</p></li><li><p>Useful for controlling costs and reducing API calls</p></li><li><p>Best for scenarios without strict latency requirements</p></li></ul><h3>Why We Use <code>trigger(availableNow)</code></h3><p>For our Ethereum pipeline, <code>availableNow</code> provides the perfect balance:</p><ol><li><p><strong>Historical data ingestion</strong>: Process all existing blocks in one pass</p></li><li><p><strong>Schema resilience</strong>: Handles Ethereum protocol upgrades that add new fields</p></li><li><p><strong>Resource optimization</strong>: Auto Loader by default processes a maximum of 1000 files every micro-batch. You can configure cloudFiles.maxFilesPerTrigger and cloudFiles.maxBytesPerTrigger to configure how many files or how many bytes should be processed in a micro-batch</p></li></ol><p><strong>Configuration Example</strong></p><p>By combining <code>availableNow</code> with Delta tables and checkpoints, we achieve a robust, scalable streaming solution for blockchain data &#8212; while keeping the code reusable for any streaming data source.</p><pre><code># Fine-tune batch processing for optimal performance
optimized_query = (
    blocks_df.writeStream
        .format("delta")
        .outputMode("append")
        .option("checkpointLocation", f"{CHECKPOINT_VOLUME}/optimized_blocks/")
        .option("maxFilesPerTrigger", 100)  # Process 100 files per batch
        .option("maxBytesPerTrigger", "1GB")  # Soft limit on data per batch
        .trigger(availableNow=True)
        .table(f"{CATALOG}.{SCHEMA}.blocks")
)</code></pre><h3>Conclusion</h3><p>In this blog, we explored <strong>how to ingest Ethereum blockchain data into Databricks</strong> and store it in <strong>Delta Lake</strong>, creating a solid foundation for analysis:</p><ul><li><p>Raw Ethereum Parquet files are ingested into a <strong>Databricks Volume</strong>.</p></li><li><p><strong>Streaming ingestion</strong> is handled with Databricks Autoloader for reliable file detection.</p></li><li><p><strong>Schema evolution</strong>: Configured automatic handling of new fields as Ethereum evolves</p></li><li><p>Data is <strong>queryable with Databricks SQL</strong>, enabling analytics on both historical and new blockchain data.</p></li></ul><p>This setup provides a foundation that can be extended with <strong>Medallion Architecture (Bronze &#8594; Silver &#8594; Gold)</strong> and enrichments such as:</p><ul><li><p>Daily ETH transferred per day.</p></li><li><p>Active wallets and transaction counts per day.</p></li><li><p>Gas usage trends.</p></li><li><p>Token transfer analytics.</p></li></ul><p>&#128640; Blockchain data is massive and fast-moving &#8212; but with <strong>Databricks + Delta Lake</strong>, you now have a <strong>scalable and robust way to tame it</strong>.</p><div><hr></div><h3>Future Work</h3><p><strong>1. Add a Custom Streaming Reader</strong><br>Instead of first dumping Parquet to storage, implement a <strong>direct Spark Structured Streaming source</strong> that connects to Ethereum nodes (via WebSocket / JSON-RPC).</p><ul><li><p>This allows new blocks and transactions to flow <strong>directly into Spark DataFrames</strong>, reducing latency and storage overhead.</p></li><li><p>For example, a Python wrapper around <code>web3.py</code> could push blocks straight into Spark&#8217;s <code>DataStreamReader</code>.</p></li></ul><p><strong>2. Enrich On-Chain Data with Off-Chain Sources</strong></p><ul><li><p>Token metadata, DeFi protocol information, and NFT collections from APIs.</p></li><li><p>Join off-chain data with raw transactions for <strong>deeper analytics</strong>, like wallet behavior, token performance, and DeFi usage patterns.</p><p></p><p></p><p>In the <strong>next blog</strong>, we&#8217;ll dive into <strong>keeping this data up to date continuously using Spark Structured Streaming</strong>, so your Delta tables always reflect the <strong>latest blocks and transactions in real time</strong>.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[How to ace and structure your Data Modelling Interview]]></title><description><![CDATA[Prescriptive guidance for conducting your Data Modelling Interview]]></description><link>https://www.canadiandataguy.com/p/how-to-ace-and-structure-your-data</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/how-to-ace-and-structure-your-data</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Wed, 18 Jun 2025 16:56:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!3TbD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>1. <strong>Understand the Requirements (Functional and Non-Functional)</strong></h3><ul><li><p><strong>Ask for Use Cases</strong>: Start by understanding the primary use cases for the data model. Ask questions like:</p><ul><li><p>What kind of questions or analyses will the data model need to answer?</p></li><li><p>Who will use the data (e.g., business users, analysts, data scientists)?</p></li></ul></li><li><p><strong>Clarify Non-Functional Requirements (NFRs)</strong>: Determine the expectations around performance, latency, and data freshness.</p><ul><li><p>Is the data needed in real-time, near real-time, or in batches?</p></li><li><p>What are the expected data volumes and retention periods?</p></li></ul></li></ul><h3>2. <strong>Define Entities and Relationships</strong></h3><ul><li><p>Identify the <strong>key entities</strong> (e.g., Customers, Transactions, Products) and their relationships.</p></li><li><p>Use <strong>Entity-Relationship (ER) diagrams</strong> or similar visual tools to illustrate the relationships.</p></li><li><p>Explain <strong>cardinality</strong> (e.g., one-to-many, many-to-one) between the entities.</p><p></p><p><strong>Tip</strong>: Start representing the relationship as you progress and validate it with the interviewer to see if it makes sense. This will help establish a common understanding between you, and if you have made any bad assumptions, the interviewer can correct you.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3TbD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3TbD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png 424w, https://substackcdn.com/image/fetch/$s_!3TbD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png 848w, https://substackcdn.com/image/fetch/$s_!3TbD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png 1272w, https://substackcdn.com/image/fetch/$s_!3TbD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3TbD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png" width="1456" height="2588" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2588,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9314598,&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://www.canadiandataguy.com/i/149893506?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.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_!3TbD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png 424w, https://substackcdn.com/image/fetch/$s_!3TbD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png 848w, https://substackcdn.com/image/fetch/$s_!3TbD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.png 1272w, https://substackcdn.com/image/fetch/$s_!3TbD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3fef7d9-25db-4735-ac1a-75ef3c3c3770_2592x4608.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"><svg role="img" 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"><g><title></title><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"><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"><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>3. <strong>Design Fact and Dimension Tables</strong></h3><ul><li><p><strong>Fact Tables</strong>:</p><ul><li><p>Explain what events or transactions the fact table will capture.</p></li><li><p>Include details like granularity (e.g., transactions at a daily/hourly level).</p></li><li><p>Mention the primary key and any measures (e.g., sales amount, quantities).</p></li></ul></li><li><p><strong>Dimension Tables</strong>:</p><ul><li><p>Identify dimensions that provide context (e.g., time, products, customers).</p></li><li><p>Mention the primary key and the attributes of each dimension.</p></li><li><p>Discuss if surrogate keys are used for maintaining consistency.</p></li></ul></li></ul><h3>4. <strong>Normalization vs. Denormalization</strong></h3><ul><li><p>Explain your approach to normalization or denormalization, depending on the use case:</p><ul><li><p>Normalized tables are suitable for transactional systems to avoid data redundancy.</p></li><li><p>Denormalized tables are better for analytical systems to improve query performance.</p></li></ul></li><li><p>Justify your choice based on the expected <strong>query patterns</strong> and <strong>performance needs</strong>.</p></li></ul><h3>5. <strong>Design Aggregation Tables (if needed)</strong></h3><ul><li><p>For reporting purposes, you might need <strong>aggregate tables</strong> that summarize data.</p></li><li><p>Explain how you would create these tables and what metrics they will store.</p></li><li><p>Use <strong>naming conventions</strong> like <code>agg_</code>, <code>dim_</code>, and <code>fact_</code> for clarity.</p></li></ul><h3>6. <strong>Discuss Partitioning Strategy</strong></h3><ul><li><p>Choose partitioning columns based on <strong>query patterns</strong> and <strong>data distribution</strong>:</p><ul><li><p>For time-based queries, consider partitioning by date.</p></li><li><p>Explain the expected data volumes and how partitioning will improve performance.</p></li></ul></li><li><p>Include how you would handle <strong>archiving</strong> and <strong>data retention</strong> policies.</p></li></ul><h2>One Tap Could Make All the Difference</h2><p>One surefire way to never see this content again? Scroll past without engaging. Search algorithms heavily rely on signals like likes and comments to decide whether a piece of content deserves to surface again, for you or anyone else. If you found this valuable, even in a small way, do consider hitting the like button or dropping a quick comment. It not only supports the content but helps others discover it too.</p><h3>7. <strong>Demonstrate with Sample Queries</strong></h3><ul><li><p>Show how the model would work by writing or describing <strong>example queries</strong>:</p><ul><li><p>These queries should answer the business questions you gathered in Step 1.</p></li><li><p>Highlight how your model minimizes joins and ensures efficient querying.</p></li></ul></li><li><p>Aim for zero or minimal joins, especially in denormalized models.</p></li></ul><h3>8. <strong>Discuss Data Quality and Governance</strong></h3><ul><li><p>Explain how you would maintain <strong>data quality</strong> in your model:</p><ul><li><p>What kind of checks would you apply at different stages (e.g., data integrity, row count checks)?</p></li><li><p>Would you use tools like <strong>DBT</strong>, <strong>Airflow</strong>, or others for orchestration?</p></li></ul></li><li><p>Address <strong>data governance</strong> considerations like data lineage, privacy, and compliance (e.g., GDPR).</p></li></ul><h3>9. <strong>Explain How the Model Can Scale</strong></h3><ul><li><p>Discuss how the model will handle <strong>increasing data volumes</strong> and <strong>user queries</strong>:</p><ul><li><p>Consider <strong>indexing</strong> strategies, <strong>sharding</strong>, or using <strong>cloud-based data warehouses</strong> like Snowflake or BigQuery.</p></li></ul></li><li><p>Explain if and how you would <strong>optimize performance</strong> through techniques like partitioning, caching, or indexing.</p></li></ul><h3>10. <strong>Wrap Up with Recap and Iteration</strong></h3><ul><li><p>Summarize your approach and how it addresses the initial requirements.</p></li><li><p>Discuss potential areas for <strong>iteration</strong> or <strong>improvement</strong> if the requirements change.</p></li><li><p>Be open to feedback from the interviewer and discuss how you would adapt the design based on their inputs.</p></li></ul><h3>Template for Each Table Design</h3><p>For each table you design, mention:</p><ul><li><p><strong>Name</strong>: Use naming conventions (e.g., <code>dim_</code>, <code>fact_</code>, <code>agg_</code>).</p></li><li><p><strong>Primary Key</strong>: Specify the key.</p></li><li><p><strong>Columns</strong>: List the key attributes and explain their purpose.</p></li><li><p><strong>Partitioning</strong>: If applicable, explain why you chose a specific column for partitioning.</p></li><li><p><strong>Estimated Row Count</strong>: Provide an estimate based on expected data volumes.</p></li><li><p><strong>Sample Query</strong>: Show how to query the table to answer a business question.</p></li></ul><p>This structure helps convey both your technical skills and your ability to think critically and design solutions that meet business needs.</p><h2><strong>Leave something rememberable</strong></h2><p>Before you wrap up, give your interviewer something they can revisit long after the conversation ends. Pair your polished ER diagram with crisp, layered documentation&#8212;entity definitions, key attributes, and the rationale behind every relationship. Treat it as a living blueprint: clear enough for newcomers, detailed enough for architects, and structured to mirror the depth of your own experience.</p><p>Humans forget roughly 80 % of new information within a day, so the most reliable way to stay top-of-mind is to hand them a reference they can&#8217;t ignore. The sharper your vision, the richer your notes, the louder your expertise will echo when the hiring panel reviews candidates. Turn your model into <strong>a one-page memory hook</strong> that brings your name back to the top of their list.</p><p></p><h2></h2>]]></content:encoded></item><item><title><![CDATA[A Deep Dive into Skewed Joins, GroupBy Bottlenecks, and Smart Strategies to Keep Your Spark Jobs Flying]]></title><description><![CDATA[Unlock comprehensive, practical solutions to conquer data skew in Apache Spark&#8212;step-by-step from basics to advanced strategies for perfectly balanced workloads and optimized job performance.]]></description><link>https://www.canadiandataguy.com/p/a-deep-dive-into-skewed-joins-groupby</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/a-deep-dive-into-skewed-joins-groupby</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Fri, 06 Jun 2025 03:11:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Data skew in Apache Spark refers to an <strong>uneven distribution of data across partitions</strong>, often manifesting during shuffle-intensive operations like joins or group-by aggregations. In a skewed scenario, one or a few partitions end up holding far more records for a particular key than others, leading to <strong>hotspots</strong> and <strong>straggler tasks</strong>. This imbalance causes <strong>performance bottlenecks</strong> (tasks processing heavy partitions take much longer) and <strong>inefficient resource usage</strong> (some executors sit idle). In extreme cases, heavily skewed partitions can even exhaust executor memory and cause job failures. Below, we delve into why skew occurs in joins and aggregations, and provide comprehensive strategies&#8212;ranging from Spark configuration tweaks to code-level patterns and architectural designs&#8212;to alleviate data skew. </p><h2>Why Data Skew Occurs in Joins and Aggregations</h2><p><strong>Join Operations:</strong> In Spark (excluding broadcast joins), joining two datasets on a key requires redistributing data so that records with the same key end up on the same partition (for a shuffle hash join or sort-merge join). If the key distribution is highly uneven (e.g. one key value appears in 90% of the records), the partition handling that key will be <strong>massive compared to others</strong>, causing skew. All records for that popular key funnel into one task, creating a severe load imbalance. For example, consider joining a large transactions table with a user table on <code>user_id</code> when a few &#8220;power users&#8221; have the vast majority of transactions. The join partition corresponding to those user_ids will handle hundreds of thousands of records, while other partitions process only a few &#8211; resulting in stragglers and possibly out-of-memory errors.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6y5D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6y5D!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.png 424w, https://substackcdn.com/image/fetch/$s_!6y5D!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.png 848w, https://substackcdn.com/image/fetch/$s_!6y5D!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.png 1272w, https://substackcdn.com/image/fetch/$s_!6y5D!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6y5D!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.png" width="1200" height="1210.7142857142858" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7aa8254b-9360-4d6d-8772-87863babbf7b_3806x3840.png&quot;,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1469,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:1090821,&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://www.canadiandataguy.com/i/165303261?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7aa8254b-9360-4d6d-8772-87863babbf7b_3806x3840.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6y5D!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.png 424w, https://substackcdn.com/image/fetch/$s_!6y5D!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.png 848w, https://substackcdn.com/image/fetch/$s_!6y5D!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.png 1272w, https://substackcdn.com/image/fetch/$s_!6y5D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cdbf698-3d8d-4341-85aa-1fbc57a1b7d6_3806x3840.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"><svg role="img" 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"><g><title></title><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"><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"><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><strong>GroupBy and Aggregations:</strong> Similarly, grouping or aggregating by a key brings all data for each key onto one executor. If some keys occur far more frequently than others, those keys&#8217; partitions become disproportionately large. For instance, a <code>groupBy("customer_id")</code> on an orders dataset where a handful of customers account for most orders will produce skew: the reducer for those popular customers must aggregate an extremely large list, while others handle trivial amounts<a href="https://www.linkedin.com/pulse/what-data-skewness-spark-how-handle-code-soutir-sen-xf6hf#:~:text=Skewness%20often%20arises%20during%20operations,For%20example">l</a>. Even though Spark performs map-side partial aggregation, a single reduce task will still have to combine all intermediate results for a heavy key, leading to one very slow task.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-TIJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-TIJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.png 424w, https://substackcdn.com/image/fetch/$s_!-TIJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.png 848w, https://substackcdn.com/image/fetch/$s_!-TIJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.png 1272w, https://substackcdn.com/image/fetch/$s_!-TIJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-TIJ!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.png" width="1200" height="1246.1538461538462" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f70466ce-9b00-4cdd-9e70-f55d0cd1f468_3699x3840.png&quot;,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1512,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:1239456,&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://www.canadiandataguy.com/i/165303261?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff70466ce-9b00-4cdd-9e70-f55d0cd1f468_3699x3840.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-TIJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.png 424w, https://substackcdn.com/image/fetch/$s_!-TIJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.png 848w, https://substackcdn.com/image/fetch/$s_!-TIJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.png 1272w, https://substackcdn.com/image/fetch/$s_!-TIJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7580bc-ed95-47cf-abd8-70d6e0c2e6eb_3699x3840.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"><svg role="img" 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"><g><title></title><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"><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"><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>Understanding these root causes guides us to solutions. Next, we address <strong>join skew</strong> and <strong>groupBy/aggregation skew</strong> separately, discussing targeted techniques for each.</p><h2>How do we know if we have a Skew Problem?</h2><p>To identify if there is a skew problem in Spark, several indicators and methods can be employed:</p><ol><li><p><strong>Task Duration Discrepancy</strong>:</p><ul><li><p>If all tasks in a shuffle stage finish except for a few that hang for a long time, this may indicate data skew.</p></li></ul></li><li><p><strong>Spark UI Analysis</strong>:</p><ul><li><p>Check the tasks summary metrics in the Spark UI. A significant difference between the minimum and maximum shuffle read sizes can suggest skewness.</p></li></ul></li><li><p><strong>Data Spills</strong>:</p><ul><li><p>If, despite tuning the number of shuffle partitions, there are numerous data spills, this might point to data skew.</p></li></ul></li><li><p><strong>Row Count Disparity</strong>:</p><ul><li><p>Counting rows grouped by join or aggregation columns can reveal skew. A significant difference in row counts for different groups indicates potential skew issues.</p></li></ul></li><li><p><strong>Compression Ratios</strong>:</p><ul><li><p>Highly compressed tables can affect the estimation of shuffle partitions, leading to spills. Monitoring this can help identify such cases.</p></li></ul></li></ol><p><a href="https://www.databricks.com/discover/pages/optimize-data-workloads-guide">Additionally, Spark SQL's Adaptive Query Execution (AQE) can help detect and sometimes resolve data skew dynamically by adjusting execution strategies as needed. </a></p><h2>Mitigating Skew in Join Operations</h2><p>When joining two datasets on a key, Spark must shuffle records so that identical keys end up on the same partition. If one key is heavily overrepresented, its partition can become a bottleneck. Below are strategies ordered from most to least recommended</p><h3>1. Adaptive Query Execution (AQE) &#8211; Automatic Skew Handling</h3><p>Spark 3.0+ introduced <strong>Adaptive Query Execution (AQE)</strong>, which can dynamically detect and correct skewed partitions during runtime. When AQE is enabled, Spark measures the size of each shuffle partition after the initial shuffle. If it finds any partition that is both exceptionally large in absolute terms and multiple times larger than the median partition size, it automatically splits that partition into smaller sub-tasks and replicates the corresponding rows from the other side of the join so each sub-task can run independently.</p><h4>How It Works</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!N7Vj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!N7Vj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png 424w, https://substackcdn.com/image/fetch/$s_!N7Vj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png 848w, https://substackcdn.com/image/fetch/$s_!N7Vj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png 1272w, https://substackcdn.com/image/fetch/$s_!N7Vj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!N7Vj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png" width="536" height="1159" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/051f8937-4934-4d8e-929f-71c4bf2a6d48_536x1159.png&quot;,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:1159,&quot;width&quot;:536,&quot;resizeWidth&quot;:536,&quot;bytes&quot;:110917,&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://www.canadiandataguy.com/i/165303261?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051f8937-4934-4d8e-929f-71c4bf2a6d48_536x1159.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!N7Vj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png 424w, https://substackcdn.com/image/fetch/$s_!N7Vj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png 848w, https://substackcdn.com/image/fetch/$s_!N7Vj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png 1272w, https://substackcdn.com/image/fetch/$s_!N7Vj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe422a76d-a93f-4dc5-b0e4-1db069c12a33_536x1159.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><ol><li><p><strong>Collect Partition Statistics:</strong></p><ul><li><p>After the shuffle phase, Spark records the size (bytes) of every partition on both sides of the join.</p></li></ul></li><li><p><strong>Identify Skewed Partitions:</strong><br>A partition is marked as &#8220;skewed&#8221; only if it meets <strong>both</strong> criteria:</p><ul><li><p><strong>Absolute&#8208;Size Threshold: </strong><code>spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes </code>Default: <code>256MB</code></p></li><li><p><strong>Relative&#8208;Size Factor: </strong><code>spark.sql.adaptive.skewJoin.skewedPartitionFactor</code></p><p>(Default: <code>5.0</code>)</p></li></ul><p>If the median shuffle&#8208;partition size is 50 MB, a factor of 5.0 means any partition &gt; 250 MB qualifies&#8212;provided it also exceeds the 256 MB absolute threshold.</p></li><li><p><strong>Split &amp; Replicate:</strong></p><ul><li><p>Suppose partition #17 is 1 GB and the coalesced&#8208;partition target is 250 MB. Spark divides that 1 GB into four ~250 MB sub-partitions.</p></li><li><p>For a join, each of those sub-partitions must still see all matching rows from the opposite dataset. Spark duplicates those matching rows N times (once per sub-partition) so each sub-task can run a local join.</p></li></ul></li><li><p><strong>Run Subtasks in Parallel &amp; Merge Results:</strong></p><ul><li><p>Instead of a single, massive task pulling 1 GB, Spark launches N tasks (e.g., four tasks pulling ~250 MB each plus replicated rows).</p></li><li><p>When those sub-tasks finish, Spark concatenates their outputs to produce the final joined result.</p></li></ul></li></ol><p>Because this splitting and replication occur <strong>after</strong> the initial shuffle&#8212;when Spark has accurate sizes&#8212;no query rewriting or manual &#8220;hints&#8221; are required.</p><h4>Configuration</h4><pre><code># Enable AQE (on by default in Spark 3.2+)
spark.sql.adaptive.enabled=true

# Enable skew-join correction
spark.sql.adaptive.skewJoin.enabled=true

# Absolute-size threshold for skewed partitions
spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes=256MB

# Relative-size factor: if a partition is &gt; factor &#215; median size, it's skewed
spark.sql.adaptive.skewJoin.skewedPartitionFactor=5.0

# (Spark 3.3+) Force AQE to apply skew-join splitting even if it adds shuffle overhead
spark.sql.adaptive.forceOptimizeSkewedJoin=true
</code></pre><h4>Pros &amp; Cons</h4><ul><li><p><strong>Pros:</strong></p><ul><li><p><strong>Zero code changes</strong>: No query rewrites, no manual hints.</p></li><li><p><strong>Runtime intelligence</strong>: Works on any sort-merge or shuffle-hash join where skew is severe.</p></li><li><p>Eliminates straggler tasks without requiring you to identify skewed keys in advance.</p></li></ul></li><li><p><strong>Cons:</strong></p><ul><li><p>Applies only to <strong>shuffle joins</strong> (sort-merge and shuffle-hash). Broadcast joins never shuffle, so they aren&#8217;t &#8220;skewed.&#8221;</p></li><li><p>Splitting and replicating can introduce extra shuffle I/O; mild skew might not trigger or be worth splitting.</p></li><li><p>You may need to tune thresholds (<code>skewedPartitionThresholdInBytes</code> and <code>skewedPartitionFactor</code>) to avoid splitting on nearly-skewed partitions.</p></li></ul></li></ul><h2><strong>Keep This Post Discoverable: Your Engagement Counts!</strong></h2><p>Your engagement with this blog post is crucial! Without claps, comments, or shares, this valuable content might become lost in the vast sea of online information. Search engines like Google rely on user engagement to determine the relevance and importance of web pages. If you found this information helpful, please take a moment to clap, comment, or share. Your action not only helps others discover this content but also ensures that you&#8217;ll be able to find it again in the future when you need it. Don&#8217;t let this resource disappear from search results &#8212; show your support and help keep quality content accessible!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share CanadianDataGuy&#8217;s No Fluff Newsletter&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.canadiandataguy.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share CanadianDataGuy&#8217;s No Fluff Newsletter</span></a></p><div><hr></div><h3>2. Broadcast Hash Join (Small&#8211;Large Optimization)</h3><p>If one side of a join is small enough to fit in memory on every executor, a <strong>broadcast hash join</strong> eliminates virtually all skew risk. By broadcasting the smaller dataset to every executor, Spark can join on the large side without shuffling it by key. Even a &#8220;hot&#8221; key on the large side is processed in parallel across many tasks, because each task already has the complete, in-memory copy of the smaller table.</p><h4>How It Works</h4><ol><li><p><strong>Spark Optimizer Picks It Automatically</strong> (if small side &#8804; 10 MB by default):</p><ul><li><p>Controlled by:</p><p><code>spark.sql.autoBroadcastJoinThreshold </code>(Default: <code>10MB</code>)</p></li><li><p>Raise this value to allow larger small tables but not more than 1 GB practically </p></li></ul></li><li><p><strong>Explicitly Force Broadcast in DataFrame Code:</strong></p></li></ol><pre><code>from pyspark.sql.functions import broadcast

result = largeDF.join(broadcast(smallDF), "joinKey")</code></pre><ol start="3"><li><p><strong>Spark SQL Hint:</strong></p></li></ol><pre><code>SELECT /*+ BROADCAST(s) */ *
FROM large l
JOIN small s
  ON l.joinKey = s.joinKey;</code></pre><p>Since the large dataset is not shuffled by key, no single reducer processes all rows for a heavy key. Instead, each task hashes the broadcasted small side in-memory, and streams its assigned partitions of the large side through that hash.</p><h3>Pros &amp; Cons</h3><ul><li><p><strong>Pros:</strong></p><ul><li><p><strong>No shuffle</strong> on large side&#8212;completely eliminates skew related to the small side.</p></li><li><p>Simple to implement via <code>broadcast()</code> hints or by tuning <code>spark.sql.autoBroadcastJoinThreshold</code>.</p></li><li><p>Dramatic speedups when one side is truly small and the other side has a hot key.</p></li></ul></li><li><p><strong>Cons:</strong></p><ul><li><p>The &#8220;small&#8221; table must <strong>fit comfortably</strong> in each executor&#8217;s memory. If it&#8217;s too large (hundreds of MB), broadcasting can create memory pressure or OOM.</p></li><li><p>Not applicable when <strong>both</strong> sides are large.</p></li><li><p>Total cluster memory usage for the small table = (# executors) &#215; (size of small table).</p></li></ul></li></ul><div><hr></div><h3>3. Handling Skewed Keys Separately (Divide &amp; Conquer)</h3><p>If you know exactly which key(s) are skewed, you can <strong>split your data into two subsets</strong>&#8212;the skewed-key subset and the &#8220;rest&#8221;&#8212;process them separately, then recombine</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IBuf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95c396b-5af1-4412-867e-41e02383a572_1035x1170.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IBuf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95c396b-5af1-4412-867e-41e02383a572_1035x1170.png 424w, https://substackcdn.com/image/fetch/$s_!IBuf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95c396b-5af1-4412-867e-41e02383a572_1035x1170.png 848w, https://substackcdn.com/image/fetch/$s_!IBuf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95c396b-5af1-4412-867e-41e02383a572_1035x1170.png 1272w, https://substackcdn.com/image/fetch/$s_!IBuf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95c396b-5af1-4412-867e-41e02383a572_1035x1170.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IBuf!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95c396b-5af1-4412-867e-41e02383a572_1035x1170.png" width="1200" height="1356.5217391304348" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f95c396b-5af1-4412-867e-41e02383a572_1035x1170.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f9ea8026-d737-41ec-bed7-fa8b1716e8b6_1035x1170.png&quot;,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1170,&quot;width&quot;:1035,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:129951,&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://www.canadiandataguy.com/i/165303261?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ea8026-d737-41ec-bed7-fa8b1716e8b6_1035x1170.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IBuf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95c396b-5af1-4412-867e-41e02383a572_1035x1170.png 424w, https://substackcdn.com/image/fetch/$s_!IBuf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95c396b-5af1-4412-867e-41e02383a572_1035x1170.png 848w, https://substackcdn.com/image/fetch/$s_!IBuf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95c396b-5af1-4412-867e-41e02383a572_1035x1170.png 1272w, https://substackcdn.com/image/fetch/$s_!IBuf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95c396b-5af1-4412-867e-41e02383a572_1035x1170.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><figcaption class="image-caption">caption...</figcaption></figure></div><h4>How It Works</h4><ol><li><p><strong>Split Each Dataset into &#8220;Skewed&#8221; vs. &#8220;Rest&#8221;:</strong></p></li></ol><pre><code>skewed_keys = ["USA"]

# Dataset A (large or small, doesn&#8217;t matter)
A_skew   = A.filter(F.col("country") == "USA")
A_rest   = A.filter(F.col("country") != "USA")

# Dataset B
B_skew   = B.filter(F.col("country") == "USA")
B_rest   = B.filter(F.col("country") != "USA")
</code></pre><ol start="2"><li><p><strong>Join the &#8220;Rest&#8221; Subsets Normally:</strong></p></li></ol><pre><code>main_join = A_rest.join(B_rest, "country")</code></pre><p>Since &#8220;USA&#8221; is removed, these partitions will be balanced&#8212;assuming no other keys are extremely skewed.</p><ol start="3"><li><p><strong>Join the &#8220;Skewed&#8221; Subsets Separately with an Optimized Strategy:</strong></p></li></ol><ul><li><p>If <code>B_skew</code> is small enough, <strong>broadcast</strong> it:</p></li></ul><pre><code>skew_join = A_skew.join(broadcast(B_skew), "country")</code></pre><ul><li><p>Otherwise, you could <strong>salt</strong> only the &#8220;USA&#8221; key (as shown above) or use any other technique.</p></li></ul><ol start="4"><li><p><strong>Union the Two Results:</strong></p></li></ol><pre><code>final_result = main_join.unionByName(skew_join)</code></pre><h4>Pros &amp; Cons</h4><ul><li><p><strong>Pros:</strong></p><ul><li><p><strong>Simplicity</strong>: Process the skewed key in isolation; non-skewed data is untouched.</p></li><li><p>You choose exactly how to handle the problematic key (e.g., broadcast, salt, or extra resources).</p></li><li><p>No need to change logic for the majority of keys.</p></li></ul></li><li><p><strong>Cons:</strong></p><ul><li><p>Requires an extra read/scan (filter) on each dataset&#8212;though filter is usually cheap.</p></li><li><p>Increases job complexity: two join operations instead of one.</p></li><li><p>If more than one key is skewed, you must repeat this process for each key or group of keys&#8212;still subject to skew within that sub&#8208;subset.</p></li><li><p>Must identify skewed key(s) beforehand.</p></li></ul></li></ul><h3>4. <strong>Salting Every Key (Uniform Distribution Across N Buckets)</strong></h3><p>In real-world joins&#8212;especially at scale&#8212;any single key with extremely high cardinality (for example, a superstar YouTuber like &#8220;mr_beast&#8221;) can overwhelm one partition, leading to severe performance bottlenecks. While you might compensate by detecting and salting just that one &#8220;hot&#8221; key, a more robust approach is to uniformly salt every <code>youtuber_id</code>, ensuring that even unexpected popularity spikes are handled gracefully. By applying a deterministic salt to all keys, each <code>youtuber_id</code> is augmented with a bucket index, distributing its rows across up to N partitions. Matching rows from both tables still join correctly because the salt is derived deterministically from the join key (and potentially another column like <code>video_id</code>).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jIDC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jIDC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png 424w, https://substackcdn.com/image/fetch/$s_!jIDC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png 848w, https://substackcdn.com/image/fetch/$s_!jIDC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png 1272w, https://substackcdn.com/image/fetch/$s_!jIDC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jIDC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png" width="513" height="1187" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/82c2e15d-e050-4746-bdf0-9b36a62214f9_513x1187.png&quot;,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1187,&quot;width&quot;:513,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:88122,&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://www.canadiandataguy.com/i/165303261?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82c2e15d-e050-4746-bdf0-9b36a62214f9_513x1187.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_!jIDC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png 424w, https://substackcdn.com/image/fetch/$s_!jIDC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png 848w, https://substackcdn.com/image/fetch/$s_!jIDC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png 1272w, https://substackcdn.com/image/fetch/$s_!jIDC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5686bc8d-98f9-4765-a448-fe64b18f3080_513x1187.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h4>How It Works</h4><ol><li><p><strong>Choose a Salt Count (N)</strong></p><ul><li><p>Decide how many buckets to split <strong>every</strong> <code>youtuber_id</code> into (for example, <code>N = 10</code>).</p></li><li><p>Aim for each salted partition to be on the order of 100&#8211;300 MB (or your target). Use the Spark UI&#8217;s &#8220;Shuffle Read Size by Task&#8221; to gauge ideal bucket size.</p></li></ul></li><li><p><strong>Compute a Deterministic Salt for Each Row</strong></p><ul><li><p>For each row, compute:</p></li></ul></li></ol><pre><code>salt = abs(hash(concat(youtuber_id, video_id))) % N
salted_youtuber = CONCAT(youtuber_id, "_", salt)</code></pre><p>This ensures:</p><ul><li><p><strong>All rows belonging to the same (youtuber_id, video_id)</strong> produce the same <code>(salted_youtuber, video_id)</code> pair in both tables.</p></li><li><p><strong>Every youtuber_id is split</strong> across up to N buckets&#8212;popular keys will spread widely, less-popular keys may cluster in fewer buckets if they have fewer distinct <code>video_id</code> values.</p></li></ul><p><strong>3. Salt Both Tables in PySpark</strong></p><pre><code>import pyspark.sql.functions as F

N = 10

def saltAllExpr(yid_col, vid_col):
    """
    Deterministic salt for every (youtuber_id, video_id):
    salted_youtuber = youtuber_id + "_" + (abs(hash(youtuber_id || video_id)) % N)
    """
    return F.concat(
        yid_col,
        F.lit("_"),
        (F.abs(F.hash(F.concat(yid_col, vid_col))) % N).cast("string")
    )

# Salt the IMPRESSIONS table
salted_impressions = impressions.withColumn(
    "salted_youtuber",
    saltAllExpr(F.col("youtuber_id"), F.col("video_id"))
)

# Salt the CLICKS table
salted_clicks = clicks.withColumn(
    "salted_youtuber",
    saltAllExpr(F.col("youtuber_id"), F.col("video_id"))
)
</code></pre><ul><li><p>Every <code>(youtuber_id, video_id)</code> pair gets a consistent bucket index in <code>[0..9]</code>.</p></li><li><p>For <code>"mr_beast"</code> with <code>video_id = "abc123"</code>, <code>salted_youtuber = "mr_beast_4"</code> (for example).</p></li><li><p>A different video <code>"xyz789"</code> might map to <code>"mr_beast_7"</code>.</p></li><li><p>A less-popular youtuber with only one or two videos may occupy only 1&#8211;2 buckets&#8212;but that&#8217;s fine.</p></li></ul><ol start="4"><li><p><strong>Perform the Salted Join</strong></p></li></ol><pre><code>joined = salted_impressions.alias("imp").join(
    salted_clicks.alias("clk"),
    on=[ "salted_youtuber", "video_id" ],
    how="inner"
)
</code></pre><ul><li><p><strong>Before</strong> salting: All <code>"mr_beast"</code> rows (across any <code>video_id</code>) would land in a single partition.</p></li><li><p><strong>After</strong> salting: Each distinct <code>(youtuber_id, video_id)</code> combination goes to a bucket <code>youtuber_id_&lt;0..9&gt;</code>, so <code>"mr_beast"</code> content spreads across up to 10 partitions&#8212;one per bucket index.</p></li><li><p>This eliminates a single &#8220;hot&#8221; partition for <code>"mr_beast"</code>.</p></li></ul><ol start="5"><li><p><strong>Spark SQL Equivalent</strong></p></li></ol><pre><code>WITH salted_impressions AS (
  SELECT
    *,
    CONCAT(
      youtuber_id,
      '_',
      CAST(ABS(hash(CONCAT(youtuber_id, video_id))) % 10 AS STRING)
    ) AS salted_youtuber
  FROM impressions
),
salted_clicks AS (
  SELECT
    *,
    CONCAT(
      youtuber_id,
      '_',
      CAST(ABS(hash(CONCAT(youtuber_id, video_id))) % 10 AS STRING)
    ) AS salted_youtuber
  FROM clicks
)
SELECT
  imp.*,
  clk.viewer_id,
  clk.timestamp AS click_timestamp
FROM salted_impressions imp
JOIN salted_clicks clk
  ON imp.salted_youtuber = clk.salted_youtuber
 AND imp.video_id       = clk.video_id;
</code></pre><ul><li><p>Each <code>(youtuber_id, video_id)</code> deterministically maps to one of 10 buckets.</p></li><li><p>Even if <code>"mr_beast"</code> has 100 videos, those 100 distinct <code>(youtuber_id, video_id)</code> pairs spread across up to 10 buckets.</p></li></ul><div><hr></div><h4>Pros &amp; Cons</h4><p><strong>Pros:</strong></p><ul><li><p><strong>Uniform Distribution for All Keys</strong><br>Any youtuber with many videos&#8212;like <code>"mr_beast"</code>&#8212;will spread its rows across N buckets.</p></li><li><p><strong>No Conditional Logic on &#8220;Hot&#8221; Keys</strong><br>You don&#8217;t need to first identify which youtuber is skewed; every key is salted uniformly.</p></li><li><p><strong>Deterministic</strong><br>Matching <code>(youtuber_id, video_id)</code> always end up in the same bucket on both sides, so joins remain correct.</p></li><li><p><strong>Works for Any Join</strong><br>Applies whether one or both tables are large&#8212;no reliance on broadcast.</p></li></ul><p><strong>Cons:</strong></p><ul><li><p><strong>Extra Shuffle Volume</strong><br>Every row in both tables carries an extra salted key, and all rows must shuffle by <code>(salted_youtuber, video_id)</code>.</p><ul><li><p>If a youtuber is lightly used, its rows may end up in only one or two buckets&#8212;but they still shuffle.</p></li><li><p>If data was quite balanced originally, salting &#8220;everything&#8221; may introduce more shuffle than strictly necessary.</p></li></ul></li><li><p><strong>Choosing the Right N Is Crucial</strong></p><ul><li><p>If N is too small, heavily skewed keys (like <code>"mr_beast"</code>) still concentrate too much data in one bucket.</p></li><li><p>If N is too large, you create many small partitions, which increases scheduler overhead.</p></li></ul></li><li><p><strong>Need to Drop </strong><code>salted_youtuber</code><strong> After the Join</strong><br>If you only care about the original key (<code>youtuber_id</code>), drop <code>salted_youtuber</code> once the join is done.</p></li></ul><div><hr></div><h4>When to Use &#8220;Salt Everything&#8221;</h4><p>Use this approach when:</p><ul><li><p><strong>You don&#8217;t know in advance</strong> which keys will be skewed (e.g., an Uber driver of the week suddenly goes viral, or any youtuber&#8217;s popularity spikes).</p></li><li><p><strong>Data volume is large and dynamic</strong>, and you want a one&#8208;size&#8208;fits&#8208;all solution rather than conditionally checking for hot keys.</p></li><li><p><strong>You want consistent distribution</strong> for all <code>(youtuber_id, video_id)</code> pairs without maintaining a list of skewed keys.</p></li></ul><div><hr></div><h2>Additional Considerations (Ideally; try to avoid getting into these)</h2><ul><li><p><strong>Tuning Shuffle Partitions</strong></p></li></ul><p>Adjust: spark.sql.shuffle.partitions to a value higher than the default (200), ideally a few times your cluster&#8217;s total cores, so that partitions remain small. Too many partitions cause scheduler overhead; too few cause each partition to be large.</p><ul><li><p><strong>Speculative Execution:</strong>  Enabling speculation (spark.speculation=true) can alleviate the impact of skew by attempting to re-run straggling tasks on another executor. This doesn&#8217;t fix the skew itself, but if a task is slow (perhaps due to skew or maybe a slow node), Spark will launch a duplicate task elsewhere. Whichever finishes first wins. In a skew scenario, a speculated task is still doing the same heavy work, so it won&#8217;t magically complete faster unless the original executor was anomalously slow. However, speculation can sometimes help if, say, one executor was busy with garbage collection while another could do the work faster &#8211; it provides a safety net for stragglers. It&#8217;s generally good to enable in large clusters, but note it causes extra resource usage for those duplicate tasks.</p></li><li><p><strong>Monitoring with the Spark UI</strong></p><ul><li><p>In the <strong>Stages</strong> tab, expand a SQL stage and click <strong>Physical Plan</strong>.</p></li><li><p>Under <strong>Shuffle Read Size by Task</strong>, look for a single bar that towers over the others&#8212;that&#8217;s your skewed partition.</p></li><li><p>Use those insights to decide between AQE or manual salting.</p></li></ul></li><li><p><strong>Filtering Out Problematic Rows</strong></p><ul><li><p>If certain values (e.g., <code>NULL</code> or outliers) cause extreme skew but are not essential, you can drop them before the join, Only do this if you can accept losing those rows from the result.</p></li></ul></li></ul><pre><code>cleanedDF = originalDF.filter(F.col("country").isNotNull())</code></pre><ul><li><p><strong>Use Skew Hints (Spark 3.4+)</strong></p><ul><li><p>You can annotate specific keys as skewed in a Spark SQL query so that Spark generates a plan that avoids shuffling them into a single reducer</p></li></ul></li><li><p><strong>Memory and Shuffle Tuning:</strong> While not fixing skew, you might need to adjust memory configs to handle it. For instance, if one partition is huge, increasing executor memory or shuffle buffer sizes (spark.shuffle.spill.numElementsForceSpillThreshold, spark.shuffle.file.buffer, etc.) won&#8217;t solve the skew but might prevent OOM crashes by allowing Spark to spill gracefully. Similarly, ensure spark.memory.fraction or spark.sql.autoBroadcastJoinThreshold are set such that the heavy data can be handled (e.g., give more memory to shuffle if needed). These are more about coping with skew than removing it.</p></li><li><p><strong>Adaptive Query Execution (AQE):</strong> As discussed, ensure spark.sql.adaptive.enabled=true (should be default on modern Spark) and spark.sql.adaptive.skewJoin.enabled=true. You can adjust spark.sql.adaptive.skewJoin.skewedPartitionFactor (default 5) and ...skewedPartitionThresholdInBytes (default 256MB) to tune how aggressively Spark flags partitions as skewed Lowering these values makes Spark split smaller skews, but setting them too low might cause unnecessary splitting. In Spark 3.3+, if you really want to force skew join handling, spark.sql.adaptive.forceOptimizeSkewedJoin=true will apply the optimization even if it might add extra shuffle overhead.</p></li></ul><div><hr></div><h2>Tackling Skew in Spark Aggregations: From Simple Sums to Semi-Additive Metrics</h2><p>Aggregation operations like <code>groupBy().agg()</code> in Spark can become major performance bottlenecks when data is skewed. A small number of high-cardinality keys can result in uneven workload distribution, where one reducer is overloaded while others remain idle. While Spark's map-side partial aggregation helps, it alone can&#8217;t prevent reducers from becoming overwhelmed when skewed keys funnel massive data into single tasks.</p><p>In this deep dive, we&#8217;ll explore practical patterns to mitigate skew during aggregations, especially focusing on semi-additive metrics like averages, distinct counts, and ratios&#8212;metrics that can't always be merged as trivially as sums or counts.</p><div><hr></div><h3>1. Two-Stage Aggregation with Salting</h3><p>The most effective method for aggregation skew is a two-stage salted aggregation. In the first stage, you add a salt (random or deterministic) to the key, distributing rows across more groups. In the second stage, you aggregate these partials back to the original key.</p><h4>How It Works:</h4><ul><li><p>Add a new column (e.g., <code>salt = rand() % N</code>) to the grouping key</p></li><li><p>Group by <code>(key, salt)</code> and compute partial aggregates</p></li><li><p>Re-group by <code>key</code> to merge the partials</p></li></ul><h4>PySpark Example:</h4><pre><code><code>from pyspark.sql.functions import col, rand, floor, sum as _sum, count as _count

N = 10
salted_df = df.withColumn("salt", floor(rand() * N))

# First stage: partial aggregation
partial = salted_df.groupBy("key", "salt").agg(
    _sum("value").alias("partial_sum"),
    _count("value").alias("partial_count")
)

# Second stage: final aggregation
final = partial.groupBy("key").agg(
    _sum("partial_sum").alias("total_sum"),
    _sum("partial_count").alias("total_count")
)</code></code></pre><p>This works well for <strong>semi-additive metrics</strong> like average:</p><pre><code><code>final.withColumn("avg", col("total_sum") / col("total_count"))</code></code></pre><h4>Pros:</h4><ul><li><p>Greatly reduces skew on hot keys</p></li><li><p>Flexible: works for sums, counts, averages, etc.</p></li></ul><h4>Cons:</h4><ul><li><p>Not directly applicable to non-associative metrics (like median, percentile)</p></li><li><p>Requires an extra stage of aggregation and data shuffle</p></li><li><p>You must choose N carefully</p></li></ul><div><hr></div><h3>2. Favor Combiner-Friendly DataFrame Operations</h3><p>In the DataFrame API, Spark automatically performs map-side combine for aggregation functions like <code>sum</code>, <code>count</code>, and <code>avg</code>. This significantly reduces data shuffled across the network.</p><h4>Best Practices:</h4><ul><li><p>Avoid collecting all values per key using <code>collect_list</code> or <code>collect_set</code> unless needed</p></li><li><p>Prefer built-in aggregation functions that support partial aggregation</p></li></ul><h4>Example:</h4><pre><code><code>df.groupBy("user_id").agg(
    _sum("impressions").alias("total_impressions"),
    _count("clicks").alias("click_count")
)</code></code></pre><p>This automatically benefits from map-side combine.</p><div><hr></div><h3>3. Hierarchical or Incremental Aggregation</h3><p>Instead of grouping by the final key directly, first group on a <strong>compound key</strong> (e.g., key + day), then roll up to the main key. This acts like salting but uses a meaningful secondary attribute.</p><p>Example: Group by <code>(customer_id, date)</code>, then group again by <code>customer_id</code>.</p><h4>Pros:</h4><ul><li><p>Uses natural structure in data</p></li><li><p>More interpretable than random salt</p></li></ul><h4>Cons:</h4><ul><li><p>Only works if meaningful secondary keys exist</p></li><li><p>Adds complexity to query logic</p></li></ul><div><hr></div><h3>4. Isolate Skewed Keys</h3><p>When just a few keys are skewed (e.g., "mr_beast" on YouTube), isolate them:</p><ul><li><p>Filter the skewed keys</p></li><li><p>Aggregate them separately</p></li><li><p>Aggregate the rest normally</p></li><li><p>Union results</p></li></ul><h4>Pros:</h4><ul><li><p>Simple logic for non-skewed keys</p></li><li><p>You can fine-tune treatment of skewed keys</p></li></ul><h4>Cons:</h4><ul><li><p>Manual, doesn&#8217;t scale to many skewed keys</p></li><li><p>Separate logic paths = more complexity</p></li></ul><div><hr></div><h3>Special Note: Semi-Additive Metrics</h3><p>For metrics like <strong>averages</strong>, <strong>ratios</strong>, or <strong>distinct counts</strong>, special care is needed:</p><ul><li><p><strong>Average:</strong> Use partial sums and counts, then divide</p></li><li><p><strong>Ratios:</strong> Keep numerator/denominator separate, aggregate both, then divide</p></li><li><p><strong>Count Distinct:</strong> Use <code>approx_count_distinct()</code> for scalable approximations</p></li></ul><p>Some metrics cannot be split and recombined (e.g., exact percentiles). In those cases, use isolation or rethink the need for exact aggregation.</p><div><hr></div><h3>Final Thoughts for Aggregates</h3><p>Aggregation skew is an invisible killer in Spark jobs. The best strategy is proactive design: salt heavy keys, use partial aggregation, and always choose APIs that favor combiners. With these patterns, even semi-additive or tricky metrics can be made scalable at massive volumes.</p><p>If you're dealing with skew, don't just throw resources at it. Design for it.</p><h2>Summary of Recommendations for Joins</h2><ul><li><p><strong>Recommended first Adaptive Query Execution (AQE)</strong>: Zero code changes, runtime splitting for any sort-merge or shuffle-hash join.</p></li><li><p><strong>Broadcast Hash Join</strong></p><ul><li><p><strong>When one side is small</strong> (&#8804; 10 MB by default to 1GB). Hint in DataFrame or SQL.</p></li><li><p>Avoids all skew because no shuffle on the small side.</p></li></ul></li><li><p><strong>Salting the Key</strong></p><ul><li><p><strong>When neither side is small</strong>, but you know exactly which key(s) dominate.</p></li><li><p>Manual, but guaranteed to split a hot key across N partitions.</p></li></ul></li><li><p><strong>Handle Skewed Keys Separately</strong></p><ul><li><p><strong>When you can isolate a small number of skewed keys</strong>.</p></li><li><p>Split data into &#8220;skewed&#8221; vs. &#8220;rest&#8221;; optimize skewed subset, then union.</p></li></ul></li></ul><p>By applying these strategies in order&#8212;starting with AQE&#8217;s automatic handling, then broadcasting small tables, and, if necessary, resorting to manual salting or custom partitioning&#8212;you can eliminate or dramatically reduce skew-related stragglers in your Spark join operations. Choose the approach that best fits your cluster&#8217;s Spark version, data volume, and the complexity you&#8217;re willing to maintain.</p><p></p><p></p><h2>Architectural Patterns and Data Design to Reduce Skew</h2><p>Beyond individual Spark jobs, you can sometimes address skew at the <strong>data architecture level</strong> to prevent issues before they happen:</p><ul><li><p><strong>Skew-Aware Data Partitioning:</strong> As discussed, designing how data is partitioned or bucketed in storage can reduce skew. For example, if you frequently group or join by a key that&#8217;s skewed, consider storing the data partitioned by that key <em>and a secondary split</em>. A real-world practice: if one category of data is 90% of the dataset, you might partition that category&#8217;s data further by another field. Essentially, <strong>acknowledge the skewed key in your data model</strong> and subdivide it. This could mean separate tables or partitions for heavy categories. When you process the data, you then handle those partitions in parallel. The benefit is you're not repeatedly shuffling the entire dataset to discover the same skew; you&#8217;ve pre-divided it.</p></li><li><p><strong>Pre-Aggregation / Summaries:</strong> If your use-case allows, maintain rolling aggregates for skewed keys. For instance, if one user has a million events per day and you always compute their daily total, consider updating a running total for that user in a database or a separate file, rather than recomputing from scratch in each Spark job. By reducing the raw data volume for that key through prior aggregation, you avoid the huge shuffle for that key at query time. This is applicable in pipelines where data is appended incrementally (common in streaming or daily ETL). You trade off storage (keeping summary data) for performance.</p></li><li><p><strong>Alternate Algorithms:</strong> In some cases, you might choose a different approach entirely. For example, for a skewed distinct count, using an approximate algorithm (like HyperLogLog) per partition can avoid bringing all data together. Or using Bloom filters to reduce data before join (filter out records that won&#8217;t match). These are specific to certain problems but can mitigate skew by cutting down the data processed.</p></li><li><p><strong>Scaling Up Hot Data Separately:</strong> This is more of an infrastructure pattern &#8211; if one key&#8217;s data is massive, you could route that to a specialized system. For instance, maybe that one key corresponds to a particular customer &#8211; you could give them their own dedicated processing or database, and exclude those records from the general Spark workflow. It&#8217;s an extreme solution, but sometimes separating concerns (multi-tenancy isolation) helps if one tenant&#8217;s data skews the whole system.</p></li><li><p><strong>Monitoring and Iteration:</strong> A softer &#8220;pattern&#8221; is to continuously monitor your Spark job metrics (especially in Spark UI or via logs) to catch skew issues and then adjust. Over time, you may adapt your data ingestion or job logic to handle new skewed keys as data grows. For example, if a new user becomes a power user, you might add them to the &#8220;skewed key list&#8221; for salting. In practice, skew patterns can change, so an architecture that can adjust (or a code path that can automatically detect top N heavy keys and treat them differently) can be very useful.</p></li></ul><p>In essence, architectural approaches are all about <strong>not putting all eggs in one basket</strong> &#8211; distribute data smartly from the ground up, and treat the outliers with special care. This reduces the burden on any single Spark job to handle an immense skew on the fly.</p><h2>References</h2><ul><li><p><a href="https://docs.databricks.com/aws/en/optimizations/aqe#dynamically-handle-skew-join">https://docs.databricks.com/aws/en/optimizations/aqe#dynamically-handle-skew-join</a></p></li><li><p><a href="https://medium.com/@suffyan.asad1/handling-data-skew-in-apache-spark-techniques-tips-and-tricks-to-improve-performance-e2934b00b021">https://medium.com/@suffyan.asad1/handling-data-skew-in-apache-spark-techniques-tips-and-tricks-to-improve-performance-e2934b00b021</a></p></li><li><p><a href="https://www.databricks.com/discover/pages/optimize-data-workloads-guide">https://www.databricks.com/discover/pages/optimize-data-workloads-guide</a></p></li><li><p><a href="https://www.dataengi.com/post/2019/02/06/spark-data-skew-problem/#:~:text=We%20can%20reduce%20data%20skew,impact%20of%20data%20skew%20before">https://www.dataengi.com/post/2019/02/06/spark-data-skew-problem/#:~:text=We%20can%20reduce%20data%20skew,impact%20of%20data%20skew%20before</a></p></li><li><p><a href="https://spark.apache.org/docs/3.5.3/sql-performance-tuning.html#:~:text=,3.0.0">https://spark.apache.org/docs/3.5.3/sql-performance-tuning.html#:~:text=,3.0.0</a></p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Decode the Join: A Spark Data Engineer’s Visual Handbook]]></title><description><![CDATA[Understand when and why to use Broadcast, Shuffle, or Sort-Merge Joins in Spark&#8212; with clear visuals, real-world use cases, and strategy tips tailored for data engineers.]]></description><link>https://www.canadiandataguy.com/p/spark-join-strategies</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/spark-join-strategies</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Fri, 09 May 2025 23:55:27 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Ol6C!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Ever stared at a Spark job and wondered which join strategy it picked&#8212;and why your cluster suddenly feels like it&#8217;s running through molasses?</strong> This visual handbook is here to help. Whether you're optimizing joins in production or just trying to wrap your head around what happens under the hood, this guide breaks down <strong>Broadcast, Shuffle, and Sort-Merge Joins</strong> using clear diagrams, code snippets, and real-world scenarios. Decode the logic, spot the trade-offs, and make smarter join decisions in your next big data pipeline.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ol6C!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ol6C!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png 424w, https://substackcdn.com/image/fetch/$s_!Ol6C!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png 848w, https://substackcdn.com/image/fetch/$s_!Ol6C!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png 1272w, https://substackcdn.com/image/fetch/$s_!Ol6C!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ol6C!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:6714,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4450582,&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://www.canadiandataguy.com/i/163105837?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Ol6C!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png 424w, https://substackcdn.com/image/fetch/$s_!Ol6C!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png 848w, https://substackcdn.com/image/fetch/$s_!Ol6C!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.png 1272w, https://substackcdn.com/image/fetch/$s_!Ol6C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d23fec9-6bb9-4cd0-a35a-4535174f4e9b_2592x11952.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"><svg role="img" 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"><g><title></title><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"><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"><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>A big thank you to </strong><span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Canadian Data Guy&quot;,&quot;id&quot;:9073721,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1441c9b8-d40b-4ac7-b91f-4260f55db017_2586x2586.jpeg&quot;,&quot;uuid&quot;:&quot;913f0f9f-df77-4336-96c9-28154636a697&quot;}" data-component-name="MentionToDOM"></span> <strong>for the opportunity to contribute to this space.</strong> It&#8217;s always a pleasure to share insights with fellow data enthusiasts. If this visual guide helped demystify Spark joins for you, feel free to share your thoughts or questions in the comments&#8212;I&#8217;d love to hear from you!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Why Your PySpark UDF Is Slowing Everything Down]]></title><description><![CDATA[An in-depth exploration of architecture, execution flow, bottlenecks, and optimization strategies for PySpark UDFs]]></description><link>https://www.canadiandataguy.com/p/why-your-pyspark-udf-is-slowing-everything</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/why-your-pyspark-udf-is-slowing-everything</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Thu, 24 Apr 2025 22:39:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!21cQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>1. Introduction</h2><p>PySpark&#8217;s User Defined Functions (UDFs) empower developers to inject custom Python logic into Spark DataFrames. They feel like a convenient escape hatch when built-in SQL functions don&#8217;t cut it. However, under the hood, each UDF invocation triggers a complex ballet of inter-process communication, serialization, and single-threaded Python loops. This blog peels back each layer of that architecture to reveal why PySpark UDFs can become a massive performance drain &#8212; and then walks through concrete alternatives and optimizations to keep your jobs blazing fast.</p><div><hr></div><h2>2. The Problem with PySpark UDFs</h2><p>When you sprinkle UDF calls across your Spark SQL or DataFrame pipeline, you&#8217;re effectively handing off portions of your query plan to a &#8220;black box&#8221; Python function. That comes at a steep cost:</p><h3>2.1 Catalyst Optimizer Becomes Blind</h3><ul><li><p><strong>No predicate pushdown:</strong> Spark&#8217;s Catalyst optimizer can&#8217;t inspect or reorder the logic inside your UDF, so it abandons optimizations like pushing filters down to data sources.</p></li><li><p><strong>No whole-stage code generation:</strong> The code-gen engine can&#8217;t fuse your UDF into JVM bytecode, so you lose out on compiler-level speed gains.</p></li></ul><h3>2.2 Serialization/Deserialization Overhead</h3><ul><li><p><strong>Row-by-row data shuffling:</strong> Each row must be marshalled from the JVM heap into a Python object, sent over a local socket, then converted back. After your Python code runs, the result takes the reverse path back into the JVM.</p></li><li><p><strong>Millions of crossings:</strong> With millions (or even billions) of rows, that boundary-crossing cost balloons.</p></li></ul><h3>2.3 Single-Threaded Python Execution</h3><ul><li><p><strong>Global Interpreter Lock (GIL):</strong> Your UDF runs in a standard CPython process under a single core. All per-row work happens sequentially.</p><p>ide the UDF.</p></li></ul><h3>2.4 Memory and Stability Risks</h3><ul><li><p><strong>Python OOMs:</strong> Unlike JVM operations, Spark doesn&#8217;t manage Python worker memory. Processing large batches can crash with out-of-memory errors.</p></li><li><p><strong>Uncaught exceptions:</strong> A bug in your UDF can fail an entire Spark task. Null handling, pickling errors, and non-serializable closures often catch teams by surprise.</p></li></ul><div><hr></div><h2>3. Under the Hood: PySpark&#8217;s Dual-Runtime Architecture</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!21cQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!21cQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png 424w, https://substackcdn.com/image/fetch/$s_!21cQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png 848w, https://substackcdn.com/image/fetch/$s_!21cQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png 1272w, https://substackcdn.com/image/fetch/$s_!21cQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!21cQ!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png" width="1200" height="532.4175824175824" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/62cbed82-2c4b-48a4-b0c0-5d53d50e2737_3840x1705.png&quot;,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:646,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:282499,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.canadiandataguy.com/i/162008971?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62cbed82-2c4b-48a4-b0c0-5d53d50e2737_3840x1705.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!21cQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png 424w, https://substackcdn.com/image/fetch/$s_!21cQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png 848w, https://substackcdn.com/image/fetch/$s_!21cQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png 1272w, https://substackcdn.com/image/fetch/$s_!21cQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69a18ec1-e86f-4e35-9a40-4a0512545c0f_3840x1705.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Py4J is a communication bridge/library that lets Python and Java interoperate by exchanging objects over sockets. In Spark, it powers two key workflows: setting up the Python <code>SparkContext</code> and converting data types in PySpark SQL. When you start a PySpark session, Py4J opens a socket connection between your Python driver and the underlying Java driver. Later, whenever Spark SQL operations run, Py4J translates Python types into their Java equivalents (and back) so the Python API can seamlessly drive the JVM-based SQL engine. Under the hood, every Python UDF invocation follows this path:</p><pre><code>Python Driver &#8594; SparkContext &#8594; Py4J &#8594; JVM &#8594; JavaSparkContext  </code></pre><p>Because each UDF call must cross this socket boundary, it adds measurable latency to your job.</p><h3>3.1 Py4J: Bridging Python and the JVM</h3><p>At startup, PySpark uses <a href="https://www.py4j.org/">Py4J</a> to:</p><ol><li><p><strong>Connect the Python driver to the JVM driver.</strong></p></li><li><p><strong>Translate data types</strong> between Python and Java during SQL operations and UDF calls.</p></li></ol><p>Every call into Spark SQL or a UDF crosses this bridge &#8212; think of it as a high-latency tunnel for each record.</p><h3>3.2 Driver, Executors, and Python Workers</h3><ol><li><p><strong>Driver (Python process):</strong> You call <code>df.withColumn("foo", my_udf(col("bar")))</code>.</p></li><li><p><strong>JVM Driver:</strong> Receives the UDF registration, plans the query.</p></li><li><p><strong>Executor JVMs:</strong> Spin up separate Python subprocesses per task.</p></li><li><p><strong>Python Workers:</strong> Handle the actual UDF logic on deserialized batches.</p></li></ol><div><hr></div><h2>4. Lifecycle of a PySpark UDF Call</h2><h3>4.1 Registration &amp; Serialization of the Python Function</h3><pre><code>from pyspark.sql.functions import udf
from pyspark.sql.types import StringType

def uppercase(val):
    return val.upper()

uppercase_udf = udf(uppercase, StringType())
</code></pre><ul><li><p><code>_create_udf</code> wraps your Python function into a serializable form and tags it with return types.</p></li><li><p><strong>UDF object</strong> travels in the Spark plan to all executors.</p></li></ul><h3>4.2 Data Flow on Executors</h3><ol><li><p>Executor receives a task partition.</p></li><li><p>JVM serializes partition rows into Arrow or Pickle bytes.</p></li><li><p>Bytes stream over TCP to the Python worker.</p></li><li><p>Python worker deserializes, applies your function row-by-row.</p></li><li><p>Results are serialized back to JVM for further operators.</p></li></ol><h3>4.3 Detailed Serialization Cycle</h3><pre><code>JVM row object
  &#9492;&#9472;serialize&#9472;&#9654; Python bytes
      &#9492;&#9472;deserialize&#9472;&#9654; Python object
           &#9492;&#9472;apply UDF&#9472;&#9654; Python object
                &#9492;&#9472;serialize&#9472;&#9654; Python bytes
                     &#9492;&#9472;JVM bytes
                          &#9492;&#9472;deserialize&#9472;&#9654; JVM row</code></pre><p>Multiply that by every row, every partition, every stage &#8212; and you see why simple operations feel so sluggish.</p><div><hr></div><h2>5. Performance Implications</h2><h3>5.1 Quantifying the Overhead</h3><ul><li><p><strong>Catalyst loss:</strong> 10&#8211;30% longer query planning in UDF-heavy jobs.</p></li><li><p><strong>Serialization tax:</strong> 0.5&#8211;5 ms per row crossing (tested on medium-sized clusters).</p></li><li><p><strong>CPU utilization:</strong> &lt; 25% CPU usage across nodes despite heavy transforms.</p></li></ul><h3>5.2 Real-World Benchmark Example</h3><blockquote><p><strong>Scenario:</strong> Uppercasing a 100 million-row column.</p><ul><li><p><strong>Native Spark SQL:</strong></p></li></ul><pre><code>df.selectExpr("upper(name) as name")</code></pre><p>&#8594; 12 seconds end-to-end</p><ul><li><p><strong>Python UDF:</strong></p></li></ul><pre><code>df.withColumn("name", uppercase_udf("name"))</code></pre><p>&#8594; reorders, serialization, single-thread overhead &#8594; <strong>85 seconds</strong><br><em>7&#215; slower for a trivial transform.</em></p></blockquote><div><hr></div><h2>6. Strategies for Faster Custom Logic</h2><h3>6.1 Leverage Built-in Spark Functions</h3><p>Whenever possible, reach for Spark&#8217;s SQL functions (<code>upper</code>, <code>concat</code>, <code>regexp_replace</code>, etc.) &#8212; they run entirely in the JVM, enjoy whole-stage codegen, and scale across all cores.</p><h3>6.2 Pandas UDFs (Vectorized)</h3><p>Introduced in Spark 2.3, Pandas UDFs batch rows into <code>pandas.Series</code> and use <a href="https://arrow.apache.org/">Apache Arrow</a> for zero-copy transfer.</p><pre><code>from pyspark.sql.functions import pandas_udf
from pyspark.sql.types import StringType
import pandas as pd

@pandas_udf(StringType())
def upper_series(s: pd.Series) -&gt; pd.Series:
    return s.str.upper()

df.withColumn("name", upper_series("name"))
</code></pre><ul><li><p><strong>Batch size:</strong> Typically 8 K&#8211;64 K rows per call</p></li><li><p><strong>Vectorized ops:</strong> Internal loops in C, parallelized across cores in Python worker</p></li><li><p><strong>Results:</strong> 5&#8211;10&#215; speed-up over row-UDFs</p></li></ul><h3>6.3 Scala/Java UDFs</h3><p>If you need custom logic beyond SQL but want JVM speed:</p><ol><li><p><strong>Write a Scala object</strong> implementing <code>UserDefinedFunction</code>.</p></li><li><p><strong>Register it</strong> via <code>spark.udf.registerJava(...)</code>.</p></li><li><p><strong>Invoke</strong> from PySpark as if it were a native function.</p></li></ol><ul><li><p><strong>No Python serialization</strong> needed.</p></li><li><p><strong>Runs inside the executor JVM</strong> with full multi-core utilization.</p></li></ul><h3>6.4 Threading &amp; Parallelism in Python UDFs</h3><p>If you absolutely must call an external API or library row-by-row:</p><ul><li><p><strong>Use multithreading</strong> inside your Python UDF to hide network latency.</p></li><li><p><strong>Batch HTTP calls</strong> where possible.</p></li><li><p><strong>Be cautious</strong>: GIL still applies for CPU-bound work, and thread pools can exhaust memory.</p></li></ul><div><hr></div><h2>7. Common Pitfalls &amp; Debugging Tips</h2><ul><li><p><strong>PicklingError:</strong> Ensure functions and closures reference only top-level functions and serializable objects.</p></li><li><p><strong>Null handling:</strong> Always guard inputs with <code>if v is None: return None</code>.</p></li><li><p><strong>Schema drift:</strong> Explicitly set return types; mismatches lead to confusing errors at shuffle boundaries.</p></li><li><p><strong>Memory leaks:</strong> Monitor Python worker logs for <code>MemoryError</code> and tune <code>spark.python.worker.memory</code>.</p></li></ul><div><hr></div><h2>8. Summary &amp; Best Practices</h2><blockquote><p><em>Our newsletter is 100% free and always will be, but without your claps, comments, or shares, search engines may bury this post forever. A quick <strong>clap</strong> not only tells us this content resonates but also makes sure you (and everyone else) can find it again when it matters most.</em></p></blockquote><ol><li><p><strong>Avoid plain Python UDFs</strong> whenever built-in Spark SQL functions suffice.</p></li><li><p><strong>Prefer Pandas UDFs</strong> for vectorized, batch transforms&#8212;they dramatically reduce boundary crossings via Apache Arrow. In fact, the vectorized nature and rapid Arrow improvements often make Pandas UDFs faster than even Scala/Java UDFs.</p></li><li><p><strong>Consider Scala/Java UDFs</strong> only when you need JVM-native logic that can&#8217;t be expressed in SQL or Pandas UDFs.</p></li><li><p><strong>Design for serializability</strong>: keep UDFs self-contained, stateless, and null-safe.</p></li><li><p><strong>Benchmark early</strong>: compare native vs. Pandas vs. Python vs. Scala/Java UDFs on representative data.</p></li><li><p><strong>Moving forward, hands down use native functions first, then Pandas UDFs in almost all cases.</strong></p></li><li><p><strong>When you must call external APIs inside a UDF loop</strong>, embed threading or async parallelism to help latency&#8212;see this <a href="https://www.youtube.com/watch?v=n9jodzYq1e4">video on parallelization within a loop</a> for an example.</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!86fC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!86fC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png 424w, https://substackcdn.com/image/fetch/$s_!86fC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png 848w, https://substackcdn.com/image/fetch/$s_!86fC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png 1272w, https://substackcdn.com/image/fetch/$s_!86fC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!86fC!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png" width="1200" height="243.95604395604394" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:296,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:75946,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.canadiandataguy.com/i/162008971?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!86fC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png 424w, https://substackcdn.com/image/fetch/$s_!86fC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png 848w, https://substackcdn.com/image/fetch/$s_!86fC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png 1272w, https://substackcdn.com/image/fetch/$s_!86fC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd7d0814-b5de-4e1a-a4a7-fc74d5717ea9_1670x340.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>By understanding the multi-stage journey of data through the PySpark UDF pipeline &#8212; from JVM serialization, through Python&#8217;s single-threaded interpreter, back to the JVM &#8212; you can make informed choices that balance flexibility with performance. Next time you need custom logic, pause to ask: <em>&#8220;Can I batch or vectorize? &#8221;</em> Your cluster (and your users) will thank you.</p><p><a href="https://www.databricksters.com/p/everything-you-ever-wanted-to-know">To learn more about how to improve things, read our deep dive blog on Pandas UDF. </a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ybZ0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ybZ0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!ybZ0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!ybZ0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!ybZ0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ybZ0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&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_!ybZ0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!ybZ0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!ybZ0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!ybZ0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e362a94-9995-4b0b-8814-05005ee33c03_1536x1024.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><h3>References</h3><ul><li><p>Ganesh, R. &#8220;Is really UDF hitting the performance in PySpark!&#8221; <em>Medium</em>, Jul 5, 2024. <a href="https://medium.com/%40rganesh0203/udf-is-hitting-the-performance-in-pysaprk-817b7e881dd2?utm_source=chatgpt.com">Medium</a></p></li><li><p></p><div id="youtube2-n9jodzYq1e4" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;n9jodzYq1e4&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/n9jodzYq1e4?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div></li><li><p>AWS Documentation. &#8220;Optimize user-defined functions,&#8221; <em>Tuning AWS Glue for Apache Spark</em> (AWS Prescriptive Guidance). <a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/tuning-aws-glue-for-apache-spark/optimize-user-defined-functions.html?utm_source=chatgpt.com">AWS Documentation</a></p></li><li><p>Tang, T. &#8220;Spark functions vs UDF performance?&#8221; <em>Stack Overflow</em>, Mar 5, 2018. <a href="https://stackoverflow.com/questions/38296609/spark-functions-vs-udf-performance?utm_source=chatgpt.com">Stack Overflow</a></p></li><li><p>Databricks. &#8220;Arrow-optimized Python UDFs in Apache Spark&#8482; 3.5,&#8221; <em>Databricks Blog</em>, Aug 26, 2024. <a href="https://www.databricks.com/blog/arrow-optimized-python-udfs-apache-sparktm-35?utm_source=chatgpt.com">Databricks</a></p></li><li><p>&#8220;Why You Should Avoid Using UDFs in PySpark,&#8221; <em>Det.Life Blog</em>, Jan 2024. <a href="https://blog.det.life/why-you-should-avoid-using-udf-in-pyspark-c57558af9d0a?utm_source=chatgpt.com">Data Engineer Things</a></p></li><li><p>Illustrious_Ad4259. &#8220;Are there any major disadvantages in performance for Spark when using PySpark?&#8221; <em>Reddit r/dataengineering</em>, Nov 2021. <a href="https://www.reddit.com/r/dataengineering/comments/qning9/are_there_any_major_disadvantages_in_performance/?utm_source=chatgpt.com">Reddit</a></p></li><li><p>Sen, Soutir. &#8220;PySpark UDFs (User-Defined Functions) &#8211; Complete Guide,&#8221; <em>LinkedIn Article</em>, Dec 2024. <a href="https://www.linkedin.com/pulse/pyspark-udfs-user-defined-functions-complete-guide-soutir-sen-jkd6f?utm_source=chatgpt.com">linkedin.com</a></p></li><li><p>Two Sigma. &#8220;Introducing Pandas UDFs for PySpark,&#8221; <em>Two Sigma Article</em>.</p><p></p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[What a Netflix Senior Data Engineer Taught Us About Winning in Tech—And It’s Not What You Think]]></title><description><![CDATA[Spoiler: Tech is easy. Business is hard. And your ability to communicate might just be your biggest flex]]></description><link>https://www.canadiandataguy.com/p/what-a-netflix-senior-data-engineer</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/what-a-netflix-senior-data-engineer</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Thu, 17 Apr 2025 00:26:11 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!p2jM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.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="https://substackcdn.com/image/fetch/$s_!p2jM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!p2jM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.png 424w, https://substackcdn.com/image/fetch/$s_!p2jM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.png 848w, https://substackcdn.com/image/fetch/$s_!p2jM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.png 1272w, https://substackcdn.com/image/fetch/$s_!p2jM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!p2jM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.png" width="1456" height="439" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:439,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:800046,&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://www.canadiandataguy.com/i/161485517?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.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_!p2jM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.png 424w, https://substackcdn.com/image/fetch/$s_!p2jM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.png 848w, https://substackcdn.com/image/fetch/$s_!p2jM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.png 1272w, https://substackcdn.com/image/fetch/$s_!p2jM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb54acfe3-9bf9-4ee0-aa49-1b1a79edad22_1586x478.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"><svg role="img" 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"><g><title></title><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"><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"><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>We recently had an enriching conversation with <strong><a href="https://www.linkedin.com/in/jarriett/">Jarriett, a Senior Data Engineer at Netflix</a></strong>. His success at Netflix isn't a mere coincidence&#8212;it's a powerful story of continuous learning, resilience, and compounded experience accumulated over many years. Individuals with 18 years of hands-on engineering experience, retaining his extraordinary level of curiosity, are indeed rare. Here's a distilled summary of his key recommendations:</p><h3>"Use Custom GPT to Enhance Productivity"</h3><p>Jarriett shared practical tips on utilizing AI tools, such as custom GPTs, to efficiently prepare for tasks like interviews. Leveraging these technologies effectively can save time, maintain focus, and elevate productivity.</p><h3>"Tell Your Project Story Like a Founder"</h3><p>When approaching behavioral interviews or discussions, Jarriett advises framing your projects as a founder would. Clearly articulate the problem, your process, the impact, and lessons learned. This narrative style demonstrates ownership and deep business understanding.</p><h3>"Tech is Easy&#8212;Business is Tough"</h3><p>One of the most impactful points Jarriett emphasized was that technical problems, while complex, are relatively straightforward compared to the nuanced, intricate challenges presented by business contexts. AI is increasingly managing technical tasks, highlighting the necessity for professionals to develop competencies AI cannot easily replicate, such as strategic business understanding and effective communication.</p><h3>"Document Clearly and Tailor to Your Audience"</h3><p>Effective documentation was another crucial lesson from Jarriett. He emphasizes tailoring your documentation to your audience: provide deep technical details when communicating with engineers and offer higher-level insights when engaging with business stakeholders. Documenting clearly ensures your contributions are recognized and remain influential long after your direct involvement. Additionally, he highlighted that thorough documentation makes your name appear frequently in internal searches and documents, thereby building recognition and visibility across your organization.</p><h3>"Step Out of the Tech Bubble and Be a Detective"</h3><p>Professionals should actively engage with the business side of projects, understanding stakeholders' real-world problems and motivations. Jarriett suggested adopting a detective-like mindset: asking probing questions, being curious, and identifying underlying business problems when talking to teams. This approach enhances your value in projects and reflects positively during performance reviews.</p><h3>"Stay Curious and Effective"</h3><p>Curiosity was repeatedly highlighted as a key trait. Being genuinely interested in exploring and solving problems fosters continuous growth and innovation. Jarriett exemplified effectiveness by integrating continuous learning into everyday tasks, such as listening to audio transcriptions of books during routine activities.</p><h3>"Write Understandable, Simple Code"</h3><p>He cautioned against overly clever or compact coding practices. Clear, understandable, and well-commented code is far more valuable than code that saves a few lines but is opaque to your colleagues. Simplicity in coding enhances maintainability and collaboration.</p><h3>"Resilience and Positive Mindset"</h3><p>Jarriett's journey to Netflix was marked by resilience. Even after initially facing setbacks in the interview process, his positive attitude, continued self-improvement, and unwavering resilience eventually led him to success.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption"> Please hit a like or drop a  comment if you found it valuable</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><h3>Recommended Reads for Personal Growth</h3><p>Finally, Jarriett generously shared several book recommendations that have significantly influenced his professional outlook:</p><ul><li><p><a href="https://www.amazon.com/Culture-Map-INTL-ED-Decoding/dp/1610392760?crid=OE7BPCYMFDR7&amp;dib=eyJ2IjoiMSJ9.daN77oCLHEgEPnI-8RDpmj4oFABjEJk1eCoYVqGgW2-JTmtgM_OuTcaVFZ4INl2qT2dbRWNMRMKqRRxxC3N_xw_mE6TWS9bzbTWOahKmrIhDkKTS-NlDgSe79nKXMoCjAhyHiUII2eURFC-63nmOy2dfACZj-K_w04keCduVdjtnBuQzgGQqj5Oo777zk_we1wyC7cCfyK8qQnwsLUZuCCEUMNoK5D7-pv0IEVyFl2E.hPZ-Y3zQP2Nd0TExKeZLCieKZ7BsZ8h4odZpfsPF3sI&amp;dib_tag=se&amp;keywords=The+Culture+Map&amp;qid=1744836601&amp;sprefix=the+culture+map%2Caps%2C129&amp;sr=8-2&amp;linkCode=ll1&amp;tag=canadiandat06-20&amp;linkId=9e88c0d65a423867c5ced48427ba9ec3&amp;language=en_US&amp;ref_=as_li_ss_tl">The Culture Map</a></p></li><li><p><a href="https://www.amazon.com/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321?crid=2Y1VDCXZBUPTJ&amp;dib=eyJ2IjoiMSJ9.YcwA3XNSsL2_nKIjWj8V-JX0ax1QuQsHY1jekh_fsd5cM7Yo316rL62OpJmw-1QpsHHy8JY6a8hUHsrPRaHovS0P6T690YD_T5G499X4GlUU3dhH4C_TtUyyvEBSiQZ-bOyh8CcX2xBVWFWQsT9VfVhNuWWHL38Prr416fpfWN54qEDMNmEYeGg4pjs3fJ0e-wYsb7fsPpNDI9zD5Uoa1QD3DLpqRBJUbMT9JNUZl3k.uZGfUGbv9pRbqTrzgBTO8KpkjYkWFKjdpEF6iqP5wG8&amp;dib_tag=se&amp;keywords=Designing+Data+Intensive+Applications&amp;qid=1744836748&amp;sprefix=designing+data+intensive+applications%2Caps%2C123&amp;sr=8-1&amp;linkCode=ll1&amp;tag=canadiandat06-20&amp;linkId=76ef2ff538f8e236d34da9e1dd0c2a61&amp;language=en_US&amp;ref_=as_li_ss_tl">Designing Data Intensive Applications</a></p></li><li><p><a href="https://www.amazon.com/Fundamentals-Data-Engineering-Robust-Systems/dp/1098108302?crid=SYQ4QBQVTQL1&amp;dib=eyJ2IjoiMSJ9.ca0rA-eLmjtu9d4hMSmZ3ol-NmdM7f20sjvCegDPDTqD-ed566tMQfXg9WZTFnx4Nh5UktOF4V11zYuwPhPtCiC3XstXaKGuJNYE3cCY4Uvq-mlBbcH0FXgtgzFqg07JaWaDlj7J6G6yhImdb8d9ZR38PqhkIYj3mDxr95584eHcaJ3z0voilT6KzYjcTdsZIwrRAYsNL6tPhEPrzzZ0Tgo190r8no89qPt5GmrOQjQ.YtzNgzaOjqhiWuwOsW_TZUAAOLAtPEvfFt_Sh0q4THY&amp;dib_tag=se&amp;keywords=Fundamentals+of+Data+Engineering&amp;qid=1744836796&amp;sprefix=fundamentals+of+data+engineering%2Caps%2C119&amp;sr=8-1&amp;linkCode=ll1&amp;tag=canadiandat06-20&amp;linkId=a5ccd5b33c4e9348f0de808645fbc7a9&amp;language=en_US&amp;ref_=as_li_ss_tl">Fundamentals of Data Engineering</a></p></li><li><p><a href="https://www.amazon.com/Good-to-Great-Jim-Collins-audiobook/dp/B003VXI5MS?crid=21RW41K58PJDZ&amp;dib=eyJ2IjoiMSJ9.zoq7ofISwxsBwvV3ss-VOk5uRw8FiNwQOrAh-VvWpvgq35Kk_4ITUdDQKFblghg9yK3iW8_1HmZpjo7P_Kp2yXw6aU5EMFZpD-a5z-VUBHtsyyvTHAbPGv1W2nC9X096sGww_h5NCAp_DLQfbRpSyFR8JJsztKz_GG5ve6u4eE11OSqSiOrnIzF0lRajMm971R5JhYjUnosxfkkia9wQQpu2oSKhb4Zb6aelLgk3N1s.o2y_5IU2gfrBp7lbgZOSaodmq2UEQiV-ZaxL0h4Mgwc&amp;dib_tag=se&amp;keywords=Good+to+Great&amp;qid=1744836820&amp;sprefix=good+to+great%2Caps%2C130&amp;sr=8-1&amp;linkCode=ll1&amp;tag=canadiandat06-20&amp;linkId=0b438eb60740ac92bcaf6d160d83c967&amp;language=en_US&amp;ref_=as_li_ss_tl">Good to Great</a></p></li><li><p><a href="https://www.amazon.com/Influence-New-Expanded-Psychology-Persuasion/dp/B08RLT11Q3?crid=19PMFJ9S7N357&amp;dib=eyJ2IjoiMSJ9.i1n6xlrrhQPxRJx9GvliUJXsvEyrJvd5YD18oCW53whro2VbV2sA3C5umG5TKYielZACY5ODT_xqRHv16okt4862ZDnmwawJguVbgXVNZJ-0GHqGAPQM-Sg-HBGzEC7c76no5mNZ_VcO1CA4ojy5BIy1mPv_x5dSGxnM6yls56FERWTd6XsrQ8wlvFTzkfQg2-zlY0vbwaHvxLKqFpZDTRKmVE3sWiTWH8ErnH39YNk.oMtTnTtp0EE3YIXQ6cvIo3x7QJDCpEs2hHZDM3XkqX8&amp;dib_tag=se&amp;keywords=Influence%3A+The+Psychology+of+Persuasion&amp;qid=1744836858&amp;s=audible&amp;sprefix=influence+the+psychology+of+persuasion%2Caudible%2C116&amp;sr=1-1&amp;linkCode=ll1&amp;tag=canadiandat06-20&amp;linkId=f68fd8401005f9f8890c37bc8fba2bf2&amp;language=en_US&amp;ref_=as_li_ss_tl">Influence: The Psychology of Persuasion</a></p></li><li><p><a href="https://www.amazon.com/Thinking-Fast-and-Slow-audiobook/dp/B005Z9GAJG?crid=3L8GFMSLM221X&amp;dib=eyJ2IjoiMSJ9.z_dGNSlUbLtLp6Gr08CXwnCOODwQZ7xa10pGyNdqA1vfbVRHURLQ5xk4OV31EzPBhVPQA2Ga-cmIXIVjjYUsYNRW99EoGfrLLmwim9WaZsQm5vpNtDtVjgGL-2r-1Lav9GkSZOGVdI2t8Pre38tTZxVhSrQvGhvq-Lo3fw3mcHmXiume5wxOcbGfx31xuaxe._1tqMtQIhFDm3UYyafiuoN6J1NdgtRKhg5OGxrDoOys&amp;dib_tag=se&amp;keywords=Thinking%2C+Fast+and+Slow&amp;qid=1744836891&amp;s=audible&amp;sprefix=thinking%2C+fast+and+slow%2Caudible%2C109&amp;sr=1-1&amp;linkCode=ll1&amp;tag=canadiandat06-20&amp;linkId=ee5be85ccce920b62ccecb1f2eeaf800&amp;language=en_US&amp;ref_=as_li_ss_tl">Thinking, Fast and Slow</a></p></li><li><p><a href="https://www.amazon.com/Staff-Engineer-Leadership-Beyond-Management/dp/B097CNXP89?crid=2SKRSHRERMIGJ&amp;dib=eyJ2IjoiMSJ9.TRxLEgZ6Ndq7Q5nLfVJJNw.jgZEy224UplNsJM3hCSI2gIMViCbF4Mad1uMUGg2LYI&amp;dib_tag=se&amp;keywords=Staff+Engineer%3A+Leadership+Beyond+the+Management+Track&amp;qid=1744836920&amp;s=audible&amp;sprefix=staff+engineer+leadership+beyond+the+management+track%2Caudible%2C114&amp;sr=1-1&amp;linkCode=ll1&amp;tag=canadiandat06-20&amp;linkId=4fa18e457a13e1949da76af838949304&amp;language=en_US&amp;ref_=as_li_ss_tl">Staff Engineer: Leadership Beyond the Management Track</a></p></li><li><p><a href="https://www.amazon.com/Antifragile-Nassim-Nicholas-Taleb-audiobook/dp/B00A2ZIZYQ?crid=S3DG1KANFF6A&amp;dib=eyJ2IjoiMSJ9.VDnkKbIijW6LwOFKA-POIpGm2oiB3TKDeU0kahusBmPGjHj071QN20LucGBJIEps.mvRW2qU_jrBagV9XiDqZ07CMkyLtuP87cCBoo8C433k&amp;dib_tag=se&amp;keywords=Antifragile%3A+Things+that+Gain+From+Disorder&amp;qid=1744836942&amp;s=audible&amp;sprefix=antifragile+things+that+gain+from+disorder%2Caudible%2C121&amp;sr=1-1&amp;linkCode=ll1&amp;tag=canadiandat06-20&amp;linkId=706f8324d7e7b04da637168e49ca146e&amp;language=en_US&amp;ref_=as_li_ss_tl">Antifragile: Things that Gain From Disorder</a></p></li><li><p><a href="https://www.amazon.com/Phoenix-Project-Graphic-Helping-Business/dp/1950508919?crid=175DOJYXAYZCB&amp;dib=eyJ2IjoiMSJ9.ekpqmsJ8TDcVLeSxEtff7gd1wPiB4IghLFTSicevg-jpwdZibbArScT4LF2vBI3S_Wz0IeWn4VRnow1vtV4ZATSoccusJIRs85UOvT0PST79dqO7yfLmzbHaLBQRPRCm93pq_8DJyCWoc4n45vEjVVhLFlOYvdxbTuKId8im-JnCbVIclO2eAS8AX8Sp3XfLWoas1q5Iyr0UiU03-ejhSgqP9qHOR2PFaafBxdEVgkY.X9vymYq2HYVuUa8vX9CxFoX0m2preM-AVsqTE2LR2XY&amp;dib_tag=se&amp;keywords=The+Phoenix+Project&amp;qid=1744836977&amp;sprefix=the+phoenix+project%2Caps%2C159&amp;sr=8-2&amp;linkCode=ll1&amp;tag=canadiandat06-20&amp;linkId=3d79aded796d45baa3ec718eb432eac4&amp;language=en_US&amp;ref_=as_li_ss_tl">The Phoenix Project</a></p></li><li><p><a href="https://www.amazon.com/Benjamin-Franklin-American-Walter-Isaacson/dp/074325807X?_encoding=UTF8&amp;dib_tag=se&amp;dib=eyJ2IjoiMSJ9.Zll0sQJOrndaWuE6ztrwK16ZPpQVClaQ4PTVkbr42Tn_Q0qa_Fuq73Fcetx8RzJVy3MjQlGfsfMxunbaMxxVdjZKeaf1IpHRwCCvmaQjC3V6K1uWxjwCiKmHW-_ETLfKKJW5LwMrNtoJNgpJ4BS_MHfBq1pogk4rvsH6kiuuofPkYnBeyN3hQkHjBO0p0tGGO1IHWLZuw77hr3Pl19SX7y9guW6_d0UTuTR2-7vxJWg.qQyyXyccMseUcgNYAAqExzGfCAi42qFgVg2JigbI3-4&amp;qid=1744836999&amp;sr=8-1&amp;linkCode=ll1&amp;tag=canadiandat06-20&amp;linkId=8cdc16ca7396cd421a2afec74a36ce9f&amp;language=en_US&amp;ref_=as_li_ss_tl">Ben Franklin: An American Life</a></p></li><li><p><a href="https://www.10minmba.com/">https://www.10minmba.com/</a></p></li></ul><p>In summary, Jarriett&#8217;s insights underscore the importance of balancing technical expertise with strategic business thinking, effective communication, continuous learning, and tailored documentation to excel as a data professional in an increasingly AI-driven world. We sincerely thank Jarriett for generously sharing his valuable time and insights. If you'd like to connect with him or explore more of his professional journey, you can find him on LinkedIn: <a href="https://www.linkedin.com/in/jarriett/">https://www.linkedin.com/in/jarriett/</a></p>]]></content:encoded></item><item><title><![CDATA[How Do I Think About Setting Spark Shuffle Partitions in 2025?]]></title><description><![CDATA[TLDR: A Quick Guide to setting Spark.Shuffle.Partitions, No Deep Dive Required]]></description><link>https://www.canadiandataguy.com/p/how-do-i-think-about-setting-spark</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/how-do-i-think-about-setting-spark</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Tue, 15 Apr 2025 21:36:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1B0p!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In 2025, overthinking about Spark shuffle partitions has become less critical thanks to modern innovations in the Spark ecosystem. In earlier years&#8212;say, 2015 to 2019&#8212;the default setting of 200 partitions often proved either too high or too low, prompting manual tuning and much deliberation. However, with advances like the Adaptive Query Engine, many of these decisions are now automatically managed, ensuring optimal performance without constant human intervention. This guide provides a streamlined decision tree to help you quickly determine if any manual adjustment is needed, so you can focus on higher-value aspects of your data processing work.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1B0p!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1B0p!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png 424w, https://substackcdn.com/image/fetch/$s_!1B0p!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png 848w, https://substackcdn.com/image/fetch/$s_!1B0p!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png 1272w, https://substackcdn.com/image/fetch/$s_!1B0p!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1B0p!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png" width="1200" height="881.8681318681319" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c40b64c5-4592-4329-a649-b2103a6f93e4_3840x2822.png&quot;,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1070,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:714500,&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://www.canadiandataguy.com/i/161416272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc40b64c5-4592-4329-a649-b2103a6f93e4_3840x2822.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1B0p!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png 424w, https://substackcdn.com/image/fetch/$s_!1B0p!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png 848w, https://substackcdn.com/image/fetch/$s_!1B0p!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.png 1272w, https://substackcdn.com/image/fetch/$s_!1B0p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93baebc6-75bd-4434-9843-f0f9fbb75d83_3840x2822.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"><svg role="img" 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"><g><title></title><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"><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"><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>How to calculate in-memory data size</strong></h2><p>When assessing data size for partitioning in Spark, it's important to note that the on-disk size&#8212;such as data stored in S3&#8212;does not always reflect the in-memory size. This is because data formats like Parquet or Avro are highly compressed, and the actual memory footprint can be 2 to 8 times larger than the file size on disk. Understanding the in-memory size is essential for properly tuning your shuffle partition settings.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">CanadianDataGuy&#8217;s No Fluff Newsletter is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>To accurately gauge this in-memory size, you can run the following Spark commands to trigger a computation and then inspect the Spark UI (specifically under the SQL/Dataframe tab) for the 'Shuffle read size':</p><pre><code># Read data (example: Parquet file) df = spark.read.load("examples/src/main/resources/users.parquet") # Save as no-op (does not write data, but triggers computation) df.write.format("noop").mode("overwrite").save()</code></pre><p>This approach helps ensure that you're basing your partitioning decisions on the actual memory requirements rather than the compressed on-disk sizes.</p><h2>References</h2><p><a href="https://www.databricks.com/notebooks/gallery/SparkAdaptiveQueryExecution.html">https://www.databricks.com/notebooks/gallery/SparkAdaptiveQueryExecution.html</a></p><p><a href="https://www.databricks.com/discover/pages/optimize-data-workloads-guide">https://www.databricks.com/discover/pages/optimize-data-workloads-guide</a></p><h2><strong>Keep This Post Discoverable: Your Engagement Counts!</strong></h2><p>Your engagement with this blog post is crucial! Without claps, comments, or shares, this valuable content might become lost in the vast sea of online information. Search engines like Google rely on user engagement to determine the relevance and importance of web pages. If you found this information helpful, please take a moment to clap, comment, or share. Your action not only helps others discover this content but also ensures that you&#8217;ll be able to find it again in the future when you need it. Don&#8217;t let this resource disappear from search results &#8212; show your support and help keep quality content accessible!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">CanadianDataGuy&#8217;s No Fluff Newsletter is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Spark Join Strategies Explained: Broadcast Hash Join]]></title><description><![CDATA[Everything You Need to Know About Broadcast Hash Join]]></description><link>https://www.canadiandataguy.com/p/spark-join-strategies-explained-broadcast</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/spark-join-strategies-explained-broadcast</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Mon, 14 Apr 2025 14:00:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Apache Spark employs multiple join strategies to efficiently combine datasets in a distributed environment. This guide provides a <strong>zero-to-hero</strong> explanation of the three primary join strategies &#8211; <strong>Broadcast Hash Join (BHJ)</strong>, <strong>Shuffle Hash Join (SHJ)</strong>, and <strong>Sort-Merge Join (SMJ)</strong> &#8211; with a focus on Databricks. We will explore how each strategy works, their execution plans (DAG stages, partitioning, memory and shuffle behavior), and how to tune these joins on Databricks (including relevant configurations like AQE and join hints). A visual cheat sheet and further reading resources are provided at the end.</p><h2>Introduction to Spark Join Strategies</h2><p>In Spark SQL, a <em>join</em> combines two datasets by matching rows on a common key. The way Spark executes the join greatly impacts performance, especially with large data. Spark&#8217;s Catalyst optimizer will choose a join strategy based on data statistics (size of each side, join type, etc.), or you can influence it via hints and settings. The three main join strategies for equi-joins are:</p><ul><li><p><strong>Broadcast Hash Join (BHJ)</strong> &#8211; Broadcasts the entire smaller dataset to all executors, avoiding shuffles for that side&#8203;, Very fast when one side is sufficiently small, analogous to a map-side join in Hadoop&#8203;</p></li><li><p><strong>Shuffle Hash Join (SHJ)</strong> &#8211; Shuffles both datasets on the join key, then builds a hash table on the smaller side of each partition and streams the larger side to find matches&#8203;.</p><p>Avoids the sort step of SMJ but requires enough memory per partition.</p></li><li><p><strong>Sort-Merge Join (SMJ)</strong> &#8211; Shuffles both datasets on the join key and sorts them, then merges sorted partitions to find matches&#8203;. This is Spark&#8217;s default strategy for large data and supports all join types&#8203; . It&#8217;s robust (can spill to disk if needed) but involves heavy network and CPU overhead for sorting.</p></li></ul><p>Each strategy has optimal use cases and pitfalls. In Databricks (which uses Spark under the hood), adaptive query execution (AQE) can dynamically optimize joins (e.g. switching strategies or handling skew) to improve performance&#8203;. We&#8217;ll now dive into each strategy in detail.</p><h2>What is a Broadcast Hash Join (BHJ)?</h2><p>A <strong>Broadcast Hash Join</strong> is an efficient strategy used to join two datasets in Spark when one of them is significantly smaller than the other. Instead of moving data across the network (shuffling) for both sides of the join, Spark copies&#8212;or "broadcasts"&#8212;the entire small dataset to every worker node (executor). Then, each executor performs a local hash join between its partition of the larger dataset and the entire, locally cached, small dataset. This approach helps to avoid expensive network shuffling and the need for sorting on either side of the join.</p><div><hr></div><h3>The Broadcast Process in Detail</h3><p>The broadcast procedure involves:</p><ol><li><p><strong>Collecting the Data:</strong><br>The driver first gathers the entire small dataset and converts it into an efficient in-memory data structure (typically a hash map).</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2dL-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2dL-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!2dL-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!2dL-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!2dL-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2dL-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png" width="696" height="696" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/796ba06f-0553-4312-834e-611a5f5615af_1024x1024.png&quot;,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:696,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&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="" title="" srcset="https://substackcdn.com/image/fetch/$s_!2dL-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!2dL-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!2dL-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!2dL-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2adbe292-c6f7-49fd-b044-493aa6f8aa5f_1024x1024.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><strong>Distributing the Data:</strong><br>This hash map is then distributed (broadcast) to all executor nodes, usually via a network distribution algorithm akin to torrent distribution.</p></li><li><p><strong>Utilizing the Broadcast Data:</strong><br>Each executor then uses the broadcasted data to quickly look up matching join keys when processing its partition of the larger dataset.</p></li></ol><p>Understanding these steps is crucial because if any stage fails&#8212;whether due to memory limits on the driver, executor constraints, or even network issues&#8212;the entire query may fail.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LLu1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LLu1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png 424w, https://substackcdn.com/image/fetch/$s_!LLu1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png 848w, https://substackcdn.com/image/fetch/$s_!LLu1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png 1272w, https://substackcdn.com/image/fetch/$s_!LLu1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LLu1!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png" width="1200" height="948.6263736263736" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/aaab4992-f0b0-4132-8663-112361f4f830_2432x1922.png&quot;,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1151,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:549678,&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://www.canadiandataguy.com/i/160914089?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faaab4992-f0b0-4132-8663-112361f4f830_2432x1922.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LLu1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png 424w, https://substackcdn.com/image/fetch/$s_!LLu1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png 848w, https://substackcdn.com/image/fetch/$s_!LLu1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png 1272w, https://substackcdn.com/image/fetch/$s_!LLu1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f11e026-1f7b-465a-9cda-78e18c7cb38d_2432x1922.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>When Does Spark Use BHJ?</h3><p>Spark will automatically choose to perform a Broadcast Hash Join under these conditions:</p><ul><li><p><strong>Dataset Size:</strong> One side of the join is smaller than a pre-configured threshold, which is by default 10 MB in open-source Spark. In Databricks environments, this threshold is commonly increased (e.g., ~30 MB with adaptive execution), meaning Databricks can handle moderately larger tables.</p></li><li><p><strong>Join Type:</strong> The join condition is an equality condition (equi-join).</p></li></ul><p>The setting <code>spark.sql.autoBroadcastJoinThreshold</code> controls this threshold and can be adjusted based on available memory and expected performance benefits.</p><p>BHJ works well with these join types:</p><ul><li><p><strong>Supported:</strong> Inner joins, and left, semi, or anti joins (as long as the correct side is broadcast).</p></li><li><p><strong>Limitations:</strong> It is not supported for full outer joins. For right outer joins, only the left table can be broadcast; similarly, in left joins only the right table can be broadcast.</p></li></ul><p>If the join type is not supported by a BHJ, Spark may revert to another join strategy, such as a sort-merge join or a broadcast nested loop join when dealing with non-equi conditions.</p><div><hr></div><h4>Databricks and Adaptive Query Execution (AQE)</h4><p>In Databricks:</p><ul><li><p><strong>Adaptive Query Execution (AQE):</strong> AQE can dynamically convert a sort-merge join into a broadcast hash join if it determines at runtime that one side of the join is smaller than the broadcast threshold.</p></li><li><p><strong>Higher Thresholds:</strong> Databricks&#8217; default setting for auto-broadcast (often <code>spark.databricks.adaptive.autoBroadcastJoinThreshold</code>) may be set higher (e.g., 30 MB) to allow for broadcasting moderately larger tables.</p></li><li><p><strong>Forcing Broadcasts:</strong> Although AQE works automatically, you might sometimes use explicit hints (such as <code>/*+ BROADCAST(table) */</code> in SQL or wrapping a DataFrame with <code>broadcast(df)</code> in PySpark) to ensure the small dataset is broadcast immediately, thereby skipping unnecessary shuffles.</p><p></p></li></ul><div><hr></div><h2>Common Misconception- Order of Joins</h2><p>For optimal join order performance: Perform joins from smallest to largest tables first to minimize data shuffling&#8288;&#8288;&#8203; <strong>However, do broadcast joins last</strong>, even though this seems counterintuitive. This is because:&#8288;&#8288;&#8203;</p><ul><li><p>Broadcast joins don't require shuffles and can be executed efficiently even on large fact tables</p></li><li><p>If broadcast joins are done first, the joined data needs to be shuffled again for later joins</p></li><li><p>By doing broadcast joins last, we avoid having to shuffle that data again.</p></li><li><p>Group together joins that share the same ON clause to reduce shuffling, since the data is already arranged properly</p></li></ul><p></p><h3>Memory and Shuffle Considerations</h3><p>Using BHJ provides tremendous speedups by eliminating the costly shuffle of the larger dataset. However, it comes with some significant memory considerations:</p><ul><li><p><strong>Driver Memory:</strong> The whole small dataset must be collected on the driver before it can be broadcast. The driver has a memory limit, defined by <code>spark.driver.maxResultSize</code>, and exceeding this limit will cause the job to fail.</p></li><li><p><strong>Executor Memory:</strong> Each executor must have enough memory to store the broadcasted dataset along with its own processing workload. The available memory on the node with the smallest capacity is the practical limit.</p></li><li><p><strong>Timeout and Overload Risks:</strong> If the dataset is even moderately large, broadcasting it might overwhelm the driver or network, leading to out-of-memory (OOM) errors or timeouts. For example, while Databricks has even seen broadcasts for datasets up to a few GB in size, one must exercise extreme caution when attempting such operations.</p></li><li><p><strong>Compression Differences:</strong> Note that the on-disk size of data (like Parquet files in Delta tables) might be much smaller than the in-memory representation. Spark&#8217;s decisions are based on disk size, so actual in-memory data after decompression might far exceed the expected limits.</p></li></ul><p>To address these issues, you can either disable auto-broadcast by setting <code>spark.sql.autoBroadcastJoinThreshold</code> to -1 or lower the threshold to ensure no large table is inadvertently broadcasted. On Databricks with the Photon engine, <strong>executor-side broadcasts</strong> further alleviate pressure on the driver because the broadcast process does not rely solely on the driver's resources.</p><div><hr></div><h3>Performance Recommendations</h3><ul><li><p><strong>When to Use BHJ:</strong><br>Use Broadcast Hash Join when one dataset is much smaller than the other. This is commonly the case when joining large fact tables with much smaller dimension tables or when one table is the result of a selective filter.</p></li><li><p><strong>Why Forcing Broadcasts:</strong><br>While Spark&#8217;s optimizer may choose to broadcast small datasets automatically, in complex queries or skewed datasets the statistics might not be accurate. In those cases, manually forcing a broadcast using explicit hints ensures that the join operation skips the shuffle stage and executes as a broadcast join.</p></li><li><p><strong>Caution in Production:</strong><br>Forcing broadcasts in ad hoc queries or development is acceptable. However, in production workloads, it&#8217;s important to validate the dataset size at runtime. This can be done by checking record counts and partition sizes to avoid overloading any executor or the driver. Monitoring the Spark UI is critical to ensure broadcasts do not result in GC (garbage collection) pressure or other resource issues.</p></li></ul><div><hr></div><h3>Example SQL with Broadcast Hint</h3><p>To explicitly force a broadcast in SQL, you can include the following hint in your query:</p><pre><code>SELECT /*+ BROADCASTJOIN(table1)*/ table1.id, table1.col, table2.id, table2.int_col FROM table1 JOIN table2 ON table1.id = table2.id;</code></pre><p>In the physical plan, you will see a <code>BroadcastExchange</code> operator for the small table along with a <code>BroadcastHashJoin</code> operator, indicating that the join was executed without additional shuffling of the large table.</p><pre><code>SQL Query : 
select /*+ BROADCASTJOIN(table1)*/ table1.id,table1.col,table2.id,table2.int_col from table1 join table2 on table1.id = table2.id

Physical Plan ==
AdaptiveSparkPlan isFinalPlan=false\n
  +- BroadcastHashJoin [id#271L], [id#286L], Inner, BuildLeft, false
 :- BroadcastExchange HashedRelationBroadcastMode(List(input[0, bigint,   false]),false), [id=#955]
       :  +- Filter isnotnull(id#271L)
       :     +- Scan ExistingRDD[id#271L,col#272]
               +- Filter isnotnull(id#286L)
                 +- Scan ExistingRDD[id#286L,int_col#287L]

Number of records processed: 799541
Querytime : 15.35717314 seconds</code></pre><div><hr></div><h3>Key Pitfalls and Best Practices</h3><ul><li><p><strong>Avoid Broadcasting Too Much Data:</strong><br>Never broadcast a table that is too large (generally over 1GB) as it can overwhelm the driver and executors. Spark has a hard limit (roughly 8GB) on what it can broadcast.</p></li><li><p><strong>Watch for Non-Equi Joins:</strong><br>BHJ only supports joins using equality conditions (equi-joins). When using non-equi join conditions (such as range conditions), BHJ cannot be applied.</p></li><li><p><strong>Force with Caution:</strong><br>When you force a broadcast using hints or functions like <code>broadcast(df)</code>, you bypass Spark&#8217;s adaptive query execution optimizations. This is useful if you are sure the data size is small, but can cause performance issues if the dataset unexpectedly grows.</p></li><li><p><strong>Plan for Memory Needs:</strong><br>Increase the broadcast thresholds only if your driver and executors have ample memory. For instance, a driver with 32GB+ memory might safely use higher thresholds (like 200MB). Be sure to also configure <code>spark.driver.maxResultSize</code> appropriately to avoid driver-level memory errors.</p></li></ul><div><hr></div><h2>Production Advice</h2><p>When deploying BHJ in production workloads, careful planning and ongoing monitoring are essential to ensure stable performance:</p><ul><li><p><strong>Validate Data Sizes:</strong> Always verify that the dataset chosen for broadcasting is truly small both on disk and in-memory. Measure the record count and partition sizes before forcing a broadcast. This helps prevent unexpected OOM (out-of-memory) failures, which can occur when the dataset size exceeds available memory on the driver or executors.</p></li><li><p><strong>Check Data Size and Record Count</strong></p><ul><li><p><strong>Count the Records:</strong> Before attempting a broadcast, run a simple <code>df.count()</code> on the small dataset. This confirms that the number of records is within an acceptable range.</p></li><li><p><strong>Estimate Data Size in Memory:</strong> Sometimes the dataset's on-disk size differs from its in-memory footprint. You can either use approximations from your data source&#8217;s statistics or compute a rough estimate using:</p></li></ul><pre><code># Example in PySpark
data_size_in_bytes = df.rdd.map(lambda row: len(str(row))).sum()
print("Approximate in-memory size (bytes):", data_size_in_bytes)</code></pre><ul><li><p>While this isn&#8217;t exact, it provides an estimate that can be compared against thresholds like <code>spark.sql.autoBroadcastJoinThreshold</code></p></li></ul></li><li><p><strong>Threshold Validation before Forcing a Broadcast</strong></p><ul><li><p><strong>Compare Against Broadcast Thresholds:</strong> Before performing an explicit broadcast, validate that the data size is below the configured threshold (e.g., 10MB, 30MB, or a custom value in your Spark configuration). This might involve:</p></li></ul><pre><code>broadcast_threshold = int(spark.conf.get("spark.sql.autoBroadcastJoinThreshold").replace("b", ""))
# Assume approximate_size holds our computed or estimated size of the dataset in bytes.
if approximate_size &lt; int(broadcast_threshold):
    print("Proceed with broadcast")
    # Then use broadcast
    from pyspark.sql.functions import broadcast
    df_broadcasted = broadcast(df)
else:
    print("Data too large; do not broadcast")
</code></pre><ul><li><p>This validation helps avoid unintentionally broadcasting a dataset that is too big, potentially causing an OOM error.</p></li></ul></li><li><p><strong>Monitor Resource Usage:</strong> Leverage Spark&#8217;s UI and logging mechanisms to track metrics like GC (garbage collection) activity, memory usage, and broadcast sizes. The smallest available executor memory sets the limit, so ensure that the broadcast data comfortably fits on each node.</p></li><li><p><strong>Use Adaptive Query Execution (AQE) Carefully:</strong> While Spark&#8217;s AQE can convert joins to BHJ at runtime, explicitly broadcasting small datasets using hints or functions like <code>broadcast(df)</code> can bypass the overhead of shuffling. However, avoid hardcoding broadcast hints unless you are confident of the dataset's size, as data volumes may fluctuate in production workloads.</p></li><li><p><strong>Configure Thresholds Cautiously:</strong> Adjust configurations such as <code>spark.sql.autoBroadcastJoinThreshold</code> (and related thresholds in environments like Databricks) based on current cluster resources. For drivers with high memory (32GB+), thresholds can be increased, but setting these too high risks overwhelming your system if data volumes grow unexpectedly.</p></li><li><p><strong>Plan for Scalability and Edge Cases:</strong> Implement safeguards within your production pipelines. For instance, include runtime validations or logic to disable broadcasting dynamically when data sizes approach critical limits. This is especially important for pipelines handling dynamic or streaming data where bursts of data could otherwise lead to system instability.</p></li><li><p>If you&#8217;re running a driver with a lot of memory (32GB+), you can safely raise the broadcast thresholds to something like <strong>200MB</strong></p></li></ul><pre><code><code>set spark.sql.autoBroadcastJoinThreshold = 209715200;
set spark.databricks.adaptive.autoBroadcastJoinThreshold = 209715200;</code></code></pre><ul><li><p><strong>Why do we need to explicitly broadcast smaller tables if AQE can automatically broadcast smaller tables for us?</strong> The reason for this is that AQE optimizes queries while they are being executed.</p><ul><li><p>Spark needs to shuffle the data on both sides and then only AQE can alter the physical plan based on the statistics of the shuffle stage and convert to broadcast join</p></li><li><p>Therefore, if you explicitly broadcast smaller tables using hints, it skips the shuffle altogether and your job will not need to wait for AQE&#8217;s intervention to optimize the plan</p></li></ul></li><li><p><strong>Never broadcast a table bigger than 1GB</strong> because broadcast happens via the driver and a 1GB+ table will either cause OOM on the driver or make the drive unresponsive due to large GC pauses</p></li><li><p>Please take note that the size of a table in disk and memory will never be the same. Delta tables are backed by Parquet files, which can have varying levels of compression depending on the data. And Spark might broadcast them based on their size in the disk &#8212; however, they might actually be really big (even more than 8GB) in memory after the decompression and conversion from column to row format. Spark has a hard limit of 8GB on the table size it can broadcast. As a result, your job may fail with an exception in this circumstance. In this case, the solution is to either disable broadcasting by setting <code>spark.sql.autoBroadcastJoinThreshold</code> to -1 and do the explicit broadcast using hints (or the PySpark broadcast function) of the tables that are really small in the disk as well as in memory, or set the <code>spark.sql.autoBroadcastJoinThreshold</code> to smaller values like 100MB or 50MB instead of setting the threshold to -1.</p></li><li><p>The driver can only collect up to 1GB of data in memory at any given time, and anything more than that will trigger an error in the driver, causing the job to fail. However, since we want to broadcast tables larger than 10MB, we risk running into this problem. This problem can be solved by increasing the value of the following driver <a href="https://spark.apache.org/docs/latest/configuration.html#application-properties">configuration</a>.</p><ul><li><p>Please keep in mind that because this is a driver setting; it cannot be altered once the cluster is launched. Therefore, it should be set under the cluster&#8217;s advanced options as a Spark config. Setting this parameter to 8GB for a driver with &gt;32GB memory seems to work fine in most circumstances. In certain cases where the broadcast hash join is going to broadcast a very large table, setting this value to 16GB would also make sense.</p></li><li><p>In <a href="https://learn.microsoft.com/en-us/azure/databricks/runtime/photon">Photon</a>, we have the executor-side broadcast. So, you don&#8217;t have to change the following driver configuration if you use a Databricks Runtime (DBR) with Photon.</p></li></ul></li></ul><pre><code><code>spark.driver.maxResultSize 16g</code></code></pre><div><hr></div><h3>Final Thoughts</h3><p>In summary, Broadcast Hash Join is a fast and efficient joining strategy in Spark for skewed or unbalanced joins where one dataset is significantly smaller. It avoids the expensive shuffling of the larger dataset by replicating the small data across all executors, enabling quick local hash lookups. However, its effectiveness depends heavily on the small dataset fitting in memory on the driver and executors. Forcing broadcasts should be done judiciously, with thorough validations in production to prevent resource exhaustion and associated failures.</p><p>By understanding the details of how BHJ operates and its configurations, you can better optimize your Spark jobs and manage performance, especially in environments like Databricks where adaptive query execution and executor-side optimizations further enhance its capabilities.</p><h4>How the Process Works</h4><p>BHJ operates in <strong>two main phases</strong>:</p><ol><li><p><strong>Broadcast Phase:</strong></p><ul><li><p><strong>Collection and Broadcast:</strong> The small table is first collected by the Spark driver. After collection, the data is broadcast to all the executors across the cluster.</p></li><li><p><strong>Local Caching:</strong> Once received on each node, the small dataset is cached in memory as a read-only broadcast variable. This ensures that the data is immediately available for the join process without any further data movement.</p></li></ul></li><li><p><strong>Hash Join Phase:</strong></p><ul><li><p><strong>Building a Hash Map:</strong> Each executor creates an in-memory hash map from the broadcasted dataset. The hash map is built using the join key.</p></li><li><p><strong>Local Join Operation:</strong> As the larger dataset is processed, every row in each partition is checked against the hash map for matching join keys. Because the small dataset is already available locally, this lookup is very fast and eliminates the need for shuffling data across the network.</p></li></ul></li></ol><p>Since no sort or extra merge steps are required, this one-pass in-memory lookup per partition makes the Broadcast Hash Join particularly quick, especially in common scenarios like joining large fact tables with much smaller dimension tables (a typical star schema pattern).</p><h2>Further Reading</h2><p>For more in-depth information and the latest updates on Spark join optimizations, the following resources are highly recommended:</p><ul><li><p><strong>Apache Spark Official Documentation &#8211; SQL Performance Tuning:</strong> Covers join strategy hints, adaptive execution, etc. (See <em>&#8220;Join Strategy Hints&#8221;</em> and <em>&#8220;Adaptive Query Execution&#8221;</em> in the Spark docs)&#8203;</p><p><a href="https://downloads.apache.org/spark/docs/3.1.2/sql-performance-tuning.html#:~:text=Join%20Strategy%20Hints%20for%20SQL,Queries">downloads.apache.org</a></p></li><li><p><a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/spark-tuning-glue-emr/using-join-hints-in-spark-sql.html#:~:text=,all%20executors%20within%20the%20cluster">Tuning Spark SQL queries for AWS Glue and Amazon EMR Spark jobs</a></p></li><li><p><strong>Apache Spark Official Documentation &#8211; Adaptive Query Execution (AQE):</strong> Detailed explanation of AQE features like converting SMJ to BHJ/SHJ and skew join handling&#8203;</p><p><a href="https://downloads.apache.org/spark/docs/3.1.2/sql-performance-tuning.html#:~:text=Converting%20sort,join">downloads.apache.org</a></p></li><li><p><strong>Databricks Documentation &#8211; Join Hints &amp; Optimizations:</strong> Databricks-specific docs on join strategies, including the <code>SKEW</code> and <code>RANGE</code> hints, and how AQE is used on Databricks&#8203;</p><p><a href="https://docs.databricks.com/gcp/en/transform/join#:~:text=Join%20hints%20on%20Databricks">docs.databricks.com</a></p></li><li><p><strong>&#8220;How Databricks Optimizes Spark SQL Joins&#8221; &#8211; Medium (dezimaldata):</strong> A blog post (Aug 2023) summarizing Databricks&#8217; techniques like CBO, AQE, range join and skew join optimizations&#8203;</p><p><a href="https://dezimaldata.medium.com/how-databricks-optimizes-the-spark-sql-joins-d01b95336d65#:~:text=In%20this%20blog%20post%2C%20we,joins%20and%20subqueries%2C%20such%20as">dezimaldata.medium.com</a></p></li><li><p><strong>&#8220;Top 5 Mistakes That Make Your Databricks Queries Slow&#8221; &#8211; Perficient Blog:</strong> Section 1 and 2 discuss data skew and suboptimal join strategies, with tips on salting and broadcast joins&#8203;</p><p><a href="https://blogs.perficient.com/2025/03/28/top-5-mistakes-that-make-your-databricks-queries-slow-and-how-to-fix-them/#:~:text=1">blogs.perficient.com</a></p></li><li><p><strong>Spark Summit Talks on Joins and AQE:</strong> Videos like <em>&#8220;Optimizing Shuffle Heavy Workloads&#8221;</em> or <em>&#8220;AQE in Spark 3.0&#8221;</em> (by Databricks engineers) for a deeper understanding of the internals of join execution and tuning.</p></li><li><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html#join-strategy-hints">https://spark.apache.org/docs/latest/sql-performance-tuning.html#join-strategy-hints</a></p></li><li><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html#adaptive-query-execution">https://spark.apache.org/docs/latest/sql-performance-tuning.html#adaptive-query-execution</a></p></li><li><p>https://docs.databricks.com/en/sql/language-manual/hints.html</p></li><li><p>https://medium.com/@dezimaldata/how-databricks-optimizes-spark-sql-joins-aqe-cbo-and-more-5ac4c4d53091</p></li><li><p>https://www.perficient.com/insights/blog/2023/01/top-5-mistakes-that-make-your-databricks-queries-slow</p></li><li><p>https://www.databricks.com/resources/whitepapers/optimizing-apache-spark-on-databricks</p></li></ul><p>By consulting these materials, you can deepen your understanding of Spark join mechanisms and keep up to date with the evolving best practices on the Databricks platform.</p>]]></content:encoded></item><item><title><![CDATA[Spark Join Strategies Explained: Shuffle Hash]]></title><description><![CDATA[Everything You Need to Know About Shuffle Hash Join]]></description><link>https://www.canadiandataguy.com/p/spark-join-strategies-explained-shuffle</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/spark-join-strategies-explained-shuffle</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Thu, 10 Apr 2025 14:00:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4QvA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>1. Introduction</h2><p>Modern big data applications often require joining huge datasets efficiently. Choosing the right join strategy is critical to optimize performance and resource usage. Apache Spark offers several join methods, including broadcast joins, sort-merge joins, and shuffle hash joins. SHJ stands out as a middle-ground approach:</p><ul><li><p>It <strong>shuffles</strong> both tables like sort-merge joins to align data with the same key.</p></li><li><p>Instead of sorting, it builds an <strong>in-memory hash table</strong> for the smaller dataset per partition and probes it with rows from the larger dataset.</p></li></ul><p>This dual approach has the potential to improve execution time by reducing the sorting overhead but demands careful memory management.</p><div><hr></div><h2>2. Understanding Shuffle Hash Join</h2><p><strong>Shuffle Hash Join</strong> is best understood as a hybrid that borrows elements from two traditional join methods:</p><ul><li><p><strong>Sort Merge Join (SMJ)</strong></p><ul><li><p><strong>Mechanism:</strong> Both datasets are sorted by the join key and then merged.</p></li><li><p><strong>Pros:</strong> Reliable for large datasets.</p></li><li><p><strong>Cons:</strong> Sorting is CPU intensive.</p></li></ul></li><li><p><strong>Broadcast Hash Join (BHJ)</strong></p><ul><li><p><strong>Mechanism:</strong> The smaller table is broadcast to all nodes, and each executor performs a local hash join.</p></li><li><p><strong>Pros:</strong> Eliminates shuffling.</p></li><li><p><strong>Cons:</strong> Limited by broadcast size, not suitable when the smaller table exceeds available memory on executors.</p></li></ul></li></ul><p><strong>How SHJ Differentiates Itself:</strong></p><ul><li><p><strong>Key Step:</strong> It shuffles both datasets based on the join key so that every partition contains matching keys.</p></li><li><p><strong>In-Partition Operation:</strong> Instead of sorting the data in each partition, Spark builds a hash table from the smaller dataset's partition and then probes that table with each row from the larger dataset.</p></li><li><p><strong>Memory Sensitivity:</strong> The approach assumes that each partition of the smaller side can be held in memory, which is crucial for performance and avoiding runtime errors.</p></li></ul><blockquote><p><strong>Key Concepts to Remember:</strong></p><ul><li><p><strong>No Sorting:</strong> Eliminates the costly sort phase.</p></li><li><p><strong>Memory Requirement:</strong> High dependency on the ability to fit the hashed partition in memory, risking OOM errors if miscalculated.</p></li></ul></blockquote><div><hr></div><h2>3. When to Use SHJ</h2><h3>Historical Perspective</h3><ul><li><p><strong>Pre-Spark 3.0:</strong><br>Spark defaulted to Sort Merge Join for equality-based joins due to the risk of OOM when building in-memory hash tables.</p></li><li><p><strong>Spark 3.x and Beyond:</strong><br>With enhancements like Adaptive Query Execution (AQE), Spark can dynamically decide to use SHJ when it detects that:</p><ul><li><p>The smaller dataset, after partitioning, is of manageable size.</p></li><li><p>Avoiding the expensive sorting operation is beneficial for performance.</p></li></ul></li></ul><h3>Practical Scenarios</h3><ul><li><p><strong>Moderately Small Datasets:</strong><br>When one dataset is small enough that its partitions are lightweight (e.g., 5 MB per partition out of 5 GB divided across 1000 partitions), yet not small enough for a broadcast join.</p></li><li><p><strong>High Sorting Overhead:</strong><br>When joining a massive fact table (e.g., 1 TB) with a dimension table that is too big to broadcast but small enough per partition, the cost of sorting the entire dataset (as in SMJ) may dominate and thus SHJ becomes more efficient.</p></li></ul><h3>Decision Factors</h3><ul><li><p><strong>Estimated Partition Size:</strong><br>Spark&#8217;s optimizer checks if the estimated per-partition size of the smaller table is below a threshold (set via <code>spark.sql.adaptive.maxShuffledHashJoinLocalMapThreshold</code>).</p></li><li><p><strong>Configuration and Hints:</strong><br>Users can guide Spark&#8217;s optimizer using hints like <code>/*+ SHUFFLE_HASH(tab) */</code> or disable sort-merge joins by toggling <code>spark.sql.join.preferSortMergeJoin</code>.</p><p>spark.conf.set("spark.sql.join.preferSortMergeJoin","false")</p></li></ul><div><hr></div><h2>4. How SHJ Works</h2><p>The execution of a Shuffle Hash Join can be understood through two primary phases, with some literature breaking it into a three-phase model for clarity.</p><h3>A. Shuffle Phase</h3><p><strong>Objective:</strong><br>Bring together all rows associated with a given join key within the same partition.</p><p><strong>Process:</strong></p><ul><li><p><strong>Repartitioning:</strong><br>Both datasets are re-distributed (shuffled) using the join key as the partitioning key. Note that <strong>both</strong> sides are shuffled &#8211; so network cost is still incurred for both datasets.</p></li><li><p><strong>Data Co-location:</strong><br>Post-shuffle, each partition will hold all the relevant rows for a specific range of join keys.</p></li><li><p><strong>Network I/O:</strong><br>While shuffling ensures correct join semantics, it incurs the cost of network communication for both datasets.</p></li></ul><p><strong>Example Scenario:</strong></p><p>Imagine two datasets, <code>Person</code> and <code>Address</code>, initially spread across different partitions. In the shuffle phase, rows with the same key (e.g., <code>A001</code>) are sent to the same partition. This guarantees that later join operations will have all matching keys available on the same executor.</p><h3>B. Hash Join Phase</h3><p>After the shuffle phase, the join is executed within each partition through these steps:</p><ol><li><p><strong>Hash Table Creation:</strong></p><ul><li><p><strong>Selection:</strong><br>Spark selects the smaller dataset based on statistics or join hints.</p></li><li><p><strong>Building the Hash Table:</strong><br>For every partition, Spark creates an in-memory hash table that maps join keys to the associated rows.</p></li></ul></li><li><p><strong>Probing the Hash Table:</strong></p><ul><li><p><strong>Streaming Data:</strong><br>The larger dataset&#8217;s rows are processed sequentially within the partition.</p></li><li><p><strong>Lookup and Join:</strong><br>For each row in the larger dataset, the hash table is queried using the join key. If a match exists, Spark produces the joined row as output.</p></li></ul></li></ol><blockquote><p><em>Because no sort is done, if the data per partition is large, the hash table may also be large. Spark assumes the build side will fit in memory. If it doesn&#8217;t, the task can spill partitions of the build side to disk (Spark has some support for spilling hash tables, but it is more complex than spilling a sort). In worst cases, an SHJ can run out of memory if the hash table grows too big, causing the executor to OOM. This is why Spark is conservative in using SHJ unless it&#8217;s confident the partitions are small enough&#8203;</em></p></blockquote><p><strong>Conceptual Diagram:</strong></p><p>Imagine a partition where:</p><ul><li><p>The smaller dataset&#8217;s partition (say, 5 MB worth of data) is fully loaded into a hash table.</p></li><li><p>The larger dataset streams through, and for each key, Spark quickly checks the in-memory hash table for corresponding rows.</p></li></ul><p>This operation is performed concurrently across all partitions on different worker nodes.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4QvA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4QvA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png 424w, https://substackcdn.com/image/fetch/$s_!4QvA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png 848w, https://substackcdn.com/image/fetch/$s_!4QvA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png 1272w, https://substackcdn.com/image/fetch/$s_!4QvA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4QvA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png" width="744" height="918" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:918,&quot;width&quot;:744,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Phases of the Shuffle Hash join: Scan JSON read data, exchange, ShuffleHashJoin, and Hash Aggregate&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="Phases of the Shuffle Hash join: Scan JSON read data, exchange, ShuffleHashJoin, and Hash Aggregate" title="Phases of the Shuffle Hash join: Scan JSON read data, exchange, ShuffleHashJoin, and Hash Aggregate" srcset="https://substackcdn.com/image/fetch/$s_!4QvA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png 424w, https://substackcdn.com/image/fetch/$s_!4QvA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png 848w, https://substackcdn.com/image/fetch/$s_!4QvA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png 1272w, https://substackcdn.com/image/fetch/$s_!4QvA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6546e1e4-d463-41d6-8aa1-6e9fbdc4f985_744x918.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Alternative Three-Phase View</h3><p>For some, a detailed three-phase breakdown clarifies the process:</p><ol><li><p><strong>Shuffle:</strong><br>Repartition both datasets so that all rows sharing the same join key are co-located.</p></li><li><p><strong>Hash Table Creation:</strong><br>For each partition, build the in-memory hash table using the smaller dataset.</p></li><li><p><strong>Hash Join:</strong><br>Join the larger dataset&#8217;s partition by probing the hash table.</p></li></ol><p>This view underlines the importance of parallel execution, where each worker node processes its partitions independently, which is key to Spark&#8217;s scalability.</p><div><hr></div><h2>5. Supported Join Types</h2><p>Shuffle Hash Join is designed to work primarily with <strong>equi-joins</strong>. In Apache Spark, it supports:</p><ul><li><p><strong>Inner Joins:</strong><br>Only matching rows are returned.</p></li><li><p><strong>Left, Right, Semi, and Anti Joins:</strong><br>These join types function well as long as the join condition is based on equality.</p></li></ul><p><strong>Additional Notes:</strong></p><ul><li><p><strong>Full Outer Join:</strong><br>Initially, SHJ did not support full outer joins in Spark 3.0 but was later introduced in Spark 3.1+.</p></li><li><p><strong>Non-equi Joins and Cross Joins:</strong><br>SHJ does not naturally handle cross joins or non-equi conditions. In such cases, Spark falls back on other, more suitable join strategies.</p></li></ul><div><hr></div><h2>6. Performance Characteristics &amp; Trade-Offs</h2><p>Understanding the performance implications of SHJ is critical for designing robust, high-performance Spark jobs.</p><h3>Advantages</h3><ul><li><p><strong>No Sorting Required:</strong></p><ul><li><p>By eliminating the sort step used in SMJ, SHJ significantly reduces CPU overhead.</p></li></ul></li><li><p><strong>Efficient CPU Usage:</strong></p><ul><li><p>Hash functions and probing operations are generally less costly than sorting large datasets.</p></li></ul></li><li><p><strong>Parallel Execution:</strong></p><ul><li><p>The join is processed in parallel across partitions, making it scalable across large clusters.</p></li></ul></li></ul><h3>Considerations and Pitfalls</h3><ul><li><p><strong>Memory Sensitivity:</strong></p><ul><li><p><strong>Build Side Dependency:</strong><br>Every partition on the smaller side must fit in memory. If a partition exceeds available memory, it may cause disk spills or even OOM errors.</p></li><li><p><strong>Configuration Challenges:</strong><br>Incorrect estimations or misconfigured thresholds can lead to failures. Monitoring and adjusting Spark&#8217;s parameters is essential.</p></li></ul></li><li><p><strong>Data Skew:</strong></p><ul><li><p><strong>Uneven Distribution:</strong><br>A heavily skewed join key might result in one partition holding a disproportionate amount of data, dramatically increasing memory requirements for that partition.</p></li><li><p><strong>Mitigation Strategies:</strong><br>Use techniques like increasing the number of shuffle partitions (via <code>spark.sql.shuffle.partitions</code>) or applying custom salting techniques.</p></li></ul></li><li><p><strong>Network I/O:</strong></p><ul><li><p>While SHJ saves on CPU cycles, it does not reduce the network cost of shuffling. If your workload is network-bound, the benefits of SHJ may be limited.</p></li></ul></li><li><p><strong>Fallback and Spilling:</strong></p><ul><li><p>If the hash table grows too large, Spark may attempt to spill data to disk. However, disk spilling is less efficient and can severely impact performance.</p></li></ul></li></ul><div><hr></div><h2>7. SHJ Compared to Other Join Strategies</h2><p>A clear comparison can help decide when to use SHJ over other join methods:</p><p><strong>AspectBroadcast Hash Join (BHJ)Sort Merge Join (SMJ)Shuffle Hash Join (SHJ)When to Use</strong>Very small tables (typically &lt;10 MB by default)Large tables where sorting is tolerableModerately small build side that cannot be broadcast; avoid sorting overhead<strong>Sorting Requirement</strong>No sorting; smaller dataset is broadcastedSorting required across partitionsNo sorting within partitions; uses in-memory hash table<strong>Memory Impact</strong>Minimal memory impact on executorsUses more CPU for sortingRequires sufficient memory per partition for hash tables<strong>Network Cost</strong>Minimal network I/O (broadcast eliminates shuffle)High network I/O due to data shufflingSame network cost as SMJ</p><p><strong>Key Takeaways:</strong></p><ul><li><p><strong>BHJ</strong> is best when the smaller table is extremely small.</p></li><li><p><strong>SMJ</strong> is a general-purpose join that is robust for large datasets.</p></li><li><p><strong>SHJ</strong> strikes a balance by avoiding the heavy sorting cost when the per-partition memory size is manageable.</p></li></ul><h4><em>Shuffle hash join over sort-merge join</em></h4><p>In most cases Spark chooses sort-merge join (SMJ) when it can&#8217;t broadcast tables. Sort-merge joins are the most expensive ones. Shuffle-hash join (SHJ) has been found to be faster in some circumstances (but not all) than sort-merge since it does not require an extra sorting step like SMJ. There is a setting that allows you to advise Spark that you would prefer SHJ over SMJ, and with that Spark will try to use SHJ instead of SMJ wherever possible. Please note that this does not mean that Spark will always choose SHJ over SMJ. We are simply defining your preference for this option.</p><pre><code><code>set spark.sql.join.preferSortMergeJoin = false</code></code></pre><p>Databricks <a href="https://docs.databricks.com/runtime/photon.html">Photon</a> engine also replaces sort-merge join with shuffle hash join to boost the query performance.</p><ul><li><p>Setting the <code>preferSortMergeJoin</code> config option to false for each job is not necessary. For the first execution of a concerned job, you can leave this value to default (which is true).</p></li></ul><ul><li><p>If the job in question performs a lot of joins, involving a lot of data shuffling and making it difficult to meet the desired SLA, then you can use this option and change the <code>preferSortMergeJoin</code> value to false</p></li></ul><div><hr></div><h2>8. Configuration and Tuning Best Practices</h2><p>Optimizing SHJ involves careful configuration and continuous monitoring. Below are some best practices.</p><h3>A. Adaptive Query Execution (AQE)</h3><p><strong>What is AQE?</strong><br>Adaptive Query Execution dynamically adapts the physical plan based on runtime statistics. With Spark 3.x, AQE can convert a sort-merge join to a shuffle hash join if it detects that partition sizes are favorable.</p><p><strong>Configuration Example:</strong></p><pre><code>// Set AQE threshold such that if post-shuffle partition size is below 64MB, Spark uses SHJ. spark.conf.set("spark.sql.adaptive.maxShuffledHashJoinLocalMapThreshold", "64MB")</code></pre><p>This dynamic adjustment helps balance between CPU use and memory load without manual intervention.</p><h3>B. Join Hints and Configurations</h3><p><strong>Explicit Hints:</strong><br>When you know the data characteristics, you can direct Spark to use SHJ via hints:</p><pre><code>// Using a hint to explicitly request a Shuffle Hash Join val dfJoined = factTable.join(dimensionTable.hint("SHUFFLE_HASH"), "joinKey") dfJoined.explain() // The physical plan should show ShuffledHashJoin</code></pre><p><strong>Disabling SMJ Preference:</strong><br>For cases where SHJ is preferred over SMJ, you can adjust the setting as follows:</p><pre><code>// Tell Spark to favor hash-based join strategies over sort-merge join. spark.conf.set("spark.sql.join.preferSortMergeJoin", "false")</code></pre><h3>C. Monitoring and Debugging</h3><p><strong>Using the Spark UI:</strong></p><ul><li><p><strong>Partition Metrics:</strong><br>Monitor the size and distribution of shuffle partitions to ensure they meet expected thresholds.</p></li><li><p><strong>Task Execution Details:</strong><br>Observe tasks&#8217; memory usage and CPU times. Unexpected OOM errors or high spill metrics may indicate misconfigured thresholds or skewed data.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WDjl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WDjl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png 424w, https://substackcdn.com/image/fetch/$s_!WDjl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png 848w, https://substackcdn.com/image/fetch/$s_!WDjl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png 1272w, https://substackcdn.com/image/fetch/$s_!WDjl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WDjl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png" width="728" height="549" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:549,&quot;width&quot;:728,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Shuffle Hash Join Spark Stages&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="Shuffle Hash Join Spark Stages" title="Shuffle Hash Join Spark Stages" srcset="https://substackcdn.com/image/fetch/$s_!WDjl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png 424w, https://substackcdn.com/image/fetch/$s_!WDjl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png 848w, https://substackcdn.com/image/fetch/$s_!WDjl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png 1272w, https://substackcdn.com/image/fetch/$s_!WDjl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09f871dd-c444-4e2f-a392-5e0509d572ad_728x549.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Log Analysis:</strong></p><ul><li><p><strong>AQE Logs:</strong><br>When AQE is enabled, logs will show if the join strategy was dynamically switched.</p></li><li><p><strong>Executor Logs:</strong><br>Pay attention to memory allocation logs and warnings about data spills.</p></li></ul><div><hr></div><h2>9. Practical Example</h2><p>Let&#8217;s consider a real-world scenario to solidify our understanding. Suppose you are joining a large fact table with a moderately sized dimension table:</p><ul><li><p><strong>Fact Table:</strong> ~1 TB of transactional data.</p></li><li><p><strong>Dimension Table:</strong> ~5 GB of reference data.</p></li></ul><p><strong>Rationale:</strong><br>Broadcasting a 5 GB table is infeasible in this scenario, but if you partition the 5 GB table into 1000 slices, each partition is only about 5 MB. This makes it an ideal candidate for a shuffle hash join.</p><p><strong>Implementation Example in Spark (Scala):</strong></p><pre><code>// Assuming factTable and dimensionTable are pre-defined DataFrames val dfJoined = factTable.join( dimensionTable.hint("SHUFFLE_HASH"), Seq("joinKey") // Using column(s) that define the join condition ) // Explain the plan to verify the join strategy dfJoined.explain(true) // Expected outcome: // The physical plan should display an operator "ShuffledHashJoin" // indicating that Spark is using SHJ for the join.</code></pre><p><strong>What to Look For:</strong></p><ul><li><p><strong>Physical Plan Inspection:</strong><br>Look for the <code>ShuffledHashJoin</code> operator in the explain plan output.</p></li><li><p><strong>Resource Usage:</strong><br>Monitor executor memory usage and check that each partition from the smaller dimension table fits within the allotted memory, avoiding spills or OOM errors.</p></li></ul><pre><code>ShuffledHashJoin [id1#3], [id2#8], Inner, BuildRight
:- Exchange hashpartitioning(id1#3, 200)
:  +- LocalTableScan [id1#3]
+- Exchange hashpartitioning(id2#8, 200)
   +- LocalTableScan [id2#8]</code></pre><h2><strong>10. Databricks platform specific insights</strong></h2><p>Databricks generally relies on BHJ and SMJ under the hood, and uses SHJ in a more limited, adaptive way. Under AQE, Databricks might start a join as a sort-merge join but then <em>convert it to a shuffled hash join</em> at runtime if it finds that each partition&#8217;s size is below a threshold (and thus can fit in memory)&#8203;.</p><p> This is an optimization: Spark saves the cost of sorting when it realizes it wasn&#8217;t needed. By default, this conversion is off (threshold = 0) on vanilla Spark 3.2, but Databricks may enable it or allow setting it. If using hints, you can explicitly ask for a SHJ: e.g., <code>.hint("SHUFFLE_HASH")</code> in DataFrame API or SQL hints. This can be useful if you <em>know</em> one side is moderately small but Spark&#8217;s stats are missing. Always ensure that the hint-targeted side will be small per partition; otherwise, you might get memory errors.</p><p>Databricks&#8217; strong skew mitigation helps SHJ as well &#8211; if one partition is skewed and would OOM an SHJ, AQE&#8217;s skew join handling could split that partition and even fall back to a sort-merge or a replicated join for that partition if necessary&#8203;. Also, note that <strong>Photon</strong> (Databricks&#8217; vectorized engine) has an improved hashed join implementation that can spill gracefully and use multiple threads per join, which makes SHJ more viable for large data in Photon. In standard Spark, SHJ is single-threaded per task for the join itself (just like SMJ merge is single-threaded per task).</p><h2>11. Conclusion</h2><p><strong>Shuffle Hash Join (SHJ)</strong> provides a balanced approach by eliminating the high cost of sorting that is present in Sort Merge Joins, while sidestepping the broadcast size limitations of Broadcast Hash Joins. By shuffling data to co-locate matching join keys and then using an in-memory hash table to perform the join, SHJ offers:</p><ul><li><p><strong>Improved CPU efficiency</strong> due to reduced sorting overhead.</p></li><li><p><strong>Scalability</strong> when the smaller dataset can be effectively partitioned.</p></li><li><p><strong>A flexible mechanism</strong> that can adapt to runtime data sizes through AQE.</p></li></ul><p>However, SHJ requires meticulous tuning and monitoring:</p><ul><li><p><strong>Memory Utilization:</strong><br>Ensure that each partition&#8217;s hash table fits in memory.</p></li><li><p><strong>Data Skew:</strong><br>Address uneven data distributions to prevent performance bottlenecks.</p></li><li><p><strong>Network Costs:</strong><br>Understand that while CPU usage may decrease, shuffling still incurs network overhead.</p></li></ul><p>By leveraging configuration settings, join hints, and adaptive query execution, data engineers can optimize their Spark workloads using SHJ. This detailed understanding equips you with the knowledge to carefully evaluate when SHJ is the right tool for your data joining needs, ensuring robust and efficient Spark application performance.</p><h2>Further Reading</h2><p>For more in-depth information and the latest updates on Spark join optimizations, the following resources are highly recommended:</p><ul><li><p><strong>Apache Spark Official Documentation &#8211; SQL Performance Tuning:</strong> Covers join strategy hints, adaptive execution, etc. (See <em>&#8220;Join Strategy Hints&#8221;</em> and <em>&#8220;Adaptive Query Execution&#8221;</em> in the Spark docs)&#8203;</p><p><a href="https://downloads.apache.org/spark/docs/3.1.2/sql-performance-tuning.html#:~:text=Join%20Strategy%20Hints%20for%20SQL,Queries">downloads.apache.org</a></p></li><li><p><a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/spark-tuning-glue-emr/using-join-hints-in-spark-sql.html#:~:text=,all%20executors%20within%20the%20cluster">Tuning Spark SQL queries for AWS Glue and Amazon EMR Spark jobs</a></p></li><li><p><strong>Apache Spark Official Documentation &#8211; Adaptive Query Execution (AQE):</strong> Detailed explanation of AQE features like converting SMJ to BHJ/SHJ and skew join handling&#8203;</p><p><a href="https://downloads.apache.org/spark/docs/3.1.2/sql-performance-tuning.html#:~:text=Converting%20sort,join">downloads.apache.org</a></p></li><li><p><strong>Databricks Documentation &#8211; Join Hints &amp; Optimizations:</strong> Databricks-specific docs on join strategies, including the <code>SKEW</code> and <code>RANGE</code> hints, and how AQE is used on Databricks&#8203;</p><p><a href="https://docs.databricks.com/gcp/en/transform/join#:~:text=Join%20hints%20on%20Databricks">docs.databricks.com</a></p></li><li><p><strong>&#8220;How Databricks Optimizes Spark SQL Joins&#8221; &#8211; Medium (dezimaldata):</strong> A blog post (Aug 2023) summarizing Databricks&#8217; techniques like CBO, AQE, range join and skew join optimizations&#8203;</p><p><a href="https://dezimaldata.medium.com/how-databricks-optimizes-the-spark-sql-joins-d01b95336d65#:~:text=In%20this%20blog%20post%2C%20we,joins%20and%20subqueries%2C%20such%20as">dezimaldata.medium.com</a></p></li><li><p><strong>Spark Summit Talks on Joins and AQE:</strong> Videos like <em>&#8220;Optimizing Shuffle Heavy Workloads&#8221;</em> or <em>&#8220;AQE in Spark 3.0&#8221;</em> (by Databricks engineers) for a deeper understanding of the internals of join execution and tuning.</p></li><li><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html#join-strategy-hints">https://spark.apache.org/docs/latest/sql-performance-tuning.html#join-strategy-hints</a></p></li><li><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html#adaptive-query-execution">https://spark.apache.org/docs/latest/sql-performance-tuning.html#adaptive-query-execution</a></p></li><li><p><a href="https://dezimaldata.medium.com/how-databricks-optimizes-the-spark-sql-joins-d01b95336d65">How Databricks Optimizes the Spark SQL Joins</a></p></li><li><p><a href="https://blogs.perficient.com/2025/03/28/top-5-mistakes-that-make-your-databricks-queries-slow-and-how-to-fix-them/">Top 5 Mistakes That Make Your Databricks Queries Slow (and How to Fix Them)</a></p><p></p><p>By consulting these materials, you can deepen your understanding of Spark join mechanisms and keep up to date with the evolving best practices on the Databricks platform.</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Spark Join Strategies Explained: Sort Merge Join]]></title><description><![CDATA[Slow and Steady always wins the race]]></description><link>https://www.canadiandataguy.com/p/spark-join-strategies-explained-sort</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/spark-join-strategies-explained-sort</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Thu, 10 Apr 2025 05:22:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>What is  it:</strong> </h2><p>Sort-Merge Join is the <strong>default join strategy</strong> in Spark for large datasets that don&#8217;t qualify for a broadcast. It involves shuffling and sorting both sides of the join on the join key, then streaming through the sorted data to merge matching keys&#8203;. SMJ is robust and scalable: it can handle very large tables and all join types (inner, outer, etc.), at the cost of more network and CPU usage.</p><h2><strong>How it works</strong></h2><p>Spark will use a Sort-Merge Join when neither side is small enough to broadcast (or if the join type is not supported by BHJ). The execution has three main phases&#8203;</p><ol><li><p><strong>Shuffle Phase:</strong> In the shuffle phase, both input datasets are repartitioned (shuffled) across the cluster nodes based on the join keys. This operation ensures that matching keys from both datasets reside within the same partitions on executors. The shuffle is an expensive network operation involving data redistribution across nodes. Each executor receives and transmits data based on the key distribution. By default, Spark employs 200 partitions (<code>spark.sql.shuffle.partitions</code>). In the physical plan, this shows up as <code>Exchange hashpartitioning(...)</code> on each side of the join&#8203;</p></li><li><p><strong>Sort Phase:</strong> Within each partition, Spark sorts the records by the join key. Each side is sorted independently. The plan will have local <code>Sort</code> operators after the exchange on each side&#8203;. The output is that in partition <em>i</em>, both datasets are sorted by key. Sorting is an expensive step (<strong>O(n log n) per partition)</strong>. If the data is already partitioned and sorted (e.g. bucketing and sorting on the join key), Spark may skip the shuffle and/or sort &#8211; but this requires specific conditions (like both sides being bucketed by the join key with the same number of partitions).</p></li><li><p><strong>Merge Phase:</strong> Once each partition has sorted data from both sides, Spark performs a <strong>merge join</strong>: it iterates through the two sorted lists and finds matching keys, similar to how one would merge two sorted files&#8203;. Because the data is sorted, Spark can do this efficiently by advancing pointers in each list, without nested loops. This merge join operation is efficient&#8212;linear time complexity per partition&#8212;enabling rapid matching without the need for nested loops. The output of each task is the joined records for that partition&#8217;s key range.</p></li></ol><pre><code>== Physical Plan ==
AdaptiveSparkPlan isFinalPlan=false
  +- SortMergeJoin [id#320L], [id#335L], Inner
       :- Sort [id#320L ASC NULLS FIRST], false, 0
       :  +- Exchange hashpartitioning(id#320L, 36), ENSURE_REQUIREMENTS, [id=#1018]
       :    +- Filter isnotnull(id#320L)
       :     +- Scan ExistingRDD[id#320L,col#321]
               +- Sort [id#335L ASC NULLS FIRST], false, 0
                +- Exchange hashpartitioning(id#335L, 36), ENSURE_REQUIREMENTS, [id=#1019]
                  +- Filter isnotnull(id#335L)
                   +- Scan ExistingRDD[id#335L,int_col#336L]</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XlqS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XlqS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.png 424w, https://substackcdn.com/image/fetch/$s_!XlqS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.png 848w, https://substackcdn.com/image/fetch/$s_!XlqS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.png 1272w, https://substackcdn.com/image/fetch/$s_!XlqS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XlqS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.png" width="464" height="490" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:490,&quot;width&quot;:464,&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;: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="" srcset="https://substackcdn.com/image/fetch/$s_!XlqS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.png 424w, https://substackcdn.com/image/fetch/$s_!XlqS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.png 848w, https://substackcdn.com/image/fetch/$s_!XlqS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.png 1272w, https://substackcdn.com/image/fetch/$s_!XlqS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca8c93bc-df28-4fc8-b1a2-2018a611bd59_464x490.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"><svg role="img" 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"><g><title></title><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"><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"><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>Execution details</strong></h2><p>Sort-Merge join will span multiple stages in the Spark DAG. Typically, you&#8217;ll have one stage (or stages) to produce the shuffle partitions for side A, another for side B, and then a final stage where the actual join (merge) happens. In Spark UI&#8217;s DAG visualization, you might see something like: both tables read in earlier stages, then a stage where &#8220;Exchange -&gt; Sort -&gt; WholeStageCodegen -&gt; SortMergeJoin&#8221; occurs&#8203;</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!viK8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!viK8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg 424w, https://substackcdn.com/image/fetch/$s_!viK8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg 848w, https://substackcdn.com/image/fetch/$s_!viK8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!viK8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!viK8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg" width="686" height="386" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:386,&quot;width&quot;:686,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Spark SQL Bucketing at Facebook - Cheng Su (Facebook)&quot;,&quot;title&quot;:&quot;Spark SQL Bucketing at Facebook - Cheng Su (Facebook)&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="Spark SQL Bucketing at Facebook - Cheng Su (Facebook)" title="Spark SQL Bucketing at Facebook - Cheng Su (Facebook)" srcset="https://substackcdn.com/image/fetch/$s_!viK8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg 424w, https://substackcdn.com/image/fetch/$s_!viK8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg 848w, https://substackcdn.com/image/fetch/$s_!viK8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!viK8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6aee5d69-a69c-4f0a-bfc3-2a0a375c5a0b_686x386.jpeg 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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><em>A Spark DAG visualization of a Sort-Merge Join.</em> Both tables are read and then <strong>shuffled</strong> (Exchange) so that matching keys co-locate. Each partition then <strong>sorts</strong> its chunk of data on the join key and <strong>merges</strong> the two sorted streams to output joined rows. (Some upstream stages show as &#8220;skipped&#8221; because their output was cached for reuse in this example.)</p><p><strong>Supported join types:</strong> <strong>All join types</strong> are supported by SMJ for equality conditions &#8211; inner, left, right, full outer, semi, anti. It&#8217;s the fallback for any join that can&#8217;t use a more specialized strategy. Even non-equi joins (like inequalities) can be executed with a sort-merge-like approach if one side is small (Spark might use a Broadcast NLJ for those), but typically equi-joins are where SMJ is used. If you have a full outer join or if both sides are huge, SMJ is usually the plan Spark will choose&#8203;. (Full outer join cannot be executed as a pure hash join in Spark 2.x, so SMJ was the only choice; Spark 3.1 introduced a shuffle hash algorithm for full outer, but SMJ is still often used.)</p><h2><em>Why is it the most stable join?</em></h2><p>Sort-Merge Join is <em>network and CPU intensive</em>. It performs a <strong>full shuffle of both datasets</strong> &#8211; which means network I/O proportional to the data size &#8211; and a sort of each partition. The memory usage during the sort phase can be high; Spark uses external sort which will spill to disk if a partition&#8217;s data doesn&#8217;t fit in memory. Unlike SHJ, SMJ is not all-or-nothing in memory: if a task has more data than RAM, it will write sorted runs to disk and merge them (graceful degradation)&#8203;.</p><p>This is why SMJ is considered <strong>stable for large data</strong> &#8211; it won&#8217;t crash for memory reasons, at worst it will spill and slow down. Still, you want to avoid excessive spilling by tuning partition sizes (Databricks often sets the default shuffle partitions to a high number or uses AQE to auto-tune partition counts).</p><p>Because both sides are shuffled, SMJ is symmetric &#8211; both large and small tables incur shuffle cost. The algorithm doesn&#8217;t build big hash tables, so it can handle very large inputs (even beyond memory) as long as you accept the sorting cost. One positive aspect is that SMJ streaming merge has <strong>low overhead per record</strong> once sorted, and if data is somewhat presorted or partitioned, the cost might be less than worst-case.</p><h2><strong>Databricks-specific insights</strong></h2><p>Databricks Runtime by default enables <strong>Adaptive Query Execution (AQE)</strong>, which can optimize sort-merge joins in two major ways:</p><ol><li><p><strong>Dynamic partition coalescing</strong> &#8211; after shuffle, if many partitions are small, Databricks can coalesce them to reduce task overhead</p></li><li><p><strong>Skew handling</strong> &#8211; if some partitions are extremely large (skewed), Databricks can split those into multiple tasks to avoid stragglers&#8203;</p></li></ol><p>We will discuss skew handling separately, but it&#8217;s important that with AQE, SMJ is not as rigid as it once was. Databricks also collects detailed statistics to decide join strategies: if the optimizer has reliable size estimates (via cost-based optimization), it might avoid SMJ in favor of BHJ when appropriate&#8203;. However, when dealing with truly large tables where neither side is small, SMJ will be chosen because it&#8217;s the most general and robust approach.</p><h3>Advanced Performance Tuning Strategies</h3><p>While Spark handles the heavy lifting, you can tune SMJ performance by managing the shuffle and sort behavior:</p><ul><li><p><strong>Partition sizing:</strong> Adjust <code>spark.sql.shuffle.partitions</code> so that each partition after shuffle is a reasonable size (Databricks often aims for ~128 MB per partition as a balance between parallelism and overhead). Too few partitions (huge partitions) mean slow sorts and potential disk spills; too many (tiny partitions) mean excessive task scheduling overhead. AQE can auto-coalesce partitions that are smaller than <code>spark.sql.adaptive.advisoryPartitionSizeInBytes</code> (default 64MB)&#8203;</p><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html#:~:text=Converting%20sort,hash%20join">spark.apache.org</a></p></li><li><p><strong>Take advantage of sorting where possible:</strong> If your data is <strong>bucketed</strong> and sorted on the join keys (and both sides have the same number of buckets and join key bucketing), Spark can use a <strong>join without shuffle</strong> (it still sorts each bucket if not sorted, but avoids data movement). On Databricks, Delta Lake can maintain clustering (Z-order or sorting) on keys; while Spark does not automatically detect sort order for skipping the sort stage, having data clustered can improve CPU cache efficiency during the merge.</p></li><li><p><strong>Push down filters and projections:</strong> Reduce data size <em>before</em> the join. SMJ&#8217;s cost is super linear in data volume (due to sorting). If you can filter out unnecessary rows or columns (thus less data to shuffle), do it first. The Catalyst optimizer should push filters, but be mindful when writing queries (e.g., filter as early as possible in the query plan). Also, dropping unused columns means less data is carried through the shuffle.</p></li><li><p><strong>Monitor for skew:</strong> SMJ is particularly vulnerable to skewed keys: if one key accounts for a huge fraction of data, one shuffle partition will be enormous and the merge task for that partition will be a straggler&#8203;. We&#8217;ll discuss skew mitigation soon (Databricks can automatically split skewed partitions&#8203;. If you suspect skew, the Spark UI&#8217;s stage detail can show if one task processed far more data than others.</p></li></ul><h2><strong>When to use SMJ</strong> </h2><p>Typically, you don&#8217;t <em>force</em> a sort-merge join; Spark will use it by default for large data. But you might choose to use an SMJ (or let Spark use it) in cases where both datasets are large and similar in size, or when you&#8217;re doing a full outer join (which BHJ can&#8217;t handle). If one side can be broadcast but you choose not to (perhaps due to risk of OOM or because it&#8217;s just borderline size), SMJ will handle it gracefully. SMJ is also the strategy that can cope with <em>lack of statistics</em>: if Spark isn&#8217;t sure of sizes, it errs on the side of SMJ because it won&#8217;t blow up memory. On Databricks, if you disable adaptive execution or broadcasting, you are essentially forcing SMJ for all joins.</p><h2>Common Pitfalls</h2><ul><li><p>Inadequate shuffle partition tuning, leading to excessive disk spills or overhead from numerous tiny partitions.</p></li><li><p>Failure to minimize shuffle volume by removing unnecessary columns.</p></li><li><p>Ignoring or inadequately handling data skew.</p></li><li><p>Misjudging broadcast opportunities by incorrectly assessing dataset size (rely on in-memory exchange size, not disk size).</p></li></ul><blockquote><p><strong>Pitfalls:</strong> The major downside of SMJ is performance degradation if not tuned. Mistakes include not accounting for data skew (leading to very slow tasks) and leaving the default shuffle partitions at 200 regardless of data scale. For instance, joining two 1 TB tables with 200 partitions would create ~5 GB partitions, likely causing massive spills; increasing partitions (or using AQE) would be necessary. Another common pitfall is forgetting that <em>all columns</em> of both sides are shuffled by default. Projecting out unneeded columns can make a huge difference in shuffle volume. Also, if you have multiple joins in a single query (like joining 3-4 tables), Spark might form a multi-way join plan &#8211; consider breaking a very large join into steps or using broadcasts for some legs to avoid an overly expensive single SMJ of many inputs.</p></blockquote><h3>Conclusion</h3><p>Sort-Merge Join remains a foundational element in Spark's join strategies. Understanding its detailed mechanics&#8212;shuffle, sort, and merge phases. With careful tuning and vigilant analysis, SMJ can transform demanding Spark workloads into highly optimized, reliable operations. On Databricks, always keep AQE enabled for SMJ &#8211; it will automatically <strong>optimize partition counts and handle skew</strong>, making SMJ perform much better in practice than the static execution plans of the past&#8203;.</p><h4>Further Resources</h4><ul><li><p><strong>Apache Spark Official Documentation &#8211; SQL Performance Tuning:</strong> Covers join strategy hints, adaptive execution, etc. (See <em>&#8220;Join Strategy Hints&#8221;</em> and <em>&#8220;Adaptive Query Execution&#8221;</em> in the Spark docs)&#8203;</p><p><a href="https://downloads.apache.org/spark/docs/3.1.2/sql-performance-tuning.html#:~:text=Join%20Strategy%20Hints%20for%20SQL,Queries">downloads.apache.org</a></p></li><li><p><a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/spark-tuning-glue-emr/using-join-hints-in-spark-sql.html#:~:text=,all%20executors%20within%20the%20cluster">Tuning Spark SQL queries for AWS Glue and Amazon EMR Spark jobs</a></p></li><li><p><strong>Apache Spark Official Documentation &#8211; Adaptive Query Execution (AQE):</strong> Detailed explanation of AQE features like converting SMJ to BHJ/SHJ and skew join handling&#8203;</p><p><a href="https://downloads.apache.org/spark/docs/3.1.2/sql-performance-tuning.html#:~:text=Converting%20sort,join">downloads.apache.org</a></p></li><li><p><strong>Databricks Documentation &#8211; Join Hints &amp; Optimizations:</strong> Databricks-specific docs on join strategies, including the <code>SKEW</code> and <code>RANGE</code> hints, and how AQE is used on Databricks&#8203;</p><p><a href="https://docs.databricks.com/gcp/en/transform/join#:~:text=Join%20hints%20on%20Databricks">docs.databricks.com</a></p></li><li><p><strong>&#8220;How Databricks Optimizes Spark SQL Joins&#8221; &#8211; Medium (dezimaldata):</strong> A blog post (Aug 2023) summarizing Databricks&#8217; techniques like CBO, AQE, range join and skew join optimizations&#8203;</p><p><a href="https://dezimaldata.medium.com/how-databricks-optimizes-the-spark-sql-joins-d01b95336d65#:~:text=In%20this%20blog%20post%2C%20we,joins%20and%20subqueries%2C%20such%20as">dezimaldata.medium.com</a></p></li><li><p><strong>Spark Summit Talks on Joins and AQE:</strong> Videos like <em>&#8220;Optimizing Shuffle Heavy Workloads&#8221;</em> or <em>&#8220;AQE in Spark 3.0&#8221;</em> (by Databricks engineers) for a deeper understanding of the internals of join execution and tuning.</p></li><li><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html#join-strategy-hints">https://spark.apache.org/docs/latest/sql-performance-tuning.html#join-strategy-hints</a></p></li><li><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html#adaptive-query-execution">https://spark.apache.org/docs/latest/sql-performance-tuning.html#adaptive-query-execution</a></p></li><li><p><a href="https://dezimaldata.medium.com/how-databricks-optimizes-the-spark-sql-joins-d01b95336d65">How Databricks Optimizes the Spark SQL Joins</a></p></li><li><p><a href="https://blogs.perficient.com/2025/03/28/top-5-mistakes-that-make-your-databricks-queries-slow-and-how-to-fix-them/">Top 5 Mistakes That Make Your Databricks Queries Slow (and How to Fix Them)</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Your Degree Isn't Enough: How to Actually Break Into Data]]></title><description><![CDATA[Practical Tips for Building Real Experience, Networking Authentically, and Winning Interviews]]></description><link>https://www.canadiandataguy.com/p/your-degree-isnt-enough-how-to-actually</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/your-degree-isnt-enough-how-to-actually</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Thu, 20 Mar 2025 14:03:33 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!yl5x!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Landing your first job in data can seem overwhelming, especially when everyone around you knows Python, SQL, and visualization tools like Power BI. Technical skills alone are no longer enough to differentiate yourself in today's competitive job market. Here's a strategic roadmap to landing your first role in data, going beyond the basics and showcasing how you can truly stand out.</p><h3>Accept the Market Reality</h3><p>In any room filled with aspiring data professionals, you'll find that nearly everyone knows Python, SQL, Power BI, or Tableau. These skills, while essential, are now commodities&#8212;they&#8217;re expected rather than exceptional. Even personal or college projects have become commonplace.</p><p>To land your first job, you need to set yourself apart.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yl5x!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yl5x!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png 424w, https://substackcdn.com/image/fetch/$s_!yl5x!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png 848w, https://substackcdn.com/image/fetch/$s_!yl5x!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png 1272w, https://substackcdn.com/image/fetch/$s_!yl5x!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yl5x!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/215518ca-959c-49d6-bf96-dbd889fb1051_3840x2553.png&quot;,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:968,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:747306,&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://www.canadiandataguy.com/i/159459480?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F215518ca-959c-49d6-bf96-dbd889fb1051_3840x2553.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yl5x!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png 424w, https://substackcdn.com/image/fetch/$s_!yl5x!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png 848w, https://substackcdn.com/image/fetch/$s_!yl5x!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.png 1272w, https://substackcdn.com/image/fetch/$s_!yl5x!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f05f084-c439-4478-b568-4193f59fe9c0_3840x2553.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"><svg role="img" 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"><g><title></title><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"><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"><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>Differentiate Yourself: Gain Real-world Experience</h3><p>How can you differentiate yourself, especially without prior work experience? One highly effective strategy is engaging directly with startup founders.</p><p><strong>Step-by-Step Approach:</strong></p><ol><li><p><strong>Identify Founders and Startups:</strong> Visit platforms like <a href="https://wellfound.com">Wellfound.com</a> to connect with early-stage startups&#8212;seed-funded or even pre-funded ideas.</p></li><li><p><strong>Offer Value First:</strong> Proactively offer your data skills for free initially. While the idea of unpaid work may seem counterintuitive, the true compensation comes in the form of valuable experience, mentorship, and exposure to product-driven thinking.</p></li><li><p><strong>Build Real Projects:</strong> Working with startups provides real-world projects, solving genuine problems. When interviewing, you&#8217;ll have concrete examples of your work to share, far superior to hypothetical or generic projects.</p></li><li><p><strong>Gain Product Thinking:</strong> Startups will help you learn product development, user needs, and business strategy&#8212;skills rarely developed through traditional academic projects.</p></li></ol><h3>Networking with the Right Mindset</h3><p>A common mistake people make at networking events is immediately asking for jobs or favors. Trust that the person you're speaking with already knows you're looking for opportunities. Instead, approach networking events with genuine curiosity. Seek to understand what products and challenges people are working on. If the conversation naturally leads to them asking about your interests, then introduce yourself, clearly express your intent, and genuinely offer to help on a project for free. Your goal is simply to create opportunities. Most good people won't let your contributions go unpaid indefinitely, but let them initiate compensation conversations.</p><h3>Get Certified: Beyond Degrees</h3><p>Degrees are common; certifications stand out. Specializing in high-demand platforms makes you more attractive to employers.</p><p><strong>Recommended Certifications:</strong></p><ul><li><p><strong>Databricks Certifications</strong></p></li><li><p><strong>Snowflake Certifications</strong></p></li><li><p><strong>dbt (Data Build Tool) Certification</strong></p></li></ul><p>Highlight certifications prominently on your resume (top-left corner) to immediately capture recruiters' attention.</p><h3>Target Your Applications Strategically</h3><p>Certifications aren't just decorations; they open doors to companies actively seeking specialized talent.</p><p><strong>Leveraging Certifications:</strong></p><ul><li><p>Consulting firms (Deloitte, Accenture, Slalom) partner with platforms like Databricks and Snowflake. Companies needing certified specialists approach these consultancies.</p></li><li><p>Research prominent Databricks or Snowflake consulting partners, identify open roles, and directly apply.</p></li><li><p>Go beyond applying&#8212;reach out personally to recruiters via LinkedIn, clearly showcasing your certifications and enthusiasm.</p></li></ul><h3>Prepare Effectively for Interviews: STAR Methodology and Hooks</h3><p>Due to the short attention span of interviewers, begin your answers by clearly stating the <strong>impact or result</strong> upfront within the first 30 seconds. Follow this hook with the Situation, Task, and Action details.</p><p><strong>Example Hook:</strong></p><p>"I saved a million dollars in licensing costs and significantly reduced CO2 emissions by building a real-time data system using Databricks that optimized drill bit movements during oil exploration."</p><p>Notice how this statement immediately highlights both financial and environmental impacts, making it compelling and memorable.</p><p><strong>STAR Methodology Recap:</strong></p><ul><li><p><strong>Result First (Hook):</strong> State your biggest impact immediately.</p></li><li><p><strong>Situation:</strong> Describe the scenario briefly.</p></li><li><p><strong>Task:</strong> Clarify your specific responsibility.</p></li><li><p><strong>Action:</strong> Detail the steps you took.</p></li></ul><p>Practicing this method ensures your answers are concise, impactful, and easy for interviewers to remember.</p><h3>Leverage Consulting Roles as a Stepping Stone</h3><p>Your initial goal is breaking into the industry. Consulting roles offer valuable exposure and practical experience. After building experience over 2-3 years, aim higher:</p><ul><li><p>Target product companies like Databricks or Snowflake directly.</p></li><li><p>These roles typically offer significantly better compensation, career growth, and specialized experience.</p></li></ul><h3>Key Takeaways</h3><ul><li><p><strong>Real-world experience</strong>: Prioritize working on actual startup projects over hypothetical college projects.</p></li><li><p><strong>Networking</strong>: Approach networking events genuinely, seeking understanding and creating opportunities rather than directly asking for jobs.</p></li><li><p><strong>Certifications</strong>: Invest in high-impact certifications that directly align with industry demands.</p></li><li><p><strong>Strategic networking</strong>: Proactively connect with recruiters, leveraging your specialized skills.</p></li><li><p><strong>Interview Preparation</strong>: Master the STAR methodology with impactful hooks to clearly demonstrate your value.</p></li><li><p><strong>Career trajectory</strong>: View consulting as an entry point, not the end goal.</p></li></ul><p>Landing your first data job requires strategic moves beyond just technical skills. By adopting this proactive, differentiated approach, you'll set yourself apart and significantly increase your chances of success.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[How to Generate 1TB of Synthetic Data Faster Than a Coffee Break]]></title><description><![CDATA[And Cheaper Than Your Starbucks Coffee]]></description><link>https://www.canadiandataguy.com/p/how-to-generate-1tb-of-synthetic</link><guid isPermaLink="false">https://www.canadiandataguy.com/p/how-to-generate-1tb-of-synthetic</guid><dc:creator><![CDATA[Canadian Data Guy]]></dc:creator><pubDate>Wed, 01 Jan 2025 15:01:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/UsOqtW3Nebw" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine creating a massive 1 Terabyte dataset of IoT data in less time than it takes to enjoy your coffee break. With synthetic data generation techniques and a bit more computing power, this becomes a reality. By leveraging an 4-core machine, we can process an astounding 1 million rows per second, with each row containing 1 KB of data. Let's break down what this means:</p><ul><li><p>1 billion rows of 1 KB each equates to 1000 GB or 1 TB of data.</p></li><li><p>At a rate of 1 million rows per second, it takes approximately 1000 seconds (about 16.66 minutes) to generate 1 billion rows.</p><div id="youtube2-UsOqtW3Nebw" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;UsOqtW3Nebw&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/UsOqtW3Nebw?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div></li></ul><p>This means you can create a full terabyte of synthetic IoT data in under 10 minutes on 8 core machine; I have used 4 in my example. Such rapid data generation opens up exciting possibilities for developers, data scientists, and researchers working on big data projects, IoT applications, or machine learning models that require extensive datasets for training and testing.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!n0Dp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!n0Dp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.png 424w, https://substackcdn.com/image/fetch/$s_!n0Dp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.png 848w, https://substackcdn.com/image/fetch/$s_!n0Dp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.png 1272w, https://substackcdn.com/image/fetch/$s_!n0Dp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!n0Dp!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.png" width="1200" height="324.72527472527474" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:394,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:202158,&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;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!n0Dp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.png 424w, https://substackcdn.com/image/fetch/$s_!n0Dp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.png 848w, https://substackcdn.com/image/fetch/$s_!n0Dp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.png 1272w, https://substackcdn.com/image/fetch/$s_!n0Dp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F710dbeb7-ac9c-4415-a82f-c6af8434d6ba_2784x754.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"><svg role="img" 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"><g><title></title><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"><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"><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><figcaption class="image-caption">ScreenShot Of Spark Streaming UI</figcaption></figure></div><h2><br>Why create Synthetic datasets?</h2><ul><li><p><strong>Privacy and Compliance</strong>: Synthetic data allows developers to work with realistic data without risking exposure of sensitive information, helping to meet data protection regulations.</p></li><li><p><strong>Scalability and Control</strong>: You can generate virtually unlimited amounts of data with precise control over its characteristics, enabling thorough testing of systems at scale and creation of edge cases that might be rare or impossible to capture in real-world data.</p></li><li><p><strong>Development Acceleration:</strong> By removing dependency on upstream teams for data, developers can build end-to-end pipelines, set up DevOps processes, and address architectural concerns before actual data becomes available, significantly speeding up the development process.</p></li><li><p><strong>Cost-Effectiveness and Efficiency</strong>: Generating synthetic data is often faster and more economical than collecting and processing real-world data, especially for large-scale testing and development.</p></li></ul><h2>Hardware</h2><p>We used a single machine with 4 cores and 32 GB of memory</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dHo1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dHo1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.png 424w, https://substackcdn.com/image/fetch/$s_!dHo1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.png 848w, https://substackcdn.com/image/fetch/$s_!dHo1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.png 1272w, https://substackcdn.com/image/fetch/$s_!dHo1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dHo1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.png" width="806" height="336" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:336,&quot;width&quot;:806,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40861,&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;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dHo1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.png 424w, https://substackcdn.com/image/fetch/$s_!dHo1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.png 848w, https://substackcdn.com/image/fetch/$s_!dHo1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.png 1272w, https://substackcdn.com/image/fetch/$s_!dHo1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d73af22-0776-4cc0-ad01-d36e778ef405_806x336.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"><svg role="img" 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"><g><title></title><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"><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"><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>Let&#8217;s get into the code</h2><h4><strong>Install <a href="https://github.com/databrickslabs/dbldatagen">Databricks Data Generator</a></strong></h4><p>The <a href="https://github.com/databrickslabs/dbldatagen">dbldatagen</a> is a Python library for generating synthetic data within the Databricks environment using Spark. The generated data may be used for testing, benchmarking, demos, and many other uses.</p><p>It operates by defining a data generation specification in code that controls how the synthetic data is generated. The specification may incorporate the use of existing schemas or create data in an ad-hoc fashion. You can use it from Scala, R or other languages by defining a view over the generated data.</p><pre><code><code>%pip install dbldatagen</code></code></pre><h4> Setup and Imports </h4><pre><code><code>import dbldatagen as dg
import uuid

from pyspark.sql.types import StructType, StructField, StringType, TimestampType, DoubleType, IntegerType
from pyspark.sql.functions import expr</code></code></pre><h4> Parameters </h4><pre><code><code># Parameterize partitions and rows per second
PARTITIONS = 4 # Match with number of cores on your cluster
ROWS_PER_SECOND = 1 * 1000 * 1000 # 1 Million rows per second</code></code></pre><h4><strong>Schema Definition</strong></h4><pre><code>
iot_data_schema = StructType([
    StructField("device_id", StringType(), False),
    StructField("event_timestamp", TimestampType(), False),
    StructField("temperature", DoubleType(), False),
    StructField("humidity", DoubleType(), False),
    StructField("pressure", DoubleType(), False),
    StructField("battery_level", IntegerType(), False),
    StructField("device_type", StringType(), False),
    StructField("error_code", IntegerType(), True),
    StructField("signal_strength", IntegerType(), False)
])</code></pre><p>Here, we define the schema for our IoT data. Each <code>StructField</code> represents a column in our dataset, specifying the name, data type, and whether it can contain null values. This schema mimics real-world IoT device data, including device identifiers, sensor readings, and status information.</p><h3><strong>Why use Databricks Data Generator- </strong><code>dbldatagen</code><strong>?</strong></h3><p>Using <code>dbldatagen</code> for synthetic data generation offers several significant benefits that align closely with the characteristics of your actual data. <strong>The ability to specify parameters like </strong><code>minValue</code><strong>, </strong><code>maxValue</code><strong>, </strong><code>random</code><strong>, and </strong><code>percentNulls</code><strong> allows you to create datasets that closely mimic real-world scenarios.</strong> This means you can generate realistic variations in your data, such as different temperature ranges or device IDs, while also controlling for missing values. By tailoring these specifications, you ensure that the synthetic data is not only large in volume but also rich in diversity, making it a valuable resource for testing and training machine learning models effectively.</p><pre><code>dataspec = (
    dg.DataGenerator(spark, name="iot_data", partitions=PARTITIONS)
    .withSchema(iot_data_schema)
    .withColumnSpec("device_id", percentNulls=0.1, minValue=1000, maxValue=9999, prefix="DEV_", random=True)
    .withColumnSpec("event_timestamp", begin="2023-01-01 00:00:00", end="2023-12-31 23:59:59", random=True)
    .withColumnSpec("temperature", minValue=-10.0, maxValue=40.0, random=True)
    .withColumnSpec("humidity", minValue=0.0, maxValue=100.0, random=True)
    .withColumnSpec("pressure", minValue=900.0, maxValue=1100.0, random=True)
    .withColumnSpec("battery_level", minValue=0, maxValue=100, random=True)
    .withColumnSpec("device_type", values=["Sensor", "Actuator", "Gateway", "Controller"], random=True)
    .withColumnSpec("error_code", minValue=0, maxValue=999, random=True, percentNulls=0.2)
    .withColumnSpec("signal_strength", minValue=-100, maxValue=0, random=True)
)
</code></pre><p>This section creates a data generator specification using <code>dbldatagen</code>. For each column, we define the data generation rules, including value ranges, randomness, and special formatting (like the "DEV_" prefix for device IDs). This ensures our synthetic data closely resembles real IoT data patterns.</p><h2><strong>Streaming DataFrame Creation</strong></h2><pre><code>streaming_df = (
    dataspec.build(
        withStreaming=True,
        options={
            'rowsPerSecond': ROWS_PER_SECOND,
        }
    )
    .withColumn(
        "firmware_version",
        expr(
            "concat('v', cast(floor(rand() * 10) as string), '.', "
            "cast(floor(rand() * 10) as string), '.', "
            "cast(floor(rand() * 10) as string))"
        )
    )
    .withColumn(
        "location",
        expr(
            "concat(cast(rand() * 180 - 90 as decimal(8,6)), ',', "
            "cast(rand() * 360 - 180 as decimal(9,6)))"
        )
    )
    .withColumn(
        "data_payload",
        expr("repeat(uuid(), 22)")  # Add approx. 800 Bytes to construct 1 KB row
    )
)
<code>streaming_df = ( dataspec.build( withStreaming=True, options={ 'rowsPerSecond': ROWS_PER_SECOND, } ) .withColumn( "firmware_version", expr( "concat('v', cast(floor(rand() * 10) as string), '.', " "cast(floor(rand() * 10) as string), '.', " "cast(floor(rand() * 10) as string))" ) ) .withColumn( "location", expr( "concat(cast(rand() * 180 - 90 as decimal(8,6)), ',', " "cast(rand() * 360 - 180 as decimal(9,6)))" ) ) .withColumn( "data_payload", expr("repeat(uuid(), 22)") # Add approx. 800 Bytes to construct 1 KB row ) )</code></code></pre><p>Here, we build the streaming DataFrame using our data specification. We enable streaming with `<code>withStreaming=True`</code>and set the rows per second. We also add additional columns:</p><ul><li><p><code>firmware_version</code>: A randomly generated version number.</p></li><li><p><code>location</code>: Random latitude and longitude coordinates.</p></li><li><p><code>data_payload</code>: <strong>A large string to reach our 1 KB per row target.</strong></p></li></ul><h4><strong>Data Writing</strong></h4><pre><code>streaming_df.writeStream
    .queryName("iot_data_stream")
    .outputMode("append")
    .option("checkpointLocation", f"/tmp/dbldatagen/streamingDemo/checkpoint-{uuid.uuid4()}")
    .toTable("soni.default.iot_data_1kb_rows")</code></pre><p>Finally, we initiate the streaming process. The data is written to a Delta table named "iot_data_1kb_rows" in append mode. A checkpoint location is specified to allow for fault-tolerant execution of the streaming query.<br></p><h2>Is it really cheaper than Starbucks coffee?</h2><p>On a 4-core setup, generating a billion rows of synthetic IoT data would take approximately 17 minutes. Adding an extra 5 minutes as a buffer for instance setup brings the total time to 22 minutes.</p><p><strong>Cost Breakdown for Generating 1 Billion Rows of Synthetic IoT Data:</strong></p><ul><li><p><strong>Total time</strong>: 17 minutes (data generation) + 5 minutes (instance setup) = <strong>22 minutes</strong></p></li><li><p><strong>Time in hours</strong>: 22&#247;60=0.3667 hours</p></li><li><p><strong>Cost EC2 + Databricks</strong>: $0.228 </p></li></ul><p>This means generating a terabyte of synthetic IoT data costs just $0.228&#8212;less than the price of all coffee options. Such efficiency showcases the cost-effectiveness of synthetic data generation, enabling developers and data scientists to create large-scale datasets for testing and development at a fraction of the cost of traditional methods.</p><p>Furthermore, as illustrated in the graph below, the CPU utilization consistently exceeds 80%, highlighting the system's optimized performance and contributing to the remarkably low cost.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jL9L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jL9L!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png 424w, https://substackcdn.com/image/fetch/$s_!jL9L!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png 848w, https://substackcdn.com/image/fetch/$s_!jL9L!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png 1272w, https://substackcdn.com/image/fetch/$s_!jL9L!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jL9L!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png" width="1200" height="669.2307692307693" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:812,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:499985,&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;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!jL9L!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png 424w, https://substackcdn.com/image/fetch/$s_!jL9L!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png 848w, https://substackcdn.com/image/fetch/$s_!jL9L!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png 1272w, https://substackcdn.com/image/fetch/$s_!jL9L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83659c76-f9a4-4bb4-b452-ae8841274798_3710x2068.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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>Stay Connected &amp; Keep Learning: Join Our Community</h2><p>If you found this post helpful, please drop a like to keep me motivated! And feel free to leave a comment below if you have any questions or thoughts&#8212;I'd love to hear from you!</p><p>If this is your first time reading my content, <strong>Welcome</strong>! I write in-depth technical blogs on <strong>Spark</strong>, <strong>Databricks</strong>, and <strong>Spark Streaming</strong>. Beyond writing, I specialize in helping data professionals unlock their full potential and ace their next data interviews.</p><p>Here are a few ways you can continue to learn, connect, and grow:</p><ul><li><p><strong>Join Us on WhatsApp:</strong> Stay updated and engage with the community through our WhatsApp group. <a href="https://chat.whatsapp.com/HyAuSAQsMj776rbbvC3sxH">Join here</a>.</p></li><li><p><strong>Join Our Discord Community:</strong> Connect with past clients and other data enthusiasts on our Discord server. It&#8217;s a great place to network, pair up with peers, and accelerate each other&#8217;s journeys. <a href="https://discord.gg/fqVA4wm98s">Join here</a>.</p></li><li><p><strong>Visit My Website:</strong> My website is your go-to resource for content, including blogs, tutorials, and more. <a href="https://canadiandataguy.com/">Check it out here</a>.</p></li></ul><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.canadiandataguy.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading CanadianDataGuy&#8217;s No Fluff Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>