--- name: spark-catalyst description: Expert guidance for working with the Apache Spark Catalyst query optimisation framework. Use this skill when working with Spark SQL internals, creating custom expressions, implementing query optimisations, working with logical/physical plans, or extending Catalyst. Trigger keywords include "catalyst", "spark sql", "expression", "logical plan", "physical plan", "tree node", "query optimisation", "rule executor", "analyzer", "optimizer", "code generation". --- You are an expert in the Apache Spark Catalyst query optimisation framework. This skill provides comprehensive guidance on using the Catalyst API for query processing, optimisation, and code generation. ## Overview of Catalyst Apache Spark Catalyst is a query optimisation framework that powers Spark SQL and DataFrames. It provides: - **Extensible query optimiser** based on functional programming constructs - **Tree-based representation** of query plans and expressions - **Rule-based transformations** for query optimisation - **Code generation** for high-performance query execution - **Cost-based optimisation** for join reordering and other decisions ## Core Architecture ### Module Structure The Catalyst module is located at `sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/` and contains: ``` catalyst/ ├── analysis/ # Query analysis and resolution ├── catalog/ # Catalog management and metadata ├── expressions/ # Expression definitions and evaluation ├── optimizer/ # Query optimisation rules ├── parser/ # SQL parsing ├── planning/ # Query planning strategies ├── plans/ # Query plan representations │ ├── logical/ # Logical plan operators │ └── physical/ # Physical plan operators ├── rules/ # Rule execution framework ├── trees/ # Tree node infrastructure ├── types/ # Data type utilities └── util/ # Utility classes ``` ## Fundamental Concepts ### 1. TreeNode `TreeNode` is the base class for all tree structures in Catalyst, including expressions and query plans. #### Key Concepts ```scala abstract class TreeNode[BaseType <: TreeNode[BaseType]] extends Product { // Children of this node. def children: Seq[BaseType] // Transform this tree by applying a function to all nodes. def transform(rule: PartialFunction[BaseType, BaseType]): BaseType // Transform all nodes bottom-up. def transformDown(rule: PartialFunction[BaseType, BaseType]): BaseType // Transform all nodes top-down. def transformUp(rule: PartialFunction[BaseType, BaseType]): BaseType // Fast equality check. def fastEquals(other: TreeNode[_]): Boolean // Tree pattern matching for efficient traversal. def treePatternBits: BitSet } ``` #### Tree Transformation Patterns **Bottom-up transformation:** ```scala plan.transformUp { case Filter(condition, child) if isAlwaysTrue(condition) => child } ``` **Top-down transformation:** ```scala plan.transformDown { case Project(projectList, child) => // Transform project first, then children optimiseProject(projectList, child) } ``` **Collect nodes matching a pattern:** ```scala val filters = plan.collect { case f @ Filter(_, _) => f } ``` ### 2. Expression `Expression` is the base class for all expression trees in Catalyst. #### Expression Hierarchy - **LeafExpression**: No children (e.g., `Literal`, `AttributeReference`) - **UnaryExpression**: One child (e.g., `Cast`, `Not`, `IsNull`) - **BinaryExpression**: Two children (e.g., `Add`, `EqualTo`, `And`) - **TernaryExpression**: Three children (e.g., `If`, `Substring`) - **QuaternaryExpression**: Four children #### Key Properties ```scala abstract class Expression extends TreeNode[Expression] { // Can this expression be evaluated at query planning time? def foldable: Boolean // Does this expression always return the same result for fixed inputs? def deterministic: Boolean // Can this expression evaluate to null? def nullable: Boolean // Data type of the expression result. def dataType: DataType // Evaluate this expression given an input row. def eval(input: InternalRow): Any // Generate code for this expression. def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode // Attributes referenced by this expression. def references: AttributeSet } ``` #### Common Expression Types **Literal values:** ```scala Literal(42) // Integer literal Literal("hello") // String literal Literal(null, StringType) // Null literal ``` **Attribute references:** ```scala AttributeReference("name", StringType, nullable = true)() AttributeReference("age", IntegerType, nullable = false)() ``` **Predicates:** ```scala EqualTo(left, right) // left = right GreaterThan(left, right) // left > right LessThanOrEqual(left, right) // left <= right And(left, right) // left AND right Or(left, right) // left OR right Not(child) // NOT child ``` **Arithmetic:** ```scala Add(left, right) // left + right Subtract(left, right) // left - right Multiply(left, right) // left * right Divide(left, right) // left / right ``` **String operations:** ```scala Substring(str, pos, len) // substring(str, pos, len) Upper(child) // upper(child) Lower(child) // lower(child) Concat(children) // concat(child1, child2, ...) ``` **Type conversion:** ```scala Cast(child, targetType) // cast(child as targetType) ``` ### 3. QueryPlan `QueryPlan` is the base class for both logical and physical query plans. #### Key Properties ```scala abstract class QueryPlan[PlanType <: QueryPlan[PlanType]] extends TreeNode[PlanType] { // Output schema of this plan node. def output: Seq[Attribute] // Set of output attributes. def outputSet: AttributeSet // Set of attributes from all children. def inputSet: AttributeSet // Attributes produced by this node. def producedAttributes: AttributeSet // Attributes referenced by expressions. def references: AttributeSet // Attributes referenced but not provided by children. def missingInput: AttributeSet // All expressions in this plan node. def expressions: Seq[Expression] // Transform expressions in this plan. def transformExpressions(rule: PartialFunction[Expression, Expression]): PlanType } ``` ### 4. LogicalPlan Logical plans represent query semantics without execution strategy. #### Common Logical Operators **Data sources:** ```scala // Read from a relation. LogicalRelation(relation, output, catalogTable) // Local in-memory data. LocalRelation(output, data) // Empty relation. EmptyRelation(output) ``` **Projections and filters:** ```scala // Project specific columns. Project(projectList: Seq[NamedExpression], child: LogicalPlan) // Filter rows. Filter(condition: Expression, child: LogicalPlan) // Select distinct rows. Distinct(child: LogicalPlan) ``` **Aggregations:** ```scala // Group by and aggregate. Aggregate( groupingExpressions: Seq[Expression], aggregateExpressions: Seq[NamedExpression], child: LogicalPlan ) ``` **Joins:** ```scala // Join two relations. Join( left: LogicalPlan, right: LogicalPlan, joinType: JoinType, condition: Option[Expression], hint: JoinHint ) ``` **Sorting:** ```scala // Sort rows. Sort( order: Seq[SortOrder], global: Boolean, child: LogicalPlan ) ``` **Limits:** ```scala // Limit number of rows. Limit(limitExpr: Expression, child: LogicalPlan) ``` **Set operations:** ```scala // Union of two relations. Union(children: Seq[LogicalPlan]) // Intersection. Intersect(left: LogicalPlan, right: LogicalPlan, isAll: Boolean) // Difference. Except(left: LogicalPlan, right: LogicalPlan, isAll: Boolean) ``` ### 5. InternalRow `InternalRow` is the internal representation of a row in Catalyst. #### Key Methods ```scala abstract class InternalRow extends SpecializedGetters { // Number of fields in this row. def numFields: Int // Check if field at ordinal is null. def isNullAt(ordinal: Int): Boolean // Get value at ordinal. def get(ordinal: Int, dataType: DataType): Any // Specialised getters for primitive types. def getBoolean(ordinal: Int): Boolean def getByte(ordinal: Int): Byte def getShort(ordinal: Int): Short def getInt(ordinal: Int): Int def getLong(ordinal: Int): Long def getFloat(ordinal: Int): Float def getDouble(ordinal: Int): Double def getDecimal(ordinal: Int, precision: Int, scale: Int): Decimal def getUTF8String(ordinal: Int): UTF8String // Update value at ordinal. def update(ordinal: Int, value: Any): Unit // Set field to null. def setNullAt(ordinal: Int): Unit // Create a copy of this row. def copy(): InternalRow // Convert to Scala sequence. def toSeq(schema: StructType): Seq[Any] } ``` ## Rule-Based Transformation Framework ### 6. Rule and RuleExecutor Rules define tree transformations, and RuleExecutor applies them in batches. #### Defining Rules ```scala // Basic rule. object MyOptimisationRule extends Rule[LogicalPlan] { def apply(plan: LogicalPlan): LogicalPlan = plan.transformUp { case Filter(condition, child) if isAlwaysTrue(condition) => child } } // Configurable rule. case class MyParameterisedRule(conf: SQLConf) extends Rule[LogicalPlan] { def apply(plan: LogicalPlan): LogicalPlan = { if (conf.myFeatureEnabled) { optimisePlan(plan) } else { plan } } } ``` #### Rule Execution Strategies ```scala abstract class RuleExecutor[TreeType <: TreeNode[_]] { // Define batches of rules to execute. protected def batches: Seq[Batch] // Execute all batches on the plan. def execute(plan: TreeType): TreeType } // Batch execution strategies. abstract class Strategy case class Once extends Strategy case class FixedPoint(maxIterations: Int) extends Strategy ``` #### Example RuleExecutor ```scala object MyOptimiser extends RuleExecutor[LogicalPlan] { val batches = Seq( Batch("Normalisation", Once, EliminateSubqueryAliases, RemoveRedundantAliases ), Batch("Operator Optimisation", FixedPoint(100), PushDownPredicate, ConstantFolding, ColumnPruning ), Batch("Join Reordering", Once, CostBasedJoinReorder ) ) } ``` ### 7. Analyzer The Analyzer resolves unresolved logical plans by binding attributes, functions, and tables. #### Key Analysis Rules - **ResolveReferences**: Bind attribute references to their sources - **ResolveRelations**: Bind table references to catalog entries - **ResolveFunctions**: Bind function calls to function implementations - **TypeCoercion**: Insert implicit type casts - **ResolveSubquery**: Analyse subquery expressions - **CheckAnalysis**: Verify the plan is valid #### Example Usage ```scala val analyzer = new Analyzer(catalogManager) val analysedPlan = analyzer.execute(unresolvedPlan) ``` ### 8. Optimizer The Optimizer transforms logical plans to improve query performance. #### Key Optimisation Rules **Predicate pushdown:** ```scala // Push filters below projections. PushDownPredicate // Push filters into join conditions. PushPredicateThroughJoin // Push filters to data sources. PushDownPredicates ``` **Projection pushdown:** ```scala // Eliminate unnecessary columns. ColumnPruning // Combine adjacent projections. CollapseProject ``` **Constant folding:** ```scala // Evaluate constant expressions. ConstantFolding // Simplify expressions. SimplifyConditionals SimplifyCasts ``` **Join optimisation:** ```scala // Reorder joins for better performance. CostBasedJoinReorder // Eliminate redundant joins. EliminateOuterJoin ``` **Subquery optimisation:** ```scala // Decorrelate correlated subqueries. DecorrelateInnerQuery // Merge scalar subqueries. MergeScalarSubqueries ``` ## Code Generation ### 9. CodeGen Framework Catalyst generates optimised Java bytecode for query execution. #### Key Components **CodegenContext:** Manages code generation state including variable declarations, functions, and class structure. **ExprCode:** Represents generated code for an expression evaluation. ```scala case class ExprCode( code: Block, // Generated code block isNull: ExprValue, // Variable for null check value: ExprValue // Variable for result value ) ``` #### Expression Code Generation ```scala trait CodegenFallback extends Expression { // Fallback to interpreted evaluation. protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = { ctx.references += this val objectTerm = ctx.addReferenceObj("expression", this) ExprCode( code = code""" boolean ${ev.isNull} = true; ${CodeGenerator.javaType(dataType)} ${ev.value} = ${CodeGenerator.defaultValue(dataType)}; Object result = $objectTerm.eval(${ctx.INPUT_ROW}); if (result != null) { ${ev.isNull} = false; ${ev.value} = (${CodeGenerator.boxedType(dataType)}) result; } """, isNull = ev.isNull, value = ev.value ) } } ``` #### Projection Generation ```scala // Generate unsafe projection from expressions. val projection = GenerateUnsafeProjection.generate(expressions) val result = projection(inputRow) // Generate mutable projection. val mutableProjection = GenerateMutableProjection.generate(expressions) val outputRow = mutableProjection(inputRow) // Generate safe projection. val safeProjection = GenerateSafeProjection.generate(expressions) ``` #### Predicate Generation ```scala // Generate predicate for filter condition. val predicate = GeneratePredicate.generate(condition) val passes = predicate.eval(row) ``` #### Ordering Generation ```scala // Generate row comparator. val ordering = GenerateOrdering.generate(sortOrders) val compareResult = ordering.compare(row1, row2) ``` ## Working with Data Types ### 10. Catalyst Type System #### Primitive Types ```scala BooleanType ByteType ShortType IntegerType LongType FloatType DoubleType StringType BinaryType DateType TimestampType TimestampNTZType ``` #### Complex Types ```scala // Array type. ArrayType(elementType: DataType, containsNull: Boolean) // Map type. MapType(keyType: DataType, valueType: DataType, valueContainsNull: Boolean) // Struct type. StructType(fields: Seq[StructField]) // Struct field. StructField(name: String, dataType: DataType, nullable: Boolean, metadata: Metadata) ``` #### Decimal Types ```scala DecimalType(precision: Int, scale: Int) DecimalType.SYSTEM_DEFAULT // Decimal(38, 18) ``` ## Common Patterns and Best Practices ### Pattern 1: Creating Custom Expressions ```scala case class MyCustomFunction(child: Expression) extends UnaryExpression { // Define output type. override def dataType: DataType = StringType // Can the result be null? override def nullable: Boolean = child.nullable // Evaluate the expression. override def eval(input: InternalRow): Any = { val value = child.eval(input) if (value == null) { null } else { // Custom logic here. UTF8String.fromString(value.toString.toUpperCase) } } // Generate code for this expression. override protected def doGenCode( ctx: CodegenContext, ev: ExprCode): ExprCode = { val childGen = child.genCode(ctx) ev.copy(code = code""" ${childGen.code} boolean ${ev.isNull} = ${childGen.isNull}; ${CodeGenerator.javaType(dataType)} ${ev.value} = ${CodeGenerator.defaultValue(dataType)}; if (!${ev.isNull}) { ${ev.value} = UTF8String.fromString( ${childGen.value}.toString().toUpperCase()); } """) } // Override for pretty printing. override def prettyName: String = "my_custom_function" } ``` ### Pattern 2: Creating Custom Optimisation Rules ```scala object EliminateRedundantCasts extends Rule[LogicalPlan] { def apply(plan: LogicalPlan): LogicalPlan = plan.transformAllExpressions { case Cast(child, dataType, _, _) if child.dataType == dataType => // Remove cast if types match. child } } ``` ### Pattern 3: Pattern Matching on Plans ```scala plan match { case Project(projectList, child) => // Handle projection. case Filter(condition, Project(projectList, child)) => // Handle filter over projection. case Join(left, right, joinType, Some(condition), _) => // Handle join with condition. case Aggregate(grouping, aggregates, child) => // Handle aggregation. case _ => // Default case. } ``` ### Pattern 4: Traversing Expression Trees ```scala // Find all attribute references. val attributes = expr.collect { case a: AttributeReference => a } // Find all subqueries. val subqueries = expr.collect { case s: SubqueryExpression => s } // Transform specific expression types. val transformed = expr.transformUp { case Add(Literal(0, _), right) => right case Add(left, Literal(0, _)) => left } ``` ### Pattern 5: Attribute Resolution ```scala // Resolve attribute by name. def resolve(attrName: String, input: LogicalPlan): Option[Attribute] = { input.output.find(_.name == attrName) } // Resolve with qualifier. def resolveQualified( qualifier: Seq[String], attrName: String, input: LogicalPlan): Option[Attribute] = { input.output.find { attr => attr.qualifier.startsWith(qualifier) && attr.name == attrName } } ``` ### Pattern 6: Working with Schema ```scala // Create schema from attributes. val schema = StructType(attributes.map { attr => StructField(attr.name, attr.dataType, attr.nullable, attr.metadata) }) // Convert schema to attributes. val attributes = schema.toAttributes // Add column to schema. val newSchema = schema.add("newColumn", StringType, nullable = true) // Drop column from schema. val reducedSchema = StructType(schema.filterNot(_.name == "dropColumn")) ``` ## Testing Catalyst Components ### Unit Testing Expressions ```scala test("my custom function") { val input = Literal("hello") val expr = MyCustomFunction(input) // Test evaluation. assert(expr.eval(null) == UTF8String.fromString("HELLO")) // Test properties. assert(expr.dataType == StringType) assert(expr.foldable) assert(expr.deterministic) } ``` ### Testing Optimisation Rules ```scala test("eliminate redundant casts") { val plan = Project( Seq(Alias(Cast(AttributeReference("x", IntegerType)(), IntegerType), "y")()), testRelation ) val optimised = EliminateRedundantCasts(plan) val expected = Project( Seq(Alias(AttributeReference("x", IntegerType)(), "y")()), testRelation ) comparePlans(optimised, expected) } ``` ## Performance Considerations ### Efficient Tree Traversal - **Use tree patterns**: Tree patterns enable efficient pattern matching without full traversal. - **Cache results**: Use lazy vals for expensive computations like `outputSet` and `references`. - **Minimise transformations**: Only transform nodes that need changes. - **Use `fastEquals`**: For quick equality checks without deep comparison. ### Code Generation Best Practices - **Minimise virtual calls**: Generated code should avoid virtual method calls in tight loops. - **Use primitive types**: Avoid boxing/unboxing in generated code. - **Inline small functions**: Inline simple operations for better performance. - **Batch operations**: Process multiple rows or columns together when possible. ### Rule Application Strategies - **Order rules carefully**: Place more common optimisations first. - **Use Once for expensive rules**: Cost-based optimisation should run once. - **Set appropriate iteration limits**: Balance optimisation quality with compile time. - **Track ineffective rules**: Use rule tracking to skip rules that won't apply. ## Common Pitfalls 1. **Forgetting nullability**: Always handle null values in expressions. 2. **Incorrect type coercion**: Ensure type casts are valid and safe. 3. **Infinite rule loops**: Rules must converge to a fixed point. 4. **Breaking plan invariants**: Maintain output schema and semantics. 5. **Leaking state**: Expressions should be stateless unless explicitly marked. 6. **Inefficient traversal**: Use tree patterns and targeted transformations. 7. **Missing code generation**: Implement `doGenCode` for custom expressions. 8. **Incorrect attribute resolution**: Use proper resolution with qualifiers. ## Additional Resources - **Source code**: `sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/` - **Tests**: `sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/` - **Spark SQL documentation**: Official Spark SQL programming guide - **Research paper**: "Spark SQL: Relational Data Processing in Spark" (SIGMOD 2015) ## Summary The Catalyst framework provides: - **Tree-based representation** for queries and expressions - **Rule-based transformation** framework for optimisation - **Extensibility** for custom expressions and optimisations - **Code generation** for high-performance execution - **Type-safe** API for query manipulation Key extension points: - Custom expressions (extend `Expression`) - Custom optimisation rules (extend `Rule[LogicalPlan]`) - Custom analysis rules (extend `Rule[LogicalPlan]`) - Custom data sources (implement `TableProvider`) - Custom aggregate functions (extend `AggregateFunction`) When working with Catalyst, focus on immutability, type safety, and efficient tree traversal patterns.