昨天我們了解了 LogicalPlan 的設計模式,包括其 Enum 結構和各種變體。今天我們要探討構成 LogicalPlan 的主要元件表達式系統(Expression System)。對一個查詢而言, 轉換成 LogicalPlan 的意義是提供一個樹狀結構來解析 SQL 的語意。但每一個 LogicalPlan 節點的內容都是由大量的表達式所組成,這些表達式定義了具體要執行什麼計算。
在 DataFusion 中,幾乎每個 LogicalPlan 節點都包含表達式。以一個 SQL 查詢為例:
SELECT
customer_id,
UPPER(customer_name) as name,
SUM(order_amount) as total_amount
FROM orders
WHERE order_date >= '2023-01-01'
GROUP BY customer_id, customer_name
HAVING SUM(order_amount) > 1000
ORDER BY total_amount DESC
這個查詢會產生如下的邏輯計劃結構,每個節點都包含相應的表達式:
// Sort 節點
Sort {
expr: vec![
col("total_amount").sort(false, true) // DESC 排序表達式
],
input: Aggregate {
// Aggregate 節點
group_expr: vec![
col("customer_id"), // 分組表達式
col("customer_name") // 分組表達式
],
aggr_expr: vec![
sum(col("order_amount")) // 聚合表達式
],
having: Some(
sum(col("order_amount")).gt(lit(1000)) // HAVING 條件表達式
),
input: Projection {
// Projection 節點
expr: vec![
col("customer_id"), // 欄位選擇表達式
upper(col("customer_name")).alias("name"), // 函數調用表達式 + 別名
col("order_amount") // 欄位選擇表達式
],
input: Filter {
// Filter 節點
predicate: col("order_date").gt_eq(lit("2023-01-01")), // WHERE 條件表達式
input: TableScan { ... }
}
}
}
}
表達式的樹狀結構
每個表達式本身也是是一個樹狀結構,稱之為表達式樹 (expression tree) 。表達式樹是一種特殊的二元樹資料結構,通常用來表示數學或邏輯表達式。它的結構特點如下:
例如一個複雜的 WHERE 條件:
WHERE (age > 18 AND status = 'active') OR (vip_level >= 3 AND last_login > '2023-01-01')
按照上述規則可以建構成以下表達式樹:
基於樹的特性,我們可以直接透過後序遍歷 (Post-order) 得到正確的計算順序,也不需要額外的表來維護計算的順序。而且子樹有事一個完整的表達式樹,便於遞迴處理和分析。
Expr
是一個龐大的 Enum,包含了數十種不同的表達式類型。讓我們來看看最重要的幾類:
pub enum Expr {
// 基礎表達式
Column(Column), // 欄位引用
Literal(ScalarValue, Option<FieldMetadata>), // 常數值
Alias(Alias), // 別名表達式
// 運算表達式
BinaryExpr(BinaryExpr), // 二元運算
Negative(Box<Expr>), // 算術負號
// 邏輯判斷
Not(Box<Expr>), // 邏輯非
IsNull(Box<Expr>), // NULL 判斷
IsNotNull(Box<Expr>), // NOT NULL 判斷
Between(Between), // BETWEEN 範圍判斷
// 函數調用
ScalarFunction(ScalarFunction), // 純量函數
AggregateFunction(AggregateFunction), // 聚合函數
WindowFunction(Box<WindowFunction>), // 視窗函數
// 類型轉換
Cast(Cast), // 強制類型轉換
TryCast(TryCast), // 安全類型轉換
// 子查詢
ScalarSubquery(Subquery), // 純量子查詢
Exists(Exists), // EXISTS 子查詢
InSubquery(InSubquery), // IN 子查詢
// 其他
Case(Case), // CASE WHEN 條件表達式
// ... 還有更多變體
}
1. Column (欄位引用): 欄位引用是最基本的表達式類型,用來引用資料表中的某個欄位:
2. Literal (常數): 常數表達式代表固定的值,DataFusion 中所有常數都是強型別的:
3. BinaryExpr (二元表達式): 二元表達式是最常見的運算表達式,包含左運算元、運算符和右運算元
pub struct BinaryExpr {
/// 左運算元
pub left: Box<Expr>,
/// 運算符
pub op: Operator,
/// 右運算元
pub right: Box<Expr>,
}
// 創建二元表達式的例子
let expr = col("age").gt(lit(18)); // age > 18
let arithmetic = col("price").plus(col("tax")); // price + tax
支援的運算符包括:
pub struct ScalarFunction {
/// 函數定義
pub func: Arc<crate::ScalarUDF>,
/// 函數參數
pub args: Vec<Expr>,
}
// 例如:UPPER(customer_name)
let upper_func = ScalarFunction::new_udf(
upper_udf, // 函數定義
vec![col("customer_name")] // 參數
);
當我們了解表達式系統時,可能會浮現一個問題:如何讓表達式知道它所引用的欄位資訊? 答案就是 Schema 啦! Schema 是 Arrow 的主要功能之一,用來定義資料在記憶體中的格式和資料型態,有點類似我們平常使用 ORM 定義資料庫表格的 model。
當們使用 DataFusion 註冊一個資料來源時,時際上背後就建立了這些資料的 Schema ,供
後續 logical plan 建立和優化時使用。然而在面對比較複雜的查詢條件時,Arrow schema 就很難完整支援了。舉例來說,當需要多表查詢時:
SELECT orders.customer_id, customers.customer_id, customers.name
FROM orders
JOIN customers ON orders.customer_id = customers.id
兩個表格如果都有相同的欄位名稱,那合併後的 schema 應該指向哪個欄位
// 兩個表都有同名欄位
let orders_schema = Schema::new(vec![
Field::new("customer_id", DataType::Int64, false),
Field::new("order_amount", DataType::Float64, false),
]);
let customers_schema = Schema::new(vec![
Field::new("id", DataType::Int64, false),
Field::new("customer_id", DataType::Int64, false),
Field::new("name", DataType::Utf8, true),
]);
// 合併後的 Schema
let merged_schema = Schema::new(vec![
Field::new("customer_id", DataType::Int64, false),
Field::new("order_amount", DataType::Float64, false),
Field::new("id", DataType::Int64, false),
Field::new("name", DataType::Utf8, true),
]);
let ambiguous_expr = col("customer_id");
所幸,你想到的問題 DataFusion 也想到了XD ,