- 数据分析
【免费下载链接】dplyr
dplyr: A grammar of data manipulation
join_by()是 dplyr 提供的一门用于描述表连接方式的"迷你领域特定语言"(domain specific language):它以表达式的方式声明左右两张表如何匹配,既支持最常见的等值连接(==),也支持不等式连接(>=、>、<=、<)、滚动连接(closest())与重叠连接(between()、within()、overlaps()),并可直接作为任意*_join()函数的by参数使用。读完本文,你将掌握join_by()的全部语法要素、列引用规则、底层解析与执行流水线,并理解其打印输出与错误消息的完整行为契约(以仓库中的快照测试为证),能够在销售时间序列、基因组区段等真实场景中正确构造复杂连接。
一、join_by()是什么:连接规范的语法核心
在 dplyr 中,left_join()、right_join()、inner_join()、full_join()、semi_join()、anti_join()等连接函数都通过by参数来决定"拿哪些列、按什么规则去匹配"。早期by只能接受字符向量或命名向量,仅支持等值匹配;dplyr 在连接机制全面重构时(见 NEWS.md 中关于 joins overhaul 的条目,灵感来自 data.table 的 join 语法,涉及 issue #5914、#5661、#5413、#2240)引入了join_by(),把by升级为可编程的规范对象。
从源码看,join_by()的核心实现位于 R/join-by.R:
join_by <- function(...) { exprs <- enquos(..., .named = NULL) exprs <- map(exprs, quo_squash) ... for (i in seq_len(n)) { bys[[i]] <- parse_join_by_expr(exprs[[i]], i, error_call = error_call) } new_join_by( exprs = exprs, condition = condition, filter = filter, x = x, y = y ) }它做了三件关键的事:
- 捕获原始表达式而不求值:使用
enquos()捕获...,再用quo_squash()将 quosure 递归压平为纯表达式,从而既能在用户环境中编程使用,又不依赖运行时求值; - 逐个解析:每个表达式交给
parse_join_by_expr()分类处理(等值、不等式、closest()、between()、within()、overlaps()); - 产出规范对象:
new_join_by()构造一个 class 为dplyr_join_by的列表,包含exprs(原始表达式)、condition(每个匹配条件对应的运算符)、filter(是否滚动及滚动方向)、x与y(两侧列名)四组平行向量。参见 R/join-by.R。
join_by()支持的连接类型(?join_by,见 man/join_by.Rd)包括:
| 类型 | 语法要素 | 典型用途 |
|---|---|---|
| 等值连接 Equality joins | ==,或直接写单列名 | 最常见,按键相等匹配 |
| 不等式连接 Inequality joins | >=、>、<=、< | 时间序列、基因组学 |
| 滚动连接 Rolling joins | closest(expr) | 无精确匹配时滚动取最近值 |
| 重叠连接 Overlap joins | between()、within()、overlaps() | 区段落在区间内/包含/相交 |
| 交叉连接 Cross joins | cross_join() | 全组合,由独立函数实现 |
其中等值、不等式、滚动、重叠连接由join_by()表达式直接描述,交叉连接通过cross_join()单独实现——这一点也体现在快照测试中:join_by()不带任何表达式时会直接报错,并提示改用cross_join()(见 tests/testthat/_snaps/join-by.md 首条快照)。
二、等值连接:==与单列名简写
等值连接要求左右两表在若干对列上取值相等。用join_by()表达等值连接有两种方式:
# 显式:x 表的 a 列 与 y 表的 b 列相等 join_by(a == b) # 简写:左右两侧列名相同时,只写一个名字即可 # join_by(x) 等价于 join_by(x == x) join_by(x)多列同时匹配时,在...中并列多个表达式即可,例如join_by(a == b, c == d)表示x$a匹配y$b且x$c匹配y$d;同名列可直接简写为join_by(a, c)。这一简写在源码中由parse_join_by_expr()处理:当表达式是单个符号或字符串时,自动改写为expr(!!expr == !!expr)(见 R/join-by.R)。
测试 tests/testthat/test-join-by.R 验证了等值条件的内部结构:
by <- join_by(x == y, a == b) # by$x -> c("x", "a") # by$y -> c("y", "b") # by$condition-> c("==", "==") # by$filter -> c("none", "none")列名既可以不带引号写成符号,也可以写成字符串(join_by("a", "b" == "c")),两者解析结果完全一致。
三、不等式连接:>=、>、<=、<
不等式连接按非等值条件匹配,常用于"某事件发生在另一个事件之前/之后"这类时间序列问题,以及基因组学的区间查询。用法与等值连接类似,把==换成不等式即可:
# 对每一行 sale_date,找出所有早于该销售的 promo_date join_by(id, sale_date >= promo_date)join_by()按位置约定表归属:条件左侧的列默认来自左表x,右侧的列默认来自右表y。所以sale_date >= promo_date意为x$sale_date >= y$promo_date。
⚠️ 需要注意:不等式连接会把x中的一行匹配到y中潜在的大量行,产生行数膨胀。官方文档(man/join_by.Rd)明确提醒:"Be extra careful when constructing inequality join specifications!"。同时,连接重构后keep默认值变为NULL:对等值条件等价于keep = FALSE,对不等式条件等价于keep = TRUE(保留两侧键),详见 NEWS.md。此外,keep = FALSE在存在非等值条件时是不允许的,join_cols()会直接报错(见 R/join-cols.R)。
四、滚动连接:closest()
滚动连接是不等式连接的变体:它同样基于不等式,但把结果限制为"最近的匹配",在精确匹配不存在时向前/向后滚动。用法是把一个不等式整体包进closest():
# 对每个 x 值,在 y 中找到"小于等于该 x 值"的最近值 join_by(closest(x >= y))关于closest()有三条关键语义:
- 内部不等式类型:
closest()的参数必须是一个使用>=、>、<=、<之一的不等式,不允许==(等值无"滚动"意义,源码在parse_join_by_closest()中专门拦截closest(a == b),见 R/join-by.R); - 主表固定为
x:无论不等式怎么书写,closest()总是以左表x为主表、在右表y中寻找最近匹配。例如closest(y$a >= x$b)恒被解释为closest(x$b <= y$a); - 滚动方向由运算符决定:
>=/>表示向前滚动取最大值,<=/<表示向后滚动取最小值。这在内部结构上体现为filter字段:测试 test-join-by.R 断言closest(a >= b)的filter为"max",closest(a <= b)的filter为"min",对应源码 R/join-by.R 中的switch映射。
closest()也可以与其他条件并列组合,例如同时要求滚动匹配与一个下界不等式(来自官方文档示例):
sales <- mutate(sales, sale_date_lower = sale_date - 1) by <- join_by(id, closest(sale_date >= promo_date), sale_date_lower <= promo_date) full_join(sales, promos, by)五、重叠连接:between()、within()、overlaps()
重叠连接是不等式连接的特例:涉及左表的一到两列"区间"与右表两列定义的区间之间的包含/相交关系。join_by()识别三个 helper,它们在底层都会展开为更简单的不等式组合。
5.1between(x, y_lower, y_upper, ..., bounds = "[]")
对左表的每个值x,找出所有落在右表区间[y_lower, y_upper]内的行。默认等价于x >= y_lower, x <= y_upper。
bounds参数控制上下界的开闭(是否包含端点),取值与展开结果对应如下(源码parse_join_by_between()中的switch,见 R/join-by.R,测试见 test-join-by.R):
bounds | 展开的不等式 | 含义 |
|---|---|---|
"[]"(默认) | x >= y_lower, x <= y_upper | 闭区间,两端都包含 |
"[)" | x >= y_lower, x < y_upper | 左闭右开 |
"(]" | x > y_lower, x <= y_upper | 左开右闭 |
"()" | x > y_lower, x < y_upper | 开区间,两端都不包含 |
5.2within(x_lower, x_upper, y_lower, y_upper)
对左表的每个区间[x_lower, x_upper],找出所有完全包含它的右表区间[y_lower, y_upper]。恒等价于x_lower >= y_lower, x_upper <= y_upper;within()没有bounds参数,因为不管区间开闭,展开结果的不等式都相同(见 R/join-by.R)。
5.3overlaps(x_lower, x_upper, y_lower, y_upper, ..., bounds = "[]")
对左表的每个区间,找出所有以任意方式与之相交的右表区间。默认等价于x_lower <= y_upper, x_upper >= y_lower。
overlaps()的bounds语义与between()略有不同:"[]"使用<=与>=,其余三种("[)"、"(]"、"()")都使用<与>,且生成的不等式完全相同。源码在parse_join_by_overlaps()中用一个closed布尔量统一处理(见 R/join-by.R),并在注释中说明这样设计是为了与between(bounds =)的参数名保持一致、便于记忆(issue #6504)。
这三个 helper 都要求区间"良构且非空":当按"[]"处理时需满足x_lower <= x_upper,否则需满足x_lower < x_upper(见 man/join_by.Rd)。
六、列引用规则:x$与y$显式指定表归属
默认情况下,join_by()按"左侧列来自x、右侧列来自y"的约定解析列名。但有时把条件两边的列写反更清晰(例如希望左表引用右表的列),这时可以用x$或y$前缀显式指定归属:
# 等价于默认写法:x 的 a 列 与 y 的 b 列相等 join_by(a == b) # 显式引用:x 的 a 列 与 y 的 b 列相等(语义相同,写法更明确) join_by(x$a == y$b) # 把"右表列"写在左边:y 的 a 列 与 x 的 b 列相等 join_by(y$a == x$b)显式引用会触发条件翻转:当左侧是y$列而右侧是x$列时,join_by()会把x/y列归位并反转运算符。源码中维护了一张翻转查找表(见 R/join-by.R):
lookup <- c("==" = "==", ">=" = "<=", ">" = "<", "<=" = ">=", "<" = ">")测试断言:join_by(y$a == x$a, y$a >= x$a, y$a > x$a, y$a <= x$a, y$a < x$a)的条件依次被归一化为c("==", "<=", "<", ">=", ">")(见 test-join-by.R)。closest()、between()、within()、overlaps()内同样支持x$/y$引用,且这些 helper 也接受命名参数写法(如between(a, y_upper = b, y_lower = c)、overlaps(y_lower = c, y_upper = d, x_lower = a, x_upper = b))。
显式引用必须在"同一表达式内引用不同表":二元表达式的左右两侧、between()的上下界对、within()/overlaps()的左右两侧上下界,都不能来自同一张表,否则会在解析阶段报错(详见第八节)。
七、打印输出与内部结构:快照测试确立的行为契约
dplyr_join_by对象有专属的打印方法(见 R/join-by.R):以Join By:为标题,每个表达式一行、以-前缀列出。快照测试 tests/testthat/_snaps/join-by.md 固定了这些输出,例如:
join_by(a, b) # Join By: # - a # - b join_by(a == a, b >= c) # Join By: # - a == a # - b >= c join_by(a == a, closest(b >= c), closest(d < e)) # Join By: # - a == a # - closest(b >= c) # - closest(d < e)两点值得注意:
- 字符串列名保留引号打印:
join_by("a", "b")打印为- "a"、- "b",join_by(a == a, b >= "c")打印为- b >= "c",与符号写法在输出上有区分; - 命名空间前缀原样保留:
join_by(dplyr::between(x, left, right))打印为- dplyr::between(x, left, right),即dplyr::前缀会在打印时保留(该能力在 #6838 引入,见 NEWS.md),但其内部展开的condition、filter、x、y与不带前缀的写法完全一致(测试allows for namespaced helpers验证了这一点)。
八、错误消息体系:几乎覆盖所有非法输入
join_by()的解析器对非法输入非常敏感,快照文件集中记录了全部错误契约。按类别梳理如下(每类均可在源码中找到对应抛出点):
8.1 顶层调用错误
| 输入 | 错误消息要点 | 源码位置 |
|---|---|---|
join_by()(空) | Must supply at least one expression.并提示改用cross_join() | R/join-by.R |
join_by(a = b) | Can't name join expressions.提示是否误用=而非== | R/join-by.R |
join_by(NULL) | Expressions can't be empty.(第 1 个表达式为空) | R/join-by.R |
join_by(1) | Each element of ... must be a single column name or a join by expression. | R/join-by.R |
join_by(foo(x > y))、join_by(x == y, x^y)、join_by(1()) | Expressions must use one of: ==, >=, >, <=, <, closest(), between(), overlaps(), or within(). | stop_invalid_top_expression(),见 R/join-by.R |
join_by(x + 1 == y)、join_by(x == y + 1) | Expressions can't contain computed columns,只能按名字或x$col/y$col引用列 | parse_join_by_name(),见 R/join-by.R |
最后一条对应官方文档的明确指引:join_by()不允许计算列,例如join_by(sales_date - 40 >= promo_date)是非法的——必须先mutate()生成新列再引用(见 man/join_by.Rd 与 NEWS.md)。
8.2$引用错误
| 输入 | 错误消息要点 |
|---|---|
join_by(x$a) | Can't use $ when specifying a single column name. |
join_by(z$a == y$b)、join_by(x$a == z$b) | The left-hand side of a $ expression must be either x$ or y$. |
join_by((x + 1)$y == b) | The left-hand side of a $ expression must be a symbol or string. |
join_by($(x) > y)(缺失 name 参数) | Expressions using $ can't contain missing arguments. |
8.3 同一表引用错误
| 输入 | 错误消息要点 |
|---|---|
join_by(x$a == x$b)、join_by(y$a == b)、join_by(closest(x$a >= x$b)) | The left and right-hand sides of a binary expression must reference different tables. |
join_by(between(x$a, x$a, x$b))等 | Expressions containing between()/within()/overlaps() can't all reference the same table. |
join_by(between(a, x$a, y$b)) | Expressions containing between() must reference the same table for the lower and upper bounds. |
join_by(within(x$a, y$b, y$a, y$b)) | ... must reference the same table for the left-hand side lower and upper bounds. |
join_by(overlaps(x$a, x$b, y$a, x$b)) | ... must reference the same table for the right-hand side lower and upper bounds. |
8.4 缺失参数错误
这类错误对"被包装的join_by()"尤其有用(快照中的nicely catches required missing arguments when wrapped案例):当fn <- function(x, y) join_by({{x}} == {{y}})只传入一个参数时,能精确报出缺失的是哪一侧的哪个参数:
Error: ! Expressions using `==` can't contain missing arguments. x Argument `y` is missing.同样的机制覆盖每个 helper:between(x)报Argument y_lower is missing,within(x)报Argument x_upper is missing,overlaps(x)报Argument x_upper is missing,closest()报Argument expr is missing。实现上由check_missing_arg()统一处理(见 R/join-by.R)。
8.5closest()专属错误
join_by(closest(a >= b, 1)):多余参数 →unused argument (1)(closest()只接受一个参数);join_by(closest(a == b)):The expression used in closest() can't use ==.;join_by(closest(x))、join_by(closest(1)):The first argument of closest() must be an expression.;join_by(closest(x + y)):The expression used in closest() must use one of: >=, >, <=, or <.(见stop_join_by_closest_invalid_expression(),R/join-by.R)。
8.6bounds与多余...错误
between()与overlaps()的bounds必须由check_bounds()校验(R/join-by.R),取值范围仅"[]"、"[)"、"(]"、"()"四种:
join_by(between(x, lower, upper, bounds = 1)) # Error: # ! `bounds` must be a string or character vector. join_by(between(x, lower, upper, bounds = "a")) # Error: # ! `bounds` must be one of "[]", "[)", "(]", or "()", not "a".两个 helper 的...是为未来扩展预留的,当前必须为空(check_join_by_dots_empty(),R/join-by.R):
join_by(between(x, lower, upper, foo = 1)) # Error: # ! `...` must be empty. # i Non-empty dots were detected inside `between()`.8.7 命名空间与by类型错误
join_by(dplyrr::between(x, left, right)):Expressions can only be namespace prefixed with dplyr::.(只允许dplyr::前缀,见stop_invalid_namespaced_expression(),R/join-by.R);as_join_by(FALSE):by must be a (named) character vector, list, join_by() result, or NULL, not FALSE.(as_join_by.default(),R/join-by.R)。
九、编程友好性:quosure、{{ }}拥抱与命名空间写法
尽管join_by()基于"纯表达式、不评估",但它专门支持整洁求值(tidy evaluation)的编程手法。源码注释说明(R/join-by.R):先enquos()捕获 quosure,再quo_squash()递归压平,从而允许{{ }}拥抱注入。测试 test-join-by.R 验证了多种场景:
fn <- function(x) { join_by({{ x }} == y) } fn("foo") # -> join_by("foo" == y) fn(a) # 表达式替换而非 quosure 求值,仍是 join_by(a == y) fn(!!a) # 可用 !! 内联求值,得到 join_by("foo" == y) # 也支持在 between() 中拥抱注入 fn <- function(x, top) { join_by(between({{ x }}, lower, {{ top }})) } fn(x, y) # -> join_by(between(x, lower, y))因此可以放心地把join_by()封装进自定义函数、用...透传,并通过{{ }}注入列名或整个表达式。从 #6838 起,helper 还可以写成dplyr::between(...)、dplyr::within(...)、dplyr::overlaps(...)、dplyr::closest(...)的命名空间形式,便于在调用环境中避免名称冲突。
十、by参数的其他形态与join_by_common()
join_by()结果只是by的合法形态之一。从as_join_by()泛型(R/join-by.R)可以看到by支持四种输入并被统一归一化为dplyr_join_by对象:
by形态 | 说明 |
|---|---|
join_by()结果 | 直接透传 |
| 字符向量 | 未命名元素按"两表同名"处理(by = c("a", "b")),命名元素按"x名 =y名"处理(by = c("x_a" = "y_a")) |
list(含x、y两个字符向量) | 编程式指定两侧列名 |
NULL | 自然连接:自动取两表公共列,并打印Joining with by = join_by(...)消息 |
NULL时由join_by_common()(R/join-by.R)自动求两表列名交集并构造等值规范,快照记录了其消息输出:
by <- join_by_common(c("x", "y"), c("x", "y")) # Message # Joining with `by = join_by(x, y)`对于需要反引号包裹的特殊列名(如_x、foo bar),消息会自动加反引号:Joining with by = join_by(_x, foo bar)。若两表没有任何公共列,则报错并要求显式提供by或使用cross_join()(快照末尾的join_by_common()错误案例)。
十一、连接执行流水线:从规范到行匹配
一条join_by()规范从构造到真正执行匹配,会依次经过以下环节(结合源码调用链):
- 构造:
join_by()解析表达式,产出dplyr_join_by(exprs/condition/filter/x/y); - 归一化:
*_join()内部把任意形态的by交给as_join_by()(NULL时先走join_by_common()),统一为dplyr_join_by; - 列定位:
join_cols()(R/join-cols.R)校验列存在性、唯一性,根据condition区分等值键与非等值键,决定输出列与keep/后缀处理;非等值条件下keep = FALSE被禁止; - 行匹配:
join_rows()(R/join-rows.R)把x、y键列与condition、filter一起交给vctrs::vec_locate_matches(),由底层 C 实现完成匹配、滚动过滤与multiple/unmatched/relationship策略处理; - 重组输出:按匹配到的行索引从两表取数并拼接。
从源码注释可见,condition与filter正是驱动底层匹配的两组参数:condition取"=="、">="、">"、"<="、"<"之一,filter取"none"(普通匹配)、"max"(向前滚动取最大)、"min"(向后滚动取最小)。compute_join_relationship()还根据condition决定是否检查 many-to-many 关系:只要存在任何非等值条件,就不做 many-to-many 检查,因为不等式与重叠连接天然容易产生多对多(见 R/join-rows.R)——这也是官方文档提醒"构造不等式连接要格外小心"的底层原因。
十二、实战示例:滚动连接与重叠连接
12.1 销售与促销:从等值到滚动
官方文档(man/join_by.Rd)用销售/促销数据完整演示了三层递进:
sales <- tibble( id = c(1L, 1L, 1L, 2L, 2L), sale_date = as.Date(c("2018-12-31", "2019-01-02", "2019-01-05", "2019-01-04", "2019-01-01")) ) promos <- tibble( id = c(1L, 1L, 2L), promo_date = as.Date(c("2019-01-01", "2019-01-05", "2019-01-02")) ) # 第 1 层:等值连接——id 匹配 id,销售日期匹配促销日期 by <- join_by(id, sale_date == promo_date) left_join(sales, promos, by) # 第 2 层:不等式连接——每个销售找所有早于它的促销 by <- join_by(id, sale_date >= promo_date) left_join(sales, promos, by) # 第 3 层:滚动连接——每个销售只找"最近一次"早于它的促销 by <- join_by(id, closest(sale_date >= promo_date)) left_join(sales, promos, by) # 若不允许精确匹配日期的促销(同日不算),改用 `>` by <- join_by(id, closest(sale_date > promo_date)) left_join(sales, promos, by)注意第四步:由于促销日期2019-01-05与销售日期同一天,改用>后它不再被视为"最近匹配"。这正是closest()中>=与>差异的直接体现。
12.2 基因组区段:between()、within()、overlaps()与bounds
用区段数据演示三种重叠语义的区别:
segments <- tibble( segment_id = 1:4, chromosome = c("chr1", "chr2", "chr2", "chr1"), start = c(140, 210, 380, 230), end = c(150, 240, 415, 280) ) reference <- tibble( reference_id = 1:4, chromosome = c("chr1", "chr1", "chr2", "chr2"), start = c(100, 200, 300, 415), end = c(150, 250, 399, 450) ) # 点的落入:segment 的 start 是否落在 reference 的 [start, end] 内 by <- join_by(chromosome, between(start, start, end)) full_join(segments, reference, by) # 完整包含:segment 区间完全落在 reference 区间内 by <- join_by(chromosome, within(x$start, x$end, y$start, y$end)) inner_join(segments, reference, by) # 任意相交:segment 区间与 reference 区间有任何重叠即匹配 by <- join_by(chromosome, overlaps(x$start, x$end, y$start, y$end)) full_join(segments, reference, by) # 右开区间 [):415 的端值不再与 415 的起始值重叠 by <- join_by(chromosome, overlaps(x$start, x$end, y$start, y$end, bounds = "[)")) full_join(segments, reference, by)这里特别展示了x$/y$显式引用的价值:在within()与overlaps()中显式写出x$start、y$start等,可以让表归属一目了然(官方文档称之为"makes your intentions clearer");bounds = "[)"则模拟了常见的半开区间(右端不包含)的生物学区间约定。
十三、注意事项与最佳实践
- 不要使用计算列:
join_by()只接受"列名 + 运算符 + 列名"的结构,join_by(sales_date - 40 >= promo_date)会报Expressions can't contain computed columns。正确做法是先用mutate()生成sales_date_lower这类列再参与连接(见 man/join_by.Rd 与 NEWS.md); - 警惕不等式连接的行数膨胀:一个
x行可能匹配y的大量行,构造条件前先想清楚基数关系;非等值条件下keep = FALSE不可用,且不会触发 many-to-many 警告,需要自行把握; closest()恒以x为主表:滚动语义始终是"在y中找x的最近匹配",需要换主表时把对应的表放在x位置;bounds只接受四种取值:"[]"、"[)"、"(]"、"()",并假设区间良构(闭区间要求lower <= upper,开区间要求lower < upper);- 生产中显式提供
by:省略by会触发自动公共列检测并打印消息,显式by = join_by(...)是官方推荐的规范写法(见 R/join.R 中by参数文档); - 复用与封装:
join_by()支持{{ }}拥抱与...透传,适合封装进自定义分析函数;helper 可加dplyr::前缀避免名称冲突(#6838)。
十四、延伸阅读
- R/join-by.R:
join_by()完整实现,包括所有解析函数与错误抛出点 - man/join_by.Rd:
?join_by官方文档与全部示例 - tests/testthat/_snaps/join-by.md:打印输出与错误消息的行为契约快照
- tests/testthat/test-join-by.R:结构断言、条件展开与编程用法测试
- R/join-rows.R 与 R/join-cols.R:行匹配与列组织的底层执行
- R/join-common-by.R:
by = NULL时自动公共列检测 - NEWS.md:
join_by()引入与连接重构的历史背景(#5914、#5661、#5413、#2240、#6838 等)
- 数据分析
【免费下载链接】dplyr
dplyr: A grammar of data manipulation
相关推荐
Polars 连接(Join)完全指南:等值连接、连接策略、非等值连接与 Asof 连接的实操详解
Polars 连接(Join)完全指南:等值连接、连接策略、非等值连接与 Asof 连接的实操详解 在 Polars 中,连接(join)是把一个或多个 Dat
数据分析大数据cuDF libcudf Column Join API 完全指南:从等值连接到条件连接的 GPU 连接算子
cuDF libcudf Column Join API 完全指南:从等值连接到条件连接的 GPU 连接算子 导读 本文围绕 cuDF(GPU DataFram
数据分析数据工程机器学习Apache Beam Schema 连接(Join)模式实战:基于 Row 类型系统的等值连接指南
Apache Beam Schema 连接(Join)模式实战:基于 Row 类型系统的等值连接指南 本文围绕 Apache Beam 官方文档中的 Schem
大数据批处理流处理数据工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考