目录

  • 概述
  • 过程分析
    • 参数介绍
    • 初始化
    • 类型兼容性检查
    • 修改表名操作
    • 其他修改操作
    • 异常处理
  • 总结

概述

Alter Table是非常常见的一种DDL操作,在Hive中,使用Alter Table可以修改表名,修改表属性,添加字段,修改字段数据类型等等。需要注意的是,ALTER TABLE修改的只是MySQL中的相关元数据,不会对表数据产生影响,用户需要手动确保实际数据符合元数据的定义,否则查询不会返回预期的结果。但在分区数比较多的情况下,修改表名这类逻辑上非常简单的操作时间开销却很大。因此对Alter Table语句的具体执行过程进行分析一探究竟。

本文基于 Apache Hive 3.1.1 版本分析,源码所在位置:alter table的源码在org.apache.hadoop.hive.metastore.HiveAlterHandler 类的alterTable方法中

过程分析

参数介绍

该方法的参数如下

public void alterTable(RawStore msdb, Warehouse wh, String catName, String dbname,String name, Table newt, EnvironmentContext environmentContext,IHMSHandler handler) throws InvalidOperationException, MetaException {
  • RawStore msdb:用以操作MySQL的接口工具,其只有一个实现类ObjectStore
  • Warehouse wh:表示表数据的存储位置
  • String catName:catalog名称(该字段是Hive3中新增的,用的很少)
  • String dbname:数据库名称
  • String name:表名
  • Table newt:新表对象的相关属性,Table类型,如表名、所属数据库名、所属用户名、分区字段等信息
  • EnvironmentContext environmentContext:顾名思义,环境相关的上下文信息
  • IHMSHandler handler:An interface wrapper for HMSHandler. This interface contains methods that need to be called by internal classes but that are not part of the thrift interface

初始化

首先进行一些变量的初始化工作,以及新表对象的相关属性(比如表名、字段类型)进行判断

catName = normalizeIdentifier(catName);
name = name.toLowerCase(); // 表名转换为小写
dbname = dbname.toLowerCase(); // 库名转换为小写final boolean cascade = environmentContext != null //判断是否使用了 CASCADE 关键字&& environmentContext.isSetProperties()&& StatsSetupConst.TRUE.equals(environmentContext.getProperties().get(StatsSetupConst.CASCADE));
if (newt == null) { // 新表对象如果为空,则抛出异常throw new InvalidOperationException("New table is null");
}String newTblName = newt.getTableName().toLowerCase();
String newDbName = newt.getDbName().toLowerCase();if (!MetaStoreUtils.validateName(newTblName, handler.getConf())) { //检查新表名是否合规throw new InvalidOperationException(newTblName + " is not a valid object name");
}
String validate = MetaStoreUtils.validateTblColumns(newt.getSd().getCols()); //检查新表的字段类型是否合规
if (validate != null) {throw new InvalidOperationException("Invalid column " + validate);
}
// 初始化表相关的路径信息,用以修改表名的操作
Path srcPath = null;
FileSystem srcFs;
Path destPath = null;
FileSystem destFs = null;boolean success = false;
boolean dataWasMoved = false;  //标记数据是否被移动了
boolean isPartitionedTable = false; // 标记是否是分区表

以及判断是否要修改表名,如果是,则需要判断新的表名是否已经存在,旧表是否存在、是否是分区表等操作

boolean rename = false;
List<Partition> parts;// 不允许对catName进行修改
if (!catName.equalsIgnoreCase(newt.getCatName())) {throw new InvalidOperationException("Tables cannot be moved between catalogs, old catalog" +catName + ", new catalog " + newt.getCatName());
}// 检查新的表名是否已经存在,只有新的表名不存在才能进行修改
if (!newTblName.equals(name) || !newDbName.equals(dbname)) {if (msdb.getTable(catName, newDbName, newTblName) != null) {throw new InvalidOperationException("new table " + newDbName+ "." + newTblName + " already exists");}rename = true;
}msdb.openTransaction();
// 旧表是否存在,不存在将抛出异常
oldt = msdb.getTable(catName, dbname, name);
if (oldt == null) {throw new InvalidOperationException("table " +Warehouse.getCatalogQualifiedTableName(catName, dbname, name) + " doesn't exist");
}
// 旧表是否是分区表
if (oldt.getPartitionKeysSize() != 0) {isPartitionedTable = true;
}

类型兼容性检查

如果参数 hive.metastore.disallow.incompatible.col.type.changes 为true(默认就是true),则会调用方法 checkColTypeChangeCompatible 检查字段类型是否兼容,比如将某个字段的类型从String修改为INT时就会报错,说明二者地段类型不兼容。

// Views derive the column type from the base table definition.  So the view definition
// can be altered to change the column types.  The column type compatibility checks should
// be done only for non-views.
if (MetastoreConf.getBoolVar(handler.getConf(),MetastoreConf.ConfVars.DISALLOW_INCOMPATIBLE_COL_TYPE_CHANGES) &&!oldt.getTableType().equals(TableType.VIRTUAL_VIEW.toString())) {// Throws InvalidOperationException if the new column types are not// compatible with the current column types.// 如果要修改字段类型,则需要检查字段类型是否兼容checkColTypeChangeCompatible(oldt.getSd().getCols(), newt.getSd().getCols());
}//check that partition keys have not changed, except for virtual views
//however, allow the partition comments to change
// 分区字段的关键字是不允许修改的(virtual views除外),但是可以修改分区的注释信息
boolean partKeysPartiallyEqual = checkPartialPartKeysEqual(oldt.getPartitionKeys(),newt.getPartitionKeys());if(!oldt.getTableType().equals(TableType.VIRTUAL_VIEW.toString())){if (!partKeysPartiallyEqual) {throw new InvalidOperationException("partition keys can not be changed.");}
}

修改表名操作

如果rename是true,则说明要对表名进行修改。但是修改表名必须要满足相关条件。从代码中可以看到,对于外部表以及virtual view 是不能修改表名的,也就是说修改表名操作只能针对内部表。

同时对于内部表(managed table),如果表的存储路径用的是默认路径(也就是创建表时用户没有使用LOCATION指定路径),hive在修改表名是就会将表的数据移动到新表名对应的目录下。如下代码,从注释信息可以看出,如果用户在创建一个内部表时使用了例如 create table tbl … location … 这样的方式,则在修改表名的时候会被当做外部表对待,因此表对应的数据目录不会发生改变。

// rename needs change the data location and move the data to the new location corresponding
// to the new name if:
// 1) the table is not a virtual view, and   (表不能是virtual view)
// 2) the table is not an external table, and (不能是外部表)
// 3) the user didn't change the default location (or new location is empty), and (没有改变表的默认路径)
// 4) the table was not initially created with a specified location
if (rename&& !oldt.getTableType().equals(TableType.VIRTUAL_VIEW.toString())&& (oldt.getSd().getLocation().compareTo(newt.getSd().getLocation()) == 0|| StringUtils.isEmpty(newt.getSd().getLocation()))&& !MetaStoreUtils.isExternalTable(oldt)) {Database olddb = msdb.getDatabase(catName, dbname);// if a table was created in a user specified location using the DDL like// create table tbl ... location ...., it should be treated like an external table// in the table rename, its data location should not be changed. We can check// if the table directory was created directly under its database directory to tell// if it is such a tablesrcPath = new Path(oldt.getSd().getLocation());String oldtRelativePath = (new Path(olddb.getLocationUri()).toUri()).relativize(srcPath.toUri()).toString();// 表的location是否是指定的,而不是使用默认的路径   boolean tableInSpecifiedLoc = !oldtRelativePath.equalsIgnoreCase(name)&& !oldtRelativePath.equalsIgnoreCase(name + Path.SEPARATOR);// 如果表使用的是默认路径,则需要对数据进行移动if (!tableInSpecifiedLoc) {srcFs = wh.getFs(srcPath);// get new locationDatabase db = msdb.getDatabase(catName, newDbName);Path databasePath = constructRenamedPath(wh.getDatabasePath(db), srcPath);destPath = new Path(databasePath, newTblName);destFs = wh.getFs(destPath);newt.getSd().setLocation(destPath.toString());// check that destination does not exist otherwise we will be// overwriting data// check that src and dest are on the same file system// 新旧路径必须是同一个filesystemif (!FileUtils.equalsFileSystem(srcFs, destFs)) {throw new InvalidOperationException("table new location " + destPath+ " is on a different file system than the old location "+ srcPath + ". This operation is not supported");}try {if (destFs.exists(destPath)) { //如果新的路径已经存在,则无法修改throw new InvalidOperationException("New location for this table " +Warehouse.getCatalogQualifiedTableName(catName, newDbName, newTblName) +" already exists : " + destPath);}// check that src exists and also checks permissions necessary, rename src to dest//检查原有路径是否存在,且权限是否满足要求,然后修改路径名称if (srcFs.exists(srcPath) && wh.renameDir(srcPath, destPath,ReplChangeManager.isSourceOfReplication(olddb))) {dataWasMoved = true; //标记数据被移动了,事实上在同一个filesystem中移动数据是非常快的,开销很低}} catch (IOException | MetaException e) {LOG.error("Alter Table operation for " + dbname + "." + name + " failed.", e);throw new InvalidOperationException("Alter Table operation for " + dbname + "." + name +" failed to move data due to: '" + getSimpleMessage(e)+ "' See hive log file for details.");}if (!HiveMetaStore.isRenameAllowed(olddb, db)) {LOG.error("Alter Table operation for " + Warehouse.getCatalogQualifiedTableName(catName, dbname, name) +"to new table = " + Warehouse.getCatalogQualifiedTableName(catName, newDbName, newTblName) +" failed ");throw new MetaException("Alter table not allowed for table " +Warehouse.getCatalogQualifiedTableName(catName, dbname, name) +"to new table = " + Warehouse.getCatalogQualifiedTableName(catName, newDbName, newTblName));}}

上面的操作只是在存储层面完成的变更,而表对应的元数据还没有进行修改。因此下面就需要对元数据进行修改了。

对于分区表和非分区表,处理的过程是不一样的。对于分区表,需要对每个分区的相关属性进行修改(如分区的存储路径、分区所属的库名,表名,分区的字段统计信息);而非分区表则直接调用alterTableUpdateTableColumnStats()方法。。如下:

if (isPartitionedTable) { //分区表的处理过程String oldTblLocPath = srcPath.toUri().getPath();String newTblLocPath = dataWasMoved ? destPath.toUri().getPath() : null;// also the location field in partitionparts = msdb.getPartitions(catName, dbname, name, -1);Map<Partition, ColumnStatistics> columnStatsNeedUpdated = new HashMap<>();for (Partition part : parts) {// 遍历每个分区String oldPartLoc = part.getSd().getLocation();if (dataWasMoved && oldPartLoc.contains(oldTblLocPath)) {URI oldUri = new Path(oldPartLoc).toUri();String newPath = oldUri.getPath().replace(oldTblLocPath, newTblLocPath);Path newPartLocPath = new Path(oldUri.getScheme(), oldUri.getAuthority(), newPath);part.getSd().setLocation(newPartLocPath.toString()); //设置新的路径}part.setDbName(newDbName); //设置库名part.setTableName(newTblName); //设置表名ColumnStatistics colStats = updateOrGetPartitionColumnStats(msdb, catName, dbname, name,part.getValues(), part.getSd().getCols(), oldt, part, null);if (colStats != null) { //如果有字段统计信息,则需要对其进行更新columnStatsNeedUpdated.put(part, colStats);}}msdb.alterTable(catName, dbname, name, newt); // alterPartition is only for changing the partition location in the table renameif (dataWasMoved) { //如果数据发生了移动,则采用批量的方式对每个分区的location信息进行修改int partsToProcess = parts.size();int partitionBatchSize = MetastoreConf.getIntVar(handler.getConf(),MetastoreConf.ConfVars.BATCH_RETRIEVE_MAX);int batchStart = 0;while (partsToProcess > 0) {int batchEnd = Math.min(batchStart + partitionBatchSize, parts.size());List<Partition> partBatch = parts.subList(batchStart, batchEnd);int partBatchSize = partBatch.size();partsToProcess -= partBatchSize;batchStart += partBatchSize;List<List<String>> partValues = new ArrayList<>(partBatchSize);for (Partition part : partBatch) {partValues.add(part.getValues());}msdb.alterPartitions(catName, newDbName, newTblName, partValues, partBatch);}}// 统计信息临时存储在columnStatsNeedUpdated中,这是一个HashMap,key是分区信息,value是对应的统计信息。for (Entry<Partition, ColumnStatistics> partColStats : columnStatsNeedUpdated.entrySet()) { ColumnStatistics newPartColStats = partColStats.getValue();newPartColStats.getStatsDesc().setDbName(newDbName);newPartColStats.getStatsDesc().setTableName(newTblName);msdb.updatePartitionColumnStatistics(newPartColStats, partColStats.getKey().getValues());// 分区对字段统计信息的元数据进行修改}
} else { //对于非分区表,处理过程则简单的多alterTableUpdateTableColumnStats(msdb, oldt, newt);
}

通过上面的分析,可以知道对于生成环境中分区数(数千甚至上万)比较多的表,这一步的操作是比较耗时的。在实际使用过程中发现,如果开启了分区的字段统计信息功能,在修改表名时,耗时通常在半个小时以上甚至更多。

至此修改表名的操作就完成了。

其他修改操作

alter table的其他操作,同样是分为非分区表和分区表。

对于分区表,则分为两种情况

  • 第一种,如果修改语句涉及字段相关的修改(比如修改字段类型,添加字段等操作),则需要一个一个分区的修改,同时如果语句中使用了 CASCADE 关键字,就将历史分区的字段元数据信息也进行修改,如果没有使用,则不对历史分区的信息进行修改。不过这里无论是否使用该关键字,都需要对元数据进行一次update操作。
  • 第二种,对于修改操作不涉及字段相关的操作,这种修改操作直接调用 alterTableUpdateTableColumnStats()方法。

对于非分区表,修改操作也是调用alterTableUpdateTableColumnStats()方法完成。

  // operations other than table renameif (MetaStoreUtils.requireCalStats(null, null, newt, environmentContext) &&!isPartitionedTable) {Database db = msdb.getDatabase(catName, newDbName);// Update table stats. For partitioned table, we update stats in alterPartition()MetaStoreUtils.updateTableStatsSlow(db, newt, wh, false, true, environmentContext);}if (isPartitionedTable) { //分区表//Currently only column related changes can be cascaded in alter tableif(!MetaStoreUtils.areSameColumns(oldt.getSd().getCols(), newt.getSd().getCols())) { // 是否修改字段相关信息parts = msdb.getPartitions(catName, dbname, name, -1);for (Partition part : parts) {Partition oldPart = new Partition(part);List<FieldSchema> oldCols = part.getSd().getCols();part.getSd().setCols(newt.getSd().getCols());ColumnStatistics colStats = updateOrGetPartitionColumnStats(msdb, catName, dbname, name,part.getValues(), oldCols, oldt, part, null);assert(colStats == null);if (cascade) { // 如果使用了CASCASE关键字,则会原有分区的信息也进行修改msdb.alterPartition(catName, dbname, name, part.getValues(), part);} else { // 否则不对原有分区的信息进行修改// update changed properties (stats)oldPart.setParameters(part.getParameters());msdb.alterPartition(catName, dbname, name, part.getValues(), oldPart);}}msdb.alterTable(catName, dbname, name, newt);} else { // 修改操作不涉及分区LOG.warn("Alter table not cascaded to partitions.");alterTableUpdateTableColumnStats(msdb, oldt, newt);}} else { //非分区表的修改alterTableUpdateTableColumnStats(msdb, oldt, newt);}
}if (transactionalListeners != null && !transactionalListeners.isEmpty()) {txnAlterTableEventResponses = MetaStoreListenerNotifier.notifyEvent(transactionalListeners,EventMessage.EventType.ALTER_TABLE,new AlterTableEvent(oldt, newt, false, true, handler),environmentContext);}// commit the changessuccess = msdb.commitTransaction();
}

