SQL Parsing Framework - Calcite

Liao Jiayi Liao Jiayi

Calcite, a widely used general-purpose SQL parsing framework.

Translated from Chinese with AI · Read the original

Calcite: A General-Purpose SQL Parsing Framework

While studying Flink, I encountered Calcite in flink-table. When I first entered big data a year ago, Hive SQL made me curious about SQL parsing, but the many unfamiliar concepts overwhelmed me and I abandoned Calcite. It now seems time for a proper review.


Background

Anyone working with data processing is familiar with SQL. Calcite is a widely used SQL processor in major open-source projects including Hive, Flink, and Beam.
Many posts about SQL parsing, such as Spark Catalyst, describe this process: SQL parsing workflow The same process applies to Calcite. Read on.


Quick Overview

First, complete SQL execution code in four steps:

  1. Parse SQL
  2. Convert SqlNodes
  3. Optimize
  4. Execute
// Convert query to SqlNode
String sql = "select price from transactions";
Config config = SqlParser.configBuilder().build();
SqlParser parser = SqlParser.create(sql, config);
SqlNode node = parser.parseQuery();

SqlParser turns a SQL statement into a tree of SqlNodes. JavaCC implements this from the Parser.jj template, so there is little handwritten code. See JavaCC Help.

// Convert SqlNode to RelNode
VolcanoPlanner planner = new VolcanoPlanner();
RexBuilder rexBuilder = createRexBuilder();
RelOptCluster cluster = RelOptCluster.create(planner, rexBuilder);
SqlToRelConverter converter = new SqlToRelConverter(...);
RelRoot root = converter.convertQuery(node, false, true);

SqlToRelConverter converts the SQL tree into Calcite RelNodes. Both are tree structures but have different meanings. SqlNodes include expressions such as MIN/MAX and relational constructs such as SELECT/JOIN. Conversion separates these into relational RelNodes and expression RexNodes.

// Optimize RelNode
RelNode optimized = planner.findBestExp();

Rules optimize RelNodes. Calcite planners can be rule-based or cost-based; the more complex cost-based implementation is discussed below.

// Execute
Interpreter interpreter = new Interpreter(dataContext, optimized);
interpreter.enumerator();

The optimized plan executes code according to each RelNode type. TableScan, for example, runs table-scanning code.


SqlNode Conversion

In SqlToRelConverter, the entry point is convertQuery.

public RelRoot convertQuery(SqlNode query, final boolean needsValidation, final boolean top) {
if (needsValidation) {
query = validator.validate(query);
}
RelNode result = convertQueryRecursive(query, top, null).rel;
....
}

convertQueryRecursive traverses the query. A series of visit methods converts SqlNodes directly into RexNodes, as shown: visit methods visit(SqlLiteral), for example, creates different RexNodes depending on the type: sqlliteral RexNodes are then assembled into RelNodes according to SqlNode.getKind(), for example Select -> Project.


Optimization

Planner.findBestExp() performs optimization using customizable strategies. Calcite provides two planners:

  • HepPlanner: repeatedly applies rules to the RelNode tree until no optimization remains.
  • VolcanoPlanner: uses rules and costs with stochastic gradient descent until each improvement becomes small.

For example, VolcanoPlanner follows this logic:

  1. Register RelNodes. When a node matches a rule, create a RuleCall and add it to ruleQueue for later optimization.
  2. Check whether cost dropped by 10 percent from the previous optimization; continue if so, otherwise exit.
  3. Retrieve a RuleCall from ruleQueue and optimize.
  4. Rebuild RelRoot and update its cost.
  5. Repeat from step 2.
  6. Exit.

VolcanoPlanner is relatively cumbersome because cost calculations must be implemented for different stores, so most big-data frameworks use rule-based planning. Optimization involves many details, including stochastic-gradient-descent controls, iteration limits, and internal RelNode replacement. One section cannot explain it fully; interested readers can inspect VolcanoPlanner.


Execution

Execution defines code implementations for different node types and proceeds upward from the lowest RelNodes, receiving data through sources and sending it through sinks. Flink’s translate functions provide a similar mechanism. Node

Calcite Source Terminology

Name Meaning Role
SqlNode Node in the SQL tree Converted into RelNode by SqlToRelConverter
RexNode Expression RexLiteral is a constant such as “123”; RexCall is a function expression such as cast(xx as xx)
RelNode Relational expression (operation) Common in plans, such as Project, Join, Aggregate
RelSubset RelNodes sharing a trait
RelSet Collection of RelSubsets
RelTrait Trait A RelNode property, such as RelCollation representing ordering in Project
TraitDef Trait definition Defines operations associated with a trait
Convention Conversion trait Used to convert RelNodes; examples include SparkConvention and FlinkConvention
Literal Constant
Planner SQL planner Parsing, optimization, and execution
Program Program Can be constructed from rules and serves a role similar to Planner

Jiayi Blog