1. 为什么你需要关注Calcite的内置函数?

如果你正在用Java处理数据,无论是从数据库里查,还是从文件、API甚至内存列表里读,你大概率写过一堆又臭又长的循环代码。比如,要算一组用户的平均年龄,你得遍历列表,累加,再除以总数;想把所有用户名转成小写,又得再来一个循环。代码写得多,还容易出错。

我自己在几年前的一个项目里就踩过这个坑。当时需要从多个CSV文件里聚合销售数据,先过滤,再分组求和,最后还要按时间排序。一开始老老实实用Java 8的Stream API写,代码写了上百行,逻辑绕来绕去,后来加个需求改起来都头疼。直到我把数据“喂”给Apache Calcite,用一句SQL就搞定了所有计算,那一刻我才真正体会到“声明式编程”的爽快感。

Apache Calcite,这个听起来有点学术味的名字,其实是个超级实用的数据管理框架。它的核心能力之一,就是让你能用标准的SQL(或者类SQL语言)去查询任何看起来像表结构的数据源,比如内存里的Java集合。而SQL的灵魂,除了SELECT ... FROM ... WHERE,就是丰富的内置函数。Calcite几乎完整实现了SQL标准中的函数,还额外加了不少“私货”。这意味着,你无需自己动手写复杂的统计、字符串处理或日期计算的逻辑,直接在你的查询语句里调用现成的函数就行。

这不仅仅是“少写代码”那么简单。更重要的是,它把数据处理逻辑从复杂的命令式代码(“怎么做”)提升到了声明式的层面(“想要什么”),让代码的可读性、可维护性大幅提升,也让团队里熟悉SQL的同事能更快地上手参与数据处理工作。

接下来,我就带你从最基础的查询开始,一步步实战Calcite的内置函数,看看如何用它们把繁琐的数据处理变得轻松简单。

2. 热身:先让你的Java对象“变身”可查询的表

在直接写SQL调用函数之前,我们得先把数据准备成Calcite能认识的样子。Calcite的世界里,一切数据源都是“表”。所以,我们的第一步就是把Java里的List<User>或者List<Person>,变成一张虚拟的数据库表。

这里就要请出Calcite的好搭档——Linq4j。你可以把它理解成Java版的LINQ(来自.NET世界),它提供了一套非常流畅的API,让你能以类似SQL链式调用的方式操作Java集合。但对我们来说,眼下更关键的是,它能把一个普通的List转换成Calcite查询引擎所需的Enumerable类型。

我举个例子,假设我们有一个Person类,有id、name、age三个字段。传统做法是写循环。用Linq4j可以这样:

import org.apache.calcite.linq4j.Enumerable;
import org.apache.calcite.linq4j.Linq4j;

List<Person> personList = Arrays.asList(
    new Person(1, "Alice", 28),
    new Person(2, "Bob", 35),
    new Person(3, "Charlie", 32)
);

// 魔法发生在这里:List 变成了 Enumerable
Enumerable<Person> enumerable = Linq4j.asEnumerable(personList);

// 现在可以用类似SQL的方式操作了
List<Person> result = enumerable
    .where(p -> p.getAge() > 30)          // 相当于 SQL WHERE age > 30
    .orderBy(p -> p.getName())            // 相当于 SQL ORDER BY name
    .toList();                            // 获取结果

result.forEach(p -> System.out.println(p.getName()));

但这只是Linq4j本身的用法。要让它和Calcite SQL协同工作,我们需要创建一个Table的实现类。别怕,代码很模板化,照着我下面这个TableForList类写,以后改改字段名就能复用。

import org.apache.calcite.DataContext;
import org.apache.calcite.linq4j.Enumerable;
import org.apache.calcite.linq4j.Linq4j;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.schema.ScannableTable;
import org.apache.calcite.schema.impl.AbstractTable;
import org.apache.calcite.sql.type.SqlTypeName;

public class TableForList extends AbstractTable implements ScannableTable {
    private List<Person> personList;

    public TableForList(List<Person> personList) {
        this.personList = personList;
    }

    @Override
    public Enumerable<Object[]> scan(DataContext root) {
        // 关键步骤:将List转换为Enumerable,并映射为Object[]数组(代表一行数据)
        return Linq4j.asEnumerable(personList)
                .select(p -> new Object[]{p.getId(), p.getName(), p.getAge()});
    }

