问题 1:聚合子查询 vs LIMIT 1
两条不同的判定路径
| 路径 |
判定函数 |
适用场景 |
空表行为 |
| is_unique |
check_right_exprs_unique → check_stmt_unique
|
标量聚合(scalar group by)、唯一键等 |
聚合仍输出 1 行 |
| is_one_row |
check_right_table_output_one_row |
右表 ref_query 有 LIMIT 1 |
扫描无数据时输出 0 行 |
check_stmt_unique 对标量聚合有专门分支:
ob_transform_utils.cppLines 4437-4439
} else if (!ignore_group && stmt->is_scala_group_by() //scalar group by
&& ObOptimizerUtil::subset_exprs(select_exprs, exprs)) {
is_unique = true;
is_scala_group_by() 即无 GROUP BY 但有聚合项(如 COUNT(*))。
你的 COUNT 例子
SELECT * FROM t1
WHERE EXISTS (SELECT COUNT(*) FROM (SELECT * FROM t2) v WHERE t1.a = v.c1);
- 右表 ref_query 是
SELECT COUNT(*) ...,没有 LIMIT 1 → is_one_row = false
- 但有标量聚合 →
is_unique = true → 走第一条路径,不是 LIMIT 1 那条
等价改写语义上是:
SELECT * FROM t1
INNER JOIN (SELECT COUNT(*) c1 FROM (SELECT * FROM t2) sub) v ON t1.a = v.c1
当 t2 为空时:
- EXISTS:子查询仍返回 1 行(COUNT=0),再按
t1.a = v.c1 过滤
- INNER JOIN:右表同样 1 行,连接条件相同
因此 COUNT 场景在现有实现下是等价的,不会误用“LIMIT 1 空表 0 行”的逻辑。
你担心的不等价,发生在把 聚合的“恒 1 行” 和 LIMIT 1 扫描的“空则 0 行” 当成同一条件时;源码里它们是两条路径。
LIMIT 1 例子
SELECT * FROM t1
WHERE EXISTS (SELECT 1 FROM (SELECT * FROM t2 LIMIT 1) v WHERE t1.a = v.c1);
右表 ref_query 为 (SELECT * FROM t2 LIMIT 1),带 LIMIT 1 → is_one_row = true。t2 空时 EXISTS 与子查询均为 0 行,也等价。
注释与实现的差距
ob_transform_semi_to_inner.cppLines 973-976
/**
*/
check_right_table_output_one_row 只检查了 LIMIT 1,注释里的 unique_key = const 未实现:
ob_transform_semi_to_inner.cppLines 977-1001
int ObTransformSemiToInner::check_right_table_output_one_row(…)
{
…
if (right_table.is_generated_table()) {
… check_limit_value(…, 1, is_one_row, …) …
}
return ret;
}
潜在边界(HAVING 等)
标量聚合 + HAVING 过滤掉聚合行(如 HAVING COUNT(*) > 0 且表空 → 0 行)时,is_scala_group_by() 仍为 true,check_stmt_unique 仍可能判 is_unique = true,存在理论上的不等价风险。你举的纯 COUNT(*) 无 HAVING 不在此列。
问题 2:全是左表 filter + LIMIT 1,空表 t2 是否考虑?
判定条件
is_all_left_filter 表示 semi condition 只引用左表(如 t1.a > 10),不引用右表:
ob_transform_semi_to_inner.cppLines 405-409
} else if (left_table_set.is_superset(expr->get_relation_ids())) {
if (OB_FAIL(filter_conds.push_back(expr))) {
…
}
} else if (OB_FALSE_IT(is_all_left_filter = false)) {
实际改写(不是只推 filter)
do_transform 不会去掉右表,而是:
- 把 semi condition 下推到 WHERE
- 把右表加入 FROM(等价 INNER JOIN / 笛卡尔积)
- 对右表加 LIMIT 1
ob_transform_semi_to_inner.cppLines 1115-1136
} else if (!need_add_distinct) {
…
} else if (OB_FAIL(stmt.add_from_item(semi_info->right_table_id_, false))) {
…
if (OB_FAIL(ret) || !right_table_need_add_limit) {
} else if (OB_FAIL(ObTransformUtils::add_limit_to_semi_right_table(&stmt, ctx_, semi_info))) {
对你举的例子,semi-to-inner 阶段更接近:
SELECT * FROM t1, (SELECT 1 FROM t2 LIMIT 1) AS v
WHERE t1.a > 10;
不是 SELECT * FROM t1 WHERE t1.a > 10。若最终计划里看不到 t2,应是后续规则(如 join elimination)再做的,不是 semi-to-inner 本身。
空表 t2 的语义
| 场景 |
EXISTS 原语义 |
改写后(INNER JOIN + LIMIT 1) |
| t2 有数据 |
右表非空 → EXISTS 为真(若 t1.a > 10) |
右表 LIMIT 1 得 1 行,与 t1 连接 → 等价 |
| t2 无数据 |
EXISTS 为假 |
LIMIT 1 得 0 行,INNER JOIN 无结果 → 等价 |
add_limit_to_semi_right_table 对基础表会先包一层 inline view 再设 LIMIT 1,目的就是让“右表是否为空”决定连接是否有行,从而对齐 EXISTS 的“右表非空”语义。