Skip to content

6.10. star_schema-tpch-spark-iceberg

This paired notebook remains the educational parity surface for the production tpch_star_schema DAG. The production application preserves the dimension projection and fact aggregation below while adding complete-publication validation, source-key checks, matching Iceberg provenance properties, and terminal Airflow/Spark confirmation. Documents the scenario's paired Jupyter (notebook.ipynb) and Zeppelin (notebook.zpln) implementations. Both notebooks implement identical logic in PySpark and Scala.

Production trust boundary: This notebook pair is not a production write path. Its direct replacements omit the production application's complete-publication, key, serialization, and provenance checks and can invalidate downstream #83. Run production writes only through tpch_star_schema; use these notebooks in an isolated educational environment.

1. Section map

Subsection Scala (Zeppelin) PySpark (Jupyter)
2.1 Setup
2.2 Read
2.3 Transform
2.4 Write
2.5 Verify

2. Walkthrough

2.1 Setup

Scala (Zeppelin):

import spark.implicits._
import org.apache.spark.sql.functions._
// spark pre-bound (Spark Connect + lakehouse catalog)

PySpark (Jupyter):

from pyspark.sql import SparkSession
from pyspark.sql import functions as F

spark = SparkSession.builder.remote("sc://spark-connect:15002").getOrCreate()

Both setup implementations choose an explicit notebook scale override before DATASET_SCALE, default to small, and call the internal resolver once. They validate and retain one verified immutable generation as datasetObjectByName / dataset_object_by_name.

2.2 Read

Scala (Zeppelin):

val orders = spark.read.parquet(datasetObjectByName("orders.parquet"))
val customer = spark.read.parquet(datasetObjectByName("customer.parquet"))
val lineitem = spark.read.parquet(datasetObjectByName("lineitem.parquet"))

PySpark (Jupyter):

orders = spark.read.parquet(dataset_object_by_name["orders.parquet"])
customer = spark.read.parquet(dataset_object_by_name["customer.parquet"])
lineitem = spark.read.parquet(dataset_object_by_name["lineitem.parquet"])

2.3 Transform

Scala (Zeppelin):

val dimCustomer = customer.select($"c_custkey", $"c_name", $"c_nationkey", $"c_mktsegment")
val fctOrders = orders.join(lineitem, orders("o_orderkey") === lineitem("l_orderkey"))
  .groupBy($"o_orderkey", $"o_custkey", $"o_orderdate")
  .agg(sum($"l_extendedprice").as("revenue"), count("*").as("line_count"))

PySpark (Jupyter):

dimCustomer = customer.select(F.col("c_custkey"), F.col("c_name"), F.col("c_nationkey"), F.col("c_mktsegment"))
fctOrders = (orders.join(lineitem, orders["o_orderkey"] == lineitem["l_orderkey"])
    .groupBy(F.col("o_orderkey"), F.col("o_custkey"), F.col("o_orderdate"))
    .agg(F.sum("l_extendedprice").alias("revenue"), F.count("*").alias("line_count")))

2.4 Write

Scala (Zeppelin):

dimCustomer.writeTo("lakehouse.gold.dim_customer").using("iceberg").createOrReplace()
fctOrders.writeTo("lakehouse.gold.fct_orders").using("iceberg").createOrReplace()

PySpark (Jupyter):

dimCustomer.writeTo("lakehouse.gold.dim_customer").using("iceberg").createOrReplace()
fctOrders.writeTo("lakehouse.gold.fct_orders").using("iceberg").createOrReplace()

2.5 Verify

Scala (Zeppelin):

spark.sql("SELECT c.c_mktsegment, sum(f.revenue) AS revenue FROM lakehouse.gold.fct_orders f JOIN lakehouse.gold.dim_customer c ON f.o_custkey = c.c_custkey GROUP BY c.c_mktsegment").show()

PySpark (Jupyter):

spark.sql("SELECT c.c_mktsegment, sum(f.revenue) AS revenue FROM lakehouse.gold.fct_orders f JOIN lakehouse.gold.dim_customer c ON f.o_custkey = c.c_custkey GROUP BY c.c_mktsegment").show()

3. Scala / PySpark parity

Both notebooks share the same numbered sections and produce identical Iceberg tables; only the language and interpreter differ.

4. How to run

Open the scenario's zeppelin/notebook.zpln on the Atlas Zeppelin UI or jupyter/notebook.ipynb on JupyterHub, then run all paragraphs/cells top to bottom.