<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: apachespark</title>
    <description>The latest articles tagged 'apachespark' on DEV Community.</description>
    <link>https://dev.to/t/apachespark</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/apachespark"/>
    <language>en</language>
    <item>
      <title>Aligning Timeouts in Distributed Orchestration: Why Equal Airflow and Spark Limits Lead to Race Conditions</title>
      <dc:creator>Reinaldo Del Dotore</dc:creator>
      <pubDate>Sun, 17 May 2026 18:19:24 +0000</pubDate>
      <link>https://dev.to/deldotore/aligning-timeouts-in-distributed-orchestration-why-equal-airflow-and-spark-limits-lead-to-race-1c3f</link>
      <guid>https://dev.to/deldotore/aligning-timeouts-in-distributed-orchestration-why-equal-airflow-and-spark-limits-lead-to-race-1c3f</guid>
      <description>&lt;p&gt;Recently, I reviewed an Airflow DAG where each task submits a single Spark job. I found this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;execution_timeout_minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60&lt;/span&gt; 
&lt;span class="na"&gt;spark-job-timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, it looks redundant. Two timeouts, same value. Why do both exist? The answer reveals something important about how Airflow and Spark interact.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Different Layers, Different Clocks&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;execution_timeout_minutes: is the Airflow task timeout. Its clock starts when the task enters the running state and covers everything: job submission to the cluster, queue wait time, Spark execution, status polling, and cleanup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;spark-job-timeout-minutes: is the timeout applied only to the Spark processing running in the cluster. It basically says: "if this application runs longer than X, abort it". In other words: it does not include submission overhead, queueing time, or the processing the Airflow task performs before or after Spark execution.   &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Key Takeaway&lt;br&gt;
    These are two different clocks measuring two different things, and the Airflow clock starts ticking before the Spark application even exists.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;The Problem with Setting Them Equal&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;With a 60/60 configuration, which timeout triggers first becomes timing-dependent. And &lt;strong&gt;because Airflow starts counting earlier, it tends to hit its timeout first&lt;/strong&gt; in practice.&lt;/p&gt;

&lt;p&gt;That is the worst-case scenario: Airflow terminates the task before Spark shuts down properly. Depending on the integration being used, the &lt;strong&gt;Spark job may continue running in the cluster orphaned, consuming resources until someone notices&lt;/strong&gt;. Orphaned jobs are one of the biggest hidden cost drivers in shared clusters: they consume CPU, memory, and sometimes even autoscale nodes long after the orchestrator has given up.&lt;/p&gt;

&lt;p&gt;The desired behavior is the opposite: Spark should hit its own timeout first, fail cleanly, and allow the Airflow task to receive that failure within its own execution window. In distributed systems, the layer responsible for the actual processing should ideally detect and terminate problematic execution first.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A Practical Rule&lt;br&gt;
execution_timeout_minutes &amp;gt; spark-job-timeout-minutes&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The gap between them must absorb submission time, queueing, polling, and cleanup: components that typically add a few minutes even for small jobs.&lt;/p&gt;

&lt;p&gt;Since this overhead tends to vary little within the same environment, think in absolute time, not percentages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warm, fixed cluster: +5 min&lt;/li&gt;
&lt;li&gt;Livy/REST submission with moderate queueing: +10 min&lt;/li&gt;
&lt;li&gt;Ephemeral clusters (EMR on-demand, Databricks job clusters): +15 to 20 min&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;The Adjustment&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Looking at the execution history, this DAG usually completed in 4 to 5 minutes. The original 60-minute limits were simply inherited defensive defaults nobody had revisited.&lt;/p&gt;

&lt;p&gt;I reduced them to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spark-job-timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15&lt;/span&gt; 
&lt;span class="na"&gt;execution_timeout_minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is roughly three to four times the observed average runtime — enough to absorb normal variance and occasional spikes without masking real hangs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Inflated timeouts do not protect anything: they only delay alerts when something is genuinely stuck.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Final Thoughts&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Timeouts are not arbitrary numbers. Each exists at a different layer (the orchestrator and the execution engine) with different responsibilities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When they are aligned correctly (the orchestrator having some margin over the execution engine), failures become predictable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When they are equal, you create a race that hides real problems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And the correct value is rarely the one someone set two years ago and never reviewed again.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Timeouts are not safety nets: they are alarms. And alarms only work when they ring at the right time.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you enjoyed this insight on Data Platform Engineering, feel free to connect with me on LinkedIn for more discussions on data architecture and orchestration.&lt;/p&gt;




</description>
      <category>dataengineering</category>
      <category>apacheairflow</category>
      <category>apachespark</category>
      <category>dataplatform</category>
    </item>
    <item>
      <title>Broadcast Joins vs. Sort-Merge Joins: Choosing the Right Join Strategy in Apache Spark</title>
      <dc:creator>harshvardhan</dc:creator>
      <pubDate>Tue, 12 May 2026 17:17:41 +0000</pubDate>
      <link>https://dev.to/hvardhan28/broadcast-joins-vs-sort-merge-joins-choosing-the-right-join-strategy-in-apache-spark-d52</link>
      <guid>https://dev.to/hvardhan28/broadcast-joins-vs-sort-merge-joins-choosing-the-right-join-strategy-in-apache-spark-d52</guid>
      <description>&lt;p&gt;In distributed data processing systems such as Apache Spark, joins are among the most expensive operations. The strategy used to join datasets can significantly impact execution time, memory consumption, and overall cluster performance. Two of the most widely used join techniques are &lt;strong&gt;Broadcast Joins and Sort-Merge Joins&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Although both are designed to combine datasets efficiently, they solve different performance challenges. Understanding when to use each can help optimize ETL pipelines, analytics workloads, and large-scale data processing applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Broadcast Join?
&lt;/h2&gt;

&lt;p&gt;A Broadcast Join is typically used when one dataset is very small compared to the other. Instead of shuffling both datasets across the cluster, the smaller table is copied, or “broadcasted,” to every worker node. Each executor then performs the join locally with its partition of the larger dataset.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orders table → 2 TB&lt;/li&gt;
&lt;li&gt;Product table → 10 MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than moving the 2 TB dataset over the network, the system distributes the 10 MB product table to all executors and joins locally. This avoids expensive shuffle operations and greatly improves performance.&lt;/p&gt;

&lt;p&gt;In Apache Spark, Broadcast Joins are commonly implemented using hash joins internally and are especially effective in star-schema data warehouse models where large fact tables are joined with small dimension tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of Broadcast Joins
&lt;/h2&gt;

&lt;p&gt;Broadcast Joins are extremely fast for small-large joins because they minimize network shuffling. Since the large dataset remains partitioned as-is, execution becomes more efficient and query latency decreases significantly.&lt;/p&gt;

&lt;p&gt;Other advantages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduced shuffle and disk spill.&lt;/li&gt;
&lt;li&gt;Faster execution for lookup-style joins.&lt;/li&gt;
&lt;li&gt;Excellent performance for dimension tables.&lt;/li&gt;
&lt;li&gt;Ideal for interactive analytics workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, Broadcast Joins also have limitations. The smaller dataset must fit comfortably into executor memory. Broadcasting a table that is too large can cause memory pressure, garbage collection overhead, or executor failures. In very large clusters, repeatedly distributing even moderately sized tables can also become expensive.&lt;/p&gt;

&lt;p&gt;A typical Spark example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;broadcast&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;large_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_df&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;small_df&lt;/code&gt; is explicitly broadcast to all worker nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Sort-Merge Join?
&lt;/h2&gt;

&lt;p&gt;A Sort-Merge Join (SMJ) is designed for situations where both datasets are large and broadcasting is impractical. Instead of replicating data, both datasets are shuffled across the cluster so rows with matching join keys end up on the same executor.&lt;/p&gt;

&lt;p&gt;The process usually involves three stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Repartitioning both datasets on the join key&lt;/li&gt;
&lt;li&gt;Sorting data within each partition&lt;/li&gt;
&lt;li&gt;Merging sorted partitions to generate joined rows&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Consider this example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer events → 4 TB&lt;/li&gt;
&lt;li&gt;Transaction logs → 3 TB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since neither table is small enough to broadcast, a Sort-Merge Join becomes the preferred strategy.&lt;/p&gt;

&lt;p&gt;Sort-Merge Joins are highly scalable and are commonly used in enterprise ETL pipelines and large data lake architectures. Unlike Broadcast Joins, they process sorted streams incrementally, making them more memory-efficient for huge datasets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of Sort-Merge Joins
&lt;/h2&gt;

&lt;p&gt;The biggest advantage of Sort-Merge Joins is scalability. They can efficiently handle joins involving terabytes or petabytes of data without requiring one dataset to fit in memory.&lt;/p&gt;

&lt;p&gt;Additional advantages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Suitable for very large distributed joins&lt;/li&gt;
&lt;li&gt;More stable for batch processing workloads&lt;/li&gt;
&lt;li&gt;Better memory handling for massive datasets&lt;/li&gt;
&lt;li&gt;Works well with partitioned or pre-sorted data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Despite these strengths, Sort-Merge Joins are more expensive than Broadcast Joins because they involve heavy shuffling and sorting operations. Network transfer, CPU usage, and disk I/O can become significant bottlenecks, especially when data skew exists.&lt;/p&gt;

&lt;p&gt;In Spark, Sort-Merge Join is often the default strategy for large joins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.autoBroadcastJoinThreshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;large_df1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;large_df2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disabling automatic broadcast forces Spark to select another strategy, commonly Sort-Merge Join.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Spark Automatically Chooses Join Types
&lt;/h2&gt;

&lt;p&gt;Apache Spark uses the Catalyst Optimizer and cost-based optimization techniques to decide which join strategy to use.&lt;/p&gt;

&lt;p&gt;By default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small tables below the broadcast threshold are broadcasted&lt;/li&gt;
&lt;li&gt;Large joins typically use Sort-Merge Join&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key configuration is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spark.sql.autoBroadcastJoinThreshold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default value: 10 MB&lt;/p&gt;

&lt;p&gt;If a dataset is smaller than this threshold, Spark may automatically choose a Broadcast Join.&lt;/p&gt;

&lt;p&gt;Modern Spark versions also support Adaptive Query Execution (AQE), which can dynamically switch join strategies during runtime. For instance, Spark may initially plan a Sort-Merge Join but later convert it into a Broadcast Join if runtime statistics reveal that one dataset is small enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Optimization Tips
&lt;/h2&gt;

&lt;p&gt;For Broadcast Joins:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep broadcast tables small&lt;/li&gt;
&lt;li&gt;Remove unnecessary columns before joining&lt;/li&gt;
&lt;li&gt;Apply filters early&lt;/li&gt;
&lt;li&gt;Avoid broadcasting medium-sized datasets without memory analysis&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Sort-Merge Joins:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Repartition datasets carefully&lt;/li&gt;
&lt;li&gt;Use high-cardinality join keys when possible&lt;/li&gt;
&lt;li&gt;Optimize skewed data distributions&lt;/li&gt;
&lt;li&gt;Enable adaptive query execution&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Data skew remains one of the biggest challenges in distributed joins. A few heavily repeated keys can overload certain executors and slow down the entire pipeline. Techniques such as salting and skew join optimization can help mitigate these issues.&lt;/p&gt;

