Spark SQL DDL操作Iceberg表:从创建到管理的完整指南
1. 项目概述:当Spark SQL遇上Iceberg表
如果你正在用Spark处理数据,并且数据量已经大到让你开始头疼Hive表分区管理、小文件合并或者数据版本回溯这些问题,那么Iceberg这个开源表格式大概率已经进入了你的视野。它通过一套精巧的元数据设计,让大数据表的管理变得像操作传统数据库一样直观和可靠。而Spark作为大数据生态中最主流的计算引擎之一,自然是与Iceberg集成最紧密的伙伴。今天我们不聊底层的原理,也不讲复杂的读写优化,就聚焦一个最实际、最高频的场景: 如何使用Spark SQL的DDL(数据定义语言)来创建和管理Iceberg表 。
这听起来基础,但却是用好Iceberg的起点。很多团队在引入Iceberg时,往往直接沿用Hive的建表语句,或者从文档里复制一段配置就开干,结果在后续的分区演化、schema变更时踩了一堆坑。Spark SQL为Iceberg提供了一套扩展的、符合ANSI SQL标准的DDL语法,理解并熟练运用它,能让你在数据湖的日常治理中事半功倍。无论是创建一个支持时间旅行和增量查询的表,还是动态地增加一个字段、调整分区策略,都可以通过几行简洁的SQL完成。接下来,我们就从零开始,拆解Spark DDL操作Iceberg表的每一个核心环节。
2. 核心概念与前置准备
在动手写DDL之前,我们需要确保两件事:环境是通的,概念是清的。很多操作失败,根源就在于这两点没做好。
2.1 环境配置与依赖管理
要让Spark认识并操作Iceberg表,首先得把“桥梁”搭建好。这里的关键是Spark的 spark.sql.extensions 配置和对应的Iceberg Jar包。
1. Spark Session初始化配置 通常,我们会在启动 spark-shell 、 pyspark 或提交Spark应用时,通过 --conf 参数或代码中配置 SparkSession.builder 。核心配置如下:
# 示例:启动spark-shell时配置
spark-shell \
--packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.0 \
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \
--conf spark.sql.catalog.spark_catalog=org.apache.iceberg.spark.SparkSessionCatalog \
--conf spark.sql.catalog.spark_catalog.type=hive \
--conf spark.sql.catalog.local=org.apache.iceberg.spark.SparkCatalog \
--conf spark.sql.catalog.local.type=hadoop \
--conf spark.sql.catalog.local.warehouse=/path/to/warehouse
我们来拆解一下这几个关键配置:
-
--packages: 自动下载指定版本的Iceberg Spark运行时Jar。这里3.5表示适配Spark 3.5.x,2.12是Scala版本,1.5.0是Iceberg版本。 务必保持版本兼容 ,你可以在Iceberg官网的兼容性矩阵里查到对应关系。 -
spark.sql.extensions: 这是“开关”,告诉Spark加载Iceberg的扩展功能,包括我们后面要用的CREATE TABLE ... USING iceberg等特殊语法。 -
spark.sql.catalog.[catalog_name]: 定义Catalog。Catalog是表的命名空间和元数据存储的入口。上面例子配置了两个Catalog:-
spark_catalog: 这是Spark的默认Catalog名。我们将其类型(type)设置为hive,并指向Iceberg的实现。这意味着当你使用未显式指定Catalog的表名(如db.table)时,Spark会通过这个Catalog去查找Iceberg表。 -
local: 这是一个自定义命名的Catalog,类型为hadoop,并指定了一个本地或HDFS路径作为仓库(warehouse)。你可以通过local.db.table来访问这个Catalog下的表。
-
注意 :在生产环境中,元数据存储(
type)的选择至关重要。hive模式依赖Hive Metastore(HMS),适合与现有Hive生态集成。hadoop模式将元数据以JSON文件形式存储在文件系统,更轻量但缺乏多客户端并发写入的锁管理。还有nessie(用于Git式分支管理)、jdbc等选项,需要根据团队协作和数据治理需求来选择。
2. 代码中配置SparkSession(以PySpark为例) 如果你在Jupyter Notebook或Python脚本中工作,配置方式如下:
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("IcebergDDLDemo") \
.config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions") \
.config("spark.sql.catalog.spark_catalog", "org.apache.iceberg.spark.SparkSessionCatalog") \
.config("spark.sql.catalog.spark_catalog.type", "hive") \
.config("spark.sql.catalog.local", "org.apache.iceberg.spark.SparkCatalog") \
.config("spark.sql.catalog.local.type", "hadoop") \
.config("spark.sql.catalog.local.warehouse", "/tmp/iceberg_warehouse") \
.config("spark.jars.packages", "org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.0") \
.getOrCreate()
配置完成后,一个简单的测试是运行 spark.sql(“SHOW TABLES”).show() ,或者尝试创建一个临时表,确保Spark能正常启动并识别扩展。
2.2 Iceberg表的核心结构理解
在写DDL之前,脑子里需要对Iceberg表的结构有个清晰画像,这能帮你理解后面每个子句的作用。一个Iceberg表在物理上由三部分组成:
- 数据文件(Data Files) :实际存储数据的文件,通常是Parquet、ORC或Avro格式。这些文件按分区规则组织在仓库目录下。
- 清单文件(Manifest Files) :记录了一次数据写入操作中,涉及了哪些数据文件,以及每个数据文件所属的分区、行数、列级统计信息(如最大值、最小值、空值数)等。它是数据文件的索引列表。
- 元数据文件(Metadata Files) :这是表的“总目录”。它记录了当前表的最新状态,包括:
- 清单列表(Manifest List) :指向构成当前表快照(Snapshot)的所有清单文件。
- Schema信息 :表的字段定义。
- 分区规范(Partition Spec) :当前表是如何分区的。
- 快照(Snapshot) :每次写入(INSERT, UPDATE, DELETE, MERGE INTO)都会生成一个新的快照,记录了此次更改的元数据(如操作类型、时间戳、生成的清单列表)。正是通过快照链,Iceberg实现了时间旅行和增量查询。
当你执行一条 CREATE TABLE 语句时,Spark会通过Iceberg的Catalog接口,在指定的元数据存储(HMS或文件系统)中注册表信息,并在仓库路径下生成初始的元数据文件。后续的DDL操作,如 ALTER TABLE ,大部分都是在修改或追加这些元数据,而不是直接重写数据文件,这正是Iceberg元数据层威力所在—— 元数据操作是原子性的、低成本的 。
3. 基础DDL操作:从建表到删表
掌握了环境和概念,我们就可以开始最核心的DDL操作了。我们会按照一个表的生命周期来展开:创建、查看、修改、删除。
3.1 创建表(CREATE TABLE)
创建表是第一步,也是定义表未来形态的关键。Iceberg支持多种建表方式,我们重点看最常用的两种。
3.1.1 使用CREATE TABLE ... USING iceberg 这是最标准、功能最全的语法,强烈推荐使用。
-- 在默认的spark_catalog下,创建于default数据库
CREATE TABLE default.user_behavior (
user_id BIGINT COMMENT ‘用户ID’,
item_id BIGINT,
category_id INT,
behavior_type STRING,
ts TIMESTAMP
)
USING iceberg
PARTITIONED BY (days(ts), category_id) -- 分区:按天和类别
TBLPROPERTIES (
‘format-version’ = ‘2’,
‘write.parquet.compression-codec’ = ‘zstd’,
‘write.target-file-size-bytes’ = ‘536870912’ -- 512MB
);
关键子句解析:
-
USING iceberg: 必须指定,告诉Spark创建的是Iceberg格式的表,而不是默认的Hive表。 -
PARTITIONED BY: 定义分区策略。上例使用了 隐藏分区 。days(ts)是一个分区转换函数,它会从ts字段中提取日期(年月日)进行分区,物理上类似于date_format(ts, ‘yyyy-MM-dd’),但逻辑上对用户透明。你查询时仍然可以用WHERE ts BETWEEN ‘2024-01-01’ AND ‘2024-01-02’,Iceberg会自动利用分区进行裁剪。除了days(),还有hours(),months(),bucket(N, column)(哈希桶),truncate(L, column)(截断)等。 将高基数字段(如user_id)放在bucket()里,将低基数、常用于过滤的字段(如日期、类别)放在days()或直接分区上,是常见的优化手段。 -
TBLPROPERTIES: 表的属性配置,这是Iceberg表调优的“工具箱”。-
format-version: 元数据格式版本。 强烈建议设置为‘2’ 。V2格式支持行级删除(DELETE)和更新(UPDATE,Merge-on-Read),是使用完整CDC(变更数据捕获)和流式更新能力的基础。V1格式仅支持追加。 -
write.*系列属性:控制写入行为。如write.target-file-size-bytes控制输出文件的大小,避免产生过多小文件。write.parquet.compression-codec指定压缩算法。 -
read.*和engine.*等属性可以配置读取行为和引擎特定优化。
-
3.1.2 使用CREATE TABLE ... LIKE 如果你想基于一个现有表(可以是Iceberg表,也可以是其他格式的表)的结构快速建表,可以使用 LIKE 子句。这常用于创建测试表或备份表结构。
-- 复制源表的结构(包括分区),但不复制数据和属性(除非指定)
CREATE TABLE new_table LIKE original_table;
-- 复制结构、分区以及TBLPROPERTIES
CREATE TABLE new_table LIKE original_table USING iceberg;
-- 注意:LIKE语法可能不会复制所有Iceberg特定的TBLPROPERTIES,建表后最好检查并手动设置关键属性如format-version。
3.1.3 通过CTAS(CREATE TABLE AS SELECT)建表 这是从查询结果直接创建新表的快捷方式,在数据迁移或中间表创建时非常有用。
CREATE TABLE iceberg_catalog.db.aggregated_sales
USING iceberg
PARTITIONED BY (sales_month)
TBLPROPERTIES (‘format-version’ = ‘2’)
AS
SELECT
region,
date_trunc(‘month’, order_date) AS sales_month,
sum(amount) AS total_amount
FROM source_orders
GROUP BY region, date_trunc(‘month’, order_date);
实操心得 :CTAS语句中的
PARTITIONED BY子句 只能引用SELECT列表中的别名 。例如上例中,分区字段是sales_month,它必须是SELECT子句中显式定义的别名。你不能写PARTITIONED BY (date_trunc(‘month’, order_date))。
3.2 查看与描述表(SHOW & DESCRIBE)
表建好后,我们需要了解它的详细信息。
-- 查看当前数据库下的所有表(包括Iceberg和非Iceberg表)
SHOW TABLES [IN database_name];
-- 扩展查看,可以显示更多信息(如表类型)
SHOW TABLE EXTENDED LIKE ‘user_behavior’;
-- 描述表结构:等同于DESC
DESCRIBE TABLE default.user_behavior;
-- 会输出字段名、类型、是否可空、注释。
-- 详细描述:显示分区信息等
DESCRIBE EXTENDED default.user_behavior;
-- 在Detailed Table Information部分,你可以找到`Provider: iceberg`, `Partition Columns: [days(ts), category_id]`等关键信息。
-- 专门查看分区信息(Iceberg扩展语法)
SHOW PARTITIONS default.user_behavior;
-- 这会列出所有现有的数据分区。对于隐藏分区(如`days(ts)`),这里显示的是转换后的值(如`ts_day=2024-01-01`)。
3.3 修改表(ALTER TABLE)
Iceberg的 ALTER TABLE 能力非常强大,大部分操作都只修改元数据,瞬间完成。
3.3.1 模式(Schema)演化 这是Iceberg最受欢迎的特性之一,可以在不重写数据文件的情况下修改表结构。
-- 1. 添加列(支持在指定位置添加)
ALTER TABLE user_behavior ADD COLUMNS (
device_type STRING COMMENT ‘设备类型’ AFTER behavior_type,
app_version STRING
);
-- 2. 删除列
ALTER TABLE user_behavior DROP COLUMN app_version;
-- 注意:这只是从元数据中标记删除,物理数据文件中的该列数据依然存在,但查询时不可见。这保证了向后兼容性。
-- 3. 重命名列
ALTER TABLE user_behavior RENAME COLUMN behavior_type TO action;
-- 4. 更新列(修改类型、注释、调整位置)
-- 修改字段类型(需兼容,如STRING转VARCHAR(100)通常可以)
ALTER TABLE user_behavior ALTER COLUMN device_type TYPE VARCHAR(100);
-- 修改字段注释
ALTER TABLE user_behavior ALTER COLUMN device_type COMMENT ‘移动设备类型’;
-- 注意:Spark SQL对ALTER COLUMN语法的支持可能因版本而异,更复杂的类型变更(如INT转BIGINT)可能需要使用重写数据的`REPLACE`操作。
3.3.2 分区演化 分区策略也能动态调整!这是传统Hive表难以做到的。
-- 添加一个新的分区字段(例如,在原有按天分区基础上,增加按小时分区)
ALTER TABLE user_behavior ADD PARTITION FIELD hours(ts);
-- 执行后,新写入的数据会按照`days(ts), category_id, hours(ts)`三个维度进行分区。
-- **重要**:已有的数据**不会**被重新分区。只有新数据才应用新的分区规范。表可以同时存在多种分区规范的数据,Iceberg能正确管理。
-- 删除一个分区字段
ALTER TABLE user_behavior DROP PARTITION FIELD category_id;
-- 同样,这只是修改了元数据中的“当前”分区规范。已有数据的分区方式不变,但后续查询将不再能利用`category_id`进行分区裁剪。
-- 替换整个分区规范
ALTER TABLE user_behavior REPLACE PARTITION FIELD days(ts) WITH months(ts);
-- 将按天分区替换为按月分区。同样只影响后续写入。
踩坑提醒 :分区演化非常灵活,但需谨慎规划。频繁增加分区字段可能导致分区目录过深,影响HDFS等文件系统的性能。建议在设计初期就考虑好核心分区维度,演化主要用于增加辅助的、低基数的过滤维度。
3.3.3 修改表属性
-- 设置或更新表属性
ALTER TABLE user_behavior SET TBLPROPERTIES (
‘write.parquet.compression-codec’ = ‘snappy’,
‘read.split.open-file-cost’ = ‘4194304’ -- 4MB,影响文件打开成本计算,用于合并小文件扫描
);
-- 删除某个表属性(如果该属性有默认值,则会恢复默认值)
ALTER TABLE user_behavior UNSET TBLPROPERTIES (‘read.split.open-file-cost’);
3.3.4 重命名表
ALTER TABLE old_name RENAME TO new_name;
-- 该操作会更新Catalog中的元数据。确保没有正在运行的作业引用旧表名。
3.4 删除与清理表(DROP TABLE & EXPIRE SNAPSHOTS)
3.4.1 删除表
DROP TABLE [IF EXISTS] default.user_behavior;
- 对于
hive类型的Catalog,这会从Hive Metastore中删除元数据。 - 对于
hadoop类型的Catalog,这会删除元数据文件(metadata.json等)。 - 默认情况下,它不会删除底层的数据文件! 这是为了防止误操作导致数据丢失。数据文件会留在仓库路径下,成为“孤儿文件”。
3.4.2 彻底清除(Purge) 如果你想连数据文件一起删除,需要使用 PURGE 选项(这是Iceberg的扩展语法,需要启用Spark扩展)。
DROP TABLE default.user_behavior PURGE;
-- 或者使用CASCADE(某些版本/环境下)
-- DROP TABLE default.user_behavior CASCADE;
警告 : PURGE 操作是 不可逆的 ,它会递归删除表在文件系统上的整个数据目录和元数据目录。执行前务必三思,最好先备份。
3.4.3 清理旧快照与孤儿文件 由于Iceberg的增量写入和版本特性,表会积累很多历史快照和不再被引用的数据文件(孤儿文件)。需要定期清理以释放存储。
-- 1. 过期旧快照:删除超过7天的历史快照
CALL spark_catalog.system.expire_snapshots(‘default.user_behavior’, TIMESTAMP ‘2024-01-01 00:00:00’);
-- 或者保留最近100个快照
CALL spark_catalog.system.expire_snapshots(‘default.user_behavior’, 100);
-- 2. 删除孤儿文件:清理那些不再被任何快照引用的数据文件
CALL spark_catalog.system.remove_orphan_files(‘default.user_behavior’);
这些维护过程通常作为定时任务(如每天一次的Airflow DAG)来执行。 expire_snapshots 会同时清理掉过期的元数据文件和清单文件。 remove_orphan_files 比较耗时,因为它需要扫描整个表目录,建议在业务低峰期执行。
4. 高级DDL与表管理技巧
掌握了基础DDL后,我们来看一些更高级但同样重要的操作和管理技巧,这些能帮助你更好地驾驭生产环境中的Iceberg表。
4.1 管理表版本(快照与回滚)
Iceberg的每次写操作都会生成一个快照(Snapshot)。你可以查看、回溯甚至回滚到某个快照。
-- 查看当前表的所有快照
SELECT * FROM spark_catalog.default.user_behavior.snapshots ORDER BY committed_at DESC;
-- 这个查询会返回快照ID、时间戳、操作类型(append, overwrite, replace等)、清单列表位置等信息。
-- 时间旅行查询:查询某个历史时刻的数据
SELECT * FROM spark_catalog.default.user_behavior TIMESTAMP AS OF ‘2024-01-15 10:00:00’;
-- 或者使用快照ID
SELECT * FROM spark_catalog.default.user_behavior VERSION AS OF 1234567890123456789;
-- 快速回滚到某个快照(元数据操作,极快)
CALL spark_catalog.system.rollback_to_snapshot(‘default.user_behavior’, 1234567890123456789);
-- 执行后,表的当前状态就回到了那个快照。之后的新写入会基于这个快照创建新的分支,被回滚的快照之后的历史快照不会被立即删除,但会过期。
注意事项 :
ROLLBACK是一个强大的“撤销”操作。但它只修改元数据中的当前指针,并不会物理删除回滚点之后写入的数据文件。那些数据文件变成了孤儿文件,等待remove_orphan_files清理。如果你误操作写入了错误数据,用ROLLBACK比全表删除再重写要高效安全得多。
4.2 处理分区与数据文件优化
小文件问题是数据湖的常见痛点。Iceberg提供了内置的过程来优化数据布局。
-- 重写数据文件以合并小文件(Compaction)
CALL spark_catalog.system.rewrite_data_files(
table => ‘default.user_behavior’,
strategy => ‘binpack’, -- 策略:binpack(默认,合并小文件), sort(按排序键重排)
options => map(
‘min-file-size-bytes’, ‘67108864’, -- 64MB,小于此值的文件会被合并
‘max-file-size-bytes’, ‘536870912’, -- 512MB,合并后文件的目标大小
‘partial-progress.enabled’, ‘true’ -- 允许部分提交,避免大任务失败全部回滚
)
);
-- 按分区重写(例如只优化最近一天的分区)
CALL spark_catalog.system.rewrite_data_files(
table => ‘default.user_behavior’,
filter => ‘ts >= “2024-01-15”’, -- 过滤条件
strategy => ‘sort’,
sort_order => ‘user_id ASC’, -- 指定排序键,可以显著提升后续点查性能
options => map(...)
);
rewrite_data_files 过程会启动一个Spark作业,读取符合条件的数据,按照策略重新写出为大小更均匀的文件。 sort 策略特别有用,如果你经常按 user_id 查询,按此字段排序存储后,可以利用Parquet的行组索引和谓词下推,大幅减少IO。
4.3 使用CALL执行存储过程
上面我们已经见到了 CALL 的用法。这是Iceberg通过Spark扩展提供的一组系统级存储过程,用于执行元数据操作和维护任务。除了 expire_snapshots 、 remove_orphan_files 、 rewrite_data_files 、 rollback_to_snapshot ,还有一些其他有用的过程:
-
migrate(‘source_table’, ‘iceberg_table’): 将非Iceberg表(如Hive表)迁移为Iceberg表。 -
add_files(‘iceberg_table’, ‘source_path’, ‘parquet’): 将外部数据文件以快照形式添加到Iceberg表,用于存量数据接入。 -
ancestors_of(‘table’, snapshot_id): 列出某个快照的所有祖先快照,用于分析数据 lineage。
使用这些过程时,务必在测试环境充分验证,因为它们会直接修改表元数据或数据。
5. 常见问题与排查技巧实录
在实际使用Spark DDL操作Iceberg时,你肯定会遇到各种“坑”。下面是我总结的一些典型问题及解决方法。
5.1 建表失败:ClassNotFound或NoSuchMethodError
问题现象 :执行 CREATE TABLE ... USING iceberg 时报错,提示找不到类或方法不匹配。
排查思路 :
- 检查版本兼容性 :这是最常见的原因。确认
iceberg-spark-runtime的版本与你的Spark版本、Scala版本完全匹配。去 Apache Iceberg官网 查看兼容性矩阵。 - 检查依赖冲突 :如果使用
--packages,确保没有其他依赖包引入了不同版本的Iceberg或Parquet等库。可以尝试使用--repositories和--exclude参数排除冲突包,或者使用--jars直接指定绝对路径的Jar包。 - 确认扩展配置 :检查
spark.sql.extensions配置是否正确,且Jar包中确实包含这个类。
5.2 查询表时报错:Not an Iceberg table
问题现象 :表创建成功了,但用 SELECT 查询时,报错说不是Iceberg表。
排查思路 :
- 检查Catalog配置 :确认你查询时使用的表名路径(如
db.table)与你建表时使用的Catalog一致。如果你用local.warehouse.db.table创建的表,用spark_catalog.db.table去查是查不到的。 - 检查元数据存储 :如果是
hive类型Catalog,去Hive Metastore里确认表是否存在,且TBLPROPERTIES中的table_type是否为ICEBERG。如果是hadoop类型,去warehouse目录下查看是否有metadata/子目录和元数据JSON文件。 - 手动修复元数据 :极少数情况下,元数据文件可能损坏。可以尝试使用
Iceberg的Java API或Spark过程来修复。
5.3 ALTER TABLE添加分区字段后,查询性能没提升
问题现象 :给一个已存在大量数据的表添加了新的分区字段,但针对新分区的过滤查询依然很慢。
原因与解决 :
- 原因 :分区演化只影响新写入的数据。历史数据并没有按照新的分区字段重新组织物理存储。
- 解决 :如果你希望历史数据也能享受新分区的查询优化,需要 重写数据 。这可以通过
INSERT OVERWRITE全表重写,或者使用rewrite_data_files过程并指定sort_order包含新的分区字段来实现。但这通常是一个资源消耗型操作,需要权衡收益和成本。
5.4 小文件过多导致查询慢
问题现象 :表目录下数据文件数量巨大,每个文件都很小(几MB甚至几十KB),导致Spark任务启动慢,元数据管理压力大。
解决方案 :
- 预防优于治疗 :在写入时控制。设置合理的
write.target-file-size-bytes(如512MB或1GB)。对于流式写入(如Structured Streaming),启用write.spark.fanout.enabled=false(禁用fanout writer,它容易产生小文件)并合理设置检查点间隔。 - 定期合并 :使用
system.rewrite_data_files过程定期合并小文件。可以编写一个定时脚本,针对过去N天内修改过的分区进行合并。 - 调整读取配置 :在查询时,可以临时设置Session配置
spark.sql.iceberg.optimize-scan-for-small-files=true(如果版本支持),让Spark在规划时尝试合并小文件的扫描任务。
5.5 时间旅行查询(TIMESTAMP AS OF)不准确
问题现象 :使用 TIMESTAMP AS OF 查询某个历史时间点的数据,结果与预期不符,可能包含了该时间点之后的数据。
排查思路 :
- 时间精度 :Iceberg快照的时间戳是毫秒级的。你提供的
TIMESTAMP字符串会被解析。确保你的时间字符串格式正确,并且考虑了时区。建议使用TIMESTAMP ‘2024-01-15 10:00:00 UTC’这种明确指定时区的格式。 - 快照生成时间 :
TIMESTAMP AS OF查找的是 提交时间(committed_at) 小于或等于你指定时间戳的 最新快照 。如果在你指定的时间点恰好有多个写入几乎同时提交,可能会有细微偏差。更精确的方式是使用VERSION AS OF指定快照ID。 - 检查时区一致性 :确保Spark Session的时区、写入作业的时区与你查询时指定的时区是一致的。不一致的时区是导致时间相关错误的常见元凶。
掌握这些DDL操作和问题排查技巧,你基本上就能游刃有余地使用Spark来管理Iceberg表了。记住,Iceberg的强大在于其元数据层提供的可靠性和灵活性,而Spark DDL是你与这层元数据交互的直接桥梁。多动手实践,从创建一张测试表开始,逐步尝试各种分区演化、模式变更和时间旅行,你会更深刻地体会到它相对于传统Hive表的巨大优势。
更多推荐
所有评论(0)