异常处理

最后的部分就是对前面一系列操作遇到的异常进行处理,如果修改操作没有成功最后会进行回滚操作

catch (InvalidObjectException e) {LOG.debug("Failed to get object from Metastore ", e);throw new InvalidOperationException("Unable to change partition or table."+ " Check metastore logs for detailed stack." + e.getMessage());
} catch (InvalidInputException e) {LOG.debug("Accessing Metastore failed due to invalid input ", e);throw new InvalidOperationException("Unable to change partition or table."+ " Check metastore logs for detailed stack." + e.getMessage());
} catch (NoSuchObjectException e) {LOG.debug("Object not found in metastore ", e);throw new InvalidOperationException("Unable to change partition or table. Database " + dbname + " does not exist"+ " Check metastore logs for detailed stack." + e.getMessage());
} finally {if (!success) {LOG.error("Failed to alter table " +Warehouse.getCatalogQualifiedTableName(catName, dbname, name));msdb.rollbackTransaction(); //事务回滚if (dataWasMoved) {try {if (destFs.exists(destPath)) {if (!destFs.rename(destPath, srcPath)) {LOG.error("Failed to restore data from " + destPath + " to " + srcPath+ " in alter table failure. Manual restore is needed.");}}} catch (IOException e) {LOG.error("Failed to restore data from " + destPath + " to " + srcPath+  " in alter table failure. Manual restore is needed.");}}}
}

总结

从上述的分析过程中,可以看出Hive中Alter Table的操作过程大致分为rename操作和其他操作两大类,而每一类的处理又分为分区表和非分区表进行处理。

对于修改表名的操作,如果该表的存储路径使用的就是默认存储路径,则修改表名的同时会先进行数据的移动(实际就是目录名的rename),而如果使用的是指定的存储路径,则不会有相应的操作。

对于字段相关的修改操作,且如果是分区表,就涉及到是否使用了CASCASE关键字,如果使用了,则会对所有分区的信息进行变更;而如果没有使用,则不会对原有分区的相关进行进行变更。但无论哪种情况,从性能开销来说二者是一样的。

因此,在生产环境中,如果要修改的表的分区数很多时(几万甚至更多),执行alter table进行表名修改或者模型变更等操作由于会对每个分区的元数据进行更新操作,因此会比较耗时,此时如果表分区还启用字段统计信息功能,则会更加的耗时。这种情况下,就不建议使用该功能了,可通过配置项 hive.stats.column.autogather (Hive 3.0后默认是true)修改。实际生成环境中,碰到过修改操作需要几个小时,后来关闭该功能后,且将对应的统计数据从元数据中清理后,执行速度有非常明显的提升。

Hive源码分析——Alter Table操作执行流程分析相关推荐

  1. Alian解读SpringBoot 2.6.0 源码(八):启动流程分析之刷新应用上下文(中)

    目录 一.背景 1.1.刷新的整体调用流程 1.2.本文解读范围 二.调用后处理器 2.1.调用在上下文中注册为beanFactory的后置处理器 2.2.invokeBeanFactoryPostP ...

  2. Alian解读SpringBoot 2.6.0 源码(七):启动流程分析之准备应用上下文

    目录 一.背景 1.1.run方法整体流程 1.2.本文解读范围 二.准备应用上下文 2.1.整体流程 2.2.设置环境 2.3.应用上下文进行后置处理 2.4.应用所有初始化器 2.5.发布应用上下 ...

  3. Alian解读SpringBoot 2.6.0 源码(六):启动流程分析之创建应用上下文

    目录 一.背景 1.1.run方法整体流程 1.2.本文解读范围 二.创建应用上下文 2.1.初始化入口 2.2.初始化AbstractApplicationContext 2.3.初始化Generi ...

  4. Alian解读SpringBoot 2.6.0 源码(八):启动流程分析之刷新应用上下文(下)

    目录 一.背景 1.1.刷新的整体调用流程 1.2.本文解读范围 二.初始化特定上下文子类中的其他特殊bean 2.1.初始化主体资源 2.2.创建web服务 三.检查监听器bean并注册它们 四.实 ...

  5. Alian解读SpringBoot 2.6.0 源码(九):启动流程分析之应用上下文刷新后处理(启动完成事件,Runner运行器,就绪事件)

    目录 一.背景 1.1.run方法整体流程 1.2.本文解读范围 二.应用上下文刷新后置处理 三.时间信息.输出日志记录执行主类名 四.发布应用上下文启动完成事件 4.1.ApplicationSta ...

  6. Alian解读SpringBoot 2.6.0 源码(三):启动流程分析之命令行参数解析

    目录 一.背景 1.1.run方法整体流程 1.2.本文解读范围 二.默认应用参数解析 2.1.接口ApplicationArguments 2.2.实现类DefaultApplicationArgu ...

  7. Spring IoC 源码系列(五)getBean 流程分析

    一.FactoryBean 用法讲解 在分析源码流程之前,我们先来看一下 FactoryBean,乍一看这家伙和 BeanFactory 很像,它们都可以用来获取 bean 对象,简单来说 Facto ...

  8. springmvc源码阅读3--dispatcherServlet reqeust的执行流程

    一.前言 太多的时候总是会遇到群里面问,报404怎么肥事呀,前台的参数怎么后台收不到呀--,上次就在群里面遇到过,围绕这一个点:input的name值是不是错了呀,人家妹子都截了好几次图说没有问题,然 ...

  9. FlinkSQL源码解析(三)执行流程

    1.前言 前面2部分主要是介绍以下2点: flink sql整体的执行流程大致为:sqlNode --> Operation --> RelNode --> 优化 --> ex ...

  10. Alian解读SpringBoot 2.6.0 源码(八):启动流程分析之刷新应用上下文(上)

    目录 一.背景 1.1.run方法整体流程 1.2.刷新的整体调用流程 1.3.本文解读范围 二.准备刷新 2.1.准备刷新的流程 2.2.初始化上下文环境中servlet相关属性源 2.3.校验re ...

最新文章

  1. 进阶之初探nodeJS
  2. JDK动态代理实现简单AOP--转
  3. vm安装u盘linux,vmware 安装centos 插入u盘报错,大神请指导
  4. 【C语言】编写程序,输出以下图形
  5. 洛阳市高中学业水平计算机考试,2019年河南洛阳市高中学生学业水平考试考点及时间...
  6. python删除列表元素 // 列表的切片
  7. 腾讯云-搭建 JAVA 开发环境
  8. deepinu盘制作工具_U盘引导盘制作工具Rufus 3.11.1678 正式版
  9. Unity3D实习生面试题总结-图形学相关
  10. 小区物业费的管理java_JAVA小区物业收费管理系统设计方案与实现.doc
  11. SQL5 查找所有员工的last_name和first_name以及对应部门编号
  12. 树莓派系统迁移到移动硬盘
  13. 关于ShadowMap中Shadow acne现象的解释
  14. 通俗易懂的欧拉回路——哥尼斯堡七桥问题
  15. Android源码目录结构详解
  16. poi excel下载
  17. XMind2020介绍、下载
  18. Google Play 开发者账户已被终止的通知
  19. 手动build unity3d的docker镜像
  20. aka鉴权 ims_IMS鉴权过程中各参数的用途

热门文章

  1. 第9课:利用 Headers 的 Cookie 和 User-agent 伪装自己
  2. 背包问题——贪婪算法
  3. 非线性方程-概念应用及解法
  4. input中加入搜索图标
  5. win7怎么关闭配置计算机,Win7电脑怎么设置定时关机?
  6. msg1500说明书_MSG1500刷机笔记
  7. 乐符识别matlab,基于DPP的自动音符切分识别研究
  8. java 发送邮件怎么抄送_javaMail发送qq邮件(二):可发送抄送密送多人,支持附件...
  9. MATLAB图像处理植物叶片面积计算
  10. Linux修改文件保存时报错E45: 已设定选项 ‘readonly‘ (请加 ! 强制执行)