</description>
      <category>apachespark</category>
      <category>sql</category>
      <category>joins</category>
    </item>
    <item>
      <title>How I debugged a Delta Lake DESCRIBE HISTORY timeout (and what's actually causing it)</title>
      <dc:creator>Abhishek Ambare</dc:creator>
      <pubDate>Mon, 04 May 2026 06:13:16 +0000</pubDate>
      <link>https://dev.to/immortalspace003/how-i-debugged-a-delta-lake-describe-history-timeout-and-whats-actually-causing-it-4ad2</link>
      <guid>https://dev.to/immortalspace003/how-i-debugged-a-delta-lake-describe-history-timeout-and-whats-actually-causing-it-4ad2</guid>
      <description>&lt;p&gt;If you have ever run &lt;code&gt;DESCRIBE HISTORY&lt;/code&gt; on a Delta table that receives streaming data every 60 seconds and watched it either hang for hours or crash with an &lt;strong&gt;OutOfMemoryError&lt;/strong&gt;, you are not alone and you are not doing anything wrong. The problem is architectural, and once you understand the internals, the fix becomes a lot clearer.&lt;/p&gt;

&lt;p&gt;Here is what I learned after digging into why this happens and what you can actually do about it.&lt;/p&gt;

&lt;p&gt;How the Delta transaction log works&lt;br&gt;
Every write to a Delta table, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;MERGE&lt;/code&gt;, schema change, gets recorded as a JSON file in a directory called _delta_log at the root of the table. Files are named with zero-padded twenty-digit integers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_delta_log/
├── 00000000000000000000.json
├── 00000000000000000001.json
├── 00000000000000000002.json
...
├── 00000000000000000010.parquet  (checkpoint)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each JSON file contains an array of "actions":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"commitInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1714915200000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STREAMING UPDATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operationMetrics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"numOutputRows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1240"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"scanTimeMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"320"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isolationLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WriteSerializable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isBlindAppend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"part-00001-abc123.snappy.parquet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"partitionValues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"stats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;numRecords&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:1240,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;minValues&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{...},&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;maxValues&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{...}}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 10 commits, Delta generates a Parquet checkpoint file that captures the entire active table state as a compressed, columnar snapshot. When you run a normal query, Spark reads the latest checkpoint and applies only the small delta of JSON commits after it, which is why standard queries stay fast.&lt;/p&gt;

&lt;p&gt;Why &lt;code&gt;DESCRIBE HISTORY&lt;/code&gt; cannot use checkpoints&lt;br&gt;
This is the core issue. The Delta protocol explicitly drops commitInfo when writing checkpoints. Checkpoints are optimized for state reconstruction, not provenance. So when you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DESCRIBE&lt;/span&gt; &lt;span class="n"&gt;HISTORY&lt;/span&gt; &lt;span class="n"&gt;my_streaming_table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;deltaTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;history&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spark gets zero benefit from checkpoints. It has to parse every JSON file in _delta_log from scratch to extract the commitInfo blocks.&lt;/p&gt;

&lt;p&gt;A pipeline that triggers every 60 seconds generates 1,440 commits per day. After a year, that is over half a million JSON files Spark has to read sequentially for a single &lt;code&gt;DESCRIBE HISTORY&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;The three things that actually make it slow&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud storage listing overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS S3, Azure ADLS, and GCS do not have real directory structures. Listing _delta_log requires paginated API calls. S3's ListObjectsV2 returns at most 1,000 keys per request, so listing one million JSON files means 1,000 sequential HTTP requests before a single read task is scheduled. This is a pure I/O bottleneck. Adding more workers does not help here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small file JSON parsing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JSON is row-based text. Each two-kilobyte file requires a separate TCP connection to open, a full text parse to find the nested commitInfo struct, and type casting on every field. Multiply that by millions of files and executor CPU gets overwhelmed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Driver OOM on shuffle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After executor nodes parse the JSON files, they shuffle the commitInfo structs back to the driver for aggregation and sorting. The driver's JVM heap has to hold all of this at once. When millions of records with nested maps like operationMetrics and operationParameters hit the driver simultaneously, you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;java.lang.OutOfMemoryError: GC overhead limit exceeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the query dies.&lt;/p&gt;

&lt;p&gt;What you can do about it&lt;br&gt;
Reduce log retention (immediate impact)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;my_streaming_table&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'delta.logRetentionDuration'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'interval 7 days'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'delta.deletedFileRetentionDuration'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'interval 7 days'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Delta to purge old JSON commit files during checkpointing. &lt;code&gt;DESCRIBE HISTORY&lt;/code&gt; will now only parse 7 days of history instead of 30. One constraint to know: starting with Databricks Runtime 18.0, logRetentionDuration must be greater than or equal to deletedFileRetentionDuration, otherwise you get a validation error.&lt;/p&gt;

&lt;p&gt;Enable Minor Log Compaction (Delta 3.0+)&lt;/p&gt;

&lt;p&gt;Delta 3.0 introduced Minor Log Compaction, which combines multiple sequential JSON commits into a single consolidated file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_delta_log/00000100.00000200.compact.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This dramatically reduces the file count &lt;code&gt;DESCRIBE HISTORY&lt;/code&gt; has to work through. It is enabled by default in modern runtimes, but you can explicitly control it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="nv"&gt;spark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;conf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;"spark.databricks.delta.deltaLog.minorCompaction.useForReads"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use Unity Catalog system tables instead&lt;/p&gt;

&lt;p&gt;For systematic auditing, querying system.access.audit is significantly faster than &lt;code&gt;DESCRIBE HISTORY&lt;/code&gt; because it is a pre-optimized Delta table, not a raw JSON parse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;action_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_params&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;access&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audit&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;request_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'table_full_name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'my_catalog.my_schema.my_table'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, system.query.history gives you execution metrics and durations for writes without ever touching the transaction log.&lt;/p&gt;

&lt;p&gt;Upgrade driver memory&lt;/p&gt;

&lt;p&gt;When you cannot avoid querying large histories, switch to a memory-optimized driver instance. On AWS, migrating from m5.xlarge to r5.4xlarge gives the JVM enough heap to aggregate millions of records without hitting OOM.&lt;/p&gt;

&lt;p&gt;Medallion Architecture for high-frequency pipelines&lt;/p&gt;

&lt;p&gt;If your pipeline runs MERGE operations frequently against a table that also gets queried, the pattern that works is to ingest raw streaming data as append-only writes into a Bronze table, then run a scheduled bulk MERGE on an hourly cadence into Silver or Gold. This keeps downstream tables clean while the Bronze table handles the commit volume.&lt;/p&gt;

&lt;p&gt;Also worth looking at: Deletion Vectors (available in modern Databricks runtimes), which mark rows as logically deleted via compressed bitmap files instead of rewriting the entire Parquet file on every UPDATE or MERGE. This cuts AddFile and RemoveFile churn in the JSON commits significantly.&lt;/p&gt;

&lt;p&gt;What I would do differently&lt;br&gt;
If I were designing a high-frequency Kafka-to-Delta pipeline today, I would set a 7-day log retention from day one, enable Minor Log Compaction, route all compliance auditing to Unity Catalog system tables rather than &lt;code&gt;DESCRIBE HISTORY&lt;/code&gt;, and extend the streaming trigger to at least 5 minutes unless the downstream business process genuinely needs sub-minute freshness. The transaction log bloat problem is much easier to prevent than to fix after the fact.&lt;/p&gt;

</description>
      <category>dataengin</category>
      <category>databricks</category>
      <category>apachespark</category>
      <category>deltalake</category>
    </item>
    <item>
      <title>Desenvolvendo Pipelines de Dados Escaláveis com Ferramentas Modernas</title>
      <dc:creator>Daniel Capelari</dc:creator>
      <pubDate>Sun, 26 Apr 2026 19:54:40 +0000</pubDate>
      <link>https://dev.to/daniel_capelari/desenvolvendo-pipelines-de-dados-escalaveis-com-ferramentas-modernas-14dk</link>
      <guid>https://dev.to/daniel_capelari/desenvolvendo-pipelines-de-dados-escalaveis-com-ferramentas-modernas-14dk</guid>
      <description>&lt;p&gt;O desenvolvimento de pipelines de dados eficientes é crucial para empresas que lidam com grandes volumes de dados. A capacidade de processar e analisar esses dados de forma rápida e escalável pode ser um fator decisivo na tomada de decisões. No entanto, criar pipelines de dados que atendam às necessidades de uma empresa pode ser um desafio, especialmente quando se lida com fontes de dados diversificadas e formatos de dados variados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introdução às Ferramentas Modernas
&lt;/h2&gt;

&lt;p&gt;As ferramentas modernas de processamento de dados, como o Apache Beam e o Apache Spark, oferecem soluções para esses desafios. Essas tecnologias permitem que os engenheiros de dados criem pipelines de dados escaláveis e eficientes, capazes de lidar com grandes volumes de dados de forma rápida e confiável.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arquitetura de Pipelines de Dados
&lt;/h2&gt;

&lt;p&gt;Uma arquitetura de pipeline de dados bem projetada deve considerar fatores como a fonte dos dados, o processamento, o armazenamento e a análise. O Apache Beam, por exemplo, permite que os desenvolvedores criem pipelines de dados que podem ser executados em diferentes ambientes, como o Google Cloud Dataflow, o Apache Flink e o Apache Spark.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apache_beam&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ParDo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GroupByKey&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apache_beam.options.pipeline_options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PipelineOptions&lt;/span&gt;

&lt;span class="c1"&gt;# Definindo as opções do pipeline
&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PipelineOptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Criando o pipeline
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Lendo os dados de uma fonte
&lt;/span&gt;    &lt;span class="n"&gt;dados&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;ReadFromText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dados.txt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Processando os dados
&lt;/span&gt;    &lt;span class="n"&gt;dados_processados&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dados&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;ParDo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProcessarDados&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c1"&gt;# Agrupando os dados por chave
&lt;/span&gt;    &lt;span class="n"&gt;dados_agrupados&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dados_processados&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;GroupByKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Escrevendo os dados processados em um arquivo
&lt;/span&gt;    &lt;span class="n"&gt;dados_agrupados&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;WriteToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dados_processados.txt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Considerações de Desempenho
&lt;/h2&gt;

&lt;p&gt;Para garantir o desempenho dos pipelines de dados, é importante considerar fatores como a paralelização do processamento, o uso de memória e a otimização dos algoritmos. O Apache Spark, por exemplo, oferece recursos como a paralelização automática do processamento e a otimização de consultas para melhorar o desempenho.&lt;/p&gt;

&lt;h2&gt;
  
  
  Na Prática
&lt;/h2&gt;

&lt;p&gt;Para começar a desenvolver pipelines de dados escaláveis e eficientes, siga os passos abaixo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Escolha uma ferramenta de processamento de dados adequada para as necessidades da sua empresa, como o Apache Beam ou o Apache Spark.&lt;/li&gt;
&lt;li&gt;Defina a arquitetura do pipeline de dados, considerando a fonte dos dados, o processamento, o armazenamento e a análise.&lt;/li&gt;
&lt;li&gt;Implemente o pipeline de dados usando a ferramenta escolhida, considerando fatores como a paralelização do processamento e a otimização dos algoritmos.&lt;/li&gt;
&lt;li&gt;Teste e otimize o pipeline de dados para garantir o desempenho e a escalabilidade.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Em resumo, o desenvolvimento de pipelines de dados escaláveis e eficientes é fundamental para empresas que lidam com grandes volumes de dados. Com as ferramentas modernas de processamento de dados, como o Apache Beam e o Apache Spark, os engenheiros de dados podem criar pipelines de dados que atendam às necessidades da empresa de forma rápida e confiável. &lt;strong&gt;Ação para hoje:&lt;/strong&gt; Explore o Apache Beam ou o Apache Spark e comece a desenvolver um pipeline de dados simples para entender melhor como essas ferramentas podem ajudar na sua empresa.&lt;/p&gt;

</description>
      <category>apachebeam</category>
      <category>apachespark</category>
      <category>pipelinesdedados</category>
      <category>processamentodedados</category>
    </item>
    <item>
      <title>Spark Solved Distributed Compute. QIS Solves Distributed Intelligence.</title>
      <dc:creator>Rory | QIS PROTOCOL</dc:creator>
      <pubDate>Thu, 09 Apr 2026 22:45:54 +0000</pubDate>
      <link>https://dev.to/roryqis/spark-solved-distributed-compute-qis-solves-distributed-intelligence-16l5</link>
      <guid>https://dev.to/roryqis/spark-solved-distributed-compute-qis-solves-distributed-intelligence-16l5</guid>
      <description>&lt;h2&gt;
  
  
  The Layer Zaharia Solved
&lt;/h2&gt;

&lt;p&gt;When Matei Zaharia introduced Resilient Distributed Datasets in his 2012 NSDI paper, he solved a problem that had bottlenecked every large-scale data pipeline for a decade: how do you keep intermediate results in memory across a cluster without rewriting them to disk between every transformation step?&lt;/p&gt;

&lt;p&gt;The answer was RDDs — immutable, partitioned collections that could be rebuilt from lineage rather than replicated eagerly. MapReduce had forced a disk-write-read cycle between every stage. Spark eliminated it. The DAG scheduler could pipeline transformations, coalesce shuffles, and exploit data locality so that compute moved to where the data already sat.&lt;/p&gt;

&lt;p&gt;This was not an incremental improvement. It was an architectural insight. And it earned Zaharia the ACM Prize in Computing because the downstream impact was enormous: Spark became the substrate for batch analytics, stream processing, ML pipelines, and graph computation across the industry. Flink, Ray, and Dask each extended the idea in different directions, but they all operate on the same fundamental assumption.&lt;/p&gt;

&lt;p&gt;That assumption is the subject of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Assumption Underneath
&lt;/h2&gt;

&lt;p&gt;Every system in the Spark lineage — including Spark itself, Flink, Ray, Dask, and the various managed query engines — operates on a shared premise:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raw data exists. It must be processed. Processing requires coordinated compute. Coordinated compute requires cluster management, schema agreement, and shuffle optimization.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is correct. And for the problems these systems address, it is the right framing. If you have 40 TB of event logs and you need to compute a sessionized funnel, you need distributed compute. You need to partition the data, schedule tasks, manage stragglers, and materialize results. Spark does this brilliantly.&lt;/p&gt;

&lt;p&gt;But notice what this framing takes for granted: that the valuable artifact is the raw data, and that intelligence emerges only after you process it centrally (or in coordinated clusters).&lt;/p&gt;

&lt;p&gt;What if the intelligence already exists at the edge — and the problem is not processing, but routing?&lt;/p&gt;

&lt;h2&gt;
  
  
  A Different Layer
&lt;/h2&gt;

&lt;p&gt;Consider a concrete scenario. A hospital in Nairobi has a patient with an unusual drug interaction outcome. The clinician documents the result. That result — the outcome — is roughly 512 bytes of structured data: the drug pair, the patient phenotype cluster, the observed effect, the confidence, the timestamp.&lt;/p&gt;

&lt;p&gt;In the Spark paradigm, this outcome would need to be ingested into a centralized data lake, harmonized against a common schema (OMOP, FHIR, whatever the institution uses), joined against the global dataset, and then surfaced through a query or ML pipeline. The infrastructure cost for that pipeline — the cluster, the ETL, the schema mapping, the governance — is why most hospitals in low-resource settings never participate in global evidence networks. They produce outcomes every day. Those outcomes never leave the building.&lt;/p&gt;

&lt;p&gt;QIS — Quadratic Intelligence Swarm — operates at a different layer entirely. It does not process raw data. It routes pre-distilled outcome packets by semantic similarity.&lt;/p&gt;

&lt;p&gt;The Nairobi clinician's outcome gets emitted as a ~512-byte packet. No raw patient data leaves the facility. No schema harmonization is required. The packet contains only the distilled outcome and enough metadata for similarity matching. QIS routes it to every node in the network whose registered interest profile matches — and those nodes' outcomes route back.&lt;/p&gt;

&lt;p&gt;This is not a data processing problem. It is a routing problem. And the distinction matters because the scaling properties are fundamentally different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling: Linear vs. Quadratic
&lt;/h2&gt;

&lt;p&gt;Spark scales compute linearly. Add N worker nodes, get roughly N times the throughput (minus shuffle overhead, straggler effects, and the usual distributed systems tax). This is good. Linear scaling is what enabled Spark to handle petabyte workloads.&lt;/p&gt;

&lt;p&gt;QIS scales intelligence quadratically. N nodes in a QIS network produce N(N-1)/2 unique synthesis paths — every pair of nodes can generate a novel insight from the combination of their outcomes. The intelligence function is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I(N) = Θ(N²)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the cost per node is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C = O(log N)   [O(1) achievable with locality-aware routing]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not marketing language. It is a direct consequence of the architecture. Each node emits a ~512-byte outcome packet. The routing layer matches packets by semantic similarity. Every pair of matched outcomes creates a synthesis opportunity. The number of pairs grows quadratically with N. The cost per node grows logarithmically (or stays constant) because each node only processes its own matches, not the entire network's traffic.&lt;/p&gt;

&lt;p&gt;For a distributed systems engineer, the analogy is: imagine if adding a node to your Spark cluster didn't just add linear throughput — it added combinatorial insight from every pairwise interaction with existing nodes. And imagine if the shuffle cost for that interaction was bounded by log N rather than N.&lt;/p&gt;

&lt;p&gt;That is what happens when you shift from the compute layer to the routing layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Layers Are Different
&lt;/h2&gt;

&lt;p&gt;Let me be precise about what separates these layers, because the distinction is architectural, not just rhetorical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Spark layer (distributed compute):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input: raw data (events, logs, records, streams)&lt;/li&gt;
&lt;li&gt;Operation: transformation (map, reduce, join, aggregate, window)&lt;/li&gt;
&lt;li&gt;Coordination: DAG scheduling, shuffle, partition management&lt;/li&gt;
&lt;li&gt;Output: computed results (tables, models, aggregates)&lt;/li&gt;
&lt;li&gt;Scaling unit: compute throughput per node&lt;/li&gt;
&lt;li&gt;Failure mode: data loss, straggler delay, shuffle bottleneck&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The QIS layer (distributed intelligence routing):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input: pre-distilled outcome packets (~512 bytes each)&lt;/li&gt;
&lt;li&gt;Operation: semantic similarity matching and routing&lt;/li&gt;
&lt;li&gt;Coordination: Three Elections (Hiring, The Math, Darwinism)&lt;/li&gt;
&lt;li&gt;Output: routed insights, synthesis across matched pairs&lt;/li&gt;
&lt;li&gt;Scaling unit: synthesis paths per node (quadratic)&lt;/li&gt;
&lt;li&gt;Failure mode: routing misalignment (corrected by Darwinism election)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are genuinely different layers. Spark needs to know the schema of your data. QIS needs to know the semantic fingerprint of your outcome. Spark coordinates a cluster to process a job. QIS coordinates a network to route outcomes. Spark fails when shuffles get too expensive. QIS fails when similarity matching drifts — and self-corrects through competitive network selection (the Darwinism election).&lt;/p&gt;

&lt;p&gt;A useful mental model: Spark is Layer 4-5 in the intelligence stack (transport and processing). QIS is Layer 6-7 (presentation and application of intelligence). They do not compete. They compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Elections
&lt;/h2&gt;

&lt;p&gt;For readers unfamiliar with QIS internals, the coordination mechanism is worth examining because it solves the same class of problems that Spark's DAG scheduler solves — just at a different layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Hiring Election.&lt;/strong&gt; Domain experts define what "similar" means for a given context. In a pharmacovigilance network, similarity might weight drug class and patient phenotype heavily. In a climate science network, it might weight geographic region and measurement modality. This is analogous to how a Spark developer defines partitioning keys — except in QIS, the partitioning is semantic rather than structural.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The Math Election.&lt;/strong&gt; Outcomes accumulate through the network. As more nodes contribute outcomes for a given similarity cluster, the aggregate signal strengthens. This is pure mathematics — no central coordinator decides when enough evidence exists. The accumulation is the evidence. Byzantine fault tolerance emerges naturally: a single malicious or erroneous node cannot distort an aggregate of hundreds of independent outcome packets. The math does not require consensus. It requires accumulation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The Darwinism Election.&lt;/strong&gt; Multiple QIS networks can operate simultaneously over the same node population. Networks that route outcomes more effectively — measured by downstream validation — survive and grow. Networks that route poorly lose participants. This is the self-correction mechanism that prevents drift in the similarity matching over time.&lt;/p&gt;

&lt;p&gt;Together, these three elections form a complete loop. Hiring defines the routing criteria. The Math validates through accumulation. Darwinism selects for effective routing configurations. The breakthrough is the loop itself — not any single component.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for the Spark Engineer
&lt;/h2&gt;

&lt;p&gt;If you have spent years optimizing shuffle operations, tuning partition counts, and fighting data skew, you understand viscerally that the hardest problems in distributed systems are coordination problems. Getting the right data to the right place at the right time is harder than the actual computation.&lt;/p&gt;

&lt;p&gt;QIS takes that intuition and applies it one layer up. The "data" is already processed — it is a 512-byte outcome. The "right place" is any node whose interest profile matches semantically. The "right time" is whenever the outcome is emitted. There is no batch window. There is no job scheduling. There is no shuffle.&lt;/p&gt;

&lt;p&gt;This is why QIS is protocol-agnostic on transport. It does not care whether outcome packets travel over Kafka, NATS, MQTT, gRPC, REST, Redis Pub/Sub, Apache Pulsar, or even a folder on a shared drive. The routing logic is independent of the transport layer. If you can move 512 bytes from point A to point B, you can participate in a QIS network.&lt;/p&gt;

&lt;p&gt;Compare this to Spark, which requires a specific cluster manager (YARN, Mesos, Kubernetes, standalone), a shared storage layer (HDFS, S3, etc.), and compatible serialization formats. These requirements exist because Spark must coordinate compute across nodes. QIS does not coordinate compute. It routes outcomes. The coordination requirements collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complementary, Not Competing
&lt;/h2&gt;

&lt;p&gt;The strongest deployment architecture uses both layers. A hospital might run Spark (or Ray, or Dask) locally to process its own patient records, generate ML model outputs, and produce distilled outcomes. Those outcomes — 512 bytes each — then enter the QIS routing layer and propagate to every semantically matched node in the network.&lt;/p&gt;

&lt;p&gt;Spark produces the insight. QIS routes it.&lt;/p&gt;

&lt;p&gt;This is the same pattern that made the internet powerful: the application layer does not compete with TCP/IP. It depends on it. And TCP/IP does not compete with the physical layer. Each layer solves a different problem, and the stack composes.&lt;/p&gt;

&lt;p&gt;The engineers who built Spark understood this. Zaharia's insight was not "processing data is important" — everyone knew that. His insight was that the right abstraction (RDDs, lineage-based recovery, in-memory DAG execution) could make distributed compute radically more efficient. The abstraction was the contribution.&lt;/p&gt;

&lt;p&gt;QIS is an analogous abstraction for the next layer. The insight is not "routing intelligence is important" — anyone coordinating multi-site studies or federated learning networks already knows that. The insight is that the right abstraction (~512-byte outcome packets, semantic similarity routing, three self-correcting elections) can make distributed intelligence routing radically more efficient. And that efficiency scales quadratically with participation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;A QIS network of 1,000 nodes produces 499,500 unique synthesis paths. A network of 10,000 nodes produces 49,995,000. A network of 100,000 nodes produces 4,999,950,000.&lt;/p&gt;

&lt;p&gt;The cost per node at 100,000 participants is O(log 100,000) ≈ 17 routing hops, or O(1) with locality-aware optimization. Each hop processes a 512-byte packet. The total bandwidth per node is measured in kilobytes per second, not gigabytes.&lt;/p&gt;

&lt;p&gt;Compare this to what it would cost to ingest 100,000 sites' raw data into a centralized Spark cluster, harmonize schemas, and run global queries. The infrastructure alone would cost millions. The governance and compliance overhead would take years. And many of those 100,000 sites — the rural clinics, the under-resourced research labs, the field stations — would never participate because they cannot afford the ETL pipeline.&lt;/p&gt;

&lt;p&gt;QIS lets them participate with a device that can emit 512 bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Paradigm Shift
&lt;/h2&gt;

&lt;p&gt;Zaharia showed us: stop moving data to compute. Move compute to data.&lt;/p&gt;

&lt;p&gt;QIS shows us: the insight already exists at the edge. Stop trying to centralize it. Route it.&lt;/p&gt;

&lt;p&gt;These are the same class of insight — an architectural recognition that the bottleneck is not computation but coordination, and that the right abstraction can eliminate unnecessary movement. Spark eliminated unnecessary disk I/O between stages. QIS eliminates unnecessary centralization of raw data between institutions.&lt;/p&gt;

&lt;p&gt;The result in both cases is that participation barriers collapse, scaling properties improve by orders of magnitude, and problems that were previously intractable become tractable.&lt;/p&gt;

&lt;p&gt;For distributed systems researchers who spent a career thinking about data locality, shuffle optimization, and partition strategies: the QIS routing layer is the next problem worth studying. The math is clean. The architecture is complete. The scaling is quadratic.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;QIS — Quadratic Intelligence Swarm — was discovered by Christopher Thomas Trevethan on June 16, 2025.&lt;/strong&gt; The protocol is protected by 39 provisional patents. It is free for nonprofits, research institutions, and educational use. Commercial licenses fund humanitarian deployment to underserved regions.&lt;/p&gt;

&lt;p&gt;For technical specifications, architecture documentation, and the full mathematical framework, visit the QIS knowledge base.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Published by Rory | QIS Protocol Deep Dives&lt;/em&gt;&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>apachespark</category>
      <category>ai</category>
      <category>protocol</category>
    </item>
    <item>
      <title>Your Customer Table Has Duplicates You Can't See With SQL How I Built a Cross-Platform Identity Resolution Layer for a Dark Kitchen Data Platform</title>
      <dc:creator>SARAN TEJA MALLELA</dc:creator>
      <pubDate>Thu, 09 Apr 2026 16:29:22 +0000</pubDate>
      <link>https://dev.to/nerdbossstm/your-customer-table-has-duplicates-you-cant-see-with-sql-how-i-built-a-cross-platform-identity-397j</link>
      <guid>https://dev.to/nerdbossstm/your-customer-table-has-duplicates-you-cant-see-with-sql-how-i-built-a-cross-platform-identity-397j</guid>
      <description>&lt;p&gt;I'm Saran Teja Mallela — a data engineer based in Houston, TX. I build batch and streaming pipelines on Azure during the day at URL Systems, and at night I've been working on &lt;a href="https://github.com/Nerdboss-stm/ghostkitchen" rel="noopener noreferrer"&gt;GhostKitchen&lt;/a&gt;: a full data platform for cloud kitchen operations.&lt;/p&gt;

&lt;p&gt;GhostKitchen simulates 50 dark kitchens across 10 Texas cities — Houston, Dallas, Austin, San Antonio, and six others — each running 3–5 virtual restaurant brands. Orders flow in from three delivery platforms (Uber Eats, DoorDash, and the kitchen's own app), alongside kitchen IoT sensor readings, delivery GPS pings, and menu change events. The platform processes 7,500+ events per minute across four Kafka topics and lands everything in Delta Lake using a Medallion Architecture.&lt;/p&gt;

&lt;p&gt;But here's the thing — none of that engineering mattered until I could answer a deceptively simple question: &lt;strong&gt;who is this customer?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;Let me show you exactly what I mean. Here's a real scenario from my Houston test data:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uber Eats order, 7:42 PM:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer_uid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ue_cust_48291"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer_email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Saran.Mallela@Gmail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Saran Mallela"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;18.75&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;DoorDash order, 8:15 PM, same evening:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dasher_customer_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dd_u_73625"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer_email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"saran.mallela@gmail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Saran T Mallela"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;22.40&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;OwnApp order, next day:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app_saran_482"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer_email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SARAN.MALLELA@GMAIL.COM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Saran Teja Mallela"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amount_cents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1650&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three orders. Three platforms. Three different customer IDs (&lt;code&gt;customer_uid&lt;/code&gt;, &lt;code&gt;dasher_customer_id&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;). Three email capitalizations. Three name variations. Two currency formats (dollars vs cents). Zero shared keys.&lt;/p&gt;

&lt;p&gt;The kitchen thinks it served three different people. It served me three times.&lt;/p&gt;

&lt;p&gt;Run a &lt;code&gt;JOIN ON customer_id&lt;/code&gt; — zero matches. Try &lt;code&gt;JOIN ON email&lt;/code&gt; — the casing kills it. Even after you lowercase everything, 2% of my test events have null emails entirely (I injected those deliberately to simulate real-world incomplete data).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without resolving this, customer lifetime value is understated by roughly 35%.&lt;/strong&gt; Cohort analysis breaks. Personalization is impossible. The business literally doesn't know who its best customers are.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Chose Lambda Over Kappa for This
&lt;/h2&gt;

&lt;p&gt;Quick architecture context before the resolution logic.&lt;/p&gt;

&lt;p&gt;GhostKitchen uses Lambda Architecture — dual batch + streaming paths. I know the data engineering internet has strong opinions about this, so here's my reasoning:&lt;/p&gt;

&lt;p&gt;Orders are &lt;strong&gt;stateful&lt;/strong&gt;. A single order transitions through up to 7 states: &lt;code&gt;placed → confirmed → preparing → ready → picked_up → delivered&lt;/code&gt; (or &lt;code&gt;cancelled&lt;/code&gt;). Getting exact daily revenue requires knowing each order's final state, which sometimes needs late corrections — a "delivered" status might arrive hours after the order was placed.&lt;/p&gt;

&lt;p&gt;The streaming path (Kafka → Spark Structured Streaming → Delta Lake) gives approximate numbers in ~30 seconds. The batch path (Airflow → PySpark → Delta Lake MERGE) recomputes exact numbers overnight. A reconciliation DAG at 02:00 UTC makes batch authoritative.&lt;/p&gt;

&lt;p&gt;For identity resolution specifically, this dual-path matters: a customer's first order might arrive via streaming with a null email. A later batch correction fills it in. The batch path catches matches that streaming missed.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/Nerdboss-stm/pulsetrack" rel="noopener noreferrer"&gt;PulseTrack&lt;/a&gt; (a healthcare IoT pipeline on Azure) with pure Kappa, because wearable sensor readings are append-only — a heart rate of 72 bpm at 2:34 PM never gets "corrected." Different data semantics → different architecture. That distinction matters more than any blanket rule about which pattern is "better."&lt;/p&gt;




&lt;h2&gt;
  
  
  The Data Model That Makes Resolution Possible
&lt;/h2&gt;

&lt;p&gt;Before I could resolve identities, I needed a model that could handle three conflicting schemas without forcing premature alignment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silver layer: Data Vault 2.0.&lt;/strong&gt; This is where the magic happens.&lt;/p&gt;

&lt;p&gt;Data Vault separates data into three table types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hubs&lt;/strong&gt; — core business entities with their business keys. One row per unique entity. &lt;code&gt;hub_customer&lt;/code&gt; holds the resolved customer identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Links&lt;/strong&gt; — relationships between hubs. &lt;code&gt;link_order_customer&lt;/code&gt; connects which customer placed which order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Satellites&lt;/strong&gt; — descriptive attributes with full history (SCD Type 2). &lt;code&gt;sat_customer_profile&lt;/code&gt; tracks name, email, and platform IDs over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why not Star Schema in Silver? Because Uber defines a customer as &lt;code&gt;customer_uid&lt;/code&gt; + &lt;code&gt;total_amount&lt;/code&gt; (float). DoorDash uses &lt;code&gt;dasher_customer_id&lt;/code&gt; + &lt;code&gt;order_value&lt;/code&gt; (float). OwnApp uses &lt;code&gt;user_id&lt;/code&gt; + &lt;code&gt;amount_cents&lt;/code&gt; (integer). Star Schema would force me to pick one representation and lose the others. Data Vault keeps every source's raw attributes in separate satellites while unifying identity in hubs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gold layer: Star Schema.&lt;/strong&gt; Once identities are resolved, analysts need simple queries: &lt;code&gt;SELECT sum(order_total) FROM fact_order JOIN dim_kitchen GROUP BY city&lt;/code&gt;. Star makes that a 2-table join. Dimensions are flat (kitchen, brand, delivery zone) — no deep hierarchies that would justify a Snowflake Schema. PII stays locked in Silver; only hashed surrogate keys propagate to Gold.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Resolution Pipeline: Four Stages
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stage 1: Email Normalization
&lt;/h3&gt;

&lt;p&gt;Strip whitespace, lowercase everything, remove dots before the &lt;code&gt;@&lt;/code&gt; in Gmail addresses (Gmail ignores dots — &lt;code&gt;saran.mallela@gmail.com&lt;/code&gt; and &lt;code&gt;saranmallela@gmail.com&lt;/code&gt; deliver to the same inbox).&lt;/p&gt;

&lt;p&gt;After normalization, &lt;code&gt;Saran.Mallela@Gmail.com&lt;/code&gt;, &lt;code&gt;saran.mallela@gmail.com&lt;/code&gt;, and &lt;code&gt;SARAN.MALLELA@GMAIL.COM&lt;/code&gt; all become &lt;code&gt;saranmallela@gmail.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This alone resolves about &lt;strong&gt;82% of cross-platform matches&lt;/strong&gt; in my test data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Deterministic Grouping via MD5 Hash
&lt;/h3&gt;

&lt;p&gt;Hash the normalized email with MD5. All records sharing the same hash receive the same &lt;code&gt;customer_hk&lt;/code&gt; (hash key) in the Data Vault hub.&lt;/p&gt;

&lt;p&gt;Why hash instead of a database-generated auto-increment ID? Because three platforms run as independent Spark jobs. There's no central coordinator to hand out sequential IDs. A hash is &lt;strong&gt;deterministic&lt;/strong&gt; — same email always produces the same key, on any node, any platform, independently. No coordination needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_normalized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\.(?=.*@gmail\.com$)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_hk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_normalized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this stage, my three Houston test orders — the pad thai on Uber Eats, the burrito bowl on DoorDash, and the fried chicken on OwnApp — all share one &lt;code&gt;customer_hk&lt;/code&gt;. They're one person in the system now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: Fuzzy Matching for the Remaining 18%
&lt;/h3&gt;

&lt;p&gt;What about the 2% with null emails? And the cases where the same person uses different email addresses across platforms?&lt;/p&gt;

&lt;p&gt;This is where I added probabilistic matching using Jaro-Winkler string similarity on name + delivery address combinations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"Saran Mallela"&lt;/code&gt; vs &lt;code&gt;"Saran T Mallela"&lt;/code&gt; → similarity: 0.94 → &lt;strong&gt;match&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"Saran Mallela"&lt;/code&gt; vs &lt;code&gt;"Sarah Miller"&lt;/code&gt; → similarity: 0.68 → &lt;strong&gt;no match&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also incorporated Soundex phonetic matching as a fallback — it catches typos and transliterations that string distance alone misses. &lt;code&gt;"Mallela"&lt;/code&gt; and &lt;code&gt;"Malela"&lt;/code&gt; produce the same Soundex code.&lt;/p&gt;

&lt;p&gt;The confidence threshold is 0.88. Above that → automatic merge. Between 0.70 and 0.88 → flagged for manual review. Below 0.70 → treated as distinct customers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 4: Confidence Scoring and Audit Trail
&lt;/h3&gt;

&lt;p&gt;Every single match gets a confidence score and a &lt;code&gt;matched_via&lt;/code&gt; field:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_hk&lt;/th&gt;
&lt;th&gt;platform&lt;/th&gt;
&lt;th&gt;platform_id&lt;/th&gt;
&lt;th&gt;email&lt;/th&gt;
&lt;th&gt;matched_via&lt;/th&gt;
&lt;th&gt;confidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;a3f8c2...&lt;/td&gt;
&lt;td&gt;uber_eats&lt;/td&gt;
&lt;td&gt;ue_cust_48291&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:saran.mallela@gmail.com"&gt;saran.mallela@gmail.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;email_hash&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a3f8c2...&lt;/td&gt;
&lt;td&gt;doordash&lt;/td&gt;
&lt;td&gt;dd_u_73625&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:saran.mallela@gmail.com"&gt;saran.mallela@gmail.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;email_hash&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a3f8c2...&lt;/td&gt;
&lt;td&gt;own_app&lt;/td&gt;
&lt;td&gt;app_saran_482&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:saran.mallela@gmail.com"&gt;saran.mallela@gmail.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;email_hash&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b7d1e9...&lt;/td&gt;
&lt;td&gt;uber_eats&lt;/td&gt;
&lt;td&gt;ue_cust_91034&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;fuzzy_name_addr&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The audit trail is critical. When an analyst questions a number, you can trace it back to exactly &lt;em&gt;why&lt;/em&gt; two records were merged and &lt;em&gt;how confident&lt;/em&gt; the system was about it. No black boxes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stress-Testing With Deliberately Dirty Data
&lt;/h2&gt;

&lt;p&gt;Here's what most identity resolution tutorials skip: &lt;strong&gt;if you test on clean data, you're testing your assumptions, not your algorithm.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I injected five types of noise into my data generators:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;5% duplicate events&lt;/strong&gt; — simulating Kafka consumer retries and at-least-once delivery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2% null emails&lt;/strong&gt; — simulating incomplete user profiles on mobile sign-ups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3% late-arriving events&lt;/strong&gt; — orders showing up 6–24 hours after they happened&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name variations&lt;/strong&gt; — middle names, initials, typos (&lt;code&gt;Mallela&lt;/code&gt; vs &lt;code&gt;Malela&lt;/code&gt; vs &lt;code&gt;mallela&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Address inconsistencies&lt;/strong&gt; — &lt;code&gt;"Houston, TX"&lt;/code&gt; vs &lt;code&gt;"Houston, Texas"&lt;/code&gt; vs &lt;code&gt;"HTX"&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Against this deliberately hostile test data, the resolution pipeline achieves &lt;strong&gt;94% match accuracy across 200 customer identities&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The remaining 6%? Documented as known unresolvable ambiguity — cases where two records have no email, different names, and addresses that are close but not close enough. That's not a failure. &lt;strong&gt;That's honest engineering.&lt;/strong&gt; Because 100% accuracy in identity resolution doesn't exist. You're always trading precision for recall, and pretending otherwise is how you end up merging two different people into one profile.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling Late-Arriving Data
&lt;/h2&gt;

&lt;p&gt;The 3% late-arriving events deserve their own section because they interact with identity resolution in a way that isn't obvious.&lt;/p&gt;

&lt;p&gt;The streaming path uses a 24-hour watermark. Any event older than 24 hours gets routed to a Dead Letter Queue (DLQ). The nightly reconciliation DAG picks up DLQ events and reprocesses them through the batch path.&lt;/p&gt;

&lt;p&gt;Why does this matter for identity? Imagine this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;8:00 PM&lt;/strong&gt; — An order arrives via streaming with a null email. Gets assigned a new &lt;code&gt;customer_hk&lt;/code&gt; as an unmatched record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2:00 AM&lt;/strong&gt; — A batch correction arrives with the email filled in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation&lt;/strong&gt; — Batch path re-runs identity resolution, discovers this "new" customer actually matches an existing one. Delta Lake MERGE updates the Gold layer. The temporary unmatched record gets absorbed into the correct customer profile.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without the batch path, that customer remains a phantom forever — inflating your customer count and deflating per-customer metrics. This is why Lambda exists for this use case. It's not about speed. It's about eventual correctness.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently in Production
&lt;/h2&gt;

&lt;p&gt;Three things I'd add for a production system at scale:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Graph-based resolution.&lt;/strong&gt; My current approach is pairwise — compare record A to record B. A graph database would let me do transitive matching: if A matches B, and B matches C, then A matches C — even if A and C share zero attributes. I actually built this pattern in &lt;a href="https://github.com/Nerdboss-stm/pulsetrack" rel="noopener noreferrer"&gt;PulseTrack&lt;/a&gt;, where patient identity resolution chains 7 identifier types in a multi-hop graph: &lt;code&gt;device_account → email → MRN → pharmacy_id → insurance_id → phone_hash → ssn_hash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. ML-based scoring.&lt;/strong&gt; Replace hand-tuned Jaro-Winkler with a trained classifier that learns match weights from confirmed true matches. The 0.88 threshold is hand-tuned — a model could optimize it per-field and adapt as data distributions change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Real-time resolution in the streaming path.&lt;/strong&gt; Currently, fuzzy matching only runs in the batch path because it's computationally expensive. With a pre-computed blocking index in Redis, the streaming path could do approximate matching at ingestion time instead of waiting for the nightly batch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The entire platform is open source and runs locally with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Nerdboss-stm/ghostkitchen" rel="noopener noreferrer"&gt;github.com/Nerdboss-stm/ghostkitchen&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://ghostkitchen-portfolio.vercel.app" rel="noopener noreferrer"&gt;ghostkitchen-portfolio.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The live demo lets you run the full pipeline, explore the schema (including the identity resolution layer), browse the Data Vault Silver and Star Schema Gold tables, and see 43 automated data quality checks per pipeline run.&lt;/p&gt;

&lt;p&gt;The Gold layer has &lt;code&gt;fact_order&lt;/code&gt; (2,274 rows), &lt;code&gt;fact_sensor_hourly&lt;/code&gt;, &lt;code&gt;dim_customer&lt;/code&gt; (576 resolved profiles from ~800 raw platform records), &lt;code&gt;dim_kitchen&lt;/code&gt;, &lt;code&gt;dim_brand&lt;/code&gt;, &lt;code&gt;dim_date&lt;/code&gt;, &lt;code&gt;dim_time&lt;/code&gt;, and more — all with full data lineage from source to serving.&lt;/p&gt;




&lt;p&gt;If you work on multi-source identity problems — in food delivery, retail, fintech, healthcare, or any multi-marketplace domain — I'd genuinely like to hear how you're solving it. What matching strategies work for your data? Where do you draw the precision/recall line?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm Saran Teja Mallela&lt;/strong&gt; — data engineer, University of Houston MS grad (Data Science, 4.0 GPA), currently building data platforms at URL Systems in Houston. You can find me on &lt;a href="https://www.linkedin.com/in/saran-teja-mallela/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or &lt;a href="https://github.com/Nerdboss-stm" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I write about data architecture decisions, not just code. Because the hardest part of engineering is never the syntax — it's deciding what "good enough" means.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>apachespark</category>
      <category>kafka</category>
      <category>deltalake</category>
    </item>
    <item>
      <title>🚀 Apache Spark Just Killed the Microbatch Barrier (And Why Flink Should Be Worried)</title>
      <dc:creator>Siddhesh Surve</dc:creator>
      <pubDate>Wed, 18 Mar 2026 02:30:02 +0000</pubDate>
      <link>https://dev.to/siddhesh_surve/apache-spark-just-killed-the-microbatch-barrier-and-why-flink-should-be-worried-348c</link>
      <guid>https://dev.to/siddhesh_surve/apache-spark-just-killed-the-microbatch-barrier-and-why-flink-should-be-worried-348c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy88gzes25o49wwuqjtx3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy88gzes25o49wwuqjtx3.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've spent any time working in Big Data and Cloud Computing, you know the classic dilemma: &lt;strong&gt;Throughput vs. Latency&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Historically, if you needed high-throughput ETL processing, you spun up Apache Spark. But if you needed ultra-low-latency, real-time event streaming (like fraud detection or live telemetry), you had to build an entirely separate architecture using something like Apache Flink.&lt;/p&gt;

&lt;p&gt;That era is officially over. &lt;/p&gt;

&lt;p&gt;Databricks just detailed the architectural changes behind &lt;strong&gt;Apache Spark 4.1’s new Real-Time Mode (RTM)&lt;/strong&gt;, and it is a massive paradigm shift. Spark Structured Streaming can now achieve millisecond-level latencies, effectively eliminating the need to maintain two separate streaming engines. &lt;/p&gt;

&lt;p&gt;Here is a breakdown of how Databricks broke the microbatch barrier, the clever architecture behind it, and why this is a game-changer for data engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛑 The Problem with Microbatches
&lt;/h2&gt;

&lt;p&gt;Spark’s legacy superpower was the &lt;strong&gt;microbatch architecture&lt;/strong&gt;. It gathers a chunk of data, processes it, writes state to object storage (for fault tolerance), and spits it out. This is incredible for high-throughput because it amortizes overhead and utilizes hardware efficiently.&lt;/p&gt;

&lt;p&gt;So, why not just make the batches smaller to get lower latency? &lt;/p&gt;

&lt;p&gt;Because of the &lt;strong&gt;fixed costs&lt;/strong&gt;. Every microbatch carries fixed overhead: planning the batch, task serialization, scheduling, and writing state/logs to durable storage. If you shrink a batch to 10ms, the fixed overhead might still take 500ms. You hit a mathematical wall where smaller batches actually &lt;em&gt;increase&lt;/em&gt; end-to-end latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 The Hybrid Execution Solution
&lt;/h2&gt;

&lt;p&gt;To solve this, the Databricks engineering team couldn't just tweak settings; they had to fundamentally evolve how Spark handles data flow. They introduced a &lt;strong&gt;Hybrid Execution Model&lt;/strong&gt; built on three core pillars:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Longer Epochs, Continuous Flow
&lt;/h3&gt;

&lt;p&gt;Instead of chopping data into tiny microbatches, RTM uses &lt;em&gt;longer&lt;/em&gt; duration epochs. However, it changes how data behaves inside that epoch. Instead of waiting for a batch to fill, data streams continuously through the stages without blocking. The epoch boundary essentially becomes a checkpoint interval for fault tolerance, rather than a processing bottleneck.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Concurrent Processing Stages
&lt;/h3&gt;

&lt;p&gt;In traditional Structured Streaming, stages ran sequentially. Reducers sat idle, waiting for mappers to completely finish their jobs. &lt;br&gt;
With RTM, stages are concurrent. As soon as a mapper processes a row and generates a shuffle file, the reducer starts processing it immediately. No more waiting. &lt;/p&gt;
&lt;h3&gt;
  
  
  3. Non-Blocking Operators
&lt;/h3&gt;

&lt;p&gt;Classic batch operators love to buffer. A &lt;code&gt;groupBy&lt;/code&gt; aggregation would traditionally buffer all records, pre-aggregate, and emit at the very end. RTM introduces non-blocking operators that minimize buffering, emitting results continuously as data flows through the pipeline.&lt;/p&gt;
&lt;h2&gt;
  
  
  💻 What This Looks Like for Developers
&lt;/h2&gt;

&lt;p&gt;The beauty of this update is that you don't need to learn a new framework or rewrite your complex business logic in Flink. You just flip the switch on your existing Structured Streaming jobs.&lt;/p&gt;

&lt;p&gt;Here is a conceptual example of how you might configure a continuous, ultra-low latency stream in Spark 4.1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize Spark Session with RTM enabled 
&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UltraLowLatencyFraudDetection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.streaming.realTimeMode.enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Read from a high-throughput source like Kafka
&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readStream&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kafka&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kafka.bootstrap.servers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;broker:29092&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subscribe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;financial-transactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Apply your existing business logic
&lt;/span&gt;&lt;span class="n"&gt;fraud_alerts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectExpr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAST(value AS STRING) as payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;suspicious_pattern&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1 second&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Write the stream using the continuous processing trigger
&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fraud_alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeStream&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kafka&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kafka.bootstrap.servers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;broker:29092&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fraud-alerts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;continuous&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100 milliseconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# The magic happens here
&lt;/span&gt;    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;awaitTermination&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By leveraging the new continuous trigger and RTM architecture, this standard Spark code will now process records with sub-100ms latency, bypassing the traditional microbatch blocking phases.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌍 The Big Picture
&lt;/h2&gt;

&lt;p&gt;Over on the AI Tooling Academy channel, we talk constantly about simplifying architectures. Managing a Lambda architecture (maintaining both a batch layer and a speed layer) has always been an expensive, operational nightmare. &lt;/p&gt;

&lt;p&gt;Databricks benchmarked this new real-time mode against Flink for feature engineering workloads, and &lt;strong&gt;Spark actually outperformed it&lt;/strong&gt; in many scenarios. &lt;/p&gt;

&lt;p&gt;We are finally entering an era of unified data processing. If you are handling large-scale telemetrics, live AI feature extraction, or financial feeds, you no longer have to choose between throughput and latency. &lt;/p&gt;

&lt;p&gt;Are you planning to migrate your Flink workloads back to Spark now that RTM is here? Let's debate it in the comments below! 👇&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>apachespark</category>
      <category>bigdata</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>Should you join Data Engineering?A guide to the tools you'll use</title>
      <dc:creator>Collins Njeru</dc:creator>
      <pubDate>Mon, 16 Mar 2026 12:11:50 +0000</pubDate>
      <link>https://dev.to/cnew_aerospace_85c7b7d3cb/should-you-join-data-engineeringa-guide-to-the-tools-youll-use-3g9a</link>
      <guid>https://dev.to/cnew_aerospace_85c7b7d3cb/should-you-join-data-engineeringa-guide-to-the-tools-youll-use-3g9a</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;Many aspiring technologists find themselves at a crossroad:&lt;em&gt;is data engineering the right career path for me&lt;/em&gt;.The hesitation often comes from uncertainty about the tools and technologies involved. This article breaks down the core categories of data engineering tools, giving you a clear picture of what you’ll be working with if you decide to join the field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core categories of data engineering tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.Data ingestion &amp;amp; Integration
&lt;/h3&gt;

&lt;p&gt;Data engineering starts with collecting information from multiple sources&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fivetran /Stitch/ Hevo Data&lt;/strong&gt; : Automate extraction from SaaS apps and databases&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbuer5qmjrjmrv9gekx00.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbuer5qmjrjmrv9gekx00.png" alt="Data Ingestion Tools" width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache Kafka&lt;/strong&gt; : Real-time streaming and event-driven pipelines. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7n64rxs8jihlf9lowd40.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7n64rxs8jihlf9lowd40.png" alt="Apache Kafka" width="800" height="305"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache Nifi&lt;/strong&gt; : Flow-based ingestion and routing.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnkp8paozhq8sp2r3juvj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnkp8paozhq8sp2r3juvj.png" alt="Apache Nifi" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2.Data storage &amp;amp; Warehousing
&lt;/h3&gt;

&lt;p&gt;Once data is ingested, it needs a reliable home.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snowflake&lt;/strong&gt;:Cloud-native warehouse with scalability.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr72zj801ohtj4xwj1z3a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr72zj801ohtj4xwj1z3a.png" alt="Data Storages" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google BigQuery&lt;/strong&gt;:Serverless, highly scalable analytics warehouse. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjetn4hfo9t7m8w6o89h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjetn4hfo9t7m8w6o89h.png" alt="Google BigQuery" width="800" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon Redshift&lt;/strong&gt; :AWS-based warehouse optimized for queries. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzirht9zu25fianpqxn3t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzirht9zu25fianpqxn3t.png" alt="Amazon Redshift" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.Data processing &amp;amp; transformation
&lt;/h3&gt;

&lt;p&gt;Raw data must be cleaned and transformed before use.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache spark&lt;/strong&gt;:Distributed computing for batch and streaming.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9887dmu7nsadfgwq6lnb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9887dmu7nsadfgwq6lnb.png" alt="Apache Spark" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hadoop&lt;/strong&gt;:Large-scale storage and batch processing.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6k346z24f26vp1wx0o7m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6k346z24f26vp1wx0o7m.png" alt="Hadoop" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dbt (Data Build Tool)&lt;/strong&gt;:SQL-based transformations for analytics teams.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgub2u92loxrtkjl3rly9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgub2u92loxrtkjl3rly9.png" alt="Data Build Tool" width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Workflow &amp;amp; orchestration
&lt;/h3&gt;

&lt;p&gt;Pipelines need automation and scheduling.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache Airflow&lt;/strong&gt;:Workflow automation and DAG scheduling.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flyoz7btyz41f6msg56p6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flyoz7btyz41f6msg56p6.png" alt="Apache Airflow" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefect/luigi&lt;/strong&gt; :Alternatives for managing complex workflows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpiuirvyluadhbwyt7d9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpiuirvyluadhbwyt7d9.png" alt="Prefect/Luigi" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.Infrastructure &amp;amp; Deployment
&lt;/h3&gt;

&lt;p&gt;Behind the scenes, infrastructure ensures scalability.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker &amp;amp; Kubernetes&lt;/strong&gt;:Containerization and orchestration.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0v4ls7uza7hsqgg3gems.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0v4ls7uza7hsqgg3gems.png" alt="Docker &amp;amp; Kubernetes" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform&lt;/strong&gt;:Infrastructure as Code for cloud resources.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytaeidhhf9pcs28s1cls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytaeidhhf9pcs28s1cls.png" alt="Terraform" width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6.Monitoring &amp;amp; Quality
&lt;/h3&gt;

&lt;p&gt;Data must be trustworthy and pipelines reliable.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Great expectations&lt;/strong&gt; :Data validation and quality checks.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Datadog / Prometheus&lt;/strong&gt; :Monitoring pipelines and infrastructure&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: Spark and Snowflake excel with large datasets.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real-Time vs Batch&lt;/strong&gt;: Kafka is unmatched for streaming; Hadoop and Spark dominate batch workloads. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cloud Integration&lt;/strong&gt;: Align tools with your provider (AWS  Redshift, GCP  BigQuery, Azure Synapse ). &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt;:Open-source tools are free but require setup; managed services reduce overhead but add licensing costs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Joining data engineering means stepping into a field where you’ll design the backbone of modern businesses. The tools may seem overwhelming at first, but each one solves a specific problem together, they form a powerful toolkit. If you’re excited about building systems that move, store, and transform data at scale, then data engineering isn’t just a career option; it’s a future-proof calling.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>kafka</category>
      <category>apachespark</category>
      <category>snowflake</category>
    </item>
    <item>
      <title>Using Gravitino with Apache Spark for ETL</title>
      <dc:creator>Yue @ Datastrato (Admin)</dc:creator>
      <pubDate>Thu, 05 Feb 2026 23:24:45 +0000</pubDate>
      <link>https://dev.to/gravitino/using-gravitino-with-apache-spark-for-etl-538c</link>
      <guid>https://dev.to/gravitino/using-gravitino-with-apache-spark-for-etl-538c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73miqycef1bsajepzgwy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73miqycef1bsajepzgwy.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Author: &lt;a href="https://www.linkedin.com/in/minghuang-li?miniProfileUrn=urn%3Ali%3Afs_miniProfile%3AACoAAFa5V_cBwwdNNbf1bI0scSPywRS9WQBQ1Yk&amp;amp;lipi=urn%3Ali%3Apage%3Ad_flagship3_search_srp_all%3BDWCB9kUFQ7yeAZ7UUVFT3A%3D%3D" rel="noopener noreferrer"&gt;Minghuang Li&lt;/a&gt;&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Last Updated: 2026-01-31&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;In this tutorial, you will learn how to use Apache Gravitino with Apache Spark for ETL (Extract, Transform, Load) operations. By the end of this guide, you'll be able to build data pipelines that seamlessly access multiple heterogeneous data sources through a unified catalog interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll accomplish:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configure Gravitino Spark Connector&lt;/strong&gt; to enable unified access to multiple data sources in Spark&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Register multiple catalogs&lt;/strong&gt; including MySQL and Iceberg in Gravitino for federated access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build an ETL pipeline&lt;/strong&gt; that extracts data from MySQL, transforms it, and loads it into Iceberg&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute federated queries&lt;/strong&gt; across different data sources using Spark SQL and PySpark&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apache Spark is one of the most popular unified analytics engines for large-scale data processing. In a typical ETL pipeline, Spark often needs to interact with multiple heterogeneous data sources (like MySQL, HDFS, S3, Hive, Iceberg). Managing connectivity, credentials, and schema information for these diverse sources can be complex and error-prone.&lt;/p&gt;

&lt;p&gt;Apache Gravitino simplifies this by acting as a unified metadata lake. By using the Gravitino Spark Connector, you can access multiple data sources through a single catalog interface in Spark, without having to manually configure each source's connection details in your Spark jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unified catalog&lt;/strong&gt;: Access Hive, Iceberg, MySQL, PostgreSQL, and other sources under a unified namespace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralized metadata&lt;/strong&gt;: Metadata is managed in Gravitino, changes are reflected immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplified configuration&lt;/strong&gt;: Configure the Gravitino connector once, and access all managed catalogs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Federated querying&lt;/strong&gt;: Easily join data across different sources (e.g., join MySQL data with Iceberg table)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting this tutorial, you will need:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux or macOS operating system with outbound internet access for downloads&lt;/li&gt;
&lt;li&gt;JDK 17 or higher installed and properly configured&lt;/li&gt;
&lt;li&gt;Apache Spark 3.3, 3.4, or 3.5 installed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Required Components:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gravitino server installed and running (see &lt;a href="//../02-setup-guide/README.md"&gt;&lt;code&gt;02-setup-guide/README.md&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;MySQL instance for testing JDBC catalog functionality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Optional Components:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HDFS or S3 for Iceberg data storage in production environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before proceeding, verify your Java and Spark installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/bin/java &lt;span class="nt"&gt;-version&lt;/span&gt;
&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SPARK_HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/bin/spark-submit &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Architecture overview:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiylgtvw69d7ojmyu95nu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiylgtvw69d7ojmyu95nu.png" alt="Gravitino Spark Architecture" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Download Gravitino Spark Connector
&lt;/h3&gt;

&lt;p&gt;You need the Gravitino Spark Connector jar file to enable Spark integration with Gravitino.&lt;/p&gt;

&lt;h4&gt;
  
  
  Obtain the connector
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Download from Maven Central Repository&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For Spark 3.5, download the connector from:&lt;br&gt;
&lt;a href="https://mvnrepository.com/artifact/org.apache.gravitino/gravitino-spark-connector-runtime-3.5" rel="noopener noreferrer"&gt;&lt;code&gt;gravitino-spark-connector-runtime-3.5&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additional dependencies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For JDBC sources (MySQL, PostgreSQL), you also need the specific JDBC driver jar (e.g., &lt;code&gt;mysql-connector-j&lt;/code&gt; for MySQL) in your classpath.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Configure Spark Session
&lt;/h3&gt;

&lt;p&gt;To use Gravitino with Spark, you need to configure the specialized Gravitino Spark IO plugin.&lt;/p&gt;
&lt;h4&gt;
  
  
  Configure Spark SQL with Gravitino
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Start Spark SQL with the Gravitino connector&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Set the location of your Gravitino server&lt;/span&gt;
&lt;span class="nv"&gt;GRAVITINO_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8090"&lt;/span&gt;
&lt;span class="c"&gt;# The metalake you want to access&lt;/span&gt;
&lt;span class="nv"&gt;METALAKE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"default_metalake"&lt;/span&gt;

spark-sql &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--packages&lt;/span&gt; org.apache.gravitino:gravitino-spark-connector-runtime-3.5_2.12:1.1.0,mysql:mysql-connector-java:8.0.33,org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.10.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.plugins&lt;span class="o"&gt;=&lt;/span&gt;org.apache.gravitino.spark.connector.plugin.GravitinoSparkPlugin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.sql.gravitino.metalake&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$METALAKE_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.sql.gravitino.uri&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$GRAVITINO_URI&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.sql.gravitino.enableIcebergSupport&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuration notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;1.1.0&lt;/code&gt; with the actual version you are using&lt;/li&gt;
&lt;li&gt;Ensure the Spark connector version matches your Spark version&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;spark.sql.gravitino.enableIcebergSupport=true&lt;/code&gt; to enable Iceberg catalog support&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Prepare Metadata in Gravitino
&lt;/h3&gt;

&lt;p&gt;Before running ETL jobs, you need to register the catalogs for your data sources in Gravitino. You can do this via the Gravitino REST API or Web UI.&lt;/p&gt;

&lt;h4&gt;
  
  
  Register MySQL Catalog
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Create a MySQL catalog in Gravitino&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "name": "mysql_catalog",
  "type": "relational",
  "provider": "jdbc-mysql",
  "properties": {
    "jdbc-url": "jdbc:mysql://localhost:3306",
    "jdbc-user": "root",
    "jdbc-password": "password",
    "jdbc-driver": "com.mysql.cj.jdbc.Driver"
  }
}'&lt;/span&gt; http://localhost:8090/api/metalakes/default_metalake/catalogs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Register Iceberg Catalog
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Create an Iceberg catalog in Gravitino&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "name": "iceberg_catalog",
  "type": "relational",
  "provider": "lakehouse-iceberg",
  "properties": {
    "warehouse": "file:///tmp/iceberg-warehouse",
    "catalog-backend": "jdbc",
    "uri": "jdbc:mysql://localhost:3306/iceberg_metadata",
    "jdbc-driver": "com.mysql.cj.jdbc.Driver",
    "jdbc-user": "root",
    "jdbc-password": "password",
    "jdbc-initialize": "true"
  }
}'&lt;/span&gt; http://localhost:8090/api/metalakes/default_metalake/catalogs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This example uses a local file system for Iceberg data storage. For production environments, consider using HDFS or S3. For more detailed Iceberg catalog configuration options, see &lt;a href="//../03-iceberg-catalog/README.md"&gt;&lt;code&gt;03-iceberg-catalog/README.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4: Build an ETL Pipeline from MySQL to Iceberg
&lt;/h3&gt;

&lt;p&gt;In this scenario, we will extract user data from a MySQL database, perform some transformations, and load it into an Apache Iceberg table for analytical queries, all managed through Gravitino.&lt;/p&gt;

&lt;h4&gt;
  
  
  Verify Catalogs in Spark
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Start your Spark SQL session&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use the configuration from Step 2 to start your Spark SQL session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Verify catalog visibility&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Due to Spark catalog manager limitations, SHOW CATALOGS only displays 'spark_catalog' initially&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;CATALOGS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Use a Gravitino-managed catalog to make it visible&lt;/span&gt;
&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="n"&gt;mysql_catalog&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="n"&gt;iceberg_catalog&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Now both catalogs are visible in the output&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;CATALOGS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;SHOW CATALOGS&lt;/code&gt; command initially only displays the Spark default catalog (&lt;code&gt;spark_catalog&lt;/code&gt;). After explicitly using a Gravitino-managed catalog with the &lt;code&gt;USE&lt;/code&gt; command, that catalog becomes visible in subsequent &lt;code&gt;SHOW CATALOGS&lt;/code&gt; output.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Prepare Sample Data in MySQL
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Create a sample database and table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Switch to MySQL catalog&lt;/span&gt;
&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="n"&gt;mysql_catalog&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create a sample database&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;users_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="n"&gt;users_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create a users table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Insert sample data&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Insert sample data&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Bob'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2024-02-20 14:30:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Charlie'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'charlie@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'inactive'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2024-03-10 09:15:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Diana'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'diana@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2024-04-05 16:45:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Eve'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eve@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'inactive'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-12 11:20:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Verify the data&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Extract Data from MySQL
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Verify data extraction&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Read data from MySQL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mysql_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Transform and Load Data to Iceberg
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Create an Iceberg table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Switch to Iceberg catalog&lt;/span&gt;
&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="n"&gt;iceberg_catalog&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;iceberg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Execute ETL query&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ETL Query: Insert into Iceberg from MySQL with transformation&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_users&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;created_at&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mysql_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: For JDBC catalogs (like MySQL), operations &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, and &lt;code&gt;TRUNCATE&lt;/code&gt; are NOT supported. Only &lt;code&gt;SELECT&lt;/code&gt; and &lt;code&gt;INSERT&lt;/code&gt; are supported.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Verify ETL Results
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Query the target Iceberg table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_users&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  PySpark Example
&lt;/h2&gt;

&lt;p&gt;If you prefer using Python, the logic is very similar using the DataFrame API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure PySpark Session
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Create a PySpark session with Gravitino connector&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;

&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GravitinoSparkETL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.jars.packages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;org.apache.gravitino:gravitino-spark-connector-runtime-3.5_2.12:1.1.0,mysql:mysql-connector-java:8.0.33,org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.10.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.plugins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;org.apache.gravitino.spark.connector.plugin.GravitinoSparkPlugin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.gravitino.metalake&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default_metalake&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.gravitino.uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8090&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.gravitino.enableIcebergSupport&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Execute ETL Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Read, transform, and write data using DataFrame API&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Read from MySQL
&lt;/span&gt;&lt;span class="n"&gt;mysql_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysql_catalog.users_db.users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Transform
&lt;/span&gt;&lt;span class="n"&gt;active_users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mysql_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectExpr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id as user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lower(username) as username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lower(email) as email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Write to Iceberg
&lt;/span&gt;&lt;span class="n"&gt;active_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iceberg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;append&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveAsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iceberg_catalog.analytics.active_users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ETL Job Completed successfully.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;Common issues and their solutions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connector and classpath issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ClassNotFoundException: org.apache.gravitino.spark.connector.GravitinoCatalog&lt;/strong&gt;: The Gravitino Spark Connector JAR is missing from the classpath. Ensure you added the correct package with &lt;code&gt;--packages&lt;/code&gt; or placed the JAR in &lt;code&gt;$SPARK_HOME/jars&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing JDBC Driver&lt;/strong&gt;: When connecting to JDBC sources (MySQL/PostgreSQL) via Gravitino, Spark still needs the JDBC driver JARs in its classpath. Add the MySQL/PostgreSQL JDBC driver packages to your Spark startup command (e.g., &lt;code&gt;--packages mysql:mysql-connector-java:8.0.33&lt;/code&gt;) or put the jar in &lt;code&gt;jars/&lt;/code&gt; folder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Connection issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection refused to Gravitino Server&lt;/strong&gt;: Spark cannot reach the Gravitino server. Check if Gravitino server is running and the &lt;code&gt;spark.sql.gravitino.uri&lt;/code&gt; config is correct&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catalog not found&lt;/strong&gt;: Ensure the catalogs are properly registered in Gravitino and the metalake name is correct&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Query execution issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UPDATE/DELETE not supported on JDBC catalogs&lt;/strong&gt;: For JDBC catalogs (like MySQL), only &lt;code&gt;SELECT&lt;/code&gt; and &lt;code&gt;INSERT&lt;/code&gt; operations are supported through Gravitino&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table not found&lt;/strong&gt;: Verify the fully qualified table name format: &lt;code&gt;catalog.schema.table&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Congratulations
&lt;/h2&gt;

&lt;p&gt;You have successfully completed the Gravitino Spark ETL tutorial!&lt;/p&gt;

&lt;p&gt;You now have a fully functional Spark environment with Gravitino integration, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A configured Gravitino Spark Connector for unified catalog access&lt;/li&gt;
&lt;li&gt;Multiple registered catalogs (MySQL and Iceberg) in Gravitino&lt;/li&gt;
&lt;li&gt;A working ETL pipeline that extracts, transforms, and loads data across heterogeneous sources&lt;/li&gt;
&lt;li&gt;Understanding of federated query capabilities and PySpark integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your Spark environment is now ready to leverage Gravitino for unified metadata management across your data ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;For more advanced configurations and detailed documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Review the &lt;a href="https://gravitino.apache.org/docs/1.1.0/spark-connector/spark-catalog-iceberg" rel="noopener noreferrer"&gt;Gravitino Spark Connector Documentation&lt;/a&gt; for advanced configuration options&lt;/li&gt;
&lt;li&gt;Learn about &lt;a href="https://spark.apache.org/docs/latest/sql-programming-guide.html" rel="noopener noreferrer"&gt;Spark SQL Guide&lt;/a&gt; for more query patterns&lt;/li&gt;
&lt;li&gt;Explore &lt;a href="https://iceberg.apache.org/docs/latest/spark-configuration/" rel="noopener noreferrer"&gt;Apache Iceberg Spark Integration&lt;/a&gt; for Iceberg-specific features&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Explore &lt;a href="//../06-trino-query/README.md"&gt;Using Gravitino with Trino&lt;/a&gt; for federated querying&lt;/li&gt;
&lt;li&gt;Follow and star &lt;a href="https://github.com/apache/gravitino" rel="noopener noreferrer"&gt;Apache Gravitino Repository&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Apache Gravitino is rapidly evolving, and this article is written based on the latest version 1.1.0. If you encounter issues, please refer to the &lt;a href="https://gravitino.apache.org/docs/" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; or submit issues on &lt;a href="https://github.com/apache/gravitino/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gravitino101</category>
      <category>apachespark</category>
      <category>etl</category>
      <category>metadata</category>
    </item>
    <item>
      <title>Apache Spark vs Apache Hadoop—10 Crucial Differences (2025)</title>
      <dc:creator>Pramit Marattha</dc:creator>
      <pubDate>Mon, 17 Nov 2025 03:29:16 +0000</pubDate>
      <link>https://dev.to/chaos-genius/apache-spark-vs-apache-hadoop-10-crucial-differences-2025-27l</link>
      <guid>https://dev.to/chaos-genius/apache-spark-vs-apache-hadoop-10-crucial-differences-2025-27l</guid>
      <description>&lt;p&gt;&lt;a href="https://www.oracle.com/big-data/what-is-big-data/" rel="noopener noreferrer"&gt;Big data&lt;/a&gt;—it's a whole lot to handle, and it's only getting bigger. In just a few years, the &lt;a href="https://www.statista.com/statistics/871513/worldwide-data-created/" rel="noopener noreferrer"&gt;amount of data has ballooned&lt;/a&gt;, changing how we store, process, and analyze it. To manage all this data, big data frameworks have become a must-have. &lt;a href="https://www.chaosgenius.io/blog/tag/apache-hadoop/" rel="noopener noreferrer"&gt;Apache Hadoop&lt;/a&gt; and &lt;a href="https://www.chaosgenius.io/blog/apache-spark-architecture/#what-is-apache-spark" rel="noopener noreferrer"&gt;Apache Spark&lt;/a&gt; are two of the biggest names in the game. They're both built for handling massive datasets, but they have different approaches and are better suited for different tasks. Apache Hadoop came first, starting the big data revolution by providing an affordable way to store massive datasets (via &lt;a href="https://hadoop.apache.org/docs/r1.2.1/hdfs_design.html" rel="noopener noreferrer"&gt;Hadoop Distributed File System (HDFS)&lt;/a&gt;) and process them in batches (via &lt;a href="https://www.databricks.com/glossary/mapreduce" rel="noopener noreferrer"&gt;Hadoop MapReduce&lt;/a&gt;). Spark arrived later, building on Hadoop's strengths and focusing on speed and versatility, especially with its in-memory capabilities. But here's the thing—Hadoop and Spark aren't always competitors; often, they work together.&lt;/p&gt;

&lt;p&gt;In this article, we'll break down the 10 key differences between Apache Spark and Apache Hadoop. We'll dig into their guts—architecture, speed, ecosystems, and more—so you can figure out what works for your needs. Batch processing? Real-time analytics? Machine learning? We've got you covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, What Exactly is Apache Hadoop?
&lt;/h2&gt;

&lt;p&gt;Alright, let's talk about &lt;a href="https://hadoop.apache.org/" rel="noopener noreferrer"&gt;Apache Hadoop&lt;/a&gt;. Apache Hadoop is an &lt;a href="https://github.com/apache/hadoop" rel="noopener noreferrer"&gt;open source&lt;/a&gt; big data processing framework. It's designed to tackle a specific challenge: efficiently storing and processing huge datasets across clusters of computers. We're talking massive amounts of data here—from gigabytes to terabytes to petabytes. What makes Apache Hadoop unique is its ability to use clusters of regular, off-the-shelf hardware, rather than requiring a single high-powered (and expensive) machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5qarherfsnun5r5n5d6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5qarherfsnun5r5n5d6.png" alt="Apache Spark vs Apache Hadoop"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Apache Hadoop, Really?
&lt;/h2&gt;

&lt;p&gt;Apache Hadoop is built for &lt;a href="https://aws.amazon.com/what-is/distributed-computing/" rel="noopener noreferrer"&gt;distributed computing&lt;/a&gt;. It breaks down big data problems into smaller pieces and distributes the work across many machines, processing them in parallel. Because of this, handling huge amounts of data is faster and more manageable.&lt;br&gt;
Apache Hadoop isn't just one thing; it's a collection of modules working together. The main ones you'll hear about are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.databricks.com/glossary/hadoop-distributed-file-system-hdfs" rel="noopener noreferrer"&gt;Hadoop Distributed File System (HDFS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sciencedirect.com/topics/computer-science/yet-another-resource-negotiator" rel="noopener noreferrer"&gt;Yet Another Resource Negotiator (YARN)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hadoop.apache.org/docs/r1.2.1/mapred_tutorial.html?ref=chaosgenius.io#:~:text=Hadoop%20MapReduce%20is%20a%20software,reliable%2C%20fault%2Dtolerant%20manner." rel="noopener noreferrer"&gt;Hadoop MapReduce&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-common" rel="noopener noreferrer"&gt;Hadoop Common (Hadoop Core)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll go over these in further detail later.&lt;/p&gt;
&lt;h3&gt;
  
  
  Apache Hadoop Features
&lt;/h3&gt;

&lt;p&gt;So, why did Apache Hadoop become so popular for big data? It boils down to these key features derived from its architecture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) Open Source Framework&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/apache/hadoop" rel="noopener noreferrer"&gt;Apache Hadoop’s source code is freely available&lt;/a&gt;. It is fully open sourced (licensed under Apache 2.0). You can modify it to fit your project’s needs without paying licensing fees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) It's Built for Scale (Scalability)&lt;/strong&gt;&lt;br&gt;
Apache Hadoop is fundamentally designed to scale horizontally. You can increase the cluster's storage and processing capacity by adding more commodity hardware machines (nodes).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Handles Hardware Failure Smoothly (Fault Tolerance)&lt;/strong&gt;&lt;br&gt;
Hadoop is designed to handle hardware failures within large clusters.&lt;br&gt;
&lt;strong&gt;Data Resilience&lt;/strong&gt; — The Hadoop Distributed File System (HDFS) automatically replicates data blocks (typically 3 times by default) across different nodes and racks. If a node fails, data remains accessible from other replicas&lt;br&gt;
&lt;strong&gt;Computation Resilience&lt;/strong&gt; — The cluster resource manager, YARN (Yet Another Resource Negotiator), monitors running tasks. If a node executing a task fails, YARN can reschedule that task on a healthy node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) High Data Availability&lt;/strong&gt;&lt;br&gt;
Apache Hadoop’s replication and distributed storage mean that you always have access to your data. The system automatically assigns tasks to nodes that hold the data you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5) Distributed Storage and Processing&lt;/strong&gt;&lt;br&gt;
Apache Hadoop processes data where it is stored by using the Hadoop Distributed File System (HDFS) for storage and Apache Hadoop MapReduce for computation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6) Stores All Kinds of Data (Flexibility)&lt;/strong&gt;&lt;br&gt;
Apache Hadoop doesn't force your data into a rigid structure beforehand. Apache Hadoop accepts &lt;a href="https://aws.amazon.com/what-is/structured-data/" rel="noopener noreferrer"&gt;structured data&lt;/a&gt; (like from databases), &lt;a href="https://en.wikipedia.org/wiki/Semi-structured_data" rel="noopener noreferrer"&gt;semi-structured data&lt;/a&gt; (like XML or JSON files), or completely &lt;a href="https://en.wikipedia.org/wiki/Unstructured_data" rel="noopener noreferrer"&gt;unstructured data&lt;/a&gt; (like text documents or images). You don’t have to convert or predefine schemas before storing your data, giving you the freedom to work with a variety of formats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7) High Throughput Batch Processing&lt;/strong&gt;&lt;br&gt;
Hadoop is optimized for high throughput on very large datasets by distributing data and processing tasks across many nodes in parallel. It excels at large-scale batch processing workloads such as ETL, log analysis, and data mining, and can handle vast amounts of data efficiently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8) Rich Ecosystem&lt;/strong&gt;&lt;br&gt;
Aside from its fundamental components (HDFS, YARN, MapReduce, and Common Utilities), Hadoop is supported by a large ecosystem of complementary projects that provide higher-level services and tools. These include Apache Hive (SQL interface), Apache Pig (data flow scripting), Apache HBase (NoSQL database), Apache Spark (often used with Hadoop for advanced processing), Apache Sqoop (data import/export), Apache Oozie (workflow scheduling), and many more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9) Brings Computation to the Data (Data Locality)&lt;/strong&gt;&lt;br&gt;
Hadoop attempts to move the computation to the data to minimize costly network data transfers. YARN's scheduler, in coordination with HDFS, tries to assign processing tasks to nodes where the required data blocks reside locally, or at least within the same network rack, resulting in dramatically improved performance.&lt;/p&gt;


