JDBC 外部表查询源码解析
导读:欢迎来到 StarRocks 源码解析系列文章,我们将为你全方位揭晓 StarRocks 背后的技术原理和实践细节,助你逐步上手这款明星开源数据库产品。本期 StarRocks 源码解析将介绍 JDBC 外部表查询源码解析
概述
StarRocks 支持通过 JDBC 接口访问外部表,外部表无需迁移数据,即可通过 StarRocks 完成查询。JDBC 外部表查询对数据库的类型没有过多限制,只要外部数据库提供 JDBC Driver 即可,像常见的MySQL/PostgreSQL/Oracle 等都可以同时查询。本文主要介绍 JDBC 外部表相关的实现。
相关概念
JDBCResource
用来描述 JDBC 资源相关的信息,由用户通过 DDL 创建,语法如下:
create external resource [RESOURCE_NAME]
properties (
"type"="jdbc",
"user"="user",
"password"="password",
"jdbc_uri"="jdbc:postgresql://127.0.0.1:5432/jdbc_test",
"driver_url"="https://repo1.maven.org/maven2/org/postgresql/postgresql/42.3.3/postgresql-42.3.3.jar",
"driver_class"="org.postgresql.Driver"
);
JDBCResource 包含几个重要的属性:
- type:描述 resource 的类型,必须指定为 jdbc
- user:访问外部表的用户名
- password:访问外部表的密码
- jdbc_uri:JDBC connection URL,需要满足对应数据库 connection URL 的语法
- driver_url:JDBC Driver 的 http下载链接
- driver_class:JDBC Driver Class 名字,每个数据库的 driver class 都不一样,需要按需设置
JDBCTable
描述外部表的元信息,由用户通过 DDL 创建,语法如下:
create external table [TABLE_NAME] (
(column_definition1[, column_definition2, ...]
) ENGINE=jdbc PROPERTIES (
"resource"="resource_name",
"table"="external_table_name"
);
JDBC 外部表的 column_definition 和普通的 OLAP 表一致,JDBC 外部表不支持索引,也不支持指定分区规则,建表时需要指定两个必要的 property:
- resource:资源名称,和上述创建资源时指定的 RESOURCE_NAME 相同
- table:对应的外部表表名
执行流程
下面详细介绍每个执行流程的实现细节
创建资源
JDBCResource 继承 Resource,用来描述资源相关信息,定义在
JDBCResource.java
创建资源时,首先根据 ResourceType 确定资源类型,然后通过 setProperties 设置属性,这部分实现在
Resource::fromStmt
public static Resource fromStmt(CreateResourceStmt stmt) throws DdlException {
Resource resource = null;
ResourceType type = stmt.getResourceType();
switch (type) {
case SPARK:
resource = new SparkResource(stmt.getResourceName());
break;
case HIVE:
resource = new HiveResource(stmt.getResourceName());
break;
case ICEBERG:
resource = new IcebergResource(stmt.getResourceName());
break;
case HUDI:
resource = new HudiResource(stmt.getResourceName());
break;
case ODBC_CATALOG:
resource = new OdbcCatalogResource(stmt.getResourceName());
break;
case JDBC:
resource = new JDBCResource(stmt.getResourceName());
break;
default:
throw new DdlException("Unsupported resource type: " + type);
resource.setProperties(stmt.getProperties());
return resource;
setProperties 阶段,JDBCResource 会对每个需要的 property 进行校验,尝试从 driver_url 下载 JDBC Driver 用来计算校验和并保存起来
setProperties 阶段,JDBCResource 会对每个需要的 property 进行校验,尝试从 driver_url 下载 JDBC Driver 用来计算校验和并保存起来
protected void setProperties(Map<String, String> properties) throws DdlException {
Preconditions.checkState(properties != null);
for (String key : properties.keySet()) {
if (!DRIVER_URL.equals(key) && !URI.equals(key) && !USER.equals(key) && !PASSWORD.equals(key)
&& !TYPE.equals(key) && !NAME.equals(key) && !DRIVER_CLASS.equals(key)) {
throw new DdlException("Property " + key + " is unknown");
configs = properties;
checkProperties(DRIVER_URL);
checkProperties(DRIVER_CLASS);
checkProperties(URI);
checkProperties(USER);
checkProperties(PASSWORD);
computeDriverChecksum();
创建外部表
JDBCTable 继承 Table,描述 JDBC 外部表的相关信息,定义在
JDBCTable.java
建表的过程比较简单,这里不需要赘述,直接看
Catalog::createTable
的实现即可
需要注意的是,建表阶段,数据类型只要满足 StarRocks 的语法即可创建成功,不会去校验外表中的数据类型是否匹配,这个是在查询阶段处理。
查询外部表
生成查询计划
描述 JDBCScan 的逻辑计划和物理计划分别定义在
LogicalJDBCScanOperator
和
PhysicalJDBCScanOperator
,在查询优化阶段,前者通过
JDBCScanImplementationRule
转换成后者。
为了尽量减少外部表和 StarRocks 之间传输的数据量,尽可能提升查询性能,我们会尝试将谓词和 limit 下推到外表。如果某些外部表的列只出现在谓词中而没有出现在投影中,那么也会对相应的列进行裁剪。
limit 下推使用的规则是
MergeLimitDirectRule
,列裁剪的规则是
PruneScanColumnRule
。
需要重点关注的是谓词下推策略,因为 StarRocks 支持的函数和运算符在外部表中不一定支持,如果它们出现在predicate 中,直接下推给外表的话会出现外表无法处理的查询。为了处理这种情况,StarRocks 采用了非常保守的策略,只尝试下推最简单的二元比较运算,in 和 between...and...运算符,其他的函数和运算符不会下推,由StarRocks 自己处理。
以下面两个查询为例,第一个查询中两个谓词都可以下推,而第二个查询中,ends_with 是个函数,不满足下推的条件,所以外表查询中的谓词只有 c_custkey > 20。
mysql> explain select * from customer where c_custkey > 10 and c_mktsegment in ('AUTOMOBILE', 'HOUSEHOLD') limit 10;
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Explain String |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| PLAN FRAGMENT 0 |
| OUTPUT EXPRS:c_custkey | c_name | c_address | c_city | c_nation | c_region | c_phone | c_mktsegment |
| PARTITION: UNPARTITIONED |
| RESULT SINK |
| 0:SCAN JDBC |
| TABLE: `customer` |
| QUERY: SELECT c_custkey, c_name, c_address, c_city, c_nation, c_region, c_phone, c_mktsegment FROM `customer` WHERE (c_custkey > 10) AND (c_mktsegment IN ('AUTOMOBILE', 'HOUSEHOLD')) |
| limit: 10 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
10 rows in set (0.01 sec)
mysql> explain select * from customer where ends_with(c_name, "25")=true and c_custkey > 20 limit 10;
+-------------------------------------------------------------------------------------------------------------------------------------------+
| Explain String |
+-------------------------------------------------------------------------------------------------------------------------------------------+
| PLAN FRAGMENT 0 |
| OUTPUT EXPRS:c_custkey | c_name | c_address | c_city | c_nation | c_region | c_phone | c_mktsegment |
| PARTITION: UNPARTITIONED |
| RESULT SINK |
| 1:SELECT |
| | predicates: ends_with(c_name, '25') = TRUE |
| | limit: 10 |