Part III — Batch processing · Chapter 7

SQL-on-Hadoop, Spark SQL and Catalyst

~40 min read6 interactive widgets4 plates

In this chapter

  1. SQL-on-Hadoop: what it is
  2. Three flavours: batch, interactive, operational
  3. Apache Hive: a warehouse over HDFS
  4. Hive data units: database, table, partition, bucket
  5. Spark SQL: DataFrames and DataSets
  6. Why structure? The pros and the cons
  7. Catalyst: logical and physical plans
  8. Logical optimization: rules to a fixed point
  9. Physical optimization: the cost model
  10. Join methods and their selection
  11. What the cost model does not see
  12. Adaptive Query Execution
  13. Test your knowledge

1. SQL-on-Hadoop: what it is

Chapter 6 ended with a promise: Spark’s third original goal — unifying real time and batch — is why the same engine later grows a streaming module and a SQL module. This chapter is that module, and it starts one step earlier, with the family of tools it belongs to.

SQL-on-Hadoop is a class of application tools that combine established SQL-style querying with newer Hadoop data framework elements. The two ingredients exist for two different reasons:

Key idea — the bet of this whole family

Every tool in this chapter is a wager that the common case matters more than the general case: most jobs are selections, projections, aggregations and joins over structured data, and a declarative statement of those jobs is enough for an optimiser to do the hard work. The RDD remains the escape hatch for everything else — the deck puts it as “whereas RDDs enable any computation through user defined functions”. SQL-on-Hadoop never claims to replace MapReduce; it claims that most computations do not need MapReduce’s full power.

2. Three flavours: batch, interactive, operational

SQL-on-Hadoop is not one thing. The deck splits the tools into three categories, and the split is worth memorising because it predicts everything else about a tool — latency, storage, engine, and how resources are allocated:

Examples: Apache Hive, Apache Spark.

SQL(-like) queries are translated into MapReduce/Spark jobs; they run on very large datasets and large ETL jobs, with latencies of minutes to hours. The Spark flavour adds in-memory SQL: load the datasets in memory, then query them.

Examples: Impala, Apache Drill.

Interactive queries to enable traditional BI and analytics, with latencies of milliseconds to minutes. They require always-on daemons — the price of interactivity is that the engines are running before any query arrives.

Examples: Apache HBase, NoSQL databases.

OLTP workloads and web applications that operate over smaller datasets, typically with insert, update, and deletes at millisecond latency. Note that HBase’s SQL support is limited compared to the other families — it earns its place in the table through the query workload, not through full SQL.

The three SQL-on-Hadoop families on a latency axis Three horizontal bands ordered by latency: operational SQL at milliseconds, interactive SQL at milliseconds to minutes, batch SQL at minutes to hours, each listing example tools and how resources are allocated. THE THREE SQL FAMILIES ON HADOOP — one axis, three workloads ms ms–min min–h OPERATIONAL SQL — HBase · NoSQL databases OLTP workloads and web applications over smaller datasets — insert, update, delete data storage: own · execution engine: own · allocation of resources: per service INTERACTIVE SQL — Impala · Apache Drill interactive queries for traditional BI and analytics — milliseconds to minutes require always-on daemons · MPP-style engines · allocation of resources: per service BATCH SQL — Apache Hive · Apache Spark SQL(-like) queries translated into MapReduce/Spark jobs — minutes to hours allocation of resources: per job · in-memory SQL loads the dataset in memory, then queries it Hive pushes execution to other tools (MapReduce, Tez, Spark) — the only engine-less tool in the table; Spark owns its engine and runs on YARN.
Plate 7.1 — The families arranged by latency. Everything downstream follows from this axis: interactive and operational tools must be standing by (per-service resources), while batch tools are spun up per job — which is exactly the resource-negotiation story of Chapter 5, now applied to query engines.

The deck condenses all of this into one comparison table. Read it as a table of responsibilities: who stores the data, who executes the queries, and who gets blamed for the resources:

CategoryQuery toolData storageExecution engineAllocation of resources
OperationalNoSQLOwnOwnPer service
OperationalHBaseDFSOwnPer service
InteractiveImpalaDFSOwn (MPP-style)Per service
InteractiveDrillAnyOwn (MPP-style)Per service
BatchSparkAnyOwn (YARN)Per job
BatchHiveDFSMapReduce, Tez, SparkPer job
For the exam