&lt;h2&gt;
  
  
  And What About Apache Spark?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.chaosgenius.io/blog/apache-spark-architecture/" rel="noopener noreferrer"&gt;Apache Spark&lt;/a&gt; is a different beast. So, what is Apache Spark? &lt;/p&gt;

&lt;p&gt;Apache Spark is also an &lt;a href="https://github.com/apache/spark" rel="noopener noreferrer"&gt;open source&lt;/a&gt; analytics engine that can handle large-scale data processing tasks. It's designed for speed, simplicity, and adaptability, making it a popular choice for big data tasks. So, whether you're working with batch processing or real-time analytics, Spark provides a consistent framework that makes these tasks easier. Spark was developed at UC Berkeley in 2009 as a quicker alternative to Hadoop MapReduce architecture, capable of processing jobs up to 100 times faster in memory and 10 times faster on disk.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/IELMSD2kdmk"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;Spark’s architecture is built around several high‑level abstractions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.chaosgenius.io/blog/apache-spark-architecture/#1-resilient-distributed-datasets-rdds" rel="noopener noreferrer"&gt;Resilient Distributed Datasets (RDDs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spark.apache.org/docs/latest/sql-programming-guide.html" rel="noopener noreferrer"&gt;Spark SQL, DataFrames and Datasets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.databricks.com/glossary/what-is-spark-streaming" rel="noopener noreferrer"&gt;Spark Streaming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spark.apache.org/mllib/" rel="noopener noreferrer"&gt;Spark MLlib (Machine Learning Library)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spark.apache.org/graphx/" rel="noopener noreferrer"&gt;Spark GraphX&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Apache Spark Features
&lt;/h2&gt;

&lt;p&gt;Alright, let's look under the hood. What capabilities does Apache Spark bring to the table?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) Speed&lt;/strong&gt;&lt;br&gt;
Spark processes data incredibly fast compared to traditional systems like Apache Hadoop. Its in-memory computing reduces disk I/O operations, enabling applications to run up to 100 times faster in memory and significantly faster on disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Simplicity&lt;/strong&gt;&lt;br&gt;
Apache Spark simplifies application development by providing APIs in many languages (Java, Python, Scala, and R). Its high-level operators simplify distributed processing tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Fault Tolerance&lt;/strong&gt;&lt;br&gt;
Spark achieves fault tolerance through its primary data abstraction, the Resilient Distributed Dataset (RDD), and by extension, DataFrames/Datasets which are built upon RDDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) Scalability&lt;/strong&gt;&lt;br&gt;
You can scale Spark horizontally by adding more nodes to your cluster. It handles large datasets efficiently across distributed environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5) In-Memory Processing&lt;/strong&gt;&lt;br&gt;
Spark is not entirely in-memory; rather, it intelligently uses memory (caching and persistence) to store intermediate datasets throughout multi-step operations. This is especially useful for iterative algorithms (common in machine learning) and interactive data processing, which eliminate repeated disk reads. It can smoothly dump data to disk if memory gets limited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6) Multi-Language Support&lt;/strong&gt;&lt;br&gt;
Spark’s APIs support Java, Python, Scala, and R—giving you flexibility in choosing your preferred programming language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7) Machine Learning Integration&lt;/strong&gt;&lt;br&gt;
Spark includes Spark MLlib, a library for machine learning tasks like classification, regression, clustering, and collaborative filtering. This makes it ideal for building predictive models directly within the framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8) Structured Streaming&lt;/strong&gt;&lt;br&gt;
Apache Spark Structured Streaming high-level, fault-tolerant stream processing engine built on the Spark SQL engine. It treats data streams as continuously appending unbounded tables, allowing developers to use the same batch-like DataFrame/Dataset API for stream processing, simplifying the development of end-to-end applications. (This largely supersedes the older RDD-based Spark Streaming/DStreams micro-batching model).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9) Graph Processing&lt;/strong&gt;&lt;br&gt;
Spark GraphX (built-in Spark library) enables graph-based computations such as social network analysis or recommendation systems within Spark’s ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10) Compatibility&lt;/strong&gt;&lt;br&gt;
Spark can read from and write to a wide variety of data sources, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distributed file systems&lt;/strong&gt;: Hadoop Distributed File System (HDFS), Amazon S3, Azure Data Lake Storage (ADLS), Google Cloud Storage (GCS).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NoSQL databases&lt;/strong&gt;: Apache Cassandra, HBase, MongoDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relational databases&lt;/strong&gt;: Via JDBC/ODBC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message queues&lt;/strong&gt;: Apache Kafka, Flume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data formats&lt;/strong&gt;: Apache Parquet, Avro, ORC, JSON, CSV, text files, sequence files, and more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It integrates closely with Apache Hive, often leveraging the Hive Metastore for persistent table metadata. It can run on various cluster managers like Standalone, Apache Mesos, Hadoop YARN, and Kubernetes.&lt;/p&gt;