    @Override
    public RelDataType getRowType(RelDataTypeFactory typeFactory) {
        // 定义表的“结构”:列名和列类型
        return typeFactory.builder()
                .add("id", typeFactory.createSqlType(SqlTypeName.BIGINT))
                .add("name", typeFactory.createSqlType(SqlTypeName.VARCHAR))
                .add("age", typeFactory.createSqlType(SqlTypeName.INTEGER))
                .build();
    }
}

这个类做了两件核心事:

  1. getRowType方法:告诉Calcite,我这表长啥样,有三列,分别是id(长整型)、name(字符串)、age(整型)。
  2. scan方法:当执行查询时,Calcite会调用这个方法获取数据。我们把List<Person>转换成了Enumerable<Object[]>,每个Object[]就是一行数据。

把数据源封装好后,剩下的就是配置Calcite并执行SQL了。这部分和上篇文章类似,我快速过一下关键代码:

// 1. 准备数据
List<Person> myData = ... // 你的数据列表
// 2. 创建表实例
TableForList myTable = new TableForList(myData);
// 3. 构建Calcite Schema(数据库模式)和连接
// ... (配置代码,通常使用Calcite的SchemaPlus和Framework)
// 4. 执行SQL
try (Connection conn = DriverManager.getConnection("jdbc:calcite:...");
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("SELECT name, age FROM mySchema.myTable WHERE age > 30")) {
    while (rs.next()) {
        System.out.println(rs.getString("name") + ": " + rs.getInt("age"));
    }
}

基础架子搭好了,数据已经“就位”。下面,我们就可以在SQL语句里,尽情调用Calcite提供的各种强大内置函数了。

3. 基础聚合:让数据自己“说话”

聚合函数是数据分析的“开胃菜”,也是使用最频繁的一类。它们能把多行数据汇总成一行有意义的统计结果。Calcite支持所有你熟悉的SQL聚合函数。

假设我们有一张员工表employee,有salary(薪资)和department(部门)字段。最常用的场景莫过于此:

-- 计算全体员工总数
SELECT COUNT(*) AS total_employees FROM employee;

-- 计算平均薪资(自动处理NULL值)
SELECT AVG(salary) AS average_salary FROM employee;

-- 找出最高和最低薪资
SELECT MAX(salary) AS max_salary, MIN(salary) AS min_salary FROM employee;

-- 计算薪资总和,这个在算总预算时特别有用
SELECT SUM(salary) AS total_salary_burden FROM employee;

但聚合函数的威力远不止于此。结合GROUP BY,我们能进行多维度的分析。比如,我想知道每个部门的薪资概况:

SELECT
    department,
    COUNT(*) AS headcount,
    AVG(salary) AS avg_salary,
    MIN(salary) AS min_salary,
    MAX(salary) AS max_salary,
    SUM(salary) AS dept_total
FROM employee
GROUP BY department;

一句SQL,就直接生成了一个部门的薪资报告。如果要用Java手写,光是分组逻辑和循环计算就够写一阵子了。

这里有个我踩过的“坑”值得提醒:对NULL值的处理。在Calcite(遵循SQL标准)里,COUNT(*)会计算所有行,包括NULL;但COUNT(column_name)会忽略该列为NULL的行。AVG、SUM等函数也会自动忽略NULL值。这通常是我们想要的,但如果你需要不同的行为(比如把NULL当0算),就得先用COALESCE函数处理一下。

-- 如果salary为NULL,则按0参与计算平均
SELECT AVG(COALESCE(salary, 0)) FROM employee;

聚合函数是构建数据洞察的基础。掌握了它们,你就已经能用SQL完成大部分简单的统计需求了。

4. 字符串处理:告别繁琐的拼接与截取

处理文本数据是另一个高频场景。用户输入、日志解析、数据清洗都离不开字符串操作。用Java的String方法不是不行,但一旦逻辑复杂,代码就显得很琐碎。Calcite的字符串函数能让这些操作在SQL层就优雅地完成。

连接与填充:CONCAT函数是最常用的,但它有个更简洁的替代品——||操作符。

SELECT name || ' (' || department || ')' AS employee_info FROM employee;
-- 结果:Alice (Engineering)

如果你想在字符串左侧或右侧填充字符到指定长度,LPAD和RPAD就派上用场了,比如生成固定宽度的报表。

