4、增量查询基础语法:Spark SQL 与 DataFrame API 两种方式

语法这东西,说白了就是工具的使用说明书。你调优调得再好,如果连查询怎么写都不对,那一切都是白搭。我个人习惯把增量查询的语法分成两条路:一条是 Spark SQL,适合数据分析师和喜欢写 SQL 的同学;另一条是 DataFrame API,适合做工程的同学,灵活度更高。

今天我们就来把这两条路都走一遍。我会把我在项目中踩过的坑、用过的技巧,都揉进代码里。

4.1 核心前提:开启增量查询模式

不管用哪种方式,你都得先告诉 Hudi:“嘿,我要查增量数据了。” 这个开关就是 hoodie.datasource.query.type,默认值是 snapshot(快照读),我们要改成 incremental。

注意: 增量查询只支持 Copy-on-Write 表。如果你用的是 Merge-On-Read 表,增量查询会报错。我在项目中就吃过这个亏,当时查了半天才发现表类型不对。

4.2 方式一:Spark SQL 增量查询

Spark SQL 的方式最直观。你只需要在查询时指定一个 hoodie 表源,然后带上几个关键参数就行。

4.2.1 基本语法

-- 查询 hudi 表的增量数据
SELECT *
FROM hudi_table
WHERE `_hoodie_commit_time` > '20240101000000'
  AND `_hoodie_commit_time` <= '20240102000000'

嗯,这里要注意:_hoodie_commit_time 是 Hudi 自动维护的元数据字段,记录的是这条数据是在哪个 commit 里写入的。你直接用它做过滤条件,Hudi 就能自动识别出你要做增量查询。

4.2.2 使用 Spark SQL 的完整示例

-- 1. 注册 Hudi 表(假设表路径为 /user/hudi/orders)
CREATE TEMPORARY VIEW orders_hudi
USING org.apache.hudi
OPTIONS (
  path = '/user/hudi/orders',
  hoodie.datasource.query.type = 'incremental',
  hoodie.datasource.query.incremental.format = 'latest_state'
);

-- 2. 查询增量数据
SELECT order_id, order_status, update_time
FROM orders_hudi
WHERE `_hoodie_commit_time` > '20240101000000'
  AND `_hoodie_commit_time` <= '20240102000000'
  AND order_status = 'shipped';
小技巧: 参数 hoodie.datasource.query.incremental.format 有两个值:latest_state(默认)和 cdc。前者只返回最新状态,后者会返回变更前后的数据。如果你要做审计或追溯,用 cdc 更合适。

4.3 方式二:DataFrame API 增量查询

DataFrame API 更适合在代码里动态拼接查询条件。我一般在做 ETL 任务时用这种方式,因为可以灵活控制时间范围。

4.3.1 基本语法

import org.apache.hudi.DataSourceReadOptions
import org.apache.hudi.DataSourceWriteOptions
import org.apache.spark.sql.SparkSession

val spark = SparkSession.builder()
  .appName("HudiIncrementalQuery")
  .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
  .getOrCreate()

// 读取 Hudi 表的增量数据
val df = spark.read
  .format("org.apache.hudi")
  .option(DataSourceReadOptions.QUERY_TYPE.key(), 
          DataSourceReadOptions.QUERY_TYPE_INCREMENTAL_VAL)
  .option(DataSourceReadOptions.BEGIN_INSTANTTIME.key(), "20240101000000")
  .option(DataSourceReadOptions.END_INSTANTTIME.key(), "20240102000000")
  .load("/user/hudi/orders")

df.show()

4.3.2 参数详解

参数 Key 说明 默认值 我的建议
hoodie.datasource.query.type 查询类型:snapshot / incremental snapshot 增量查询时必须设为 incremental
hoodie.datasource.query.incremental.format 增量格式:latest_state / cdc latest_state 需要变更记录时用 cdc
hoodie.datasource.read.begin.instanttime 起始 commit 时间(包含) 无 必填,格式 yyyyMMddHHmmss
hoodie.datasource.read.end.instanttime 结束 commit 时间(包含) 最新 commit 可选,不填则查到最后
核心要点: 增量查询的时间范围是左闭右闭的。也就是说,begin_instanttime 和 end_instanttime 对应的 commit 数据都会被包含在结果中。

4.4 两种方式的对比

你可能会问:到底用哪种好?我个人的经验是:

  • Spark SQL:适合临时查询、数据分析、快速验证。写起来快,但参数配置不够灵活。
  • DataFrame API:适合生产环境、定时任务、动态参数。可以结合变量、循环、条件判断,灵活度更高。

举个例子,我在做实时数仓的增量同步时,都是用 DataFrame API。因为每天要同步的时间范围是动态计算的,用 SQL 的话每次都要改语句,太麻烦了。

4.5 避坑指南

我曾经在项目中遇到过几个坑,今天一并分享给你:

  • 时间格式必须严格:begin_instanttime 和 end_instanttime 必须是 yyyyMMddHHmmss 格式,少一位都不行。我试过传 20240101,结果 Hudi 直接报错。
  • 不要用 _hoodie_commit_time 做字符串比较:虽然它看起来是字符串,但 Hudi 内部是按时间戳处理的。如果你用 > '2024-01-01' 这种格式,可能会出问题。
  • 增量查询不支持跨表 JOIN:如果你需要 JOIN 其他表,建议先查出增量数据,再单独做 JOIN 操作。

4.6 知识体系图

下面这张图帮你理清增量查询的两种方式及其核心参数:

Hudi 增量查询语法体系 Hudi 增量查询语法体系 增量查询 Spark SQL 方式 DataFrame API 方式 CREATE TEMP VIEW WHERE 过滤 OPTIONS 配置 .format("hudi") .option() 链式 .load() 加载 核心参数 query.type = incremental begin_instanttime / end_instanttime

4.7 实战小贴士

最后,分享几个我在生产环境中用到的实用技巧:

  • 用变量控制时间范围:在 DataFrame API 中,把 begin_instanttime 和 end_instanttime 定义成变量,方便调试和复用。
  • 先查元数据再查数据:如果你不确定有哪些 commit 时间,可以先查 _hoodie_commit_time 的 distinct 值,确认范围后再做增量查询。
  • 增量查询 + 分区过滤:如果表有分区,建议同时加上分区过滤条件,能大幅减少扫描的数据量。
我的习惯: 在写增量查询之前,我会先跑一次快照查询,确认表结构和数据量。这样能避免因为表路径写错或者权限问题导致的尴尬。