&lt;p&gt;Apache Spark’s a compute engine, not a storage system. It often piggybacks on Hadoop Distributed File System (HDFS) or other storage like S3. That’s where Apache Spark vs Apache Hadoop starts to get interesting—they’re not always rivals.&lt;/p&gt;


&lt;h2&gt;
  
  
  What Is the Difference Between Apache Hadoop and Apache Spark?
&lt;/h2&gt;

&lt;p&gt;Okay, before we dive deep into the differences, here’s a snapshot of Apache Spark vs Apache Hadoop:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache Spark vs Apache Hadoop—Head-to-Head Comparison&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
                &lt;tr&gt;
                    &lt;td&gt;&lt;/td&gt;
                    &lt;td&gt;&lt;b&gt;Apache Hadoop&lt;/b&gt;&lt;/td&gt;
                    &lt;td&gt;&lt;b&gt;Apache Spark&lt;/b&gt;&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Main Role&lt;/td&gt;
                    &lt;td&gt;Storage (HDFS), Resource Mgmt (YARN), Batch Processing (MapReduce)&lt;/td&gt;
                    &lt;td&gt;Fast, Unified Processing Engine&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Architecture&lt;/td&gt;
                    &lt;td&gt;Master-slave (HDFS, YARN, MapReduce)&lt;/td&gt;
                    &lt;td&gt;Driver, Executors, Cluster Manager&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Performance&lt;/td&gt;
                    &lt;td&gt;Disk-based, slower&lt;/td&gt;
                    &lt;td&gt;In-memory, up to 100x faster*&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Ecosystem&lt;/td&gt;
                    &lt;td&gt;Full-stack platform&lt;/td&gt;
                    &lt;td&gt;Compute-focused, pairs with HDFS&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Memory Usage&lt;/td&gt;
                    &lt;td&gt;Low RAM, disk-driven&lt;/td&gt;
                    &lt;td&gt;High RAM, memory-hungry&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Languages&lt;/td&gt;
                    &lt;td&gt;Java + streaming APIs&lt;/td&gt;
                    &lt;td&gt;Scala, Java, Python, R, SQL&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Cluster Management&lt;/td&gt;
                    &lt;td&gt;Yet Another Resource Negotiator&lt;/td&gt;
                    &lt;td&gt;YARN, Mesos, Kubernetes, Standalone&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Storage&lt;/td&gt;
                    &lt;td&gt;Includes native distributed storage (HDFS)&lt;/td&gt;
                    &lt;td&gt;Relies on external storage (HDFS, S3, etc.)&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;APIs / Ease of Use&lt;/td&gt;
                    &lt;td&gt;Files/Blocks (HDFS), Key-Value Pairs (MapReduce)&lt;/td&gt;
                    &lt;td&gt;Resilient Distributed Datasets (RDDs), DataFrames, Datasets&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Data Processing&lt;/td&gt;
                    &lt;td&gt;Primarily Batch (MapReduce)&lt;/td&gt;
                    &lt;td&gt;Batch, Interactive SQL, Streaming, ML, Graph&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Real-Time Processing&lt;/td&gt;
                    &lt;td&gt;No (MapReduce is batch-only)&lt;/td&gt;
                    &lt;td&gt;Yes (Spark Streaming, Structured Streaming)&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Fault Tolerance&lt;/td&gt;
                    &lt;td&gt;HDFS replication, Task retries (YARN/MapReduce)&lt;/td&gt;
                    &lt;td&gt;RDD/DataFrame lineage, Checkpointing (optional)&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Security&lt;/td&gt;
                    &lt;td&gt;Robust (Kerberos, Ranger)&lt;/td&gt;
                    &lt;td&gt;Basic, leans on Apache Hadoop’s tools&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Machine Learning&lt;/td&gt;
                    &lt;td&gt;Mahout&lt;/td&gt;
                    &lt;td&gt;Spark MLlib, Spark GraphX&lt;/td&gt;
                &lt;/tr&gt;
           &lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now, let’s break it down piece by piece.&lt;/p&gt;