大小写转换与修剪:数据清洗时,统一格式是第一步。

-- 统一转为小写,便于比较或存储
SELECT LOWER(email) AS normalized_email FROM user;
-- 统一转为大写,常用于显示或编码
SELECT UPPER(status) AS status_code FROM log;
-- 去掉字符串首尾的空格(非常实用!)
SELECT TRIM(name) AS clean_name FROM customer;

子串提取与查找:这是字符串处理中的“精细手术”。

-- 提取域名。SUBSTRING(string FROM start [FOR length])
SELECT SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain FROM user;
-- 查找位置。找不到返回0
SELECT POSITION('.' IN filename) AS dot_index FROM files;
-- 更灵活的查找,可以用LIKE或更强大的正则表达式函数(如果Calcite配置了相关扩展)

替换与分割:

-- 把所有的‘old’替换为‘new’
SELECT REPLACE(description, 'old', 'new') FROM products;
-- 分割字符串(需注意Calcite版本支持,或使用特定扩展)
-- 例如,一些版本支持 SPLIT(string, delimiter)

我印象最深的一次是处理一批用户上传的文件名,需要提取扩展名并统一改成小写。用Java写要循环、lastIndexOf、substring、toLowerCase,好几行代码。用Calcite SQL,一行搞定:

SELECT LOWER(SUBSTRING(filename FROM POSITION('.' IN filename) + 1)) AS ext FROM file_list;

这种表达上的简洁,极大地提升了开发效率和代码的可读性。

5. 数学计算与逻辑判断:在查询中完成复杂运算

别以为SQL只能做加减乘除。Calcite内置的数学函数库相当丰富,足以应对大多数业务计算。更重要的是,结合条件表达式,你可以在数据查询阶段就完成复杂的业务逻辑判断,减少后续代码处理的压力。

基础与进阶数学运算:

-- 绝对值,处理差值时常用
SELECT ABS(score1 - score2) AS diff FROM exam;
-- 四舍五入,财务计算必备
SELECT ROUND(total_amount, 2) AS rounded_amount FROM orders;
-- 幂运算和开方,比如计算面积、增长率
SELECT POWER(side, 2) AS area FROM square;
SELECT SQRT(area) AS side_length FROM square;
-- 对数,在分析指数增长数据时有用
SELECT LOG(10, view_count) AS log_views FROM articles;
-- 三角函数,处理地理空间或图形数据
SELECT SIN(radians), COS(radians) FROM angles;

条件逻辑函数:让查询更智能。这是将业务规则嵌入查询的利器。

  1. CASE WHEN表达式:功能最强大,相当于SQL里的if-else if-else。

    SELECT name,
           salary,
           CASE
               WHEN salary > 100000 THEN '高薪'
               WHEN salary > 60000 THEN '中等'
               ELSE '普通'
           END AS salary_level
    FROM employee;
    

    这个例子直接为每个员工打上了薪资等级的标签,输出结果可以直接用于报表。

  2. COALESCE函数:处理NULL值的首选。它返回参数列表中第一个非NULL的值。

    -- 优先取昵称,没有昵称再用本名
    SELECT COALESCE(nickname, real_name) AS display_name FROM users;
    -- 将NULL的销售额视为0参与计算
    SELECT SUM(COALESCE(sales, 0)) FROM regional_data;
    
  3. NULLIF函数:它接收两个参数,如果两者相等,则返回NULL,否则返回第一个参数。一个巧妙的用法是避免除零错误。

    -- 如果total_orders为0,则返回NULL,避免除零错误
    SELECT revenue / NULLIF(total_orders, 0) AS avg_order_value FROM stats;
    

把数学计算和条件逻辑放在SQL里做,最大的好处是下推计算。如果是连接远程数据库,这些计算会在数据库服务器端完成,只把最终结果传回应用,节省了网络传输和客户端计算资源。即使数据源在内存中,这种声明式的写法也让业务逻辑更集中、更清晰。

6. 玩转时间与日期:再也不用手动解析时间戳

时间和日期处理是编程中的常见痛点,时区、格式、计算都很麻烦。Calcite的日期时间函数遵循SQL标准,帮你把这些复杂问题抽象成简单的函数调用。

获取当前时间:这是最简单的入门函数。

