什么是下推
比如以下的sql
select name from mysql.config.user_info where id = 123
简单看起来,将sql直接交给mysql执行,presto拿最终的返回结果就行了。
不过,presto最大的特性在于跨源,不同数据源的sql语法可能会有细微差异(比如在支持的函数上)。所以,presto需要实现自己的一套sql语法,直接将sql交给mysql肯定是不行了。
而且,在跨源场景下的一些复杂sql(比如不同数据源里表的join),肯定是需要对sql进行解析,并将不同的部分交给不同的数据源去执行的。
比如以下的sql,涉及两个mysql实例,从mysql1里和mysql2里关联查数据。
select a.id, a.count
from
(select id, count
from mysql1.statis.table_a
where count > 100) a
left join
(select id
from mysql2.config.table_b
where create_time >= '2021-01-01') b
on a.id = b.id
where b.id is null
对于该sql,有两种执行的想法。如下图所示。
左边的是一个带着优化思路的正常想法。而右边的思路,对于sql解析的程序实现上,却是较为规范统一的。
实际上,presto是先按照右图的思路,根据sql生成执行计划。此时,where count > 100仍然是在presto服务上执行的。之后,presto会对执行计划进行优化,将where count > 100推给mysql执行。这就是所谓的“下推”了。
下推之后,presto的实际执行其实就符合左图的思路了。其实,下推的不仅仅是where count > 100,还包含select id, count。这样,mysql就只需要返回过滤后的id和count,而不是所有字段。
下推的源码执行
sql的执行简单说起来可以是这样
- coordinator将presto的sql解析成一系列的内部数据(如语法树、执行计划)
- worker在加载mysql数据的时候,又根据这些内部数据重新拼接成mysql的sql,交给mysql去执行
我们以最开始的这个简单例子,粗略看下有关下推的代码执行(依据的presto版本是0.213,比较老,只挑选了部分代码以简化展示)。
select name from mysql.config.user_info where id = 123
- 生成和优化执行计划
//presto-main
class LogicalPlanner {
public Plan plan(Analysis analysis, Stage stage) {
PlanNode root = planStatement(analysis, analysis.getStatement()); //根据语法树生成执行计划
for (PlanOptimizer optimizer : planOptimizers) { //遍历所有的优化器,优化执行计划
root = optimizer.optimize(root, ...);
}
}
优化前root包含的TableScanNode节点如下:
优化后root包含的TableScanNode节点如下:
可以看到,优化是会直接修改执行计划树的。
- 在worker上组装sql
//presto-base-jdbc
class QueryBuilder {
public PreparedStatement buildSql(...) {
StringBuilder sql = new StringBuilder()
sql.append("SELECT ")
...
List<String> clauses = toConjuncts(columns, tupleDomain, accumulator) //这里的tupleDomain就是执行计划树root中的那个tupleDomain
if (!clauses.isEmpty()) {
sql.append(" WHERE ").append(Joiner.on(" AND ").join(clauses))
}
PreparedStatement statement = client.getPreparedStatement(connection, sql.toString())
statement.setInt(...)
}
}