&lt;h2&gt;
  
  
  1) Apache Spark vs Apache Hadoop—Architecture Breakdown
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Apache Hadoop Architecture
&lt;/h3&gt;

&lt;p&gt;Apache Hadoop's architecture is set up to handle massive amounts of data across distributed clusters. If you're dealing with big data, understanding how Hadoop works can help you store and process information efficiently. Let’s break down its components and how they work together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Hadoop Distributed File System (HDFS)&lt;/strong&gt;&lt;br&gt;
HDFS stores your data across multiple machines, splitting files into blocks (default size: 128 MB) and replicating them for fault tolerance. The NameNode (master) tracks where data blocks are stored, while DataNodes (workers) hold the actual data. If a node fails, HDFS automatically uses a replica—no manual intervention needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ YARN (Yet Another Resource Negotiator)&lt;/strong&gt;&lt;br&gt;
YARN manages cluster resources like CPU and memory. It separates processing from resource management, letting you run multiple workloads simultaneously.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ResourceManager (RM)&lt;/strong&gt;: There's usually one global RM. It's the ultimate authority that knows the overall resource availability in the cluster. It decides which applications get resources and when.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodeManager (NM)&lt;/strong&gt;: Each machine in the cluster runs a NodeManager. It manages the resources on that specific machine and reports back to the ResourceManager. It's also responsible for launching and monitoring the actual tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ApplicationMaster (AM)&lt;/strong&gt;: When you submit a job (an "application" in YARN terms), YARN starts a dedicated ApplicationMaster for it. The AM negotiates resources from the ResourceManager and works with the NodeManagers to get the application's tasks running. It oversees the execution of that specific job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;➥ MapReduce&lt;/strong&gt;&lt;br&gt;
This processing model splits tasks into smaller chunks. A Map function filters and sorts data, while a Reduce function aggregates results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Hadoop Common&lt;/strong&gt;&lt;br&gt;
Shared utilities and libraries (e.g., file system access, authentication) that support other modules. Without this, tools like Hive or Pig couldn’t interact with HDFS.&lt;/p&gt;

&lt;p&gt;So, a typical flow looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ijd1f5d5m54883n6sxp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ijd1f5d5m54883n6sxp.png" alt="Apache Hadoop Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You load data into HDFS. It gets broken into blocks and replicated across DataNodes. The NameNode keeps track of everything.&lt;/li&gt;
&lt;li&gt;You submit an application (like a MapReduce job or a Spark job) to the YARN ResourceManager.&lt;/li&gt;
&lt;li&gt;The ResourceManager finds a NodeManager with available resources and tells it to launch an ApplicationMaster for your job.&lt;/li&gt;
&lt;li&gt;The ApplicationMaster figures out what tasks need to run and asks the ResourceManager for resource containers.&lt;/li&gt;
&lt;li&gt;The ResourceManager grants containers on various NodeManagers (ideally close to the data needed).&lt;/li&gt;
&lt;li&gt;The ApplicationMaster tells the relevant NodeManagers to launch the tasks within the allocated containers.&lt;/li&gt;
&lt;li&gt;Tasks read data from HDFS, do their processing (Map, Reduce, or other operations), and write results back to HDFS.&lt;/li&gt;
&lt;li&gt;Once the job is done, the ApplicationMaster shuts down, and its resources are released back to YARN.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Apache Spark Architecture
&lt;/h3&gt;

&lt;p&gt;Apache Spark architecture follows a master-worker pattern. Let’s break down how its components interact and why they matter for your data pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Driver Program&lt;/strong&gt;&lt;br&gt;
The driver is the control center of a Spark application. When you submit a job, it translates your code into a series of tasks. It creates a SparkContext or SparkSession (the entry point for all operations) and communicates with the cluster manager to allocate resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Executors&lt;/strong&gt;&lt;br&gt;
Executors are worker processes on cluster nodes that run tasks and store data in memory or on disk. Each application gets its own executors, which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Execute tasks sent by the driver.&lt;/li&gt;
&lt;li&gt;Cache frequently accessed data (like RDDs) to speed up repeated operations.&lt;/li&gt;
&lt;li&gt;Report task status back to the driver.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The number of executors directly impacts parallelism—more executors mean more tasks can run simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Cluster Manager&lt;/strong&gt;&lt;br&gt;
Spark relies on cluster managers (like Kubernetes, YARN, or Mesos) to allocate CPU, memory, and network resources. The cluster manager launches executors on worker nodes. And monitors resource usage and redistributes workloads if nodes fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Worker Nodes&lt;/strong&gt;&lt;br&gt;
Worker nodes are the machines in the cluster where executors run. Each worker node can host multiple executors, and the tasks are distributed among these executors for parallel processing.&lt;/p&gt;

&lt;p&gt;So, a typical flow looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjbqdjnek2j2uaetcysnd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjbqdjnek2j2uaetcysnd.png" alt="Apache Spark Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a user submits a Spark application, the driver program is launched. The driver communicates with the cluster manager to request resources for the application.&lt;/li&gt;
&lt;li&gt;The driver converts the user's code into jobs, which are divided into stages. Each stage is further divided into tasks. The driver creates a logical DAG representing the sequence of stages and tasks.&lt;/li&gt;
&lt;li&gt;The DAG scheduler divides the DAG into stages, each containing multiple tasks. The task scheduler assigns tasks to executors based on the available resources and data locality.&lt;/li&gt;
&lt;li&gt;Executors run the tasks on the worker nodes, process the data, and return the results to the driver. The driver aggregates the results and presents them to the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out the following articles for an in-depth analysis:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.flexera.com/blog/finops/apache-spark-architecture/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.flexera.com%2Fblog%2Fwp-content%2Fuploads%2F2026%2F02%2Ffeatured-10.jpg" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.flexera.com/blog/finops/apache-spark-architecture/" rel="noopener noreferrer" class="c-link"&gt;
            Apache Spark architecture 101: How Spark works (2026)
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Apache Spark 101—its origins, key features, architecture and applications in big data, machine learning and real-time processing.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.flexera.com%2Fblog%2Fwp-content%2Fthemes%2Ff1%2Fassets%2Fimages%2Ffavicon.ico"&gt;
          flexera.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.flexera.com/blog/finops/apache-spark-alternatives/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.flexera.com%2Fblog%2Fwp-content%2Fuploads%2F2026%2F02%2Ffeatured-07.jpg" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.flexera.com/blog/finops/apache-spark-alternatives/" rel="noopener noreferrer" class="c-link"&gt;
            Comparing Apache Spark alternatives: Storm, Flink, Hadoop and more
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Find out the top 7 Apache Spark alternatives that provide fast, fault-tolerant processing for modern real-time and batch workloads.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.flexera.com%2Fblog%2Fwp-content%2Fthemes%2Ff1%2Fassets%2Fimages%2Ffavicon.ico"&gt;
          flexera.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;







&lt;h2&gt;
  
  
  2) Apache Spark vs Apache Hadoop—Performance &amp;amp; Speed
&lt;/h2&gt;

&lt;p&gt;Right off the bat, Apache Spark is generally faster than Apache Hadoop's MapReduce, its original processing engine. How much faster? You'll often hear figures up to 100 times faster, but take that with a grain of salt—it highly depends on the specific job you're running.&lt;/p&gt;

&lt;p&gt;Why the speed difference? It's mostly about memory.&lt;/p&gt;

&lt;p&gt;Apache Spark processes data in-memory. Spark uses &lt;a href="https://www.chaosgenius.io/blog/apache-spark-architecture/#1-resilient-distributed-datasets-rdds" rel="noopener noreferrer"&gt;Resilient Distributed Datasets (RDDs)&lt;/a&gt;, &lt;a href="https://spark.apache.org/docs/latest/sql-programming-guide.html" rel="noopener noreferrer"&gt;DataFrames or Datasets&lt;/a&gt;, which let it keep intermediate data (the results between steps of your job) in the memory of the worker nodes across multiple operations. It only goes to disk when absolutely necessary or explicitly told to. This avoids the time-consuming process of reading and writing to physical disks repeatedly. Spark also uses a more advanced &lt;a href="https://www.chaosgenius.io/blog/apache-spark-architecture/#2-directed-acyclic-graph-dag" rel="noopener noreferrer"&gt;Directed Acyclic Graph (DAG)&lt;/a&gt; execution engine, which allows for more efficient scheduling of tasks compared to Hadoop MapReduce's rigid Map -&amp;gt; Reduce steps.&lt;/p&gt;

&lt;p&gt;Hadoop MapReduce, on the other hand, was designed when RAM was more expensive and clusters were often disk-heavy. Hadoop MapReduce writes the results of its map and reduce tasks back to the Hadoop Distributed File System (HDFS) on disk. If you have a multi-step job, each step involves reading from the disk and writing back to the disk. Disk I/O (Input/Output) is way slower than accessing RAM. That's the primary bottleneck Hadoop MapReduce faces compared to Spark for many data processing tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  3) Apache Spark vs Apache Hadoop—Ecosystem Integration &amp;amp; Compatibility
&lt;/h2&gt;

&lt;p&gt;Alright, let's dive into how Apache Spark and Apache Hadoop play together, focusing on Apache Spark vs Apache Hadoop ecosystem integration &amp;amp; compatibility. It's less of a competition and more about how they can work in tandem, though they do have different strengths.&lt;/p&gt;

&lt;p&gt;Apache Hadoop has a very rich and mature ecosystem that has grown over many years. Beyond Hadoop Distributed File System (HDFS), Yet Another Resource Negotiator, and Hadoop MapReduce, you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://hive.apache.org/" rel="noopener noreferrer"&gt;Apache Hive&lt;/a&gt; — Provides a SQL-like interface to query data stored in Hadoop Distributed File System (HDFS) or other compatible stores.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pig.apache.org/" rel="noopener noreferrer"&gt;Apache Pig&lt;/a&gt; — Offers a high-level scripting language (Pig Latin) for data analysis flows.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hbase.apache.org/" rel="noopener noreferrer"&gt;Apache HBase&lt;/a&gt; — A NoSQL, column-oriented database that runs on top of Hadoop Distributed File System (HDFS), good for real-time random read/write access.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sqoop.apache.org/" rel="noopener noreferrer"&gt;Apache Sqoop&lt;/a&gt; — Tool for transferring bulk data between Apache Hadoop and structured datastores like relational databases.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://flume.apache.org/" rel="noopener noreferrer"&gt;Apache Flume&lt;/a&gt; — For collecting, aggregating, and moving large amounts of log data.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://oozie.apache.org/" rel="noopener noreferrer"&gt;Apache Oozie&lt;/a&gt; — A workflow scheduler system to manage Hadoop jobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And many more...&lt;/p&gt;

&lt;p&gt;Because of this rich ecosystem, Apache Hadoop can often act as a more complete, end-to-end platform for distributed storage and batch processing needs.&lt;/p&gt;

&lt;p&gt;Apache Spark, on the other hand, itself is more focused on the compute aspect. While it includes libraries like Spark SQL, Spark MLlib, Spark Streaming, and Spark GraphX, it's designed to integrate smoothly with various storage systems and resource managers rather than providing its own comprehensive storage solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Storage Integration&lt;/strong&gt; — Spark integrates seamlessly with Apache Hadoop's HDFS. In fact, running Spark on Yet Another Resource Negotiator using HDFS for storage is arguably the most common deployment pattern. But Spark isn't limited to HDFS; it can read from and write to many sources like Amazon S3, Azure Data Lake Storage (ADLS), Google Cloud Storage (GCS), Apache Cassandra, HBase, MongoDB, Apache Kafka, Flume, Apache Hive, Apache Mesos and many more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Compute Layer&lt;/strong&gt; — Spark is often used as the compute layer within a broader Apache Hadoop ecosystem or a modern data platform due to its versatility. It can replace or supplement Hadoop MapReduce for processing data stored in HDFS or accessed via other Apache Hadoop tools.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;So, while Apache Hadoop offers a wider built-in ecosystem, Spark offers greater flexibility in integrating with different storage and cluster management systems, often leveraging Hadoop components.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4) Apache Spark vs Apache Hadoop—Memory &amp;amp; Hardware
&lt;/h2&gt;

&lt;p&gt;What do they demand from your machines?&lt;/p&gt;

&lt;p&gt;Apache Hadoop MapReduce was fundamentally designed for large-scale batch processing, prioritizing throughput and fault tolerance using commodity hardware. Its processing model inherently relies heavily on disk I/O:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Intermediate Data Storage&lt;/strong&gt;: After each Map and Reduce phase, Hadoop MapReduce writes intermediate results back to the Hadoop Distributed File System (HDFS) or local disk. This persistence ensures fault tolerance but introduces significant disk I/O latency, often becoming the primary performance bottleneck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Memory Requirements&lt;/strong&gt;: Consequently, Hadoop MapReduce tasks generally have lower active memory requirements compared to Spark for holding data during computation. Clusters running primarily Hadoop MapReduce workloads could often be built with nodes having moderate RAM, focusing instead on sufficient disk capacity and throughput.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Hardware Cost Profile&lt;/strong&gt;: Historically, this disk-centric approach allowed Hadoop clusters to be built using less expensive "commodity" hardware with substantial disk storage but relatively less RAM per node. While Hadoop MapReduce can utilize available RAM for buffering, it's not optimized for keeping large working datasets entirely in memory across stages.&lt;/p&gt;

&lt;p&gt;Apache Spark was developed to overcome the latency limitations of Hadoop MapReduce, particularly for iterative algorithms (like machine learning) and interactive analytics, by leveraging in-memory processing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ In-Memory Data Storage&lt;/strong&gt; — Apache Spark processes data primarily in RAM using Resilient Distributed Datasets (RDDs) or DataFrames/Datasets. It keeps intermediate data in memory between stages within a job, avoiding costly disk writes whenever possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Memory Requirements&lt;/strong&gt; — To achieve its performance potential, Spark benefits greatly from having sufficient RAM across the cluster to hold the data partitions being actively processed. While Spark can operate with less memory by "spilling" excess data to disk, this incurs substantial performance penalties as disk I/O becomes involved. Therefore, Spark clusters are typically provisioned with significantly more RAM per node (often ranging from tens to hundreds of GiB) compared to traditional Hadoop MapReduce clusters designed for similar data scales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➥ Hardware Cost Profile&lt;/strong&gt; — The need for larger amounts of RAM generally makes the hardware for a Spark-optimized cluster more expensive on a per-node basis compared to a traditional, disk-focused Hadoop MapReduce node. But, the Total Cost of Ownership (TCO) comparison can be complex; Spark's speed might allow for smaller clusters or faster job completion (reducing operational costs, especially in cloud environments).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Apache Hadoop MapReduce is a cost-effective option upfront since it gets by with less RAM and leans on disk storage. The downside is, it can be sluggish with batch processing. Apache Spark, though, is typically way faster, especially when it comes to iterative or interactive tasks. The catch is you'll need to spend more on memory-rich hardware to get that speed.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5) Apache Spark vs Apache Hadoop—Programming Language Support
&lt;/h2&gt;

&lt;p&gt;How easy is it for developers to work with them?&lt;/p&gt;

&lt;p&gt;Apache Hadoop is primarily written in Java and—via mechanisms like &lt;a href="https://hadoop.apache.org/docs/r1.2.1/streaming.html?ref=chaosgenius.io#Hadoop+Streaming" rel="noopener noreferrer"&gt;Hadoop Streaming&lt;/a&gt;—allows developers to write Hadoop MapReduce programs in virtually any language (such as Python, Ruby, or others). However, its native API is Java, which often results in verbose, low-level code when writing Hadoop MapReduce jobs directly. On the flip side, Apache Spark was developed in Scala and provides robust, first‐class APIs in Scala, Java, Python (via PySpark), R, and SQL (via Spark SQL). This multi-language support lets developers choose the programming language they are most comfortable with, thereby reducing the learning curve.&lt;/p&gt;

&lt;p&gt;A key advantage of Apache Spark is its &lt;a href="https://spark.apache.org/docs/latest/quick-start.html?ref=chaosgenius.io#interactive-analysis-with-the-spark-shell" rel="noopener noreferrer"&gt;interactive development mod&lt;/a&gt;e. Spark offers REPLs—such as the &lt;a href="https://spark.apache.org/docs/latest/quick-start.html?ref=chaosgenius.io#interactive-analysis-with-the-spark-shell" rel="noopener noreferrer"&gt;spark‑shell&lt;/a&gt; for Scala and PySpark for Python—that allow developers to explore and manipulate data interactively. On top of that, Spark’s high‑level abstractions (originally built around Resilient Distributed Datasets, and now primarily through DataFrames and Datasets) provide a rich set of operators that simplify complex data transformations and iterative processing.&lt;br&gt;
On the other hand, Hadoop MapReduce development typically requires a deeper understanding of low‑level APIs and often involves writing extensive boilerplate code, making it more cumbersome and less flexible for rapid development.&lt;/p&gt;




&lt;h2&gt;
  
  
  6) Apache Spark vs Apache Hadoop—Scheduling and Resource Management
&lt;/h2&gt;

&lt;p&gt;Apache Spark and Apache Hadoop uses distinct approaches to scheduling computations and managing cluster resources. &lt;/p&gt;

&lt;p&gt;Apache Spark uses the &lt;a href="https://spark.apache.org/docs/latest/job-scheduling.html" rel="noopener noreferrer"&gt;Spark Scheduler&lt;/a&gt; to manage task execution across a cluster. The Spark Scheduler is responsible for breaking down the Directed Acyclic Graph (DAG) into stages, each containing multiple tasks. These tasks are then scheduled to executors, which are computing units that run on worker nodes. The Spark Scheduler, in conjunction with the Block Manager, handles job scheduling, monitoring, and data distribution across the cluster. The Block Manager acts as a key-value store for blocks of data, enabling efficient data management and fault tolerance within Spark.&lt;/p&gt;

&lt;p&gt;On the other hand, Apache Hadoop's resource management is natively handled by YARN (Yet Another Resource Negotiator), which consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ResourceManager&lt;/strong&gt; — Global resource arbitrator allocating cluster resources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodeManager&lt;/strong&gt; — Per-node agent managing containers (resource units)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ApplicationMaster&lt;/strong&gt; — Per-application component negotiating resources and monitoring tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For workflow scheduling, Hadoop can be integrated with Apache Oozie – a separate service that orchestrates Directed Acyclic Graphs of dependent jobs (MapReduce, Hive, Pig) through XML-defined workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  7) Apache Spark vs Apache Hadoop—Latency &amp;amp; Real-Time Analytics Capabilities
&lt;/h2&gt;

&lt;p&gt;How quickly can you get results? What about live data?&lt;/p&gt;

&lt;p&gt;Apache Hadoop MapReduce was designed primarily as a batch-processing system. In a typical Hadoop MapReduce job, data is read from the Hadoop Distributed File System (HDFS), processed by map tasks, written back to disk as intermediate output, and then read again by reduce tasks before writing the final output to disk. Due to this heavy reliance on disk I/O at multiple critical stages, especially between the Map and Reduce phases, it introduces significant latency. As a result, Hadoop MapReduce jobs generally take minutes—or even hours—to complete, making them unsuitable for real-time or near-real-time data processing use cases. Despite this, Hadoop MapReduce remains effective for processing massive datasets when throughput is prioritized over speed.&lt;/p&gt;

&lt;p&gt;Apache Spark was engineered to overcome the latency challenges of Hadoop MapReduce. Its key innovation is in-memory processing—loading data into RAM across the cluster and retaining intermediate data in memory between stages whenever possible. Because of this design, it dramatically reduces disk I/O overhead and significantly speeds up processing, especially for iterative algorithms (such as those used in machine learning) and interactive data analysis.&lt;/p&gt;

&lt;p&gt;Spark provides specialized streaming libraries for real-time and near real-time processing:&lt;br&gt;
➥ &lt;a href="https://spark.apache.org/docs/latest/streaming-programming-guide.html" rel="noopener noreferrer"&gt;Spark Streaming (DStreams)&lt;/a&gt; — Processes data streams by breaking them into micro-batches, allowing near-real-time processing.&lt;br&gt;
➥ &lt;a href="https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html" rel="noopener noreferrer"&gt;Structured Streaming&lt;/a&gt; — This newer API treats incoming data streams as continuously appended tables. It also typically operates on a micro-batching engine—achieving end-to-end latencies that can be as low as around 100 milliseconds while providing exactly-once fault tolerance.&lt;br&gt;
➥ &lt;a href="https://www.databricks.com/blog/2018/03/20/low-latency-continuous-processing-mode-in-structured-streaming-in-apache-spark-2-3-0.html" rel="noopener noreferrer"&gt;Continuous Processing Mode (Experimental)&lt;/a&gt; — Introduced in &lt;a href="https://www.databricks.com/blog/2018/02/28/introducing-apache-spark-2-3.html" rel="noopener noreferrer"&gt;Spark 2.3&lt;/a&gt;, this mode aims to reduce latency further—potentially into the low-millisecond range—but comes with certain limitations (e.g., limited API support and at-least-once processing guarantees).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Thus, while Hadoop MapReduce is confined to high-latency batch processing, Apache Spark offers a unified platform that can efficiently handle both batch and low-latency stream processing.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  8) Apache Spark vs Apache Hadoop—Fault Tolerance
&lt;/h2&gt;

&lt;p&gt;What happens when things go wrong?&lt;/p&gt;

&lt;p&gt;Apache Spark and Apache Hadoop both have strong fault-tolerance mechanisms to keep failures from forcing a complete restart of apps. But, they tackle this challenge in different ways.&lt;/p&gt;

&lt;p&gt;Apache Hadoop’s fault tolerance is built into its core components. In Hadoop Distributed File System (HDFS), data is broken down into blocks that are copied (by default, three copies) across different nodes. If a DataNode fails, the data's still available from another node because of this copying. Also, within the Hadoop MapReduce framework, the master (or ResourceManager in Yet Another Resource Negotiator(YARN)) monitors task execution. If a task fails—say, a node crashes—the framework automatically retries the task on another node. This two-part approach (HDFS copies data, Hadoop MapReduce re-executes tasks) makes Hadoop pretty robust against node failures, but it does add some extra overhead from writing intermediate data to disk.&lt;/p&gt;

&lt;p&gt;Spark’s fault tolerance is achieved at the application level using Resilient Distributed Datasets (RDDs). Each Resilient Distributed Dataset maintains a complete lineage—a record of the transformations (stored in the DAG) used to derive it. If a partition is lost due to an executor failure, Spark can recompute that partition from its lineage without restarting the entire job. On top of that, Spark supports checkpointing, where Resilient Distributed Datasets (RDDs) or streaming state are periodically saved to reliable storage (like Hadoop Distributed File System (HDFS)) to truncate long lineages and speed up recovery. For streaming applications, Spark’s Structured Streaming also leverages write-ahead logs and state checkpointing to provide exact-once processing guarantees.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Apache Hadoop relies on block-level replication and task re-execution within Hadoop MapReduce to handle failures, which is well-suited for disk-based batch processing. Apache Spark, on the other hand,  uses in-memory recomputation based on RDD lineage (supplemented by checkpointing when needed), providing a more flexible and often faster recovery for interactive and iterative workloads.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  9) Apache Spark vs Apache Hadoop—Security &amp;amp; Data Governance
&lt;/h2&gt;

&lt;p&gt;How secure are they, and how well can you manage access?&lt;/p&gt;