SELECT
    CURRENT_DATE AS today,      -- 当前日期
    CURRENT_TIME AS now_time,   -- 当前时间
    CURRENT_TIMESTAMP AS now    -- 当前日期时间戳
FROM (VALUES (1)); -- 需要一个虚拟表来执行

日期时间的提取与构造:

-- 从时间戳中提取年份、月份、小时等
SELECT
    EXTRACT(YEAR FROM order_time) AS order_year,
    EXTRACT(MONTH FROM order_time) AS order_month,
    EXTRACT(DAY FROM order_time) AS order_day
FROM orders;
-- 如果你有一个表示日期的字符串,可以用标准格式解析它
-- 注意:具体解析函数可能因Calcite配置的方言而异,通常支持 CAST('2023-10-01' AS DATE)

日期的加减运算:这是最实用的功能之一。

-- 计算三天后的日期
SELECT CURRENT_DATE + INTERVAL '3' DAY AS future_date;
-- 计算一小时前的时间戳
SELECT CURRENT_TIMESTAMP - INTERVAL '1' HOUR AS one_hour_ago;
-- 计算两个日期之间的天数差
SELECT DATE_DIFF('DAY', start_date, end_date) AS duration_days FROM projects;

INTERVAL关键字用起来非常直观,你可以方便地加减年、月、日、时、分、秒。

日期格式化:如何把数据库里的时间戳变成用户友好的字符串?DATE_FORMAT或TO_CHAR函数(取决于SQL方言)是你的好帮手。

-- 假设你的Calcite支持类似MySQL的DATE_FORMAT
SELECT name,
       DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS formatted_time
FROM users;

这样,前端显示时就直接拿到了格式化的字符串,无需在Java里再用SimpleDateFormat转一遍。

在实际项目中,我常用日期函数来做数据分区和生成时间序列报告。比如,快速查询“上周每天的订单总数”:

SELECT
    DATE_TRUNC('DAY', order_time) AS order_day, -- 按天截断
    COUNT(*) AS daily_orders
FROM orders
WHERE order_time >= CURRENT_DATE - INTERVAL '7' DAY
GROUP BY DATE_TRUNC('DAY', order_time)
ORDER BY order_day;

DATE_TRUNC函数可以把时间戳截断到指定的精度(年、季、月、日、小时等),对于按时间维度聚合数据特别方便。

7. 进阶利器:窗口函数与自定义函数初探

当你熟练使用基础函数后,Calcite还有更强大的武器等你解锁——窗口函数。它允许你在不折叠数据行的前提下,进行跨行的计算,为每一行生成一个基于“窗口”内其他行的计算结果。

排名与序列:这是窗口函数最经典的应用。

SELECT
    department,
    name,
    salary,
    -- 为每个部门内的员工按薪资排名
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
    -- 计算每个部门内的薪资累计和
    SUM(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS running_total
FROM employee;

ROW_NUMBER、RANK、DENSE_RANK可以处理各种排名需求。SUM、AVG等聚合函数配合OVER子句,就变成了窗口聚合函数,能计算移动平均、累计求和等。

前后行对比:LAG和LEAD函数让你能轻松访问当前行前面或后面指定偏移量的行数据。

SELECT
    order_date,
    daily_revenue,
    -- 计算日环比:与前一天收入的差值
    daily_revenue - LAG(daily_revenue, 1) OVER (ORDER BY order_date) AS day_over_day_change
FROM daily_sales;

这个查询直接算出了每天收入的日环比变化,对于分析趋势至关重要,而用普通SQL或Java代码实现会非常笨拙。

自定义函数:如果内置函数还不够用怎么办?Calcite允许你注册自定义函数(UDF)。虽然这需要写一些Java代码,但过程并不复杂。基本步骤是:1)创建一个实现ScalarFunction接口的类,包含你的计算逻辑;2)在Calcite的Schema配置中注册这个函数。之后,你就可以在SQL里像调用内置函数一样使用它了。比如,你可以封装一个复杂的业务规则,或者一个Calcite尚未支持的特定算法。

窗口函数和自定义函数将Calcite SQL的表达能力提升到了一个新的高度。它们让你能够直接在数据查询层解决复杂的、需要上下文关联的分析问题,把原本需要在应用层用大量循环和状态变量才能实现的逻辑,用几句清晰的SQL描述出来。这不仅仅是代码量的减少,更是思维模式的升级——你开始更多地用集合和声明的视角来思考数据处理。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