The two cells that break the pattern are Hive (batch, storage on DFS, but no engine of its own) and Spark (batch, any storage, own engine on YARN). Both are allocated per job — that is the family trait of batch SQL, and it is why the interactive tools, with their always-on daemons, cost resources even when idle.

3. Apache Hive: a warehouse over HDFS

Apache Hive is the oldest and most influential member of the family: a data warehouse software that facilitates reading, writing, and managing large datasets residing in distributed storage using SQL.

Three properties define how it works:

Watch out

Hive’s best use is batch jobs over large sets of append-only data. It is explicitly not for real-time queries (high latency) and not designed for OLTP. When the slides say “data warehouse” they mean the analytic kind: many reads, few writes, no interactive expectations. This is the same boundary the interactive family exists to serve.

And the ecosystem note worth remembering: AWS Athena enables SQL querying over S3 data, relies on Apache Hive for data modeling, and relies on Presto (an MPP-like tool) to run queries — a managed service standing on the shoulders of the very tools this chapter describes.

4. Hive data units: database, table, partition, bucket

Hive organises data in four nested units, and each unit is practically a folder (or a set of files) in HDFS. The deck gives the exact paths, so the picture is worth drawing once:

Hive data units as folders in HDFS A folder tree: the warehouse folder contains a database folder, which contains a table folder, which contains partition folders named column=value, each containing bucket files. HIVE DATA UNITS — every unit is a folder (or a file) in HDFS /user/hive/warehouse/ dbname.db tablename/ department=IT/ bucket01 bucket02 department=Sales/ bucket01 Database — container of tables practically, a folder: /user/hive/warehouse/dbname.db Table — homogeneous records, same schema a set of files: .../tablename/* — the number of files depends on how the data was partitioned when inserted Partition — one folder level per column value faster search on partitioning columns, but potential “explosion” of folders — ideal for low-cardinality columns (e.g., departments) Bucket — fixed number of files from a hash column stored as a single file: .../tablename/bucket01 faster search (and skipped shuffle) on bucketing columns — ideal for high-cardinality columns (e.g., IDs)
Plate 7.2 — The four units, nested like a file system because they are a file system. Partitioning adds folder levels by column value; bucketing fixes the number of files by hashing a column. The two techniques aim at the same goal — reading less data — and they target opposite cardinalities.
UnitWhat it isHDFS location
DatabaseContainer of tables/user/hive/warehouse/dbname.db
TableHomogeneous set of records with the same schema/user/hive/warehouse/dbname.db/tablename/*
Partition (optional)Records split by the value of one or more columns — a further level of folders.../tablename/column=value/*
Bucket (optional)Records grouped into a fixed number of files, based on the hash value of some column — one file per bucket.../tablename/bucket01
For the exam

The cardinality rule is the examinable nugget. Partition on low-cardinality columns (a handful of distinct values, e.g., departments) because each value is worth a whole folder; partitioning on a high-cardinality column would create thousands of near-empty folders — the “explosion” the slides warn about. Bucket on high-cardinality columns (e.g., IDs) because hashing spreads records evenly across a fixed number of files, and bucketing columns give faster search and a skipped shuffle on joins — a first taste of Chapter 8’s partitioning logic, at the storage layer.

5. Spark SQL: DataFrames and DataSets

Hive showed that structure can be bolted onto Hadoop. Spark SQL does it inside the engine itself. The definition in the deck is one sentence long, and it is a direct continuation of Chapter 6:

Key idea

RDDs are immutable distributed collection of objects. DataFrames and DataSets are immutable distributed collection of records organized into named columns (i.e., a table). Simply put: RDDs with a schema attached.

Everything else about them follows from that one sentence:

The one difference between the two APIs is when type conformity is checked:

Records organized into named columns, with the schema checked at runtime. This is the more permissive, dynamically-typed feel — and the slide explicitly connects it to Chapter 6’s warning: type inference is a Scala feature and is different from dynamic typing.

The same collection of records with named columns, but type conformity is checked at compile time (the DataSet is the typed API). Checking earlier avoids querying columns that do not really exist — the error is caught before the job is even submitted, which is exactly the “anticipate programming errors” benefit of Chapter 6’s type inference.

And then the sentence that carries the chapter: they are still lazily evaluated — nothing from Chapter 6 is thrown away — but they support under-the-hood optimizations and code generation:

For the exam

Three one-line distinctions to keep ready: RDD = collection of objects, DataFrame/DataSet = collection of records with named columns (an RDD with a schema); DataFrame checks types at runtime, DataSet at compile time; RDDs are lazily evaluated and so are DataFrames/DataSets — but the latter get Catalyst optimizations and JVM code generation on top. The laziness is the same laziness; the difference is that the schema gives the optimiser something to work with.

6. Why structure? The pros and the cons

The deck stops to justify the whole design, and the honesty is refreshing: structure is a trade, not a gift. The slides put it as a balanced list:

Argument
Cons Structure imposes some limits — whereas RDDs enable any computation through user defined functions, a schema only fits computations that can be expressed on top of it.
Pros The most common computations are supported — selections, aggregations, joins cover the majority of real workloads.
Language simplicity — SQL is widely understood, so analysts and developers share one language.
It opens the room to optimizations — it is hard to optimize a user defined function, because the optimiser cannot see inside it; a declarative plan, it can.
Key idea — the trade is the whole chapter

The cons column is exactly why RDDs still exist, and the pros column is exactly why Catalyst can exist. A user defined function is a black box: Spark cannot reorder it, prune columns through it, or push predicates into it. A relational plan is a tree of operators with known semantics, and a tree can be rewritten. Structure is not a restriction the user suffers; it is the information the optimiser needs. Chapter 6’s laziness is what makes the rewriting possible — the plan exists before any data moves.

7. Catalyst: logical and physical plans

The Catalyst optimizer creates optimized execution plans by working on two distinct representations of the query, and the deck draws the boundary sharply:

PlanIt describes
Logical PlanWhich computations must be done — the relational operations, independent of any algorithm.
Physical PlanWhich computations must be done and how to conduct them — i.e., which algorithms are used.

This is the same split as Chapter 6 at one level up: the logical DAG of RDDs became a physical plan of stages and tasks; here, the logical query plan becomes a physical plan of algorithms. The whole journey of a query, from text to execution:

From SQL text to execution through Catalyst SQL text is parsed into a logical plan, rewritten by rule-based logical optimization into an optimized logical plan, compiled into physical plans among which the cost model selects, and executed; AQE reviews the plan at stage boundaries. AQE (Spark 3.0) — a layer on top of Catalyst: the plan is not final, it reviews the plan at each stage boundary using actual intermediate data SQL text Parser Logical plan which computations must be done Optimized logical plan LOGICAL OPTIMIZATION rules (Scala functions): constant folding, predicate pushdown, column pruning, join reordering — applied recursively and iteratively until a fixed point Physical plans which algorithms are used — the cost model selects the best one Cost = α · costCPU + (1−α) · costIO = α · cardinality + (1−α) · size Execution — stages & tasks LOGICAL — what to compute PHYSICAL — how to compute it
Plate 7.3 — The journey of a query. The logical plan says what; rules rewrite it until a fixed point; the physical layer says how and the cost model chooses; AQE then sits on top of the whole thing, revising the plan between stages. Each arrow is a moment where Chapter 6’s laziness pays off — nothing has moved yet.

8. Logical optimization: rules to a fixed point

Logical optimization is based on rules: a rule is a function that can be applied on a portion of the logical plan, implemented as a Scala function. The deck lists several types of rules:

RuleWhat it does
Constant foldingResolve constant expressions at compile time — e.g., WHERE price > 10 AND 1 = 1 is simplified before any data is read.
Predicate pushdownPush selection predicates close to the sources — the filter happens as early (and as cheaply) as possible, ideally during the physical scan.
Column pruningProject only the required column — drop every column the query never uses.
Join reorderingChange the order of join operations — joining small inputs first beats joining large ones first.

The rules are applied recursively and iteratively until the plan reaches a fixed point — run them until none of them changes anything anymore. Each application may enable another: pruning columns makes a join cheaper, which may make reordering it worthwhile, and so on.

One query through the rules

Select each line to see what Catalyst does at that step. The SQL is the same query from the exam material — only the plan underneath it changes.

Key idea — rules are the reason for the schema

None of these rules could exist on plain RDDs, because an RDD carries no information about what its elements mean. The moment records have named columns, a rule can ask “is this predicate applicable here?” and answer with the schema. Constant folding, predicate pushdown, column pruning and join reordering are four small rewrites, but they are applied until fixed point — and the combination is where the real gains come from, exactly as with Chapter 6’s pipelining of narrow dependencies.

9. Physical optimization: the cost model

Once the logical plan is optimized, one or more physical execution plans are defined — each a concrete choice of algorithms — and a cost model is used to select the best one. The model in the slides is deliberately simple:

Cost(table) = α · costCPU + (1−α) · costIO
            = α · cardinality + (1−α) · size

Two components, one knob: costCPU is the cardinality (how many records), costIO is the size (how many bytes), and α decides how much each matters. On top of the cost model there are also rule-based physical optimizations:

The cost model, live

Slide α and the two components, and watch the cost table move. The model only ever sees these two numbers — that is its strength and, as section 11 shows, its blind spot.

10. Join methods and their selection

The most visible effect of physical optimization is join method selection: the same join can be executed by three different algorithms, and the cost model picks one. All three are worth drawing because the exam routinely asks for their conditions:

The three join methods of Spark SQL Broadcast Hash join replicates the small table to every executor and joins locally; Shuffle Hash join repartitions both sides by join key and hashes the smaller partition; Shuffle Sort Merge join repartitions both sides, sorts keys per partition, and iterates the sorted lists. THREE JOIN STRATEGIES — the cost model picks one per join BROADCAST HASH JOIN — no shuffle A1 A2 A3 HJ Table B — the small table, fully loaded into each executor no shuffle at all: the small table is replicated like a broadcast variable (Chapter 8), and each executor joins its local A partition against it. SHUFFLE HASH JOIN — repartition by join key A1 A2 A3 HJ B1 B2 B3 shuffle: both tables are repartitioned by the join key (a wide dependency — stage boundary, Chapter 6). The hash table of the smaller partition is loaded into memory; for each record of the bigger partition, matching records are found by using the hash table. SHUFFLE SORT MERGE JOIN — repartition by join key A1 A2 SMJ B1 B2
Plate 7.4 — The three algorithms. Broadcast avoids the shuffle entirely; the two shuffle methods differ in what happens after the repartition — hash probing for one, sorted-merge iteration for the other. The arrows into the operators are the shuffles, and a shuffle is a Chapter 6 wide dependency: it is where the stage boundary goes.

Formally, the deck defines them as:

When is each one selected?

The selection is a small decision tree, and the conditions are exact:

The deck closes with a caveat that matters for the exam’s tone: join methods are under continuous refinement, and a few more exist for certain situations (e.g., cartesian product).

Which join method?

Toggle the three conditions and apply the decision tree. The verdict is what Catalyst’s cost model would conclude.

For the exam

The three conditions, verbatim: Broadcast if one RDD is smaller than the threshold (autoBroadcastJoinThreshold, default 10MB); Shuffle Hash if Broadcast does not apply and one RDD is 3× smaller and the smaller partitions fit in memory as hash tables; Shuffle Sort Merge otherwise. Notice the logic is exclusion-based: Broadcast first, then Shuffle Hash, then Sort Merge as the residual case — the same “conditions that do not apply” phrasing appears in the slides, and the exam mirrors it.

11. What the cost model does not see

The deck is candid about the model’s limits: Catalyst only considers the size and the cardinality of tables. Everything else that determines a query’s real cost is missing from the formula:

Key idea — why the simplification exists

A cost model is only useful if it can be evaluated before the query runs, with information the optimiser already has. Cardinality and size are known at planning time; network and disk throughput are properties of a cluster that can change between jobs, and data locality is a scheduling decision made after the plan is fixed. The simplification is not ignorance — it is a choice of which costs are plannable. What the deck calls research work is precisely the attempt to make the model probabilistic and accurate anyway.

And the research is from the course’s own group — worth citing, because it is the visible frontier of this chapter’s topic:

Both attack the same gap: defining a more accurate, probabilistic cost model — one that turns Chapter 8’s tuning knobs into something the optimiser itself can reason about.

12. Adaptive Query Execution

The chapter ends where the current version of Spark ends: Adaptive Query Execution (AQE), introduced with version 3.0. Its main idea in one sentence:

Key idea — the plan is not final

The execution plan is not final. Reviews are made at each stage boundary, and additional optimizations are possibly applied, given the information available on the intermediate data. AQE can be defined as a layer on top of Spark Catalyst which will modify the Spark plan on the fly.

Why does this make sense? The cost model of section 9 works on estimates. Once a stage has actually run, Spark knows the real cardinalities and sizes — so the plan can be revised with facts instead of guesses. The deck lists the two drawbacks with equal honesty:

And four concrete optimizations, each fixing one specific planning blind spot:

AQE optimizationWhat it does
Adaptive number of shuffle partitionsSpark SQL used to set a default number of 200 partitions at each stage; AQE automatically adjusts it at runtime based on the actual data volume.
Dynamically converting Sort Merge joins to Broadcast joinsA dynamic switch of join strategies based on actual table sizes — a table estimated as large but actually small becomes a Broadcast join mid-flight.
Dynamically coalesce shuffle partitionsMerge the partitions that ended up nearly empty, so the next stage does not schedule tiny tasks.
Dynamically optimize skewed joinsSplit the partitions dominated by a single popular key, so one straggler no longer dictates the join’s runtime.

AQE, stage by stage

Step through a two-stage query and watch the plan change at the stage boundary. Each review uses data the planner could only guess before.

Watch out

AQE is not a fourth join method and not a new optimizer — it is a layer on top of Catalyst that re-runs parts of the optimization with real statistics. And its drawback list is examinable too: the stop-at-each-boundary pause, and the plan that is harder to read because the original multi-stage job becomes a set of single-stage jobs.

For the exam

Chapter 6 said stages’ boundaries are defined by shuffle operations; AQE is exactly what makes those boundaries useful: they are the points where the engine may stop, look at real intermediate data, and re-plan. If you are asked why AQE exists, answer in two clauses: the cost model plans from estimates, and the stage boundary is where the estimates become facts.

Test your knowledge

What is SQL-on-Hadoop, and why does it exist?

SQL-on-Hadoop is a class of application tools that combine established SQL-style querying with newer Hadoop data framework elements. It exists because MapReduce-style programming is the native and most comprehensive way to design jobs, but SQL is a simple and widely understood language — so most computations can be expressed declaratively and let an optimiser do the hard work, while RDDs remain the escape hatch for everything a schema cannot express.

Distinguish batch, interactive and operational SQL on Hadoop, with examples.

Batch SQL (Apache Hive, Apache Spark): SQL(-like) queries translated into MapReduce/Spark jobs, on very large datasets and large ETL jobs, minutes to hours; Spark adds in-memory SQL (load datasets in memory, then query them). Interactive SQL (Impala, Apache Drill): interactive queries for traditional BI and analytics, milliseconds to minutes, requiring always-on daemons. Operational SQL (Apache HBase, NoSQL databases): OLTP workloads and web applications over smaller datasets, typically with insert, update and deletes at millisecond latency.

In the SQL-on-Hadoop comparison table, which tools break the pattern, and how?

Hive breaks the execution-engine column: it stores on DFS but pushes execution to other tools (MapReduce, Tez, Spark), being the only engine-less tool. Spark breaks the storage column: it can read/write from/to any source and runs its own engine on YARN. Both are batch tools with per-job allocation of resources, unlike the interactive/operational families which are allocated per service.

What is Apache Hive, and what does its Metastore do?

Hive is a data warehouse software that facilitates reading, writing, and managing large datasets residing in distributed storage using SQL. Structure is projected onto data already in storage (external tables) through metadata, and that metadata is stored in a Metastore using an RDBMS on a single node — a single point of failure, although High-Availability is supported. Data is queried via HiveQL (a subset of SQL), queries are translated to MapReduce/Spark jobs, and a procedural language (HPL-SQL) is supported.

Which workloads is Hive best for, and which is it not designed for?

Best use: batch jobs over large sets of append-only data. It is not for real-time queries (high latency) and not designed for OLTP.

Describe the four Hive data units and their HDFS locations.

Database: container of tables, practically a folder — /user/hive/warehouse/dbname.db. Table: homogeneous set of records with the same schema, stored as a set of files — .../tablename/*. Partition (optional): records split by the value of one or more columns, implemented as a further level of folders — .../tablename/column=value/*. Bucket (optional): records grouped into a fixed number of files based on the hash value of some column — .../tablename/bucket01.

When do you partition, when do you bucket, and why?

Partition on low-cardinality columns (e.g., departments): each value deserves a folder, and search on partitioning columns is faster — but there is a potential “explosion” of folders if cardinality is high. Bucket on high-cardinality columns (e.g., IDs): hashing spreads records evenly across a fixed number of files, giving faster search and a skipped shuffle on bucketing columns.

What are DataFrames/DataSets, and how do they differ from RDDs?

RDDs are immutable distributed collections of objects; DataFrames and DataSets are immutable distributed collections of records organized into named columns (i.e., a table) — simply put, RDDs with a schema attached. They support both relational and procedural processing (e.g., SQL and Scala), complex data types (struct, array) and user defined types, are cached using columnar storage, and can be built from DBMSs, files, other tools (e.g., Hive) and RDDs.

Where is type conformity checked for DataFrames versus DataSets, and what does that prevent?

At compile time for DataSets; at runtime for DataFrames. Checking earlier — as the DataSet API does — avoids querying columns that do not really exist, catching the error before the job is submitted.

What optimizations does the Catalyst optimizer bring, and what is JVM code generation?

Catalyst creates optimized execution plans for the still-lazily-evaluated DataFrames/DataSets: IO optimizations such as skipping blocks in Parquet files and logical push-down of selection predicates. JVM code generation produces code for all supported languages, even non-native JVM ones such as Python, and consists in clubbing multiple physical operations together to form a single Java function — so records flow through fused operations without interpreter round-trips.

What are the pros and cons of structured processing versus RDDs?

Con: structure imposes some limits — whereas RDDs enable any computation through user defined functions. Pros: the most common computations are supported; language simplicity; and structure opens the room to optimizations — it is hard to optimize a user defined function, because the optimiser cannot see inside it.

Distinguish the logical plan from the physical plan, and name the four logical optimization rules.

The logical plan describes which computations must be done; the physical plan describes which computations must be done and how to conduct them (i.e., which algorithms are used). The four logical rules: constant folding (resolve constant expressions at compile time), predicate pushdown (push selection predicates close to the sources), column pruning (project only the required column), join reordering (change the order of join operations). Rules are implemented as Scala functions, applied recursively and iteratively until the plan reaches a fixed point.

State the physical cost model and what it ignores.

Cost(table) = α·costCPU + (1−α)·costIO = α·cardinality + (1−α)·size. The model only considers the size and the cardinality of tables; it ignores network throughput, disk throughput, the allocation of resources (number of executors, cores per executor) and the allocation of tasks (data locality probability). Rule-based physical optimizations (operation pipelining, predicate push-down during the physical scan) run alongside it.

Describe the three join methods and the conditions for selecting each.

Broadcast Hash join: the smaller table is fully loaded into each executor’s memory, similar to broadcast variables — selected when one RDD is smaller than the threshold spark.sql.autoBroadcastJoinThreshold (default 10MB). Shuffle Hash join: both tables repartitioned by join key; the hash table of the smaller partition is loaded into memory and the bigger partition probes it — selected when Broadcast does not apply, one RDD is 3 times smaller than the other, and the smaller partitions fit in memory as hash tables. Shuffle Sort Merge join: both tables repartitioned by join key, keys sorted within each partition, join by iterating on the sorted lists — selected when neither of the previous conditions applies.

What is AQE, when was it introduced, and what are its four optimizations and its drawbacks?

Adaptive Query Execution, introduced with Spark 3.0: the execution plan is not final; reviews are made at each stage boundary and additional optimizations are applied given the information on the intermediate data. It is a layer on top of Catalyst that modifies the Spark plan on the fly. Optimizations: adaptive number of shuffle partitions (the default 200 per stage is adjusted at runtime), dynamically converting Sort Merge joins to Broadcast joins (a dynamic switch of join strategies based on actual table sizes), dynamically coalescing shuffle partitions, and dynamically optimizing skewed joins. Drawbacks: execution stops at each stage boundary for the review (usually worth it), and the execution plan is harder to read — the original multi-stage job becomes a set of single-stage jobs.