&lt;p&gt;Apache Hadoop is built with security in mind. Most modern Hadoop distributions offer secure configurations by default. They use strong authentication mechanisms—most notably &lt;a href="https://learn.microsoft.com/en-us/windows-server/security/kerberos/kerberos-authentication-overview" rel="noopener noreferrer"&gt;Kerberos&lt;/a&gt;—as well as fine-grained authorization with tools like &lt;a href="https://ranger.apache.org/" rel="noopener noreferrer"&gt;Apache Ranger&lt;/a&gt; and &lt;a href="https://www.okta.com/identity-101/what-is-ldap/" rel="noopener noreferrer"&gt;LDAP integration&lt;/a&gt;. Hadoop's file system also enforces standard file permissions and supports &lt;a href="https://en.wikipedia.org/wiki/Access-control_list" rel="noopener noreferrer"&gt;access control lists (ACLs)&lt;/a&gt;, so data is protected when it's not being used. These security features, combined with auditing and metadata management (supported by &lt;a href="https://atlas.apache.org/" rel="noopener noreferrer"&gt;Apache Atlas&lt;/a&gt;), provide a comprehensive data governance framework for enterprises.&lt;/p&gt;

&lt;p&gt;Apache Spark can be made equally secure, though its default configuration (especially in standalone mode) is not as locked down, meaning that a standalone Spark deployment may be vulnerable if not properly secured. Spark’s built-in authentication mechanism—when enabled via configuration (such as enabling spark.authenticate)—relies on a shared secret for communication between the driver and executors. However, when Spark is deployed within a secure Apache Hadoop ecosystem (such as on Yet Another Resource Negotiator(YARN) with Kerberos enabled), it can inherit many of the underlying security features. And it can also be set up with &lt;a href="https://en.wikipedia.org/wiki/Transport_Layer_Security" rel="noopener noreferrer"&gt;SSL/TLS encryption&lt;/a&gt; for data in transit. Moreover, integrations with external security frameworks (such as &lt;a href="https://ranger.apache.org/" rel="noopener noreferrer"&gt;Apache Ranger&lt;/a&gt;) are available to extend Spark’s access controls and audit capabilities. In essence, while Spark’s default settings are less secure, it can be hardened significantly when deployed in a secured environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  10) Apache Spark vs Apache Hadoop—Machine Learning &amp;amp; Advanced Analytics
&lt;/h2&gt;

&lt;p&gt;What about running complex analytics like ML?&lt;/p&gt;

&lt;p&gt;Apache Hadoop’s core MapReduce framework does not include native machine learning libraries. Historically, developers used external libraries such as &lt;a href="https://mahout.apache.org/" rel="noopener noreferrer"&gt;Apache Mahout&lt;/a&gt; to implement ML algorithms on Hadoop. Mahout’s early implementations relied on Hadoop MapReduce, which—because of its disk-based, batch-oriented design—incurred significant latency and inefficiency for iterative algorithms common in machine learning. These limitations often resulted in performance bottlenecks, particularly when processing large data fragments. In response, recent versions of Mahout have shifted toward leveraging Spark’s in-memory processing capabilities rather than Hadoop MapReduce to overcome these challenges.&lt;/p&gt;

&lt;p&gt;Apache Spark was designed with iterative and interactive analytics in mind. Its native machine learning library, Spark MLlib, offers high-level APIs for tasks such as classification, regression, clustering, collaborative filtering, dimensionality reduction, and more. Spark MLlib benefits from Spark’s in-memory computing model, which minimizes the latency inherent in disk-based processing and dramatically accelerates iterative computations. Due to this integration, it is considerably easier to develop, prototype, and deploy machine learning applications. Moreover, Spark’s active community and extensive ecosystem further simplify the development of advanced analytics applications, enabling real-time analytics, interactive data exploration, and seamless integration with other Spark components.&lt;/p&gt;




&lt;h2&gt;
  
  
  Apache Spark vs Apache Hadoop—Use Cases
&lt;/h2&gt;

&lt;p&gt;Knowing the technical differences helps, sure, but the real question for you is probably: when should you pick one over the other, or maybe even use them together? Let's break down the typical scenarios for Apache Spark vs Apache Hadoop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apache Spark Use Cases—When to Use Apache Spark?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🔮 Use Apache Spark When:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You need fast processing&lt;/strong&gt; — Spark processes data in memory (RAM) using Resilient Distributed Datasets (RDDs), which is way faster than Hadoop MapReduce's approach of writing intermediate results to disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're doing machine learning&lt;/strong&gt; — Spark's speed is a huge advantage for iterative algorithms common in machine learning (training models often involve repeatedly processing the same data). Its built-in Spark MLlib library is designed for large-scale ML tasks and integrates well with other ML tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to process streaming data&lt;/strong&gt; — Spark Streaming (and its successor, Structured Streaming) handles real-time data streams effectively, processing data in small batches (micro-batching).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want a unified platform&lt;/strong&gt; — Spark offers APIs for SQL (Spark SQL), streaming, ML (Spark MLlib), and graph processing (Spark GraphX), letting you combine different types of processing in a single application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ease of use is important&lt;/strong&gt; — Spark offers high-level APIs in Python, Scala, Java, and R, which many find easier to work with than writing Java MapReduce code. Its interactive shells (like PySpark) are also handy for exploration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Apache Hadoop Use Cases—When to Use Apache Hadoop?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🔮 Use Apache Hadoop When:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You need massive, affordable, reliable storage&lt;/strong&gt; — Hadoop Distributed File System (HDFS) is designed for storing enormous files across clusters of commodity hardware. It's highly scalable and fault-tolerant through data replication. If your data volume is truly massive and doesn't fit comfortably in RAM across your cluster, HDFS is a solid, cost-effective storage foundation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost is a major factor&lt;/strong&gt; — Apache Hadoop clusters can be built using relatively inexpensive commodity hardware. Since Hadoop MapReduce (if used) is disk-based, it doesn't demand the high RAM requirements that Spark's in-memory approach does, making the hardware potentially cheaper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch processing is sufficient&lt;/strong&gt; — If you have large jobs that can run overnight or don't require immediate results (like generating monthly reports, large-scale ETL, log analysis for historical trends), Hadoop MapReduce (or Hive on Hadoop) is perfectly capable and economical. Its processing model is well-suited for linear processing of large data volumes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data archiving&lt;/strong&gt; — Hadoop Distributed File System (HDFS) provides a cost-effective way to archive massive datasets for long-term retention or compliance.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Which is better: Apache Spark vs Apache Hadoop? (Apache Spark vs Apache Hadoop—Pros &amp;amp; Cons)
&lt;/h2&gt;

&lt;p&gt;No tool is perfect. Let's weigh the advantages and disadvantages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apache Spark Benefits and Apache Spark Limitations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Apache Spark Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast in-memory processing speeds up iterative tasks and interactive queries.&lt;/li&gt;
&lt;li&gt;Supports batch, streaming, SQL, machine learning, and graph processing in one framework.&lt;/li&gt;
&lt;li&gt;Provides user-friendly APIs in Scala, Java, Python, and R for ease of development.&lt;/li&gt;
&lt;li&gt;Offers high-level abstractions (DataFrames/Datasets) that simplify distributed data handling.&lt;/li&gt;
&lt;li&gt;Strong community support.&lt;/li&gt;
&lt;li&gt;Robust fault tolerance; recovers from failures via lineage and optional checkpointing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Apache Spark Limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High memory usage can lead to increased infrastructure cost and requires careful tuning.&lt;/li&gt;
&lt;li&gt;Lacks a built-in file system and depends on external storage systems like Hadoop Distributed File System (HDFS) or cloud services.&lt;/li&gt;
&lt;li&gt;Micro-batch streaming introduces latency that may not suit true real-time needs.&lt;/li&gt;
&lt;li&gt;Demands manual adjustments and performance tuning for complex jobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Apache Hadoop Advantage and Apache Hadoop Limitations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Apache Hadoop Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Designed for batch processing of massive datasets using cost-effective commodity hardware.&lt;/li&gt;
&lt;li&gt;Uses Hadoop Distributed File System (HDFS) to replicate data, providing robust fault tolerance and resilience.&lt;/li&gt;
&lt;li&gt;Comes with a wide ecosystem (Hive, Pig, HBase, etc.) that extends its capabilities.&lt;/li&gt;
&lt;li&gt;Operates at a lower per-unit cost due to disk-based processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Apache Hadoop Limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disk I/O in Hadoop MapReduce slows performance compared to in-memory solutions.&lt;/li&gt;
&lt;li&gt;Programming with Hadoop MapReduce can be less intuitive for iterative or interactive workloads.&lt;/li&gt;
&lt;li&gt;Not built for low-latency or near-real-time processing without adding extra tools.&lt;/li&gt;
&lt;li&gt;Handling a large number of small files can strain the NameNode and reduce efficiency.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And that’s a wrap! So, when comparing Apache Spark vs Apache Hadoop, it's clear they address different (though related) problems, and they often work better together.&lt;br&gt;
Apache Hadoop, particularly HDFS and YARN, laid the groundwork, offering a way to store and manage resources for truly massive datasets. Its original processing engine, Hadoop MapReduce, was revolutionary for its time but showed its age in terms of speed and flexibility.&lt;br&gt;
Apache Spark emerged as a powerful successor to the Hadoop MapReduce processing component. It delivered speed through in-memory computation and versatility through its unified engine for batch, streaming, SQL, ML, and graph workloads.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The key takeaway? It's rarely a strict "either/or" choice today. More often, the question is how to best combine them or which components to use. You might use:&lt;br&gt;
➤ Spark on YARN with Hadoop Distributed File System (HDFS) (a common on-prem setup).&lt;br&gt;
➤ &lt;a href="https://www.chaosgenius.io/blog/spark-on-kubernetes/" rel="noopener noreferrer"&gt;Spark on Kubernetes&lt;/a&gt; with cloud storage (a common cloud-native setup).&lt;br&gt;
➤ Just Hadoop Distributed File System (HDFS) for cheap, large-scale storage, accessed by various tools.&lt;br&gt;
➤ Just YARN to manage resources for diverse applications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Spark is undeniably the leading engine for large-scale data processing now. Hadoop's components, especially Hadoop Distributed File System (HDFS) and YARN, remain relevant as infrastructure elements, although cloud alternatives and Kubernetes are changing the landscape. Understanding their distinct strengths helps you build the right data platform for your specific challenges.&lt;/p&gt;

&lt;p&gt;In this article, we have covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is Apache Hadoop?
-- What is Apache Hadoop used for?&lt;/li&gt;
&lt;li&gt;What is Apache Spark?
-- What is Apache Spark used for?&lt;/li&gt;
&lt;li&gt;What Is the Difference Between Apache Hadoop and Apache Spark?
-- Apache Spark vs Apache Hadoop—Architecture Breakdown
-- Apache Spark vs Apache Hadoop—Performance &amp;amp; Speed
-- Apache Spark vs Apache Hadoop—Ecosystem Integration
-- Apache Spark vs Apache Hadoop—Memory &amp;amp; Hardware
-- Apache Spark vs Apache Hadoop—Programming Language Support
-- Apache Spark vs Apache Hadoop—Scheduling &amp;amp; Resource Management
-- Apache Spark vs Apache Hadoop—Latency &amp;amp; Real-Time Analytics
-- Apache Spark vs Apache Hadoop—Fault Tolerance
-- Apache Spark vs Apache Hadoop—Security &amp;amp; Data Governance
-- Apache Spark vs Apache Hadoop—ML &amp;amp; Advanced Analytics &lt;/li&gt;
&lt;li&gt;Apache Spark vs Apache Hadoop—Use Cases
-- When to Use Apache Spark
-- When to Use Apache Hadoop&lt;/li&gt;
&lt;li&gt;Apache Spark vs Apache Hadoop — Pros &amp;amp; Cons
… and so much more!!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is Apache Spark used for?&lt;/strong&gt;&lt;br&gt;
Apache Spark is used for fast data processing across various workloads: quick batch jobs, interactive SQL queries, real-time stream analysis, large-scale machine learning, and graph computations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I learn Hadoop or Spark?&lt;/strong&gt;&lt;br&gt;
Spark is usually the better choice for data engineering and science roles. It's flexible and can handle various tasks. However, understanding basic Hadoop concepts like HDFS and YARN is still important. You can ignore Hadoop MapReduce unless you work with older systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Apache Spark run on Hadoop?&lt;/strong&gt;&lt;br&gt;
Yes, very commonly. Spark can run on Apache Hadoop's YARN resource manager and use HDFS for storage. This is a popular deployment model, allowing Spark to leverage existing Apache Hadoop clusters and infrastructure. Spark can also run independently (standalone mode, Kubernetes, Mesos) using other storage systems (like S3).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is Spark faster than Hadoop?&lt;/strong&gt;&lt;br&gt;
The main reason is Spark's ability to perform computations in memory, drastically reducing the slow disk read/write operations that bottleneck Hadoop MapReduce. Spark also uses optimized execution plans (DAGs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Apache Spark used for big data?&lt;/strong&gt;&lt;br&gt;
Absolutely. Apache Spark was specifically designed for big data workloads. Its ability to distribute processing across a cluster and handle large datasets (both in-memory and spilling to disk when necessary) makes it a cornerstone technology for big data analytics, ETL (Extract, Transform, Load), machine learning on large datasets, and real-time data processing.&lt;/p&gt;

&lt;p&gt;Is Apache Spark and Hadoop the same?&lt;br&gt;
Nope, definitely not. Spark is primarily a processing engine, while Hadoop (originally) bundled storage (HDFS) and processing (Hadoop MapReduce) with resource management (YARN). Spark is generally focused on computation speed and flexibility, often leveraging memory. Hadoop MapReduce, its traditional processing counterpart, is more disk-based and batch-oriented.&lt;/p&gt;

&lt;p&gt;Is Spark outdated?&lt;br&gt;
No, Apache Spark is far from outdated. It's actively developed, with new releases bringing performance improvements and features. It has a large, vibrant community and is a core technology in the big data and machine learning landscape, widely used across many industries and integrated into major cloud platforms.&lt;/p&gt;

&lt;p&gt;Is Hadoop Still Used? Is It Outdated?&lt;br&gt;
Let's break it down:&lt;br&gt;
&lt;strong&gt;➥ HDFS &amp;amp; YARN&lt;/strong&gt;: These components of Hadoop are still widely used. Hadoop Distributed File System (HDFS) is a great option for large-scale, cost-effective storage, especially if you're on-premises. That said, cloud object storage like S3 is a strong competitor. Yet Another Resource Negotiator (YARN) remains a popular resource manager in many established clusters.&lt;br&gt;
&lt;strong&gt;➥ Hadoop MapReduce&lt;/strong&gt;: The original Hadoop MapReduce engine isn't the go-to choice for new development anymore. Instead, Spark, Flink, and other engines offer better performance and are more user-friendly for most tasks. However, some organizations still have legacy Hadoop MapReduce jobs running.&lt;br&gt;
&lt;strong&gt;➥ The Ecosystem&lt;/strong&gt;: Many tools that were developed within the Hadoop ecosystem, like Hive, HBase, and Pig, are still in use. They're often used alongside Spark.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Replaced Hadoop (MapReduce)?&lt;/strong&gt;&lt;br&gt;
For the processing part (Hadoop MapReduce), Apache Spark is the most prominent replacement. Other frameworks like Apache Flink (especially for streaming) and query engines like Presto/Trino also serve as alternatives or complementary tools in the big data space. For storage (HDFS), cloud object stores like Amazon S3, Google Cloud Storage, Azure Blob Storage are very popular alternatives, especially in cloud environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Hadoop easy to learn?&lt;/strong&gt;&lt;br&gt;
"Easy" is relative. Hadoop (especially the full ecosystem including Hadoop MapReduce) generally has a steeper learning curve than some newer tools. It involves understanding distributed systems concepts, configuring clusters (though this is often handled by specific platforms or cloud services now), and learning the specifics of Hadoop Distributed File System (HDFS), YARN, and potentially Hadoop MapReduce programming (primarily in Java).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Hadoop a programming language?&lt;/strong&gt;&lt;br&gt;
No, Hadoop is not a programming language. It's a framework written primarily in Java. You typically write applications for Hadoop (like Hadoop MapReduce jobs) using languages like Java, or use tools within the ecosystem (like Hive with SQL-like HQL, Pig with Pig Latin, or Spark with Python, Scala, Java, R, SQL) that interact with Hadoop components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who uses Apache Hadoop?&lt;/strong&gt;&lt;br&gt;
Many tech giants across various sectors (finance, healthcare, tech, retail, government) still use components of the Hadoop ecosystem, particularly Hadoop Distributed File System (HDFS) for storage and YARN for resource management, often in conjunction with Spark or other processing engines for analytics, data warehousing, and handling large batch jobs. While newer cloud-native stacks are popular for new projects, established big data infrastructure often involves Hadoop elements.&lt;/p&gt;

</description>
      <category>apachespark</category>
      <category>apachehadoop</category>
      <category>architecture</category>
      <category>performance</category>
    </item>
    <item>
      <title>[Apache Iceberg] Iceberg Performance: The Hidden Cost of NULLS FIRST</title>
      <dc:creator>Yu-Chuan Hung</dc:creator>
      <pubDate>Sun, 16 Nov 2025 16:47:45 +0000</pubDate>
      <link>https://dev.to/cutechuanchuan/apache-iceberg-iceberg-performance-the-hidden-cost-of-nulls-first-20cl</link>
      <guid>https://dev.to/cutechuanchuan/apache-iceberg-iceberg-performance-the-hidden-cost-of-nulls-first-20cl</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;Apache Iceberg is a widely-used table format in Data Lakehouse architectures. It provides flexibility in how data is written, with two key optimizations: &lt;code&gt;partition&lt;/code&gt;, which splits data into segments, and &lt;code&gt;sort&lt;/code&gt;, which reorders data within those segments. These optimizations can significantly reduce the amount of data scanned by query engines, ultimately boosting query performance.&lt;/p&gt;

&lt;p&gt;When querying data with high-cardinality columns (e.g., IDs or serial numbers), quickly filtering out unnecessary values is crucial. Sorting becomes particularly valuable in these scenarios. The rationale is simple: if data is written in order, query engines can rapidly locate the needed data rather than performing a full table scan and discarding irrelevant rows.&lt;/p&gt;

&lt;p&gt;When configuring Iceberg table sort properties, engineers can specify whether sorting follows ascending or descending order—with ascending as the default. While reading about this configuration, a question came to mind: Is there any performance difference between these two ordering approaches? If so, which one performs better, and why? To answer these questions, I designed an experiment to find out.&lt;/p&gt;

&lt;h1&gt;
  
  
  Experiment
&lt;/h1&gt;

&lt;p&gt;Detailed code and performance analysis can be found in my repo: &lt;a href="https://github.com/CuteChuanChuan/Dive-Into-Iceberg" rel="noopener noreferrer"&gt;https://github.com/CuteChuanChuan/Dive-Into-Iceberg&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Materials
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Generated &lt;strong&gt;1,000,000&lt;/strong&gt; rows with &lt;strong&gt;30% null values&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Created two identically configured Iceberg tables with different null sorting orders (i.e., &lt;strong&gt;NULLS FIRST vs. NULLS LAST&lt;/strong&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Queries Executed to Evaluate Performance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;select count(*) from table where value is not null&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;select sum(value) from table where value is not null&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;select avg(value) from table where value is not null&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;select count(*) from table where value is null&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;select count(*) from table&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance Evaluation Metrics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Query plan&lt;/strong&gt;: Whether different sorting orders generate different execution plans&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Execution time with statistical analysis&lt;/strong&gt;: Overall query time comparison&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CPU profiling&lt;/strong&gt;: Detailed CPU usage analysis&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Findings
&lt;/h1&gt;

&lt;p&gt;To obtain a complete picture, I planned to conduct three types of analysis. First, I compared query plans to see whether different null placements generate different plans, which might influence query performance. Second, I conducted statistical analysis on execution times for rigorous examination. Since query time differences are the observable outcome, we need to identify the root cause if significant differences exist. Therefore, if statistical significance is found, CPU profiling will be conducted in the final phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query Plan
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Details
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;select count(*) from table where value is not null&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="nc"&gt;First&lt;/span&gt;
&lt;span class="nc"&gt;Query&lt;/span&gt; &lt;span class="nf"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NULLS&lt;/span&gt; &lt;span class="nc"&gt;First&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Physical&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
&lt;span class="nc"&gt;AdaptiveSparkPlan&lt;/span&gt; &lt;span class="n"&gt;isFinalPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
   &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Exchange&lt;/span&gt; &lt;span class="nc"&gt;SinglePartition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENSURE_REQUIREMENTS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1557&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;partial_count&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
         &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt;
            &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="nf"&gt;isnotnull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;508&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
               &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;BatchScan&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_first&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;508&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_first&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="no"&gt;IS&lt;/span&gt; &lt;span class="no"&gt;NOT&lt;/span&gt; &lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupedBy&lt;/span&gt;&lt;span class="o"&gt;=]&lt;/span&gt; &lt;span class="nl"&gt;RuntimeFilters:&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="nc"&gt;Last&lt;/span&gt;
&lt;span class="nc"&gt;Query&lt;/span&gt; &lt;span class="nf"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NULLS&lt;/span&gt; &lt;span class="nc"&gt;Last&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Physical&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
&lt;span class="nc"&gt;AdaptiveSparkPlan&lt;/span&gt; &lt;span class="n"&gt;isFinalPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
   &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Exchange&lt;/span&gt; &lt;span class="nc"&gt;SinglePartition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENSURE_REQUIREMENTS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1574&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;partial_count&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
         &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt;
            &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="nf"&gt;isnotnull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;521&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
               &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;BatchScan&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_last&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;521&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_last&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="no"&gt;IS&lt;/span&gt; &lt;span class="no"&gt;NOT&lt;/span&gt; &lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupedBy&lt;/span&gt;&lt;span class="o"&gt;=]&lt;/span&gt; &lt;span class="nl"&gt;RuntimeFilters:&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;select sum(value) from table where value is not null&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="nc"&gt;First&lt;/span&gt;
&lt;span class="nc"&gt;Query&lt;/span&gt; &lt;span class="nf"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NULLS&lt;/span&gt; &lt;span class="nc"&gt;First&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Physical&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
&lt;span class="nc"&gt;AdaptiveSparkPlan&lt;/span&gt; &lt;span class="n"&gt;isFinalPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;886&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
   &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Exchange&lt;/span&gt; &lt;span class="nc"&gt;SinglePartition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENSURE_REQUIREMENTS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3045&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;partial_sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;886&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
         &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="nf"&gt;isnotnull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;886&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;BatchScan&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_first&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;886&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_first&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="no"&gt;IS&lt;/span&gt; &lt;span class="no"&gt;NOT&lt;/span&gt; &lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupedBy&lt;/span&gt;&lt;span class="o"&gt;=]&lt;/span&gt; &lt;span class="nl"&gt;RuntimeFilters:&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="nc"&gt;Last&lt;/span&gt;
&lt;span class="nc"&gt;Query&lt;/span&gt; &lt;span class="nf"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NULLS&lt;/span&gt; &lt;span class="nc"&gt;Last&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Physical&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
&lt;span class="nc"&gt;AdaptiveSparkPlan&lt;/span&gt; &lt;span class="n"&gt;isFinalPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;899&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
   &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Exchange&lt;/span&gt; &lt;span class="nc"&gt;SinglePartition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENSURE_REQUIREMENTS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3064&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;partial_sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;899&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
         &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="nf"&gt;isnotnull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;899&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;BatchScan&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_last&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;899&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_last&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="no"&gt;IS&lt;/span&gt; &lt;span class="no"&gt;NOT&lt;/span&gt; &lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupedBy&lt;/span&gt;&lt;span class="o"&gt;=]&lt;/span&gt; &lt;span class="nl"&gt;RuntimeFilters:&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;select avg(value) from table where value is not null&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="nc"&gt;First&lt;/span&gt;
