大数据SQL调优专题——Hive执行原理
引入
Apache Hive 是基于Hadoop的数据仓库工具,它可以使用SQL来读取、写入和管理存在分布式文件系统中的海量数据。在Hive中,HQL默认转换成MapReduce程序运行到Yarn集群中,大大降低了非Java开发者数据分析的门槛,并且Hive提供命令行工具和JDBC驱动程序,方便用户连接到Hive进行数据分析操作。
严格意义上,Hive并不属于计算引擎,而是建立在Hadoop生态之上的数据仓库管理工具。它将繁杂的MapReduce作业抽象成SQL,使得开发及维护成本大幅降低。得益于HDFS的存储和MapReduce的读写能力,Hive展现出了强大的兼容能力、数据吞吐能力和服务稳定性,时至今日依然是大数据架构中不可或缺的一部分。
Hive的核心特点
-
Hive是基于Hadoop的数仓工具,底层数据存储在HDFS中;
-
Hive提供标准SQL功能,支持SQL语法访问操作数据;
-
Hive适合OLAP数据分析场景,不适合OLTP数据处理场景,所以适合数据仓库构建;
-
HQL默认转换成MapReduce任务执行,也可以配置转换成Apache Spark、Apache Tez任务运行;
-
Hive中支持定义UDF、UDAF、UDTF函数扩展功能。
Hive的架构设计
Hive用户接口
访问Hive可以通过CLI、Beeline、JDBC/ODBC、WebUI几种方式。在Hive早期版本中可以使用Hive CLI来操作Hive,Hive CLI并发性能差、脚本执行能力有限并缺乏JDBC驱动支持,从Hive 4.x版本起废弃了Hive CLI推荐使用Beeline。Beeline是一个基于JDBC的Hive客户端,支持并发环境、复杂脚本执行、JDBC驱动等,在Hive集群内连接Hive可以使用Beeline方式。在Hive集群外,通过代码或者工具连接操作Hive时可以通过JDBC/ODBC方式。通过WebUI方式可以通过浏览器查看到Hive集群的一些信息。
HiveServer2服务
HiveServer2服务提供JDBC/ODBC接口,主要用于代理远程客户端对Hive的访问,是一种基于Thrift协议的服务。例如通过JDBC或者Beeline连接访问Hive时就需要启动HiveServer2服务,就算Beeline访问本机上的Hive服务也需要启动HiveServer2服务。
HiveServer2代理远程客户端对Hive操作时会涉及到操作HDFS数据,就会有操作权限问题,那么操作HDFS中数据的用户是启动HiveServer2的用户还是远程客户端的用户需要通过“hive.server2.enable.doAs” 参数决定,该参数默认为true,表示HiveServer2操作HDFS时的用户为远程客户端用户,如果设置为false表示操作HDFS数据的用户为启动HiveServer2的用户。
MetaStore服务
MetaStore服务负责存储和管理Hive元数据,为HiverServer2提供元数据访问接口。Hive中的元数据包括表的名字,表的列和分区及其属性,表的属性(表拥有者、是否为外部表等),表的数据所在目录等。
Hive MetaStore可以将元数据存储在mysql、derby数据库中。
Hive Driver
Driver中包含解释器(SQL Parser)、编译器(Compiler)、优化器(Optimizer),负责完成HQL查询语句从词法分析、语法分析、编译、优化以及查询计划的生成。生成的查询计划存储在HDFS中,并在随后有执行器(Executor)调用MapReduce执行。
对于Hive有了一个初步认识,我们下面开始梳理Hive的执行原理。
Hive的执行原理
Hive无论采用哪种调用方式,最终都会辗转到org.apache.hadoop.hive.ql.Driver类。SQL语句在Driver类中,通过Antlr框架进行解析编译,将SQL转换成最终执行的MapReduce任务。
如果直接盲目的去看Driver类的代码,会很容易看懵逼,我们需要再往前一点。
SQLOperation
先看org.apache.hive.service.cli.operation.SQLOperation 类,它负责创建Driver对象、编译SQL、异步执行SQL。其中核心的就是 runInternal()方法,主要进行如下两个步骤:
- Driver对象创建并编译SQL,将SQL编译成Query Plan执行计划。
- 对QueryPaln 进行处理,转换成MR 任务执行。
runInternal() 方法源码内容如下:
/*** 内部运行方法,用于执行SQL操作。** @throws HiveSQLException 如果在执行过程中发生Hive SQL异常。*/public void runInternal() throws HiveSQLException {// 设置操作状态为PENDINGsetState(OperationState.PENDING);// 判断是否应该异步运行boolean runAsync = shouldRunAsync();// 判断是否应该异步编译final boolean asyncPrepare = runAsync&& HiveConf.getBoolVar(queryState.getConf(),HiveConf.ConfVars.HIVE_SERVER2_ASYNC_EXEC_ASYNC_COMPILE);// 如果不是异步编译,则同步准备查询if (!asyncPrepare) {//创建Driver对象,编译SQL//Driver经过:SQL -> AST(抽象语法树) -> QueryBlock(查询块) -> Operator(e逻辑执行计划) -> TaskTree(物理执行计划) -> QueryPlan(查询计划)prepare(queryState);}// 如果不是异步运行,则同步运行查询if (!runAsync) {runQuery();} else {// 我们将在后台线程中传递ThreadLocals,从前台(处理程序)线程传递。// 1) ThreadLocal Hive对象需要在后台线程中设置// 2) Hive中的元数据存储客户端与正确的用户相关联。// 3) 当前UGI将在元数据存储处于嵌入式模式时被元数据存储使用Runnable work = new BackgroundWork(getCurrentUGI(), parentSession.getSessionHive(),SessionState.getPerfLogger(), SessionState.get(), asyncPrepare);try {// 如果没有可用的后台线程来运行此操作,此提交将阻塞Future<?> backgroundHandle = getParentSession().submitBackgroundOperation(work);// 设置后台操作句柄setBackgroundHandle(backgroundHandle);} catch (RejectedExecutionException rejected) {// 设置操作状态为ERRORsetState(OperationState.ERROR);// 抛出HiveSQLException异常throw new HiveSQLException("The background threadpool cannot accept" +" new task for execution, please retry the operation", rejected);}}}
1.Driver对象创建并编译SQL,将SQL编译成Query Plan执行计划
其中核心的是prepare()方法,它的源码在2.x和3.x、4.x有一些区别,不过其核心功能是没变的,主要是创建Driver对象,并编译SQL,然后通过Driver将SQL最终转换成Query Plan。
prepare()方法3.x的源码如下:
/*** 准备执行SQL查询的操作。* 此方法负责初始化Driver,设置查询超时,编译查询语句,并处理可能的异常。** @param queryState 包含查询状态信息的对象。* @throws HiveSQLException 如果在准备过程中发生Hive SQL异常。*/public void prepare(QueryState queryState) throws HiveSQLException {// 设置操作状态为运行中setState(OperationState.RUNNING);try {// 创建Driver实例,返回的Driver对象是 ReExecDriverdriver = DriverFactory.newDriver(queryState, getParentSession().getUserName(), queryInfo);// 如果查询超时时间大于0,则启动一个定时任务来取消查询if (queryTimeout > 0) {// 创建一个单线程的定时任务执行器timeoutExecutor = new ScheduledThreadPoolExecutor(1);// 创建一个定时任务,在查询超时后取消查询Runnable timeoutTask = new Runnable() {@Overridepublic void run() {try {// 获取查询IDString queryId = queryState.getQueryId();// 记录日志,查询超时并取消执行LOG.info("Query timed out after: " + queryTimeout+ " seconds. Cancelling the execution now: " + queryId);// 取消查询SQLOperation.this.cancel(OperationState.TIMEDOUT);} catch (HiveSQLException e) {// 记录日志,取消查询时发生错误LOG.error("Error cancelling the query after timeout: " + queryTimeout + " seconds", e);} finally {// 关闭定时任务执行器timeoutExecutor.shutdown();}}};// 安排定时任务在查询超时后执行timeoutExecutor.schedule(timeoutTask, queryTimeout, TimeUnit.SECONDS);}// 设置查询显示信息queryInfo.setQueryDisplay(driver.getQueryDisplay());// 设置操作句柄信息,以便Thrift API用户可以使用操作句柄查找Yarn ATS中的查询信息String guid64 = Base64.encodeBase64URLSafeString(getHandle().getHandleIdentifier().toTHandleIdentifier().getGuid()).trim();driver.setOperationId(guid64);// 编译SQL查询并响应 ReExecDriver.compileAndRespond(...) -> Driver.compileAndRespond(...)response = driver.compileAndRespond(statement);// 如果响应代码不为0,则抛出异常if (0 != response.getResponseCode()) {throw toSQLException("Error while compiling statement", response);}// 设置是否有结果集setHasResultSet(driver.hasResultSet());} catch (HiveSQLException e) {// 设置操作状态为错误setState(OperationState.ERROR);// 抛出异常throw e;} catch (Throwable e) {// 设置操作状态为错误setState(OperationState.ERROR);// 抛出异常throw new HiveSQLException("Error running query: " + e.toString(), e);}}
2.x与3.x源码最核心的区别就是在创建Driver,其对应源码是:
driver = new Driver(queryState, getParentSession().getUserName());
而4.x与3.x源码最核心的区别如下:
- 利用 Java 8 的 Lambda 表达式特性,简化代码逻辑,提高代码的可读性和可维护性。
- 通过将 queryTimeout 的类型改为 long,支持了更大的超时值,避免了溢出问题。
- 在资源管理方面,对调度器的生命周期管理也进行了优化,不需要显式的关闭操作。
4.x对应源码是:
if (queryTimeout > 0L) {timeoutExecutor = Executors.newSingleThreadScheduledExecutor();timeoutExecutor.schedule(() -> {try {final String queryId = queryState.getQueryId();log.info("Query timed out after: {} seconds. Cancelling the execution now: {}", queryTimeout, queryId);SQLOperation.this.cancel(OperationState.TIMEDOUT);} catch (HiveSQLException e) {log.error("Error cancelling the query after timeout: {} seconds", queryTimeout, e);}return null;}, queryTimeout, TimeUnit.SECONDS); }
在DriverFactory.newDriver()方法中返回 ReExecDriver
对象,该对象表示执行过程失败可重试的Driver对象,然后调用 Driver.compileAndRespond() 方法进行编译SQL。
2.对QueryPaln 进行处理,转换成MR 任务执行
BackgroundWork是一个线程,负责异步处理QueryPlan,通过submitBackgroundOperation(work)提交运行,执行到SQLOperator.BackgroundOperation.run()方法,最终调用到Driver.run() 方法。
Driver
下面我们再来Driver类,它在不同版本中也有一些差别,比如2.x版本是直接 implements CommandProcessor,而3.x和4.x版本则是implements IDriver,而IDriver 则是 extends CommandProcessor。本质是为了更好的解耦和扩展性,使得代码更加模块化,易于维护和扩展。同时,通过继承 CommandProcessor 接口,也保持了与旧版本的兼容性,确保了功能的连续性。不过其核心功能是没变的,主要包含编译、优化及执行。
执行步骤
为了方便理解,我们先梳理整个执行步骤如下:
-
通过Antlr解析SQL语法规则和语法解析,将SQL语法转换成AST(抽象语法树) 。
-
遍历AST(抽象语法树) 将其转化成Query Block(查询块,可以看成查询基本执行单元)。
-
将Query Block(查询块) 转换成OperatorTree(逻辑执行计划),并进行优化。
-
OperatorTree(逻辑执行计划)转换成TaskTree(物理执行计划,每个Task对应一个MR Job任务)。
-
TaskTree(物理执行计划)最终包装成Query Plan(查询计划)。
简单总结执行流程如下:
SQL -> AST(抽象语法树) -> QueryBlock(查询块) -> Operator(逻辑执行计划) -> TaskTree(物理执行计划) -> QueryPlan(查询计划)。
下面我们再结合SQLOperation调用的Driver类里面的核心方法,来看看底层源码是如何实现的:
compileAndRespond方法
首先第一个核心方法是
response = driver.compileAndRespond(statement);
compileAndRespond()方法2.x源码如下:
/*** 编译给定的 SQL 命令并返回一个命令处理器响应。* 此方法调用 compileInternal 方法进行实际的编译操作,并使用编译结果创建一个命令处理器响应。** @param command 要编译的 SQL 命令* @return 包含编译结果的命令处理器响应*/public CommandProcessorResponse compileAndRespond(String command) {return createProcessorResponse(compileInternal(command, false));}
3.x和4.x会有些区别,会返回以下方法的调用结果:
coreDriver.compileAndRespond(statement);
无论哪个版本,最终compileAndRespond()方法都会调用到 compileInternal()方法,我们继续看2.x版本compileInternal()方法源码如下:
private int compileInternal(String command, boolean deferClose) {int ret;// 获取Metrics实例,如果存在则增加等待编译操作的计数器Metrics metrics = MetricsFactory.getInstance();if(metrics != null) {metrics.incrementCounter(MetricsConstant.WAITING_COMPILE_OPS, 1);}// 尝试获取编译锁,如果获取失败则返回编译锁超时错误码final ReentrantLock compileLock = tryAcquireCompileLock(isParallelEnabled, command);if(compileLock == null) {return ErrorMsg.COMPILE_LOCK_TIMED_OUT.getErrorCode();}try {// 如果Metrics实例存在,减少等待编译操作的计数器if(metrics != null) {metrics.decrementCounter(MetricsConstant.WAITING_COMPILE_OPS, 1);}// 进行Hive SQL编译ret = compile(command, true, deferClose);} finally {// 无论编译结果如何,最终都要释放编译锁compileLock.unlock();}// 如果编译失败,尝试释放锁并回滚事务if(ret != 0) {try {releaseLocksAndCommitOrRollback(false, null);} catch(LockException e) {// 记录释放锁时的异常信息LOG.warn("Exception in releasing locks. " + org.apache.hadoop.util.StringUtils.stringifyException(e));}}// 保存编译时的性能日志,用于WebUI展示// 执行时的性能日志由另一个线程的PerfLogger或重置后的PerfLogger完成PerfLogger perfLogger = SessionState.getPerfLogger();queryDisplay.setPerfLogStarts(QueryDisplay.Phase.COMPILATION, perfLogger.getStartTimes());queryDisplay.setPerfLogEnds(QueryDisplay.Phase.COMPILATION, perfLogger.getEndTimes());return ret;}
3.x和4.x的源码相比起来有一些区别,但是都是通过执行Driver.compile()方法,由于4.x代码这块改动较大,做了很多解耦的操作,其核心内容还是变化不大,加上目前几乎很少应用4.x版本的hive,下面我们重点看看2.x和3.x版本的compile()方法内容。
compile方法
compile()方法2.x源码如下:
/*** 编译一个新的查询,可选择重置任务ID计数器并决定是否延迟关闭。* * @param command 要编译的HiveQL查询。* @param resetTaskIds 如果为true,则重置任务ID计数器。* @param deferClose 如果为true,则在编译过程被中断时延迟关闭/销毁操作。* @return 0表示编译成功,否则返回错误代码。*/
// deferClose 表示当进程被中断时,是否应该推迟关闭/销毁操作。如果 compile 方法是在另一个方法(如 runInternal)中被调用,并且该方法会将关闭操作推迟到其内部处理,那么 deferClose 应该设置为 true。
public int compile(String command, boolean resetTaskIds, boolean deferClose) {// 获取性能日志记录器,并开始记录编译过程的性能PerfLogger perfLogger = SessionState.getPerfLogger(true);perfLogger.PerfLogBegin(CLASS_NAME, PerfLogger.DRIVER_RUN);perfLogger.PerfLogBegin(CLASS_NAME, PerfLogger.COMPILE);// 锁定驱动状态,将驱动状态设置为编译中lDrvState.stateLock.lock();try {lDrvState.driverState = DriverState.COMPILING;} finally {lDrvState.stateLock.unlock();}// 对查询命令进行变量替换command = new VariableSubstitution(new HiveVariableSource() {@Overridepublic Map<String, String> getHiveVariable() {return SessionState.get().getHiveVariables();}}).substitute(conf, command);// 存储查询字符串String queryStr = command;try {// 对查询命令进行脱敏处理,避免记录敏感数据queryStr = HookUtils.redactLogString(conf, command);} catch(Exception e) {// 若脱敏失败,记录警告信息LOG.warn("WARNING! Query command could not be redacted." + e);}// 检查编译过程是否被中断,若中断则处理中断并返回错误代码if(isInterrupted()) {return handleInterruption("at beginning of compilation."); //indicate if need clean resource}// 如果上下文不为空且解释分析状态不为运行中,则关闭现有上下文if(ctx != null && ctx.getExplainAnalyze() != AnalyzeState.RUNNING) {// close the existing ctx etc before compiling a new query, but does not destroy drivercloseInProcess(false);}// 如果需要重置任务ID,则重置任务工厂的IDif(resetTaskIds) {TaskFactory.resetId();}// 获取查询IDString queryId = conf.getVar(HiveConf.ConfVars.HIVEQUERYID);// 保存查询信息,用于Web UI显示this.queryDisplay.setQueryStr(queryStr);this.queryDisplay.setQueryId(queryId);// 记录编译开始信息LOG.info("Compiling command(queryId=" + queryId + "): " + queryStr);// 设置查询的当前时间戳SessionState.get().setupQueryCurrentTimestamp();// 标记编译过程中是否发生错误boolean compileError = false;try {// 初始化事务管理器final HiveTxnManager txnManager = SessionState.get().initTxnMgr(conf);// 移除旧的关闭hookShutdownHookManager.removeShutdownHook(shutdownRunner);// 创建新的关闭hook,用于在JVM关闭时释放锁shutdownRunner = new Runnable() {@Overridepublic void run() {try {releaseLocksAndCommitOrRollback(false, txnManager);} catch(LockException e) {// 若释放锁时发生异常,记录警告信息LOG.warn("Exception when releasing locks in ShutdownHook for Driver: " + e.getMessage());}}};// 添加新的关闭hookShutdownHookManager.addShutdownHook(shutdownRunner, SHUTDOWN_HOOK_PRIORITY);// 再次检查编译过程是否被中断if(isInterrupted()) {return handleInterruption("before parsing and analysing the query");}// 如果上下文为空,则创建新的上下文if(ctx == null) {ctx = new Context(conf);}// 设置上下文的重试次数、命令和HDFS清理标志ctx.setTryCount(getTryCount());ctx.setCmd(command);ctx.setHDFSCleanup(true);/*** 把 HQL命令 翻译成一个 ASTNode Tree* 封装了 ParseDriver 对 HQL 的解析工作* ParseDriver 对 command 进行词法分析和语法解析(统称为语法分析),返回一个抽象语法树AST*/// 开始记录解析过程的性能perfLogger.PerfLogBegin(CLASS_NAME, PerfLogger.PARSE);// 解析查询命令,得到抽象语法树ASTNode tree = ParseUtils.parse(command, ctx);// 结束记录解析过程的性能perfLogger.PerfLogEnd(CLASS_NAME, PerfLogger.PARSE);// 加载查询hookqueryHooks = loadQueryHooks();// 如果查询hook不为空且不为空列表,则触发查询hook的编译前操作if(queryHooks != null && !queryHooks.isEmpty()) {QueryLifeTimeHookContext qhc = new QueryLifeTimeHookContextImpl();qhc.setHiveConf(conf);qhc.setCommand(command);for(QueryLifeTimeHook hook : queryHooks) {hook.beforeCompile(qhc);}}// 开始记录语义分析过程的性能perfLogger.PerfLogBegin(CLASS_NAME, PerfLogger.ANALYZE);// 获取语义分析器BaseSemanticAnalyzer sem = SemanticAnalyzerFactory.get(queryState, tree);// 获取语义分析hookList<HiveSemanticAnalyzerHook> saHooks = getHooks(HiveConf.ConfVars.SEMANTIC_ANALYZER_HOOK, HiveSemanticAnalyzerHook.class);// 刷新元数据存储缓存,确保获取最新的元数据Hive.get().getMSC().flushCache();// 进行语义分析和计划生成if(saHooks != null && !saHooks.isEmpty()) {HiveSemanticAnalyzerHookContext hookCtx = new HiveSemanticAnalyzerHookContextImpl();hookCtx.setConf(conf);hookCtx.setUserName(userName);hookCtx.setIpAddress(SessionState.get().getUserIpAddress());hookCtx.setCommand(command);hookCtx.setHiveOperation(queryState.getHiveOperation());// 触发语义分析hook的预分析操作for(HiveSemanticAnalyzerHook hook : saHooks) {tree = hook.preAnalyze(hookCtx, tree);}/*** sem 是一个 SemanticAnalyzer(语义分析器) 对象* 主要的工作是将 ASTNode 转化为 TaskTree,包括可能的 optimize,过程比较复杂** tree: AST 抽象语法树 ===> TaskTree* TaskTree : 物理执行计划** 把抽象语法树交给 SemanticAnalyzer 执行语法解析* 1、从 AST 转成 解析树* 2、通过解析树 再生成 QB 在查询快* 3、从 QB 树在生成 OperatorTree (Logical Plan)* 4、逻辑执行计划的优化* 5、OperatorTree转变成TaskTree* 6、再针对物理执行计划执行优化* 7、生成QueryPlan*/// 进行语义分析sem.analyze(tree, ctx);// 更新hook上下文hookCtx.update(sem);// 触发语义分析hook的后分析操作for(HiveSemanticAnalyzerHook hook : saHooks) {hook.postAnalyze(hookCtx, sem.getAllRootTasks());}} else {// 若没有语义分析hook,直接进行语义分析sem.analyze(tree, ctx);}// 记录查询中发现的ACID文件接收器acidSinks = sem.getAcidFileSinks();// 记录语义分析完成信息LOG.info("Semantic Analysis Completed");// 验证语义分析生成的计划是否有效sem.validate();// 检查查询中是否包含ACID操作acidInQuery = sem.hasAcidInQuery();// 结束语义分析阶段的性能日志记录perfLogger.PerfLogEnd(CLASS_NAME, PerfLogger.ANALYZE);// 检查编译过程是否被中断,如果中断则处理中断情况并返回if(isInterrupted()) {return handleInterruption("after analyzing query.");}// 根据语义分析结果和配置信息获取查询的输出模式schema = getSchema(sem, conf);/*** 把 TaskTree 生成一个 QueryPlan* 通过 Exeuctor 提交的方法,要接受的参数就是 QueryPlan*/// 根据查询字符串、语义分析器、开始时间、查询ID、操作类型和输出模式创建查询计划plan = new QueryPlan(queryStr, sem, perfLogger.getStartTime(PerfLogger.DRIVER_RUN), queryId, queryState.getHiveOperation(), schema);// 设置查询字符串到配置中conf.setQueryString(queryStr);// 设置MapReduce工作流ID到配置中conf.set("mapreduce.workflow.id", "hive_" + queryId);// 设置MapReduce工作流名称到配置中conf.set("mapreduce.workflow.name", queryStr);// 如果查询计划中包含FetchTask,则对其进行初始化if(plan.getFetchTask() != null) {plan.getFetchTask().initialize(queryState, plan, null, ctx.getOpContext());}// 进行授权检查,如果语义分析不跳过授权且开启了授权功能if(!sem.skipAuthorization() && HiveConf.getBoolVar(conf, HiveConf.ConfVars.HIVE_AUTHORIZATION_ENABLED)) {try {// 开始记录授权过程的性能日志perfLogger.PerfLogBegin(CLASS_NAME, PerfLogger.DO_AUTHORIZATION);// 执行授权操作doAuthorization(queryState.getHiveOperation(), sem, command);} catch(AuthorizationException authExp) {// 如果授权失败,打印错误信息并设置错误状态和返回码console.printError("Authorization failed:" + authExp.getMessage() + ". Use SHOW GRANT to " + "get" + " more details.");errorMessage = authExp.getMessage();SQLState = "42000";return 403;} finally {// 结束记录授权过程的性能日志perfLogger.PerfLogEnd(CLASS_NAME, PerfLogger.DO_AUTHORIZATION);}}// 如果配置中开启了记录EXPLAIN输出的功能if(conf.getBoolVar(ConfVars.HIVE_LOG_EXPLAIN_OUTPUT)) {// 获取查询的EXPLAIN输出String explainOutput = getExplainOutput(sem, plan, tree);if(explainOutput != null) {if(conf.getBoolVar(ConfVars.HIVE_LOG_EXPLAIN_OUTPUT)) {// 记录EXPLAIN输出到日志中LOG.info("EXPLAIN output for queryid " + queryId + " : " + explainOutput);}if(conf.isWebUiQueryInfoCacheEnabled()) {// 如果开启了Web UI查询信息缓存,将EXPLAIN计划设置到查询显示信息中queryDisplay.setExplainPlan(explainOutput);}}}// 编译成功,返回0return 0;} catch(Exception e) {// 如果编译过程中被中断,处理中断情况并返回if(isInterrupted()) {return handleInterruption("during query compilation: " + e.getMessage());}// 标记编译过程出现错误compileError = true;// 获取错误信息ErrorMsg error = ErrorMsg.getErrorMsg(e.getMessage());// 构建错误消息errorMessage = "FAILED: " + e.getClass().getSimpleName();if(error != ErrorMsg.GENERIC_ERROR) {errorMessage += " [Error " + error.getErrorCode() + "]:";}// HIVE-4889if((e instanceof IllegalArgumentException) && e.getMessage() == null && e.getCause() != null) {errorMessage += " " + e.getCause().getMessage();} else {errorMessage += " " + e.getMessage();}if(error == ErrorMsg.TXNMGR_NOT_ACID) {errorMessage += ". Failed command: " + queryStr;}// 设置SQL状态码SQLState = error.getSQLState();// 记录下游错误信息downstreamError = e;// 打印错误信息和详细堆栈跟踪console.printError(errorMessage, "\n" + org.apache.hadoop.util.StringUtils.stringifyException(e));// 返回错误代码return error.getErrorCode();// since it exceeds valid range of shell return values} finally {// 触发编译后的hook函数try {if(queryHooks != null && !queryHooks.isEmpty()) {QueryLifeTimeHookContext qhc = new QueryLifeTimeHookContextImpl();qhc.setHiveConf(conf);qhc.setCommand(command);for(QueryLifeTimeHook hook : queryHooks) {hook.afterCompile(qhc, compileError);}}} catch(Exception e) {// 如果触发hook函数时出现异常,记录警告信息LOG.warn("Failed when invoking query after-compilation hook.", e);}/*** 计算任务总耗时*/// 结束编译阶段的性能日志记录并计算耗时double duration = perfLogger.PerfLogEnd(CLASS_NAME, PerfLogger.COMPILE) / 1000.00;// 获取编译过程中HMS调用的时间统计信息ImmutableMap<String, Long> compileHMSTimings = dumpMetaCallTimingWithoutEx("compilation");// 设置查询显示信息中的HMS时间统计信息queryDisplay.setHmsTimings(QueryDisplay.Phase.COMPILATION, compileHMSTimings);// 检查编译过程是否被中断boolean isInterrupted = isInterrupted();if(isInterrupted && !deferClose) {// 如果被中断且不延迟关闭,关闭正在进行的操作closeInProcess(true);}// 锁定驱动状态lDrvState.stateLock.lock();try {if(isInterrupted) {// 如果被中断,根据是否延迟关闭设置驱动状态lDrvState.driverState = deferClose ? DriverState.EXECUTING : DriverState.ERROR;} else {// 如果未被中断,根据编译是否出错设置驱动状态lDrvState.driverState = compileError ? DriverState.ERROR : DriverState.COMPILED;}} finally {// 解锁驱动状态lDrvState.stateLock.unlock();}if(isInterrupted) {// 如果编译过程被中断,记录中断信息LOG.info("Compiling command(queryId=" + queryId + ") has been interrupted after " + duration + " seconds");} else {// 如果编译过程未被中断,记录编译完成信息LOG.info("Completed compiling command(queryId=" + queryId + "); Time taken: " + duration + " " + "seconds");}}
}
compile()方法在3.x和4.x有一些区别,但是都有以下三个核心方法:
- 首先是通过ParseUtils.parse(command, ctx)将Hive SQL转换成AST(抽象语法树),即:HQL -> AST(抽象语法树)转换;
- 然后是通过BaseSemanticAnalyzer.analyze()方法将AST(抽象语法树)解析生成TaskTree(物理执行计划);
- 最后将BaseSemanticAnalyzer传入QueryPlan的构造函数,来创建QueryPlan(查询计划)。
其核心正是引入篇我们提到的解析(Parsing)、校验(Validation)、优化(Optimization)和执行(Execution)。
下面我们深入这几个方法看看:
parse方法
在compile()方法中,首先是通过ParseUtils.parse(command, ctx)进行词法分析与解析,将Hive HQL转换成AST抽象语法树。
我们来看看parse()方法的源码:
/*** 解析 HQL。* * 此方法接收一个 Hive 查询命令和上下文对象,调用另一个重载的 parse 方法进行实际的解析操作,* 并将视图的全限定名参数设为 null。* * @param command 要解析的 Hive 查询命令* @param ctx 查询的上下文对象* @return 解析后的 AST 节点* @throws ParseException 如果解析过程中出现异常*/
public static ASTNode parse(String command, Context ctx) throws ParseException {return parse(command, ctx, null);
}
继续往里走,对应源码如下:
/*** 解析HQL * ParseDriver对command进行词法分析和语法解析(统称为语法分析),返回一个抽象语法树AST* * @param command 要解析的Hive查询命令* @param ctx 查询上下文信息* @param viewFullyQualifiedName 视图的完全限定名称* @return 解析后的AST节点* @throws ParseException 如果解析过程中出现错误*/public static ASTNode parse(String command, Context ctx, String viewFullyQualifiedName) throws ParseException {// 创建一个ParseDriver实例用于解析命令ParseDriver pd = new ParseDriver();// 使用ParseDriver解析命令,得到AST节点ASTNode tree = pd.parse(command, ctx, viewFullyQualifiedName);// 查找根节点中第一个具有非空令牌的节点tree = findRootNonNullToken(tree);// 处理设置列引用的情况handleSetColRefs(tree);// 返回处理后的AST节点return tree;}
在pd.parse()方法中,核心调用的是HiveLexer和HiveParser这两个类,它们分别负责SQL的词法分析和语法解析,我们继续看看其中源码:
/*** 解析给定的命令字符串,将其转换为抽象语法树(AST)节点。** @param command 要解析的命令字符串。* @param ctx 解析上下文,可包含配置信息和tokens重写流。* @param viewFullyQualifiedName 视图的完全限定名称,如果不是视图解析则为 null。* @return 解析后的 AST 节点。* @throws ParseException 如果解析过程中出现错误。*/public ASTNode parse(String command, Context ctx, String viewFullyQualifiedName)throws ParseException {// 如果启用了调试日志,则记录正在解析的命令if (LOG.isDebugEnabled()) {LOG.debug("Parsing command: " + command);}/*** Antlr对语法文件 HiveLexer.g 编译后自动生成的词法解析和语法解析类(HiveLexerX,HiveParser)* 文件 HiveLexer.g 定义了一些 hive 的关键字,form、where,数字的定义格式【0–9】,分隔符,比较符之类的。* 每一个关键字分支都会变成一个 token。** HiveLexerX 是 antlr 根据词法规则文件,通过编译生成的一个代码类* 能够执行词法和语法的解析 * 最终生成一个 ASTNode*/// 创建一个不区分大小写的字符流,并使用它初始化词法分析器HiveLexerX lexer = new HiveLexerX(new ANTLRNoCaseStringStream(command));/*** 根据词法分析的结果得到tokens的,此时不只是单纯的字符串,* 而是具有特殊意义的字符串的封装,其本身是一个流。* lexer 把 SQL 语句中的各个语法分支,都转换成底层引擎能识别的各种 Token*/// 创建一个tokens重写流,用于处理词法分析器生成的tokensTokenRewriteStream tokens = new TokenRewriteStream(lexer);// 如果提供了上下文,则根据是否为视图设置tokens重写流,并设置词法分析器的配置if (ctx != null) {if (viewFullyQualifiedName == null) {// 顶层查询ctx.setTokenRewriteStream(tokens);} else {// 这是一个视图ctx.addViewTokenRewriteStream(viewFullyQualifiedName, tokens);}lexer.setHiveConf(ctx.getConf());}// 语法解析 HiveParser是 Antlr 根据 HiveParser.g 生成的文件// 使用tokens重写流初始化语法解析器HiveParser parser = new HiveParser(tokens);// 如果提供了上下文,则设置解析器的配置if (ctx != null) {parser.setHiveConf(ctx.getConf());}// 设置解析器的树适配器,用于创建 AST 节点parser.setTreeAdaptor(adaptor);// 声明一个变量来存储解析结果HiveParser.statement_return r = null;try {/*** 转化为 ASTTree 放在 ASTNode 中的 tree 属性中。 通过 r.getTree() 获取返回。* 当前这句代码完成了从 Tok 树到 AST 的转变* 把结果放在了 HiveParser.statement_return*/// 调用解析器的 statement 方法进行解析r = parser.statement();} catch (RecognitionException e) {// 打印异常堆栈跟踪信息e.printStackTrace();// 如果解析过程中出现识别异常,则抛出解析异常throw new ParseException(parser.errors);}// 检查词法分析器和解析器是否有错误if (lexer.getErrors().size() == 0 && parser.errors.size() == 0) {// 如果没有错误,则记录解析完成的日志LOG.debug("Parse Completed");} else if (lexer.getErrors().size() != 0) {// 如果词法分析器有错误,则抛出解析异常throw new ParseException(lexer.getErrors());} else {// 如果解析器有错误,则抛出解析异常throw new ParseException(parser.errors);}// 获取解析结果的树,并将其转换为 AST 节点ASTNode tree = (ASTNode) r.getTree();// 设置 AST 节点的未知tokens边界tree.setUnknownTokenBoundaries();// 返回解析后的 AST 节点return tree;}
pd.parse()方法将sql语法转换成抽象语法树 AST,Hive中通过使用 Antlr(Another Tool for Language Recognition)进行词法分析和语法解析。
Antlr主要作用:
- 词法分析:将输入的HiveQL查询字符串分解成一系列的Token,这些Token是语法分析的基础。Antlr生成的词法分析器(Lexer)负责将输入的HiveQL查询字符串分解成一个个Token,这些Token表示查询中的关键字、标识符、运算符等基本元素。
- 语法解析:根据词法分析器生成的Token序列,解析HiveQL查询语句,生成AST抽象语法树。Antlr生成的语法解析器(Parser)负责读取Token序列,并根据语法规则解析这些Token,生成对应的AST抽象语法树。Token 对应 SQL中的每个关键字。
analyze方法
通过上一个步骤并获取到 ASTNode之后,需要对其进行进一步的抽象和结构化处理,以便能够更便捷地将其转换为MapReduce程序。为此将会初始化类BaseSemanticAnalyzer,并通过SemanticAnalyzerFactory确定SQL的类型,进而调用analyze()方法进行分析,其对应源码如下:
BaseSemanticAnalyzer sem = SemanticAnalyzerFactory.get(queryState, tree);
sem.analyze(tree, ctx);
其中 sem 是一个 SemanticAnalyzer(语义分析器)对象,主要的工作是将 ASTNode 转化为 TaskTree(物理执行计划),包括可能的 optimize(优化),也就是前面执行步骤第2~5步做的内容。
首先看analyze()对应源码如下:
/*** 分析给定的抽象语法树(AST)节点,并使用提供的上下文进行初始化。* * 此方法首先初始化上下文,然后初始化分析器的内部状态。* 最后,调用 `analyzeInternal` 方法对 AST 进行实际的分析。* * @param ast 要分析的抽象语法树节点。* @param ctx 分析过程中使用的上下文。* @throws SemanticException 如果在分析过程中发生语义错误。*/public void analyze(ASTNode ast, Context ctx) throws SemanticException {// 初始化上下文initCtx(ctx);// 初始化分析器的内部状态,清除部分缓存init(true);// 调用内部分析方法对 AST 进行分析analyzeInternal(ast);}
可以看到,除了进行必要的初始化之外,还会调用analyzeInternal()方法,对应源码如下:
/*** 对抽象语法树(AST)进行内部语义分析。* 此方法为抽象方法,具体实现需在子类中完成。* 它负责对传入的AST进行详细的语义分析,以确保查询语句的合法性和正确性。** @param ast 待分析的抽象语法树节点* @throws SemanticException 如果在语义分析过程中发现错误*/public abstract void analyzeInternal(ASTNode ast) throws SemanticException;
可以看到analyzeInternal()是一个抽象方法,它有多种具体实现,通过断点查看,会发现流程是跳转到了org.apache.hadoop.hive.ql.parse.SemanticAnalyzer类,其源码注释如下:
Implementation of the semantic analyzer. It generates the query plan.
There are other specific semantic analyzers for some hive operations such as DDLSemanticAnalyzer for ddl operations.翻译:
语义分析器的实现。它用于生成查询计划。
对于某些 Hive 操作,还有其他特定的语义分析器,例如用于 DDL 操作的 DDLSemanticAnalyzer。
这个类有点复杂,Hive优化的秘密全在于此,将AST抽象语法树解析生成TaskTree(物理执行计划)的全流程,包括逻辑执行计划、逻辑执行计划的优化、物理执行计划的切分、物理执行计划的优化、以及 MapReduce 任务的生成全部都在其中,下面我们就看看其中实现的analyzeInternal()方法源码:
/*** 对传入的AST节点进行内部分析,生成查询计划。** @param ast 抽象语法树节点* @param pcf 计划上下文工厂* @throws SemanticException 语义分析异常*/void analyzeInternal(ASTNode ast, PlannerContextFactory pcf) throws SemanticException {LOG.info("Starting Semantic Analysis");// 1. 从语法树生成解析树boolean needsTransform = needsTransform();// 改变位置别名处理的位置processPositionAlias(ast);PlannerContext plannerCtx = pcf.create();if (!genResolvedParseTree(ast, plannerCtx)) {return;}if (HiveConf.getBoolVar(conf, ConfVars.HIVE_REMOVE_ORDERBY_IN_SUBQUERY)) {for (String alias : qb.getSubqAliases()) {removeOBInSubQuery(qb.getSubqForAlias(alias));}}// 检查查询结果缓存。// 如果不需要进行掩码/过滤,则可以在生成操作符树和进行CBO之前检查缓存。// 否则,必须等到掩码/过滤步骤之后。boolean isCacheEnabled = isResultsCacheEnabled();QueryResultsCache.LookupInfo lookupInfo = null;if (isCacheEnabled && !needsTransform && queryTypeCanUseCache()) {lookupInfo = createLookupInfoForQuery(ast);if (checkResultsCache(lookupInfo)) {return;}}ASTNode astForMasking;if (isCBOExecuted() && needsTransform &&(qb.isCTAS() || qb.isView() || qb.isMaterializedView() || qb.isMultiDestQuery())) {// 如果使用CBO并且可能应用掩码/过滤策略,则创建ast的副本。// 原因是操作符树的生成可能会修改初始ast,但如果需要第二次解析,我们希望解析未修改的ast。astForMasking = (ASTNode) ParseDriver.adaptor.dupTree(ast);} else {astForMasking = ast;}// 2. 从解析树生成操作符树Operator sinkOp = genOPTree(ast, plannerCtx);boolean usesMasking = false;if (!unparseTranslator.isEnabled() &&(tableMask.isEnabled() && analyzeRewrite == null)) {// 在这里重写 * 以及掩码表ASTNode rewrittenAST = rewriteASTWithMaskAndFilter(tableMask, astForMasking, ctx.getTokenRewriteStream(),ctx, db, tabNameToTabObject, ignoredTokens);if (astForMasking != rewrittenAST) {usesMasking = true;plannerCtx = pcf.create();ctx.setSkipTableMasking(true);init(true);// 改变位置别名处理的位置processPositionAlias(rewrittenAST);genResolvedParseTree(rewrittenAST, plannerCtx);if (this instanceof CalcitePlanner) {((CalcitePlanner) this).resetCalciteConfiguration();}sinkOp = genOPTree(rewrittenAST, plannerCtx);}}// 检查查询结果缓存// 在需要进行行或列掩码/过滤的情况下,不支持缓存。// TODO: 为带有掩码/过滤的查询启用缓存if (isCacheEnabled && needsTransform && !usesMasking && queryTypeCanUseCache()) {lookupInfo = createLookupInfoForQuery(ast);if (checkResultsCache(lookupInfo)) {return;}}// 3. 推导结果集模式if (createVwDesc != null && !this.ctx.isCboSucceeded()) {resultSchema = convertRowSchemaToViewSchema(opParseCtx.get(sinkOp).getRowResolver());} else {// 如果满足以下条件,resultSchema将为null:// (1) cbo被禁用;// (2) 或者cbo启用但使用AST返回路径(无论是否成功,resultSchema都将重新初始化)// 只有在cbo启用且使用新返回路径并且成功时,resultSchema才不为null。if (resultSchema == null) {resultSchema = convertRowSchemaToResultSetSchema(opParseCtx.get(sinkOp).getRowResolver(),HiveConf.getBoolVar(conf, HiveConf.ConfVars.HIVE_RESULTSET_USE_UNIQUE_COLUMN_NAMES));}}// 4. 为优化器和物理编译器生成解析上下文copyInfoToQueryProperties(queryProperties);ParseContext pCtx = new ParseContext(queryState, opToPartPruner, opToPartList, topOps,// 使用菱形操作符简化泛型类型声明new HashSet<>(joinContext.keySet()),// 使用菱形操作符简化泛型类型声明new HashSet<>(smbMapJoinContext.keySet()),loadTableWork, loadFileWork, columnStatsAutoGatherContexts, ctx, idToTableNameMap, destTableId, uCtx,listMapJoinOpsNoReducer, prunedPartitions, tabNameToTabObject, opToSamplePruner,globalLimitCtx, nameToSplitSample, inputs, rootTasks, opToPartToSkewedPruner,viewAliasToInput, reduceSinkOperatorsAddedByEnforceBucketingSorting,analyzeRewrite, tableDesc, createVwDesc, materializedViewUpdateDesc,queryProperties, viewProjectToTableSchema, acidFileSinks);// 在解析上下文中设置半连接提示pCtx.setSemiJoinHints(parseSemiJoinHint(getQB().getParseInfo().getHintList()));// 如果需要禁用映射连接提示,则设置pCtx.setDisableMapJoin(disableMapJoinWithHint(getQB().getParseInfo().getHintList()));// 5. 处理视图创建if (createVwDesc != null) {if (ctx.getExplainAnalyze() == AnalyzeState.RUNNING) {return;}if (!ctx.isCboSucceeded()) {saveViewDefinition();}// 此时验证创建视图语句,createVwDesc包含语义检查所需的所有信息validateCreateView();if (createVwDesc.isMaterialized()) {createVwDesc.setTablesUsed(getTablesUsed(pCtx));} else {// 由于我们只是创建视图(不执行它),因此不需要优化或转换计划(实际上,这些过程可能会干扰视图创建)。所以跳过此方法的其余部分。ctx.setResDir(null);ctx.setResFile(null);try {PlanUtils.addInputsForView(pCtx);} catch (HiveException e) {throw new SemanticException(e);}// 为创建视图语句生成谱系信息// 如果配置了LineageLoggerhook。// 添加计算谱系信息的转换。Set<String> postExecHooks = Sets.newHashSet(Splitter.on(",").trimResults().omitEmptyStrings().split(Strings.nullToEmpty(HiveConf.getVar(conf, HiveConf.ConfVars.POSTEXECHOOKS))));if (postExecHooks.contains("org.apache.hadoop.hive.ql.hooks.PostExecutePrinter")|| postExecHooks.contains("org.apache.hadoop.hive.ql.hooks.LineageLogger")|| postExecHooks.contains("org.apache.atlas.hive.hook.HiveHook")) {// 使用菱形操作符简化泛型类型声明ArrayList<Transform> transformations = new ArrayList<>();transformations.add(new HiveOpConverterPostProc());transformations.add(new Generator(postExecHooks));for (Transform t : transformations) {pCtx = t.transform(pCtx);}// 我们仅使用视图名称作为位置。queryState.getLineageState().mapDirToOp(new Path(createVwDesc.getViewName()), sinkOp);}return;}}// 6. 如果需要,生成表访问统计信息if (HiveConf.getBoolVar(this.conf, HiveConf.ConfVars.HIVE_STATS_COLLECT_TABLEKEYS)) {TableAccessAnalyzer tableAccessAnalyzer = new TableAccessAnalyzer(pCtx);setTableAccessInfo(tableAccessAnalyzer.analyzeTableAccess());}// 7. 执行逻辑优化if (LOG.isDebugEnabled()) {LOG.debug("Before logical optimization\n" + Operator.toString(pCtx.getTopOps().values()));}// 创建一个优化器实例,并对解析上下文进行逻辑优化。Optimizer optm = new Optimizer();// 设置优化器的解析上下文optm.setPctx(pCtx);// 初始化优化器optm.initialize(conf);// 执行优化操作,并更新解析上下文pCtx = optm.optimize();// 检查优化后的解析上下文中是否包含列访问信息if (pCtx.getColumnAccessInfo() != null) {// 设置列访问信息,用于视图列授权setColumnAccessInfo(pCtx.getColumnAccessInfo());}// 如果启用了调试日志,则输出优化后的操作符树信息if (LOG.isDebugEnabled()) {LOG.debug("After logical optimization\n" + Operator.toString(pCtx.getTopOps().values()));}// 8. Generate column access stats if required - wait until column pruning// takes place during optimization// 检查是否需要收集列访问信息用于授权或统计boolean isColumnInfoNeedForAuth = SessionState.get().isAuthorizationModeV2()&& HiveConf.getBoolVar(conf, HiveConf.ConfVars.HIVE_AUTHORIZATION_ENABLED);if (isColumnInfoNeedForAuth|| HiveConf.getBoolVar(this.conf, HiveConf.ConfVars.HIVE_STATS_COLLECT_SCANCOLS)) {// 创建列访问分析器实例ColumnAccessAnalyzer columnAccessAnalyzer = new ColumnAccessAnalyzer(pCtx);// 分析列访问信息,并更新列访问信息// view column access info is carried by this.getColumnAccessInfo().setColumnAccessInfo(columnAccessAnalyzer.analyzeColumnAccess(this.getColumnAccessInfo()));}// 9. Optimize Physical op tree & Translate to target execution engine (MR,// TEZ..)// 检查是否需要进行逻辑解释,如果不需要则进行物理操作树的优化和编译if (!ctx.getExplainLogical()) {// 获取任务编译器实例TaskCompiler compiler = TaskCompilerFactory.getCompiler(conf, pCtx);// 初始化任务编译器compiler.init(queryState, console, db);// 编译解析上下文,生成任务和输入输出信息compiler.compile(pCtx, rootTasks, inputs, outputs);// 获取获取任务fetchTask = pCtx.getFetchTask();}//find all Acid FileSinkOperatorS// 创建查询计划后处理器实例,但该实例未被使用,后续可考虑移除QueryPlanPostProcessor qp = new QueryPlanPostProcessor(rootTasks, acidFileSinks, ctx.getExecutionId());// 10. Attach CTAS/Insert-Commit-hooks for Storage Handlers// 查找根任务列表中的第一个TezTaskfinal Optional<TezTask> optionalTezTask =rootTasks.stream().filter(task -> task instanceof TezTask).map(task -> (TezTask) task).findFirst();if (optionalTezTask.isPresent()) {// 获取第一个TezTask实例final TezTask tezTask = optionalTezTask.get();// 遍历根任务列表,为满足条件的DDLWork添加插入提交hook任务rootTasks.stream()// 过滤出工作类型为DDLWork的任务.filter(task -> task.getWork() instanceof DDLWork)// 将任务转换为DDLWork类型.map(task -> (DDLWork) task.getWork())// 过滤出预插入表描述不为空的DDLWork.filter(ddlWork -> ddlWork.getPreInsertTableDesc() != null)// 获取预插入表描述.map(ddlWork -> ddlWork.getPreInsertTableDesc())// 创建插入提交hook描述.map(ddlPreInsertTask -> new InsertCommitHookDesc(ddlPreInsertTask.getTable(),ddlPreInsertTask.isOverwrite()))// 为TezTask添加依赖任务.forEach(insertCommitHookDesc -> tezTask.addDependentTask(TaskFactory.get(new DDLWork(getInputs(), getOutputs(), insertCommitHookDesc), conf)));}LOG.info("Completed plan generation");// 11. put accessed columns to readEntity// 检查是否需要收集扫描列的统计信息if (HiveConf.getBoolVar(this.conf, HiveConf.ConfVars.HIVE_STATS_COLLECT_SCANCOLS)) {// 将访问的列信息添加到读取实体中putAccessedColumnsToReadEntity(inputs, columnAccessInfo);}// 检查是否启用了查询结果缓存,并且查找信息不为空if (isCacheEnabled && lookupInfo != null) {// 检查查询是否可以被缓存if (queryCanBeCached()) {// 创建缓存查询信息QueryResultsCache.QueryInfo queryInfo = createCacheQueryInfoForQuery(lookupInfo);// Specify that the results of this query can be cached.// 指定该查询的结果可以被缓存setCacheUsage(new CacheUsage(CacheUsage.CacheStatus.CAN_CACHE_QUERY_RESULTS, queryInfo));}}}
简单总结一下,首先输入的是AST抽象语法树,主要经历了以下步骤:
- Generate Resolved Parse tree from syntax tree 从语法树生成解析树
- Gen OP Tree from resolved Parse Tree 从解析树生成Gen OP树 OperatorTree
- Deduce Resultset Schema(selct ...... 每个字段,我给你构造成一个 Field)推导结果集模式 CBO优化
- Generate ParseContext for Optimizer & Physical compiler 为优化器和物理编译器生成解析上下文
- Take care of view creation 注意视图创建
- Generate table access stats if required 生成表访问统计信息(如果需要)
- Perform Logical optimization 执行逻辑执行计划的优化
- Generate column access stats if required - wait until column pruning takes place during optimization
根据需要生成列访问统计信息-等待优化期间进行列裁剪
sql当中写了很多的无用的字段,但是最终执行逻辑不需要这些字段,就需要列裁剪。 - Optimize Physical op tree & Translate to target execution engine (MR, Spark, TEZ..) 优化物理操作树并转换为目标执行引擎(MR,TEZ ..)
- put accessed columns to readEntity 将访问的列放入 ReadEntity(要读取的列的信息)
- if desired check we're not going over partition scan limits 如果需要检查,我们不会超过分区扫描限制
生成QueryPlan
这一系列操作完成后,最后就是把得到的 TaskTree 生成一个 QueryPlan,相关源码如下:
/*** 创建一个新的 QueryPlan 对象。* * @param queryStr 要执行的查询字符串* @param sem 语义分析器对象,用于对查询进行语义分析* @param startTime 驱动程序开始运行的时间,通过 perfLogger 获取* @param queryId 查询的唯一标识符* @param hiveOperation Hive 操作类型* @param schema 查询结果的输出模式*/
plan = new QueryPlan(queryStr, sem, perfLogger.getStartTime(PerfLogger.DRIVER_RUN), queryId, queryState.getHiveOperation(), schema);
总结
本文介绍了Hive,并通过源码梳理了Hive的执行原理,其核心正是引入篇我们提到的解析(Parsing)、校验(Validation)、优化(Optimization)和执行(Execution)。
总结起来主要有以下四个步骤:
- 词法分析与解析
将SQL语法转换成AST(抽象语法树) - 语义分析
将AST进行进一步的抽象和结构化处理,通过遍历AST(抽象语法树) 将其转化成Query Block - 逻辑优化
到了第三步时,操作符树虽然已经勾勒出执行任务的先后顺序和上下游依赖,但细节还比较粗糙,例如存在重复的数据扫描、不必要的Shuffle操作等,因此还需要进行进一步优化。通过优化,Hive可以改进查询的执行计划,并生成更高效的作业图以在分布式计算框架中执行。这些优化可以提高查询的性能和效率,并减少资源开销。 - 物理优化
在逻辑优化阶段结束后,输入的SQL语句也逐步转换为优化后的逻辑计划,不过此时的逻辑计划仍然不能直接执行,还需要进一步转换成可以识别并执行的MapReduce Task,首先将优化后的OperatorTree(逻辑执行计划)转换成TaskTree(物理执行计划,每个Task对应一个MR Job任务),并对物理执行计划进行一些优化,然后依次调用执行。
有朋友看了初版觉得写的不够细,私信让我迭代丰富一下,但还有一些有意思的细节,比如4.x源码的区别等,感兴趣的小伙伴可以自行深入探索一下。
相关文章:
大数据SQL调优专题——Hive执行原理
引入 Apache Hive 是基于Hadoop的数据仓库工具,它可以使用SQL来读取、写入和管理存在分布式文件系统中的海量数据。在Hive中,HQL默认转换成MapReduce程序运行到Yarn集群中,大大降低了非Java开发者数据分析的门槛,并且Hive提供命令…...
MySQL常见错误码及解决方法(1130、1461、2003、1040、2000、1049、1062、1129、2002、1690等)
目录 【问题1】、FATAL: error 1130: Unknown error 1130 【问题2】、FATAL: error: 1461 【问题3】、ERROR 2003 (HY000): Cant connect to MySQL server on "" (113) 【问题4】、FATAL: error 2003: Cant connect to MySQL server on 172.19.111.151 (111) 【问…...
类和对象详解(下)-----运算符重载
目录 1.运算符重载 2.赋值运算符重载 3.取地址运算符重载 3.1const成员函数 3.2取地址运算符重载 1.运算符重载 什么是运算符重载呢?简单举个例子就懂了。 就是我想实现日期的加法,而“”这个运算符C只实现了内置类型的加法,而我们要想实…...
Visonpro 检测是否有缺齿
一、效果展示 二、上面是原展开工具CogPolarUnwrapTool; 第二种方法: 用Blob 和 CogCopyRegionTool 三、 用预处理工具 加减常数,让图片变得更亮点 四、圆展开工具 五、模板匹配 六、代码分解 1.创建集合和文子显示工具 CogGraphicCollec…...
(萌新入门)如何从起步阶段开始学习STM32 ——2 我应该学习HAL库还是寄存器库?
概念 笔者下面需要介绍的是库寄存器和HAL库两个重要的概念,在各位看完之后,需要决定自己的学习路线到底是学习HAL呢?还是寄存器呢?还是两者都学习呢? 库寄存器 库寄存器就是简单的封装了我们对寄存器的操作…...
【SQL技术】不同数据库引擎 SQL 优化方案剖析
一、引言 在数据处理和分析的世界里,SQL 是不可或缺的工具。不同的数据库系统,如 MySQL、PostgreSQL(PG)、Doris 和 Hive,在架构和性能特点上存在差异,因此针对它们的 SQL 优化策略也各有不同。这些数据库…...
什么是原型?
在 JavaScript 中,原型(Prototype)是每个 JavaScript 对象都有的一个属性,用来实现对象之间的继承。原型是 JavaScript 面向对象编程的核心概念之一,通过原型链(prototype chain),一…...
【第10章:自然语言处理高级应用—10.4 NLP领域的前沿技术与未来趋势】
各位技术探险家们,今天我们要开启一场穿越语言智能奇点的时空之旅。从正在改写物理定律的万亿参数大模型,到能看懂《星际穿越》剧本的跨模态AI,再到正在颠覆编程方式的神经-符号混合系统……这篇万字长文将带你摸清NLP技术进化的七块关键拼图。(建议边读边做笔记,文末有技…...
41.日常算法
1.面试题 02.04. 分割链表 题目来源 给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。你不需要 保留 每个分区中各节点的初始相对位置。 示例 1: 输入:…...
CPP集群聊天服务器开发实践(五):nginx负载均衡配置
1 负载均衡器的原理与功能 单台Chatserver可以容纳大约两万台客户端同时在线聊天,为了提升并发量最直观的办法需要水平扩展服务器的数量,三台服务器可以容纳六万左右的客户端。 负载均衡器的作用: 把client的请求按照负载均衡算法分发到具体…...
Java 中的 HashSet 和 HashMap 有什么区别?
一、核心概念与用途 特性HashSetHashMap接口实现实现 Set 接口(存储唯一元素)实现 Map 接口(存储键值对)数据存储存储单个对象(元素唯一)存储键值对(键唯一,值可重复)典…...
AI大模型的技术突破与传媒行业变革
性能与成本:AI大模型的“双轮驱动” 过去几年,AI大模型的发展经历了从实验室到产业化的关键转折。2025年初,以DeepSeek R1为代表的模型在数学推理、代码生成等任务中表现超越国际头部产品,而训练成本仅为传统模型的几十分之一。这…...
Golang学习01:Go安装和配置+Vscode、GoLand安装激活+Go环境变量避坑的超详细教程
🪁🍁 希望本文能给您带来帮助,如果有任何问题,欢迎批评指正!🐅🐾🍁🐥 文章目录 一、背景二、Go语言安装2.1 Go语言环境安装2.2 Go语言环境验证2.3 其他配置 三、开发环境…...
案例-06.部门管理-根据ID查询
一.根据ID查询-接口文档 二.根据ID查询-Controller层 package com.gjw.controller;/*** 部门管理Controller*/import com.gjw.anno.Log; import com.gjw.pojo.Dept; import com.gjw.pojo.Result; import com.gjw.service.DeptService; import com.gjw.service.impl.DeptServi…...
解决No matching client found for package name xxx编译报错的问题
如果Android工程编译报错,并且信息如下: Execution failed for task :app:processDebugGoogleServices. > No matching client found for package name com.demo.test可能的原因为google-services.json中定义的package_name属性跟app当前的包名不符&…...
基于deepseek api和openweather 天气API实现Function Calling技术讲解
以下是一个结合DeepSeek API和OpenWeather API的完整Function Calling示例,包含意图识别、API调用和结果整合: import requests import json import os# 配置API密钥(从环境变量获取) DEEPSEEK_API_KEY os.getenv("DEEPSEE…...
什么是全局污染,怎么避免全局污染?
具体表现: 全局变量:当变量在全局作用域(通常是 window 对象)中定义时,它会在整个应用程序中都可访问。这个变量可能会被其他部分的代码意外修改或覆盖,导致难以追踪和调试错误。 命名冲突:全局…...
机器视觉--switch语句
引言 在 Halcon 这个强大的机器视觉软件里,编程控制结构对于高效处理图像任务至关重要。其中,Switch 语句作为一种多分支选择结构,能够根据不同的条件值执行不同的代码块,让程序的逻辑更加清晰和简洁。本文将全面深入地介绍 Halc…...
C++ std::atomic可以使用复杂类型(类和结构体)吗
目录 1.引言 2.std::atomic 支持的复杂类型 3.std::atomic与无锁 4.如何使用 std::atomic 保护复杂类型 4.1.使用互斥锁(Mutex) 4.2.使用 std::atomic_flag 和自旋锁 4.3.原子共享指针(Atomic Shared Pointers) 4.4.使用高…...
音乐随想、日语认识
Rapport的日文歌词(初) Rapport - キタニタツヤ 词:キタニタツヤ 《《 ki ta ni ta tsu ya 歌手的名字,全是片假名,不是本土的平假名(为了国外市场的做法?) 》》 曲:キタニタツヤ 编曲&am…...
SpringBoot速成(11)更新用户头像,密码P13-P14
更新头像: 1.代码展示: 1.RequestParam 是 Spring MVC 中非常实用的注解,用于从 HTTP 请求中提取参数并绑定到控制器方法的参数上。 2.PatchMapping 是 Spring MVC 中的一个注解,用于处理 HTTP 的 PATCH 请求。PATCH 请求通常用于对资源的部…...
自动化测试面试会问哪些?
自动化测试面试1: 1、使用什么测试框架做的上一个项目的自动化测试。 2、自己最熟悉哪个库,如何使用这些库的,是否做了基于复用的封装,怎么考虑的这些封装 3、如何定位app上的元素 4、//*[contains(text,"登录")] 是…...
SQL Server 导入Excel数据
1、选中指定要导入到哪个数据库,右键选择 》任务 》导入数据 2、数据源 选择Excel,点击 下一步(Next) 3、目前 选择OLE DB Provider ,点击 下一步(Next) 4、默认 ,点击 下一步(Next)…...
车载音频架构图详解(精简)
目录 上图是车载音频架构图,对这个图我们进行详细的分析 左边第一层 是 app 常用的类有MediaPlayer和MediaRecorder, AudioTrack和AudioRecorder 第二层 是framework提供给应用的多媒体功能的API类,封装在android.media.* API包中。编译后,在framework.jar中。...
基于SpringBoot+Vue的智慧校园管理系统设计和实现(源码+文档+部署讲解)
🎬 秋野酱:《个人主页》 🔥 个人专栏:《Java专栏》《Python专栏》 ⛺️心若有所向往,何惧道阻且长 文章目录 .🚀 技术架构技术栈全景 🎯 功能模块功能矩阵表📊 数据库设计核心ER关系图 💻 核心…...
浏览器打印局部网页,设置页眉
占位的页眉 重点部分 1.样式间隙 page { margin-top: 60px; /* 为页眉留出空间,页眉的高度要和他一样 */ top-right { height: 60px; 同时右侧,内容布局右上角要留出60px的 2.背景图片 如果页眉…...
腿足机器人之六- 前向运动学
腿足机器人之六- 前向运动学 刚体运动学基础坐标系定义旋转矩阵与欧拉角齐次变换矩阵(平移旋转的统一表示) 运动链建模串联运动链结构(从基座到末端的关节连接)标准Denavit-Hartenberg(D-H)参数法改进D-H参…...
对openharmony HDF驱动框架的C/S设计模式和单例类的说明
在分析openharmony的HDF驱动框架时我们会发现用了很多面向对象的思想,例如类继承、接口、单例类等,本来应该是好事情,**但使用时对象之间的关系交错复杂,不太符合linux内核分层分模块的思路,导致整体理解起来比较困难&…...
kamailio中Core Cookbook 核心配置手册
Core Cookbook 核心配置手册 版本: Kamailio SIP 服务器 v6.0.x (稳定版) 概述 本教程收集了 Kamailio 核心导出到配置文件的功能和参数。 注意: 本页参数未按字母顺序排列。 结构 kamailio.cfg 的结构可分为三部分: 全局参数模块设置路由块 建议按此顺序排列以保持清晰…...
AI 编程工具—Cursor 进阶篇 数据分析
AI 编程工具—Cursor 进阶篇 数据分析 上一节课我们使用Cursor 生成了北京房产的销售数据,这一节我们使用Cursor对这些数据进行分析,也是我们尝试使用Cursor 去帮我们做数据分析,从而进一步发挥Cursor的能力,来帮助我们完成更多的事情 案例一 房产销售数据分析 @北京202…...
HTML、Vue和PHP文件的区别与联系
一、核心区别 类型性质执行环境功能特点.html静态标记语言浏览器直接解析定义页面结构和内容,无逻辑处理能力.vue前端框架组件文件浏览器/构建工具整合HTML模板JS逻辑CSS样式,支持动态数据绑定和组件化开发.php服务器端脚本语言文件Web服务器执行动态生…...
Map 和 Set
目录 一、搜索 概念: 模型: 二、Map 编辑 1.Map 实例化: 2. Map的常见方法: 3.Map的常见方法演示: 1. put(K key, V value):添加键值对 3. containsKey(Object key):检查键是否存在 4.…...
白话大模型LLM-通用基础入门知识-适合给纯小白的入门!
文章目录 什么是大模型大模型训练预训练监督微调SFTRLHF基于人类反馈的强化学习 大模型分类大语言模型-LLM多模态模型-VLM视觉模型音频模型 大模型工作流程分词化与词表映射大模型回答过程 & 基于token的概率预测 Agent导论子任务拆分 什么是大模型 大模型就是训练的一个能…...
线程进入WAITING的N种方式
目录 一、调用 Object 的 wait 方法 二、调用 Thread.join 方法 三、调用LockSupport.park()方法 一、调用 Object 的 wait 方法 public static void main(String[] args) throws InterruptedException {// 创建一个锁对象Object lock new Object();Thread thread new Thr…...
智能车摄像头开源—8 元素处理
目录 一、前言 二、无元素状态 三、直线与弯道 四、十字与环岛 1、十字识别处理 2、环岛识别处理 五、坡道 六、障碍物 七、斑马线 八、入库 九、出界停车 一、前言 在写这篇文章之前,考虑了很久到底该写到什么程度,但思来想去,不同…...
【从0做项目】Java搜索引擎(4)——性能优化~烧脑~~~
本篇文章将对项目搜索引擎(1)~(3)进行性能优化,包括测试,优化思路,优化前后对比 目录 一:文件读取 二:实现多线程制作索引 1:代码分析 2:代码…...
人工智障的软件开发-git仓库篇-弃gitlab,走gitea
指令接收:「开始构建代码宇宙」 系统检测:需求模糊度99.9% 启动应急协议:构建最小可行性生态圈 核心组件锁定:代码基因库(人类称之为Git仓库) 需求分析:论人类语言的艺术性 人类指令翻译机 表…...
Spring Boot 如何实现自动配置?
欢迎并且感谢大家指出我的问题,由于本人水平有限,有些内容写的不是很全面,只是把比较实用的东西给写下来,如果有写的不对的地方,还希望各路大牛多多指教!谢谢大家!🥰 大家如果对Java…...
STM32H743ZIT6 FreeRTOS CMSIS_V2 Lwip DP83848/LAN8720 最新HAL V1.12.1版本 AC6编译器,速通。
HAL库版本:V1.12.1 最新版 这版CUBEmx生成的LAN8742 的驱动文件有问题,无法正常初始化,导致无法PING通。 lwip 内存池 不需要手动指定0x30040200区域,lwipopts.h已作配置 开启DCACH 和ICACH 和 D2域SRAM3 时钟 /*** brief Th…...
C# 添加图标
一、前言 为应用程序添加图标是优化用户界面、提升应用辨识度的重要操作。合适的图标能帮助用户快速识别和区分不同应用,增强应用的易用性和专业性。 本指南旨在为你提供详细、易懂的步骤,教你如何为应用程序的窗体添加图标。从图标素材的获取到具体的…...
MVC模式和MVVM模式
目录 一、MVC模式和MVVM模式 1. MVC模式 2. MVVM 模式 3.在Qt中的应用示例 4.总结 二、MVC与MVVM模式的共同点和区别 1.共同点 2.区别 3.交互流程 4.总结 MVC(Model-View-Controller)和MVVM(Model-View-ViewModel)是两种…...
【kafka系列】Kafka如何实现高吞吐量?
目录 1. 生产者端优化 核心机制: 关键参数: 2. Broker端优化 核心机制: 关键源码逻辑: 3. 消费者端优化 核心机制: 关键参数: 全链路优化流程 吞吐量瓶颈与调优 总结 Kafka的高吞吐能力源于其生…...
如何学习Elasticsearch(ES):从入门到精通的完整指南
如何学习Elasticsearch(ES):从入门到精通的完整指南 嘿,小伙伴们!如果你对大数据搜索和分析感兴趣,并且想要掌握Elasticsearch这一强大的分布式搜索引擎,那么你来对地方了!本文将为…...
GDB QUICK REFERENCE (GDB 快速参考手册)
GDB QUICK REFERENCE {GDB 快速参考手册} References GDB QUICK REFERENCE GDB Version 4 https://users.ece.utexas.edu/~adnan/gdb-refcard.pdf 查看方式:在新标签页中打开图片 References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/ [2] gdb-refc…...
Flutter_学习记录_动画的简单了解
用AnimationController简单实现如下的效果图: 1. 只用AnimationController实现简单动画 1.1 完整代码案例 import package:flutter/material.dart;class AnimationDemo extends StatefulWidget {const AnimationDemo({super.key});overrideState<AnimationDe…...
【JavaEE进阶】验证码案例
目 🌲实现说明 🎄Hutool介绍 🌳准备工作 🌴约定前后端交互接口 🚩接口定义 🚩实现服务器后端代码 🚩前端代码 🚩整体测试 🌲实现说明 随着安全性的要求越来越⾼…...
SQL复习
SQL复习 MySQL MySQL MySQL有什么特点? MySQL 不支持全外连接。 安装 数据类型 MySQL中的数据类型分为哪些? MySQL中的数据类型主要分为三大类:数值类型、字符串类型、日期时间类型。 其中, 数值类型又分为七种:T…...
景联文科技:以精准标注赋能AI未来,打造高质量数据基石
在人工智能蓬勃发展的时代,数据已成为驱动技术革新的核心燃料,而高质量的数据标注则是让AI模型从“感知”走向“认知”的关键桥梁。作为深耕数据服务领域的创新者,景联文科技始终以“精准、高效、安全”为核心理念,为全球AI企业提…...
蓝桥杯(B组)-每日一题(阶乘求和)
题目 代码解析: #include<iostream> using namespace std;long long multiply(int x) {long long sum1;//定义longlong类型初始为1 for(int i1;i<x;i)sumsum*i;//每一项的阶乘 return sum;//将阶乘结果返回 }int main() {int n;cin>>n;long long r…...
大模型应用开发时如何调试提示词?
在编程领域,调试通常依赖于断点、堆栈跟踪和详细的错误信息。然而,在提示调试的上下文中,这些传统工具变得不再适用。提示调试更多地依赖于对任务的理解、对提示的精细调整,以及对结果的迭代优化。在本文,我们将深入探…...