iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Rust

DataFusion 闖關攻略:30 天學習 Rust 查詢引擎之旅系列 第 13

Day 13: LogicalPlan 深入理解 Part 2 - 表達式系統

  • 分享至 

  • xImage
  •  

前言

昨天我們了解了 LogicalPlan 的設計模式,包括其 Enum 結構和各種變體。今天我們要探討構成 LogicalPlan 的主要元件表達式系統(Expression System)。對一個查詢而言, 轉換成 LogicalPlan 的意義是提供一個樹狀結構來解析 SQL 的語意。但每一個 LogicalPlan 節點的內容都是由大量的表達式所組成,這些表達式定義了具體要執行什麼計算。

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) 。表達式樹是一種特殊的二元樹資料結構,通常用來表示數學或邏輯表達式。它的結構特點如下:

  • 葉節點 (Leaf Nodes):存放操作數 (operands),如常數、變數
  • 內部節點 (Internal Nodes):存放操作符 (operators),如 +, -, *, /, AND, OR, =, > 等

例如一個複雜的 WHERE 條件:

WHERE (age > 18 AND status = 'active') OR (vip_level >= 3 AND last_login > '2023-01-01')

按照上述規則可以建構成以下表達式樹:

Untitled-2024-02-27-2300.png

基於樹的特性,我們可以直接透過後序遍歷 (Post-order) 得到正確的計算順序,也不需要額外的表來維護計算的順序。而且子樹有事一個完整的表達式樹,便於遞迴處理和分析。

Expr 的主要變體

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

支援的運算符包括:

  • 算術運算符:+, -, *, /, %
  • 比較運算符:=, !=, <, <=, >, >=
  • 邏輯運算符:AND, OR
  • 字串運算符:||(字串連接)
  1. ScalarFunction (純量函數): 純量函數對每一行數據產生一個結果值
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")]  // 參數
);

DFSchema 介紹

當我們了解表達式系統時,可能會浮現一個問題:如何讓表達式知道它所引用的欄位資訊? 答案就是 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 ,

參考資料

  1. DataFusion Expression Documentation
  2. Apache Arrow Schema Documentation
  3. DataFusion DFSchema Source Code
  4. Working with Exprs Guide

上一篇
Day 12: LogicalPlan 深入理解 Part 1 - 設計模式
下一篇
Day 14 優化器框架深層解析 - 從設計哲學到實作細節
系列文
DataFusion 闖關攻略:30 天學習 Rust 查詢引擎之旅14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言