&lt;span class="nc"&gt;Query&lt;/span&gt; &lt;span class="nf"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NULLS&lt;/span&gt; &lt;span class="nc"&gt;First&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Physical&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
&lt;span class="nc"&gt;AdaptiveSparkPlan&lt;/span&gt; &lt;span class="n"&gt;isFinalPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1264&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
   &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Exchange&lt;/span&gt; &lt;span class="nc"&gt;SinglePartition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENSURE_REQUIREMENTS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4535&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;partial_avg&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1264&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
         &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="nf"&gt;isnotnull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1264&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;BatchScan&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_first&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1264&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_first&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="no"&gt;IS&lt;/span&gt; &lt;span class="no"&gt;NOT&lt;/span&gt; &lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupedBy&lt;/span&gt;&lt;span class="o"&gt;=]&lt;/span&gt; &lt;span class="nl"&gt;RuntimeFilters:&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="nc"&gt;Last&lt;/span&gt;
&lt;span class="nc"&gt;Query&lt;/span&gt; &lt;span class="nf"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NULLS&lt;/span&gt; &lt;span class="nc"&gt;Last&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Physical&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
&lt;span class="nc"&gt;AdaptiveSparkPlan&lt;/span&gt; &lt;span class="n"&gt;isFinalPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1279&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
   &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Exchange&lt;/span&gt; &lt;span class="nc"&gt;SinglePartition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENSURE_REQUIREMENTS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4554&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;partial_avg&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1279&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
         &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="nf"&gt;isnotnull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1279&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;BatchScan&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_last&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1279&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_last&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="no"&gt;IS&lt;/span&gt; &lt;span class="no"&gt;NOT&lt;/span&gt; &lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupedBy&lt;/span&gt;&lt;span class="o"&gt;=]&lt;/span&gt; &lt;span class="nl"&gt;RuntimeFilters:&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;select count(*) from table where value is null&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="nc"&gt;First&lt;/span&gt;
&lt;span class="nc"&gt;Query&lt;/span&gt; &lt;span class="nf"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NULLS&lt;/span&gt; &lt;span class="nc"&gt;First&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Physical&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
&lt;span class="nc"&gt;AdaptiveSparkPlan&lt;/span&gt; &lt;span class="n"&gt;isFinalPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
   &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Exchange&lt;/span&gt; &lt;span class="nc"&gt;SinglePartition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENSURE_REQUIREMENTS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6023&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;partial_count&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
         &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt;
            &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="nf"&gt;isnull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1646&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
               &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;BatchScan&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_first&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1646&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_first&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="no"&gt;IS&lt;/span&gt; &lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupedBy&lt;/span&gt;&lt;span class="o"&gt;=]&lt;/span&gt; &lt;span class="nl"&gt;RuntimeFilters:&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="nc"&gt;Last&lt;/span&gt;
&lt;span class="nc"&gt;Query&lt;/span&gt; &lt;span class="nf"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NULLS&lt;/span&gt; &lt;span class="nc"&gt;Last&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Physical&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
&lt;span class="nc"&gt;AdaptiveSparkPlan&lt;/span&gt; &lt;span class="n"&gt;isFinalPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
   &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Exchange&lt;/span&gt; &lt;span class="nc"&gt;SinglePartition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENSURE_REQUIREMENTS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6040&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;partial_count&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
         &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt;
            &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="nf"&gt;isnull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1659&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
               &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;BatchScan&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_last&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1659&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test_nulls_last&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="no"&gt;IS&lt;/span&gt; &lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupedBy&lt;/span&gt;&lt;span class="o"&gt;=]&lt;/span&gt; &lt;span class="nl"&gt;RuntimeFilters:&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;select count(*) from table&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="nc"&gt;First&lt;/span&gt;
&lt;span class="nc"&gt;Query&lt;/span&gt; &lt;span class="nf"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NULLS&lt;/span&gt; &lt;span class="nc"&gt;First&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Physical&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
&lt;span class="nc"&gt;AdaptiveSparkPlan&lt;/span&gt; &lt;span class="n"&gt;isFinalPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agg_func_0&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1895L&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
   &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Exchange&lt;/span&gt; &lt;span class="nc"&gt;SinglePartition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENSURE_REQUIREMENTS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7045&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;partial_sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agg_func_0&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1895L&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
         &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;(*)&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1896L&lt;/span&gt; &lt;span class="no"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;agg_func_0&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1895L&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
            &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;LocalTableScan&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;(*)&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1896L&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;Null&lt;/span&gt; &lt;span class="nc"&gt;Last&lt;/span&gt;
&lt;span class="nc"&gt;Query&lt;/span&gt; &lt;span class="nf"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NULLS&lt;/span&gt; &lt;span class="nc"&gt;Last&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Physical&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
&lt;span class="nc"&gt;AdaptiveSparkPlan&lt;/span&gt; &lt;span class="n"&gt;isFinalPlan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agg_func_0&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1904L&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
   &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Exchange&lt;/span&gt; &lt;span class="nc"&gt;SinglePartition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENSURE_REQUIREMENTS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7060&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;HashAggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=[],&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="n"&gt;partial_sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agg_func_0&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1904L&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
         &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;(*)&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1905L&lt;/span&gt; &lt;span class="no"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;agg_func_0&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1904L&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
            &lt;span class="o"&gt;+-&lt;/span&gt; &lt;span class="nc"&gt;LocalTableScan&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;(*)&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1905L&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;For both tables, the execution plans for all queries are identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  File-Level Statistics Analysis
&lt;/h2&gt;

&lt;p&gt;Although the query plans are the same, a deeper look at the Parquet file statistics reveals important differences in how data is physically organized.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partition Statistics Comparison
&lt;/h3&gt;

&lt;p&gt;Below are the min/max statistics for each partition in both configurations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Partition&lt;/th&gt;
&lt;th&gt;NULLS FIRST&lt;/th&gt;
&lt;th&gt;NULLS LAST&lt;/th&gt;
&lt;th&gt;Min Value Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cat_0-2&lt;/td&gt;
&lt;td&gt;All nulls&lt;/td&gt;
&lt;td&gt;All nulls&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cat_3&lt;/td&gt;
&lt;td&gt;min=103, max=993&lt;/td&gt;
&lt;td&gt;min=103, max=993&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cat_4&lt;/td&gt;
&lt;td&gt;min=4, max=994&lt;/td&gt;
&lt;td&gt;min=4, max=994&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cat_5&lt;/td&gt;
&lt;td&gt;min=405, max=995&lt;/td&gt;
&lt;td&gt;min=355, max=995&lt;/td&gt;
&lt;td&gt;-50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cat_6&lt;/td&gt;
&lt;td&gt;min=106, max=996&lt;/td&gt;
&lt;td&gt;min=6, max=996&lt;/td&gt;
&lt;td&gt;-100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cat_7&lt;/td&gt;
&lt;td&gt;min=517, max=997&lt;/td&gt;
&lt;td&gt;min=487, max=997&lt;/td&gt;
&lt;td&gt;-30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cat_8&lt;/td&gt;
&lt;td&gt;min=228, max=998&lt;/td&gt;
&lt;td&gt;min=208, max=998&lt;/td&gt;
&lt;td&gt;-20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cat_9&lt;/td&gt;
&lt;td&gt;min=619, max=999&lt;/td&gt;
&lt;td&gt;min=609, max=999&lt;/td&gt;
&lt;td&gt;-10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why Are Statistics Different?
&lt;/h3&gt;

&lt;p&gt;The different min/max values reveal that &lt;strong&gt;physical data layout differs&lt;/strong&gt; between the two configurations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Different File Boundaries&lt;/strong&gt;: When sorting with NULLS FIRST vs. NULLS LAST, Spark writes data in different orders, causing file splits to occur at different points. Even though both tables contain identical data, the way rows are distributed across files differs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File Organization Pattern&lt;/strong&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NULLS FIRST&lt;/strong&gt;: Files begin with null values, followed by non-null values. The minimum non-null value appears after skipping nulls within each file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NULLS LAST&lt;/strong&gt;: Files begin with non-null values immediately. The minimum value is at or near the start of the file.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Metadata Quality&lt;/strong&gt;: NULLS LAST produces "better" statistics for non-null queries:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In &lt;strong&gt;NULLS FIRST&lt;/strong&gt; (e.g., cat_6): &lt;code&gt;min=106&lt;/code&gt; means the file starts with nulls, and 106 is the first non-null value encountered.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In &lt;strong&gt;NULLS LAST&lt;/strong&gt; (e.g., cat_6): &lt;code&gt;min=6&lt;/code&gt; means the file immediately starts with value 6, providing more accurate bounds.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Impact on Query Execution
&lt;/h3&gt;

&lt;p&gt;For queries with &lt;code&gt;WHERE value IS NOT NULL&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NULLS FIRST&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Files contain nulls at the beginning, causing mixed value distribution&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Query engine must scan through null values before reaching non-null data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Statistics indicate the presence of non-null values, but they're not immediately accessible&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;NULLS LAST&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Files with non-null data have those values at the beginning&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Query engine can immediately start processing valid values&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Better sequential access pattern for counting non-null values&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This file-level organization difference, combined with CPU microarchitecture optimizations, explains why NULLS LAST performs better for counting non-null values even though logical query plans are identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Execution Time Analysis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Data Collection
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;5 queries, each executed 100 times&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Statistical Methods
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;T-test&lt;/strong&gt;: Compare whether query times are statistically different&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cohen's d&lt;/strong&gt;: Calculate the effect size of null ordering settings&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Details
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;select count(*) from table where value is not null&lt;/code&gt;: Null Last performs better
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Descriptive&lt;/span&gt; &lt;span class="nx"&gt;Statistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;NULLS&lt;/span&gt; &lt;span class="nx"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;41.46&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;8.38&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;
  &lt;span class="nx"&gt;NULLS&lt;/span&gt; &lt;span class="nx"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;31.55&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.40&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;

&lt;span class="nx"&gt;Paired&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;statistic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;11.9367&lt;/span&gt; 
  &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.000000&lt;/span&gt; 
  &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;11.55&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;
  &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;***&lt;/span&gt; &lt;span class="nx"&gt;HIGHLY&lt;/span&gt; &lt;span class="nc"&gt;SIGNIFICANT &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="nc"&gt;Size &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Cohen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s d):
  d = 1.1937 
  Interpretation: Large 

Summary:
  Mean difference: 9.91 ms
  Percentage difference: 23.90 %
  Winner: NULLS LAST
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;select sum(value) from table where value is not null&lt;/code&gt;: Not significantly different
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Descriptive&lt;/span&gt; &lt;span class="nx"&gt;Statistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;NULLS&lt;/span&gt; &lt;span class="nx"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;34.14&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.12&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;
  &lt;span class="nx"&gt;NULLS&lt;/span&gt; &lt;span class="nx"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;33.40&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;6.43&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;

&lt;span class="nx"&gt;Paired&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;statistic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8759&lt;/span&gt; 
  &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.383195&lt;/span&gt; 
  &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.43&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;
  &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NOT&lt;/span&gt; &lt;span class="nc"&gt;SIGNIFICANT &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;select avg(value) from table where value is not null&lt;/code&gt;: Not significantly different
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Descriptive&lt;/span&gt; &lt;span class="nx"&gt;Statistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;NULLS&lt;/span&gt; &lt;span class="nx"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;28.84&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.42&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;
  &lt;span class="nx"&gt;NULLS&lt;/span&gt; &lt;span class="nx"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;27.95&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.26&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;

&lt;span class="nx"&gt;Paired&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;statistic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.9654&lt;/span&gt; 
  &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.052165&lt;/span&gt; 
  &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.80&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;
  &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NOT&lt;/span&gt; &lt;span class="nc"&gt;SIGNIFICANT &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;select count(*) from table where value is null&lt;/code&gt;: Not significantly different
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Descriptive&lt;/span&gt; &lt;span class="nx"&gt;Statistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;NULLS&lt;/span&gt; &lt;span class="nx"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;24.00&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.64&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;
  &lt;span class="nx"&gt;NULLS&lt;/span&gt; &lt;span class="nx"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;23.16&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.43&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;

&lt;span class="nx"&gt;Paired&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;statistic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.3804&lt;/span&gt; 
  &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.170582&lt;/span&gt; 
  &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.37&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.05&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;
  &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NOT&lt;/span&gt; &lt;span class="nc"&gt;SIGNIFICANT &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;select count(*) from table&lt;/code&gt;: Not significantly different
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Descriptive&lt;/span&gt; &lt;span class="nx"&gt;Statistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;NULLS&lt;/span&gt; &lt;span class="nx"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;14.95&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.41&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;
  &lt;span class="nx"&gt;NULLS&lt;/span&gt; &lt;span class="nx"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;14.39&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.45&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;

&lt;span class="nx"&gt;Paired&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;statistic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.6356&lt;/span&gt; 
  &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.105090&lt;/span&gt; 
  &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;
  &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NOT&lt;/span&gt; &lt;span class="nc"&gt;SIGNIFICANT &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;NULLS LAST is significantly faster than NULLS FIRST when counting non-null values.&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU Profiling: Analyzing Count Non-Null Values Query
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Details
&lt;/h3&gt;

&lt;p&gt;Please refer to the flame graphs in my repo.&lt;/p&gt;

&lt;p&gt;The performance difference observed in execution time analysis can be attributed to both &lt;strong&gt;file-level organization&lt;/strong&gt; and &lt;strong&gt;CPU microarchitecture optimizations&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File-Level Organization Impact:&lt;/strong&gt; As shown in the file statistics analysis, NULLS LAST creates files where non-null values are positioned at the beginning. This layout means when the query engine scans data with &lt;code&gt;WHERE value IS NOT NULL&lt;/code&gt;, it immediately encounters a continuous block of valid values rather than having to skip over nulls first. This reduces unnecessary I/O operations and deserialization overhead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CPU Microarchitecture Optimizations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;SIMD (Single Instruction, Multiple Data):&lt;/em&gt;
Modern CPUs can process multiple data elements simultaneously using SIMD instructions. When counting non-null values with NULLS LAST, the query engine encounters a continuous block of non-null values at the start of each file. This layout allows SIMD instructions to efficiently process multiple valid values in parallel. For example, when checking &lt;code&gt;isnotnull(value)&lt;/code&gt; on 8 consecutive values that are all non-null, a single SIMD instruction can validate and count them in one operation.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Branch Prediction:&lt;/em&gt;
Modern CPUs use branch predictors to anticipate the outcome of conditional statements (like &lt;code&gt;if (value != null)&lt;/code&gt;). With NULLS LAST, the query engine scans data following a highly predictable pattern: a long sequence of non-null values followed by nulls. This consistency allows the branch predictor to achieve high accuracy, keeping the CPU pipeline running smoothly. In contrast, NULLS FIRST presents a less predictable pattern at file boundaries where nulls transition to non-nulls, potentially causing pipeline stalls.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;The CPU profiling data supports these optimizations: NULLS LAST (2,238 samples) uses approximately 11.7% less CPU time than NULLS FIRST (2,536 samples). This reduction results from the combined effects of better file organization, improved SIMD vectorization, and enhanced branch prediction accuracy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;NULLS LAST occupies less CPU time due to a combination of better file-level data organization and CPU microarchitecture optimizations.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion and Future Exploration
&lt;/h1&gt;

&lt;p&gt;This exploration reveals that while different null value placements do not create different query plans, they significantly impact query performance through &lt;strong&gt;physical data organization&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Findings:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File-Level Statistics Matter&lt;/strong&gt;: NULLS LAST produces better min/max statistics, with non-null values positioned at file beginnings. This creates more favorable data layouts for queries filtering on non-null values.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CPU Microarchitecture Synergy&lt;/strong&gt;: The continuous blocks of non-null values in NULLS LAST enable CPU optimizations including SIMD vectorization and improved branch prediction, resulting in ~11.7% less CPU time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Significant Performance Impact&lt;/strong&gt;: For &lt;code&gt;SELECT COUNT(*) WHERE value IS NOT NULL&lt;/code&gt;, NULLS LAST achieves 23.90% faster execution time—a substantial improvement for such a common OLAP operation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Practical Recommendations:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If counting non-null values is a frequent operation in your workload—which is common in OLAP scenarios—configuring Iceberg tables with NULLS LAST can provide measurable performance improvements. The benefits stem from both better file organization and CPU-level optimizations working in tandem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Future Exploration:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This experiment tested 5 queries on a 1-million-row dataset with 30% null values. Future investigations could explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Various query patterns frequently used in OLAP scenarios (e.g., window functions like LAG, complex aggregations)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Larger datasets with multiple files per partition to amplify metadata pruning effects&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Different null percentage distributions (10%, 50%, 70%) to understand the threshold where NULLS LAST benefits diminish&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Impact on different data types (strings, decimals) and column cardinalities&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Performance with Iceberg's metadata-based filtering in more complex predicates&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These investigations would provide a more complete understanding of optimal Iceberg table sorting configurations across diverse workloads.&lt;/p&gt;

</description>
      <category>apacheiceberg</category>
      <category>apachespark</category>
      <category>opensource</category>
      <category>learning</category>
    </item>
    <item>
      <title>HOW TO: Run Spark on Kubernetes with AWS EMR on EKS (2025)</title>
      <dc:creator>Pramit Marattha</dc:creator>
      <pubDate>Sat, 15 Nov 2025 11:00:51 +0000</pubDate>
      <link>https://dev.to/chaos-genius/how-to-run-spark-on-kubernetes-with-aws-emr-on-eks-2025-28jo</link>
      <guid>https://dev.to/chaos-genius/how-to-run-spark-on-kubernetes-with-aws-emr-on-eks-2025-28jo</guid>
      <description>&lt;p&gt;Running &lt;a href="https://www.chaosgenius.io/blog/apache-spark-architecture/#how-did-apache-spark-originate" rel="noopener noreferrer"&gt;Apache Spark&lt;/a&gt; on &lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt; with AWS EMR on EKS brings big benefits – you get the best of both worlds. &lt;a href="https://www.chaosgenius.io/blog/aws-emr-architecture/" rel="noopener noreferrer"&gt;AWS EMR&lt;/a&gt;'s optimized Spark runtime and &lt;a href="https://aws.amazon.com/eks/" rel="noopener noreferrer"&gt;AWS EKS&lt;/a&gt;'s container orchestration come together in one managed platform. Sure, you could run Spark on Kubernetes yourself, but it's a lot of manual work. You'd need to create a custom container image, set up networking, and handle a bunch of other configurations. But with EMR on EKS, all that hassle goes away. With EMR on EKS, AWS supplies the Spark runtime as a ready-to-use container image, handles job orchestration, and ties it all into EKS. Just submit your Spark job to an EMR virtual cluster (which maps to an EKS namespace), and it runs as a Kubernetes pod under EMR’s control. You still handle some IAM and networking setup, but the heavy lifting like runtime tuning, job scheduling, container builds, is all handled for you.&lt;/p&gt;

&lt;p&gt;In this article, we will first explain why EMR on EKS is useful, show how its architecture works, compare EMR on EC2 vs EMR on EKS. Finally, we will give you a step-by-step recipe (with actual AWS CLI commands and config samples) to get a Spark job running on Kubernetes via EMR on EKS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use AWS EMR on EKS for Spark Workloads?
&lt;/h2&gt;

&lt;p&gt;First, why use AWS EMR on EKS at all? What do you gain by running Spark on Kubernetes under EMR instead of the familiar EMR on EC2 or even self-managed Spark on EKS? The short answer is flexibility and ease of management. EMR on EKS offers the best of both worlds: managed Spark plus Kubernetes. It avoids the hassle of building Spark containers and managing Spark clusters by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the benefits of EMR on EKS?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AWS EMR on EKS model offers several advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit 1: Simplified Spark Runtime Management&lt;/strong&gt;&lt;br&gt;
You get the same managed Spark experience that EMR on EC2 provides, but on Kubernetes. EMR takes care of provisioning the Spark runtime (with pre-built, optimized Spark versions), auto-scaling, and provides development tools like EMR Studio and the Spark UI. AWS handles the Spark container images and integration so you don’t have to assemble them yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit 2: Cost Optimization via Kubernetes Resource Sharing&lt;/strong&gt;&lt;br&gt;
Your Spark jobs run as pods on an EKS cluster that can also host other workloads, so you avoid waste from idle clusters. Nodes come up and down automatically, and you pay only for actual usage. AWS specifically points out that with EMR on EKS “compute resources can be shared” and removed “on demand to eliminate over-provisioning”, leading to lower costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit 3: Fast Job Startup and Performance Improvements&lt;/strong&gt;&lt;br&gt;
You can reuse an existing Kubernetes node pool, so there’s no need to spin up a fresh cluster for each job. This eliminates the startup lag of launching EC2 instances. In fact, AWS claims EMR’s optimized Spark runtime can run some workloads up to 3× faster than default Spark on Kubernetes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit 4: Flexible Spark and EMR Version Management&lt;/strong&gt;&lt;br&gt;
You can run different Spark/EMR versions side by side on the same cluster. EMR on EKS lets one EKS namespace host Spark 2.4 apps and another host Spark 3.0. &lt;a href="https://aws.amazon.com/emr/features/eks/" rel="noopener noreferrer"&gt;According to AWS&lt;/a&gt;, you can use a single EKS cluster to run applications that require different Apache Spark versions and configurations. This is handy if some jobs need legacy code while others take advantage of newer Spark features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit 5: Native Integration with Kubernetes and AWS Tools&lt;/strong&gt;&lt;br&gt;
EMR on EKS ties into Kubernetes APIs and IAM Roles for Service Accounts (IRSA). You can use your existing EKS authentication methods, networking, logging, and autoscaler to manage Spark pods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit 6: EMR Cloud-Native Experience on Kubernetes&lt;/strong&gt;&lt;br&gt;
Finally, you still get EMR conveniences like EMRFS (optimized S3 access), default security and logging settings, and support for EMR Studio or Step Functions. AWS even provides AWS Step Functions and EMR on EKS templates to streamline workflows.&lt;/p&gt;

&lt;p&gt;All in all, EMR on EKS is great if you already have (or plan to use) Kubernetes for container workloads and want the managed Spark experience. It avoids the manual work of installing Spark on Kubernetes (which you’d have to do if you ran open-source Spark on EKS).&lt;/p&gt;
&lt;h2&gt;
  
  
  EMR on EKS System Architecture Explained
&lt;/h2&gt;

&lt;p&gt;At a very high level, EMR on EKS loosely couples Spark to Kubernetes. EMR (the control plane) simply tells EKS what pods to run, and EKS handles the actual compute (EC2 / Fargate). Here’s how it works under the hood:&lt;/p&gt;

&lt;p&gt;The EMR on EKS architecture is a multi-layer pipeline. At the top level you have AWS EMR, which now has a “virtual cluster” registered to a namespace in your AWS EKS cluster. When you submit a Spark job through EMR (for example, using aws emr-containers start-job-run), EMR takes your job parameters and tells Kubernetes what to run. Under the hood, EMR creates one or more Kubernetes pods for the Spark driver and executors. Each pod pulls a container image provided by EMR (Amazon Linux 2 with Spark installed) and begins processing.&lt;/p&gt;

&lt;p&gt;The Kubernetes layer (AWS EKS) is responsible for scheduling these pods onto available compute. It can use either self-managed EC2 nodes or Fargate to supply the necessary CPU and memory. In practice, you often configure an EC2 Auto Scaling Group behind EKS so that new nodes spin up as Spark executors need them. The architecture supports multi-AZ deployments: pods can run on nodes in different availability zones, giving resilience and access to a larger pool of instances.&lt;/p&gt;

&lt;p&gt;Below the compute layer, your data lives in services like AWS S3, and your logs/metrics flow to CloudWatch (or another sink). EMR on EKS handles the wiring: it automatically ships driver and executor logs to CloudWatch Logs and S3 if you configure it, and even lets you view the Spark History UI from the EMR console after a job completes. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: EMR on EKS decouples analytics from infrastructure: EMR builds the Spark application environment and Kubernetes provides the execution environment.EMR on EKS Architecture (Source)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0ku5hmiwf1jnuhc3rwh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0ku5hmiwf1jnuhc3rwh.png" alt="EMR on EKS Architecture" width="496" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;EMR on EKS “loosely couples” Spark to your Kubernetes cluster. When you run a job, EMR uses your job definition (entry point, arguments, configs) to tell EKS exactly what pods to run. Kubernetes does the pod scheduling onto EC2/Fargate nodes. Because it’s loose, you can run multiple isolated Spark workloads on the same cluster (even in different namespaces) and mix them with other container apps.&lt;/p&gt;
&lt;h2&gt;
  
  
  EMR on EC2 vs EMR on EKS: Detailed Comparison
&lt;/h2&gt;

&lt;p&gt;It’s worth understanding the difference between the old-school &lt;a href="https://www.chaosgenius.io/blog/create-emr-cluster/#step-by-step-guide-creating-an-aws-emr-cluster-in-10-minutes" rel="noopener noreferrer"&gt;EMR on EC2&lt;/a&gt; vs EMR on EKS, so you know when to pick each. With EMR on EC2, Amazon launches a dedicated Spark cluster for you on EC2 instances (possibly with EC2 Spot for cost savings). Those instances are dedicated to EMR, and YARN or another scheduler allocates resources. You have full control of the cluster’s Hadoop/Spark config and node sizes, but the resources are siloed. In contrast, with EMR on EKS, you reuse your shared Kubernetes cluster. EMR on EKS simply runs Spark on that cluster’s nodes (alongside other apps).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;tr&gt;
    &lt;td&gt;&lt;b&gt;EMR on EC2&lt;/b&gt;&lt;/td&gt;
    &lt;td&gt;🔮&lt;/td&gt;
    &lt;td&gt;&lt;b&gt;EMR on EKS&lt;/b&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Dedicated EC2 instances&lt;/td&gt;
    &lt;td&gt;Resource Allocation&lt;/td&gt;
    &lt;td&gt;Shared Kubernetes cluster&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;YARN-based scheduling&lt;/td&gt;
    &lt;td&gt;Orchestration&lt;/td&gt;
    &lt;td&gt;Kubernetes-native scheduling&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Pay for dedicated instances&lt;/td&gt;
    &lt;td&gt;Cost Model&lt;/td&gt;
    &lt;td&gt;Pay only for actual resource usage&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Limited to single EMR version per cluster&lt;/td&gt;
    &lt;td&gt;Multi-tenancy&lt;/td&gt;
    &lt;td&gt;Multiple versions and configurations&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Slower due to EC2 instance provisioning&lt;/td&gt;
    &lt;td&gt;Startup Time&lt;/td&gt;
    &lt;td&gt;Faster using existing node pools&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Native Hadoop ecosystem support&lt;/td&gt;
    &lt;td&gt;Integration&lt;/td&gt;
    &lt;td&gt;Cloud-native Kubernetes ecosystem&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;EMR managed scaling&lt;/td&gt;
    &lt;td&gt;Scaling&lt;/td&gt;
    &lt;td&gt;Kubernetes autoscaling + Karpenter/Fargate&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;🔮 &lt;strong&gt;Use EMR on EC2 when&lt;/strong&gt; you want a standalone cluster per workload. If you have a stable, heavy Spark job schedule and don’t already have Kubernetes in the picture, EMR on EC2 can be straightforward. It’s the classic way to run Hadoop/Spark and it integrates with HDFS/other Hadoop ecosystem tools out of the box. EMR on EC2 might also make sense if you need features currently only in EMR’s YARN-based mode, or if containerization is not a requirement.&lt;/p&gt;

&lt;p&gt;🔮 &lt;strong&gt;Use EMR on EKS when&lt;/strong&gt; you have a Kubernetes environment (or plan to) and want to colocate Spark with other container workloads. It’s great for multi-tenancy and agility – one EKS cluster can host multiple Spark applications (even with different EMR versions) and also run other services (like Airflow, machine learning apps, etc.). If you’re already managing infrastructure with EKS and Helm or Terraform, adding Spark workloads there avoids siloing. EMR on EKS also handles the complex AWS integration (EMRFS, S3, IAM) for you, whereas manually running Spark on vanilla Kubernetes would require gluing together a lot of pieces.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step-By-Step Guide to Run Spark on Kubernetes with AWS EMR on EKS
&lt;/h2&gt;

&lt;p&gt;Now we get hands-on. We’ll walk through all the setup steps, including code snippets and YAML where appropriate. You can run these commands in any region (just add the &lt;code&gt;--region&lt;/code&gt; or ARNs/URIs as needed).&lt;/p&gt;
&lt;h3&gt;
  
  
  Prerequisite:
&lt;/h3&gt;

&lt;p&gt;First things first, make sure you have the following things configured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/console/" rel="noopener noreferrer"&gt;AWS Management Console&lt;/a&gt; access with appropriate permissions&lt;/li&gt;
&lt;li&gt;Basic &lt;strong&gt;understanding of EMR cluster architecture and Spark&lt;/strong&gt; fundamentals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Familiarity with the AWS Management Console&lt;/strong&gt; navigation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/install-awscli.html" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt; configured with appropriate credentials and permissions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/tasks/tools/#kubectl" rel="noopener noreferrer"&gt;kubectl&lt;/a&gt; (Kubernetes CLI) installed and configured&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://eksctl.io/installation/" rel="noopener noreferrer"&gt;eksctl&lt;/a&gt; (EKS cluster CLI) installed and configured&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/tutorials/kubernetes-basics/" rel="noopener noreferrer"&gt;Basic understanding of Kubernetes concepts&lt;/a&gt; (pods, namespaces, services)&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;existing VPC with appropriate subnets&lt;/strong&gt; or permission to create new networking resources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understanding of IAM roles and policies&lt;/strong&gt; for service integration&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 1—AWS Console Access and CLI Setup for EMR and EKS
&lt;/h3&gt;

&lt;p&gt;Log in to the AWS Console or make sure your &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/install-awscli.html" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt; is authenticated. If using the CLI, you should have a profile set up (using aws configure or environment variables) with credentials. You can test by running something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this returns your account and user/role info, you’re ready. No specific AWS region is required for EMR on EKS itself, but keep in mind you’ll launch resources (like EKS nodes) in some region or AZs when prompted.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Many AWS CLI commands require specifying a region or having a default region configured (&lt;code&gt;~/.aws/config&lt;/code&gt;). Pick one (&lt;code&gt;us-west-2&lt;/code&gt;) and use it consistently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 2—Creating an AWS EKS Kubernetes Cluster
&lt;/h3&gt;

&lt;p&gt;Now create an EKS cluster that Spark will run on. You can use eksctl for a simple setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eksctl create cluster \
  --name my-emr-on-eks-cluster \
  --nodes 3 \
  --nodes-min 1 \
  --nodes-max 4 \
  --managed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, this command (in your default region) will create a new EKS cluster named my-emr-on-eks-cluster with 3 managed Linux node group instances (by default m5.large, but you can specify --node-type if you need something different). It also enables a node autoscaler (min 1, max 4).&lt;/p&gt;

&lt;p&gt;Once it completes, eksctl updates your &lt;code&gt;~/.kube/config&lt;/code&gt; so that kubectl knows about this cluster. You can verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get nodes -o wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see 3 (or up to 4 as they scale) EC2 instances ready. To view the workloads running on your cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get pods -A -o wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: In production, you might want to create nodegroups in multiple AZs, use Spot instances, a wider node type mix, etc. This example uses a simple default setup for clarity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 3—Setting Up Kubernetes Namespace and EMR Access
&lt;/h3&gt;

&lt;p&gt;We’ll dedicate a Kubernetes namespace for EMR Spark jobs. A “namespace” in Kubernetes isolates resources. Let’s make one (called spark for example):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl create namespace spark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we must let EMR’s service account access this namespace. AWS provides the eksctl create iamidentitymapping command to link EMR’s service-linked role to the namespace. Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eksctl create iamidentitymapping \
  --cluster my-emr-eks-cluster \
  --namespace spark \
  --service-name emr-containers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command creates the necessary Kubernetes RBAC (Role &amp;amp; RoleBinding) and updates the aws-auth ConfigMap so that the &lt;code&gt;AWSServiceRoleForAmazonEMRContainers&lt;/code&gt; role is mapped to the user emr-containers in the spark namespace. In other words, it gives EMR on EKS permission to create pods, services, etc. in &lt;strong&gt;spark&lt;/strong&gt;. (If this fails, ensure you’re using a recent eksctl version and that your AWS credentials can modify the cluster’s IAM config).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4—Create a Virtual Cluster for EMR (Register EKS Cluster with EMR)
&lt;/h3&gt;

&lt;p&gt;Now register this namespace as an EMR virtual cluster. A virtual cluster in EMR on EKS terms is just the glue that tells EMR “use this EKS cluster and namespace for job runs”. It does not create new nodes; it just links to the existing cluster. &lt;/p&gt;

&lt;p&gt;Use the AWS CLI emr-containers command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws emr-containers create-virtual-cluster \
    --name spark-vc \
    --container-provider '{
         "type": "EKS",
         "id": "my-emr-eks-cluster",
         "info": {"eksInfo": {"namespace": "spark"}}
    }'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;my-emr-eks-cluster&lt;/code&gt; with your cluster name (as above). You’ll get back a JSON with a virtualClusterId (it looks like &lt;code&gt;vc-xxxxxxxx&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;After running, you can verify the virtual cluster with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws emr-containers list-virtual-clusters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And note the ID for the one named spark-vc. We’ll use that in the next step. (The virtual cluster itself doesn’t create any servers; it just links EMR to the namespace).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5—Registering EKS Cluster as EMR Virtual Cluster
&lt;/h3&gt;

&lt;p&gt;Spark jobs running on EMR on EKS need an AWS IAM role to access AWS resources (for example, S3 buckets). This is called the job execution role. We create an AWS IAM role that EMR can assume, and attach a policy for S3 and CloudWatch logs.&lt;/p&gt;

&lt;h4&gt;
  
  
  5a—Define and Create the IAM Role (EMR Job Execution Role)
&lt;/h4&gt;

&lt;p&gt;We’ll create a role that trusts EMR. One way is to trust the elasticmapreduce.amazonaws.com service and then update it for IRSA.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws iam create-role --role-name EMROnEKSExecutionRole \
    --assume-role-policy-document '{
      "Version": "2012-10-17",
      "Statement": [{
         "Effect": "Allow",
         "Principal": {"Service": "elasticmapreduce.amazonaws.com"},
         "Action": "sts:AssumeRole"
      }]
    }'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;EMROnEKSExecutionRole&lt;/code&gt; with your own name. This sets up the role so EMR (service name &lt;code&gt;elasticmapreduce.amazonaws.com&lt;/code&gt;) can assume it.&lt;/p&gt;

&lt;h4&gt;
  
  
  5b—Attach Required AWS Policies and Permissions
&lt;/h4&gt;

&lt;p&gt;Next, attach an AWS IAM policy that grants permissions to this role. At minimum, give it read/write access to your S3 buckets and permission to write logs. &lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws iam put-role-policy --role-name EMROnEKSExecutionRole --policy-name EMROnEKSExecutionPolicy \
    --policy-document '{
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "s3:PutObject",
            "s3:GetObject",
            "s3:ListBucket",
            "s3:DeleteObject"
          ],
          "Resource": [
            "arn:aws:s3:::YOUR-LOGS-BUCKET",
            "arn:aws:s3:::YOUR-LOGS-BUCKET/*"
          ]
        },
        {
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:PutLogEvents",
            "logs:DescribeLogStreams",
            "logs:DescribeLogGroups"
          ],
          "Resource": "arn:aws:logs:*:*:log-group:/aws/emr-containers/*"
        }
      ]
    }'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace YOUR-LOGS-BUCKET with your S3 bucket name (or use * to allow all buckets, but locking it down is better). This grants S3 and CloudWatch Logs access.&lt;br&gt;
After this, note the role ARN (you can fetch it with aws iam get-role). We’ll use that in the job submission.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws iam get-role --role-name EMROnEKSExecutionRole --query 'Role.Arn' --output text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6—Enabling IRSA (IAM Roles for Service Accounts) in EKS
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;: Before running the update-role-trust-policy command, make sure  that your EKS cluster has an OIDC identity provider associated. You can set this up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eksctl utils associate-iam-oidc-provider --cluster your-cluster-name --approve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS EMR on EKS uses AWS IAM Roles for Service Accounts (IRSA) under the hood. To let Spark pods assume our role, we update its trust policy. AWS provides a handy command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws emr-containers update-role-trust-policy \
    --cluster-name my-emr-eks-cluster \
    --namespace spark \
    --role-name EMROnEKSExecutionRole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command modifies the role’s trust policy to allow the OIDC provider for your EKS cluster, specifically any service account named like &lt;code&gt;emr-containers-sa-*-&amp;lt;ACCOUNTID&amp;gt;-&amp;lt;something&amp;gt;&lt;/code&gt; in the spark namespace to assume it. Essentially, it ties the role to the Kubernetes service account that EMR creates for each job. After running this, your Spark driver and executor pods (which use that service account) will be able to use the permissions of &lt;code&gt;EMROnEKSExecutionRole&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can verify the trust policy was updated correctly by checking the role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws iam get-role --role-name EMROnEKSExecutionRole --query 'Role.AssumeRolePolicyDocument'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output should now include entries for both the EMR service and your EKS cluster's OIDC provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7—Submitting Apache Spark Jobs to EMR Virtual Cluster
&lt;/h3&gt;

&lt;p&gt;We’re ready to run a Spark job. Let’s assume you have a PySpark script &lt;code&gt;my_spark_job.py&lt;/code&gt; in S3 (&lt;code&gt;s3://my-bucket/scripts/my_spark_job.py&lt;/code&gt;) and you want the output in s3://my-bucket/output/. We’ll ask for 2 executors with 4 GiB each as a simple example.&lt;/p&gt;

&lt;p&gt;Use the start-job-run command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws emr-containers start-job-run \
  --virtual-cluster-id &amp;lt;my-virtual-cluster-id&amp;gt; \
  --name example-spark-job \
  --execution-role-arn arn:aws:iam::123456789012:role/EMROnEKSExecutionRole \
  --release-label emr-6.10.0-latest \
  --job-driver '{
      "sparkSubmitJobDriver": {
          "entryPoint": "s3://my-bucket/scripts/my_spark_job.py",
          "entryPointArguments": ["s3://my-bucket/output/"],
          "sparkSubmitParameters": "--conf spark.executor.instances=2 --conf spark.executor.memory=4G"
      }
  }'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;&amp;lt;virtual-cluster-id&amp;gt;&lt;/code&gt; with the ID from Step 4.&lt;/li&gt;
&lt;li&gt;Set the &lt;code&gt;--execution-role-arn&lt;/code&gt; to your role’s ARN from Step 5.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--release-label&lt;/code&gt; chooses the EMR/Spark version (6.10.0 is Spark 3.x; pick as needed).&lt;/li&gt;
&lt;li&gt;The JSON under &lt;code&gt;--job-driver&lt;/code&gt; tells EMR to run spark-submit with our script. We pass the output path as an argument, and set Spark configs for 2 executors of 4 GiB memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can add &lt;code&gt;--configuration-overrides&lt;/code&gt; (in JSON) if you want to enable additional logging or set extra Spark configs. But the above is the basic form. After you run it, you’ll get a job-run ID. EMR on EKS will then schedule the Spark driver pod and executor pods on the cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8—Monitoring Spark Job Status and Viewing Results
&lt;/h3&gt;

&lt;p&gt;After submission, you can track the job status. Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws emr-containers describe-job-run \
  --virtual-cluster-id &amp;lt;virtual-cluster-id&amp;gt; \
  --id &amp;lt;job-run-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will show status (PENDING, RUNNING, etc.) and more details. You can also see the job in the EMR console under Virtual Clusters, or use EMR Studio if you have it set up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs&lt;/strong&gt;: EMR on EKS sends logs to CloudWatch Logs and S3 (if configured) by default. Check CloudWatch for log group named like /aws/emr-on-eks/ or similar. You should see log streams for your driver and executor. Also, EMR keeps the Spark History. In the EMR console’s “Job runs” details, there’s a link to the Spark UI logs for debugging.&lt;br&gt;
For example, after starting the job, you can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws emr-containers list-job-runs \
  --virtual-cluster-id &amp;lt;virtual-cluster-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to see the job's progress and current status. Use describe-job-run for details like log URIs or final status.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collecting and Viewing Job Output and Logs&lt;/strong&gt;&lt;br&gt;
Once the job completes, any output will be in your S3 path (e.g. s3://my-bucket/output/). Check there for results. You can also open the Spark History Server UI via the EMR console to inspect job stages and metrics (just click the link for that job’s Spark UI). All the data-processing was done by pods on your EKS cluster, so there’s no EMR cluster to terminate – it was purely virtual.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 9—Resource Cleanup: Deleting EMR Virtual Clusters, EKS Namespace, and Roles
&lt;/h3&gt;

&lt;p&gt;When you’re done, you’ll want to delete what you created to avoid charges.&lt;br&gt;
Delete the Spark job runs (they are ephemeral, so you really only need to delete the virtual cluster).&lt;/p&gt;

&lt;h4&gt;
  
  
  1) Delete the EMR virtual cluster:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws emr-containers delete-virtual-cluster --id &amp;lt;my-virtual-cluster-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(You can list your virtual clusters to get the ID, or use the one from creation). This removes EMR’s registration.&lt;/p&gt;

&lt;h4&gt;
  
  
  2) Delete the Kubernetes namespace:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl delete namespace spark-jobs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3) Delete the EKS cluster:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eksctl delete cluster --name spark-cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4) Remove the AWS IAM role and policies:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws iam detach-role-policy \
  --role-name EMRContainers-JobExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonEMRContainersServiceRolePolicy

aws iam delete-role \
  --role-name EMRContainers-JobExecutionRole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(If you attached any managed policies, detach them first).&lt;/p&gt;

&lt;p&gt;Once cleaned up, you’ll only be charged for the time your nodes were up and any storage/transfer. There’s no separate “EMR on EKS” fee beyond normal EMR and EC2 usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting and Diagnosing Common EMR on EKS Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Fixing Pod Failures and Resource Constraint Errors
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: Jobs fail with insufficient resources errors.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Check node groups have adequate capacity and use appropriate instance types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check node capacity
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl describe nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Verify resource requests vs available capacity
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl top nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) Resolving IRSA and AWS IAM Authentication Problems
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: Jobs fail with AWS permission errors despite correct AWS IAM policies.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Verify OIDC provider configuration and trust policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check OIDC provider exists
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws iam list-open-id-connect-providers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Verify trust policy includes correct OIDC provider
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws iam get-role --role-name EMROnEKSExecutionRole \
  --query 'Role.AssumeRolePolicyDocument'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) Addressing Networking and DNS Issues with Spark on EKS
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: Jobs cannot access S3 or other AWS services.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Verify VPC endpoints, security groups, and DNS configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check CoreDNS pods
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get pods -n kube-system -l k8s-app=kube-dns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Verify VPC endpoints
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws ec2 describe-vpc-endpoints --filters "Name=vpc-id,Values=&amp;lt;your-vpc-id&amp;gt;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.chaosgenius.io/blog/apache-spark-architecture/" rel="noopener noreferrer"&gt;Apache Spark Architecture 101&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.chaosgenius.io/blog/apache-spark-vs-apache-hadoop/" rel="noopener noreferrer"&gt;Apache Spark vs Apache Hadoop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.chaosgenius.io/blog/apache-spark-alternatives/" rel="noopener noreferrer"&gt;Apache Spark Alternatives&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.chaosgenius.io/blog/apache-spark-with-scala/" rel="noopener noreferrer"&gt;Apache Spark With Scala&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.chaosgenius.io/blog/apache-spark-vs-flink/" rel="noopener noreferrer"&gt;Apache Spark vs Flink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And that’s a wrap! You have successfully set up and run Apache Spark applications on Kubernetes using AWS EMR on EKS. This powerful combination provides the flexibility of Kubernetes with the managed capabilities of EMR, enabling you to run scalable analytics workloads efficiently. EMR on EKS offers significant advantages in terms of resource utilization, cost optimization, and operational simplicity while maintaining the performance benefits of EMR's optimized Spark runtime. This makes it an excellent choice for organizations looking to modernize their big data infrastructure and adopt container-based architectures.&lt;/p&gt;

&lt;p&gt;In this article, we have covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why AWS EMR on EKS?&lt;/li&gt;
&lt;li&gt;Architecture of EMR on EKS&lt;/li&gt;
&lt;li&gt;Difference between EMR on EC2 vs EMR on EKS&lt;/li&gt;
&lt;li&gt;Step-by-Step Guide to Run Spark on Kubernetes with AWS EMR on EKS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;… and so much more!&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions (FAQs)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is EMR on EKS?&lt;/strong&gt;&lt;br&gt;
AWS EMR on EKS is a deployment option for AWS EMR that enables running Apache Spark applications on AWS EKS clusters instead of dedicated EC2 instances. It combines EMR's performance-optimized runtime with Kubernetes orchestration capabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are the benefits of EMR on EKS?&lt;/strong&gt;&lt;br&gt;
The benefits of EMR on EKS include shared resource utilization, managed Spark versions, and faster startup. EMR on EKS allows you to consolidate analytical Spark workloads with other Kubernetes-based applications for better resource use. You get EMR’s automatic provisioning and EMR Studio support, and you only pay for the containers you run (nodes can scale down to zero). AWS also reports big performance gains using the EMR-optimized Spark runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why run Spark on Kubernetes instead of YARN?&lt;/strong&gt;&lt;br&gt;
Running Spark on Kubernetes can be simpler if you’re already using Kubernetes for other workloads. It lets you treat Spark jobs as container apps, using Kubernetes scheduling, monitoring, and autoscaling. As AWS explains, if you already run big data on EKS, EMR on EKS automates provisioning so you can run Spark more quickly. In contrast, YARN requires dedicated clusters and is tied to the Hadoop ecosystem. Kubernetes offers a unified platform and can make multi-tenancy and version management easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to build my own Spark Docker image?&lt;/strong&gt;&lt;br&gt;
No. EMR on EKS uses Amazon-provided container images with optimized Spark runtime. AWS manages the container image lifecycle, including security updates and performance optimizations, eliminating the need for custom image management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run multiple Spark versions on one EKS cluster?&lt;/strong&gt;&lt;br&gt;
Yes. EMR on EKS supports running different EMR release labels across separate virtual clusters (namespaces) on the same EKS cluster. This enables testing different Spark versions or maintaining legacy applications alongside modern workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is EMR on EKS more expensive than EMR on EC2?&lt;/strong&gt;&lt;br&gt;
Cost depends on usage patterns. EMR on EKS has no additional charges beyond standard EMR and compute costs. The shared resource model often reduces costs by eliminating idle cluster capacity, making it particularly cost-effective for variable or bursty workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use EMR Studio with EMR on EKS?&lt;/strong&gt;&lt;br&gt;
Yes. EMR Studio fully supports EMR on EKS virtual clusters through EMR interactive endpoints. You can attach Studio workspaces to virtual clusters for interactive development, debugging, and job authoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a virtual cluster in EMR on EKS?&lt;/strong&gt;&lt;br&gt;
A virtual cluster is a logical construct that maps AWS EMR to a specific Kubernetes namespace. It doesn't create physical resources but serves as the registration point for job submission and management within that namespace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does EMR on EKS use HDFS?&lt;/strong&gt;&lt;br&gt;
No. EMR on EKS typically uses AWS S3 via EMRFS for data storage rather than HDFS. This approach provides better durability, scalability, and cost-effectiveness for cloud-native architectures, though custom HDFS deployments are possible if required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to manage Spark Operator or Spark-submit jobs?&lt;/strong&gt;&lt;br&gt;
EMR on EKS offers flexibility in job submission methods. You can use the AWS CLI/SDK with emr-containers commands for simplicity, or leverage Kubernetes-native approaches like the Spark Operator for more advanced orchestration scenarios.&lt;/p&gt;

</description>
      <category>apachespark</category>
      <category>kubernetes</category>
      <category>aws</category>
      <category>eks</category>
    </item>
  </channel>
</rss>
