今天聊聊Hive分区与分桶。做数据仓库的人,几乎每天都要和这两个概念打交道,但真正能把分区粒度选准、分桶数量定好、动态分区参数一次配明白的人,说实话不多。我经常遇到同事来问:为什么我的Hive表加了分区还是慢?为什么增量导入之后目录下全是几十KB的小文件?这些坑我都踩过一轮,这篇文章就把分区和分桶从原理到实战一次性讲透。内容偏向实际落地,适合数仓开发、数据工程师和正在学习Hive的读者参考,看完能直接拿自己项目里的表做一次体检和优化。
这里先明确一点:Hive里的“分区”是表数据的逻辑切分,和操作系统磁盘分区的概念完全是两码事。别搞混。Hive分区在HDFS上表现为不同目录,查询时通过分区裁剪只读需要的目录;分桶则是把数据按哈希散列到固定数量的文件或桶中。两者都是大数据存储优化的重要手段,但解决的不是同一个问题。
1. 用分区表给数据做一次“抽屉式”收纳
1.1 分区的本质:目录即过滤条件
Hive分区表的物理呈现形式很简单:在表目录之下,每个分区值对应一个独立的目录。比如一张订单表按日期dt分区,建表后HDFS上会长成这样的结构:
text复制/user/hive/warehouse/dwd.db/dwd_order_detail/
├── dt=2024-06-01/
│ ├── part-00000-xxxx
│ └── part-00001-xxxx
└── dt=2024-06-02/
├── part-00000-xxxx
└── part-00001-xxxx
查询时如果带上WHERE dt='2024-06-01',Hive并不会傻乎乎扫整个表,而是直接定位到dt=2024-06-01这个目录,只读里面的文件。这就是分区裁剪(Partition Pruning)的核心逻辑。
打个比方,这就好比你去图书馆找一本2024年出版的某类书。如果图书馆按类别分架,你只需要走到对应书架那一层;如果没有分架,你得把整个图书馆的书都翻一遍。分区表就是给数据提前做了书架分类,代价是维护成本高一丢丢,收益是查询扫描量指数级下降。
建表时用PARTITIONED BY声明分区字段,建表语法是这样的:
sql复制CREATE TABLE dwd_order_detail (
order_id BIGINT,
user_id BIGINT,
amount DECIMAL(10,2),
pay_status TINYINT
)
PARTITIONED BY (dt STRING)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t';
注意分区字段dt只出现在PARTITIONED BY里,不能出现在表字段列表里,否则会报重复定义。在MySQL那种OLTP库里你会觉得加个日期字段很自然,但Hive里分区字段是独立于普通列之外的一个维度,普通列管数据内容,分区列管数据位置。
1.2 静态分区、动态分区和混合分区怎么选
实际写入数据时,分区有三种玩法:静态分区、动态分区、混合分区。
静态分区是手动指定分区值,每次写数据前你已经知道这波数据属于哪个分区:
sql复制INSERT OVERWRITE TABLE dwd_order_detail PARTITION(dt='2024-06-01')
SELECT order_id, user_id, amount, pay_status
FROM tmp_order_ods
WHERE order_date = '2024-06-01';
优点是执行计划明确,不会误写分区;缺点是一次只能写一个分区。如果业务上每天跑批量任务,你写2个静态分区就够了;可要是补数据补30天,就得写30条SQL,烦不烦?这时候动态分区就派上用场了。
动态分区可以不指定分区值,让Hive根据查询结果自动判断数据该进哪个分区,分区字段的值直接取SELECT出来的最后一列:
sql复制SET hive.exec.dynamic.partition=true;
SET hive.exec.dynamic.partition.mode=nonstrict;
INSERT OVERWRITE TABLE dwd_order_detail PARTITION(dt)
SELECT order_id, user_id, amount, pay_status, order_date
FROM tmp_order_ods;
注意我写了两个SET参数。hive.exec.dynamic.partition当然要开;更关键的是第二个参数hive.exec.dynamic.partition.mode,默认值是strict,在严格模式下,你必须至少指定一个静态分区,不允许只靠动态分区写入。改成nonstrict之后才允许全动态分区。
这两个参数在Hive CLI、Beeline里每次会话都要重新设置,别指望改了配置文件一劳永逸。如果你在调度平台跑HiveSQL,建议在脚本头部固定写上这两个参数,别省这几行。
混合分区则是前面静态后面动态,比如按天加省份两级分区时,把日期写死,省份走动态:
sql复制INSERT OVERWRITE TABLE dwd_order_detail PARTITION(dt='2024-06-01', province)
SELECT order_id, user_id, amount, pay_status, order_province
FROM tmp_order_ods
WHERE order_date = '2024-06-01';
这里有个硬性规则:静态分区必须放在动态分区前面。上面的例子dt在前、province在后就合法;反过来写SQL直接报错。
1.3 分区粒度不是越细越好
很多新人刚学会分区,恨不得按天、按小时、按分钟全部分区,觉得分区越细查询越快。这个想法方向对,但剂量容易失控。
分区太细带来的直接问题是小文件泛滥。举一个我实际算过的例子:假设一天流水100GB,HDFS默认块大小128MB,理想情况下一天的数据量对应800个块左右。如果按天分区,每个分区几百个文件,每个文件几十MB到一百多MB,还在可接受范围。如果按小时分区,每个分区约4GB,文件数会少一些,但一天就有24个分区。如果按5分钟粒度分区,一天就是288个分区,平均每个分区350MB,但文件可能碎成几十份,单文件常常不到10MB。
小文件的危害在于NameNode元数据膨胀。每个文件、每个目录、每个Block都会对应NameNode内存里的一条元数据记录,文件数量上千万之后,NameNode内存吃紧,整个HDFS都跟着变慢。你为了让单条查询扫描量变小,结果把全集群的命脉堵住了,得不偿失。
所以我个人建议:分区粒度最多到小时级,按天分区最常见;再细的粒度宁可靠其他方式来收敛,不要无脑加分区。另外,分区字段类型尽量用STRING,别用INT,日期字符串可读性好,也避免查询时因为类型转换导致分区裁剪失效。
还有一点经常被忽略:内部表和外部表在删表时的行为不同。分区表也一样——内部表DROP TABLE会连数据目录一起删掉,外部表只删元数据,HDFS上的目录和文件都还在。ODS层、原始日志表这些别乱用内部表,万一删错了元数据数据还能抢救一下。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分桶表:把数据更均匀地“撒”进固定格子
2.1 分桶到底怎么分:哈希取模原理
分区解决的是数据“按业务维度切分”的问题,比如按日期、按省份。但有时候业务维度不够用,比如用户画像表,你总不能按用户ID前几位分几千个分区吧?这时分桶表就出来了。
分桶的底层原理是哈希取模。Hive根据分桶字段计算哈希值,然后对桶数取余,决定这一行数据放进哪个桶:
text复制bucket_id = hash(bucket_field) % num_buckets
这个计算对全表所有数据都生效,所以分桶可以做到数据分布相对均匀。还是用生活类比:好比食堂有32个打菜窗口,每个来吃饭的学生拿号牌按照号码取模到对应窗口排队,理论上每个窗口的人数差不多。
分桶表建表时用CLUSTERED BY声明分桶字段,INTO ... BUCKETS声明桶数:
sql复制CREATE TABLE dim_user_bucketed (
user_id BIGINT,
user_name STRING,
reg_time STRING
)
CLUSTERED BY (user_id) INTO 32 BUCKETS
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t';
建好后的表目录下并不像分区表那样分出多级子目录,而是平铺若干个文件,文件名类似part-00000、part-00001一直到part-00031。每个文件就是一个桶。
分桶字段的选择很有讲究。第一,必须选择查询中高频过滤或高频join的字段。第二,这个字段的基数不能太低,比如gender男女两种取值,分32个桶,绝大多数桶是空的,等于白分。第三,字段值不能过度集中,比如某个热门商品占了80%的数据,用它分桶,两个典型场景你都会吃亏:一是join时倾斜,二是桶间数据严重不均。
分桶数选多少?实践里多数团队用32、64、128、256这种2的幂次,原因和后续map端分桶join的扩展性有关。如果只是单表抽样,16或32通常够用;如果是给大表join打辅助,桶数要结合对方表情况,最好保持整数倍关系。
2.2 分桶表的建造与数据写入
分桶表建好之后,写入方式比普通表“挑剔”很多。最省事的LOAD DATA直接灌数据,在分桶表上是不行的,或者说灌进去也并没有真正分桶——Hive不会自动帮你重新分配数据。
正确姿势是开启强制分桶开关,然后走INSERT OVERWRITE查询写入。这样Hive在写入时会按照分桶字段做一次distribute by,保证每个桶收到的数据进入对应文件:
sql复制SET hive.enforce.bucketing=true;
INSERT OVERWRITE TABLE dim_user_bucketed
SELECT user_id, user_name, reg_time
FROM dim_user;
这里要专门提一个版本差异的坑:Hive老版本里hive.enforce.bucketing默认是false,你不设置它,写进去的文件数量和桶数会对不上,实际文件可能更多或更少,分桶名存实亡。新版本Hive默认值在不同发行版里表现不完全一致。所以最稳妥的做法是不管什么环境,脚本里显式写上SET hive.enforce.bucketing=true;,避免换一套环境行为就变了。
如果分桶表还带了排序桶需求,建表时可以加一句SORTED BY (event_time),让桶内数据按时间有序。这对后面要做高效范围查询、排序merge join会方便不少,但写入阶段额外排序也会增加成本。
还有个大坑:分桶数一旦定了,后续想改很难。你无法通过简单的ALTER TABLE让存量数据重新分桶,每个桶对应的文件并不会自动重排。真要改桶数,老老实实建新表,把数据重写一遍。所以建表之前务必把桶数估准,别随手填一个。
2.3 分桶带来的三个具体价值
第一说的是抽样。普通表做随机抽样得全表跑一遍,分桶表可以直接取某个桶作为近似样本:
sql复制SELECT * FROM dim_user_bucketed
TABLESAMPLE(BUCKET 3 OUT OF 32 ON user_id);
上面这句的意思是:把32个桶中的第3个桶抽出来,得到的数据大约是全表的1/32。做特征分析、数据探查时非常实用。
第二说的是Map端Join优化。当一个大表和一个小表做join,如果两边都按join字段分桶,且大表桶数是小表桶数的整数倍,就可以启用Bucket Map Join,让部分join在小数据量的分桶内部直接完成,跳过全量shuffle。开启方式:
sql复制SET hive.optimize.bucketmapjoin=true;
老规矩,查看执行计划确认有没有真的走BucketMapJoin。你可以用EXPLAIN看执行计划里面是否出现BucketMapJoin字眼,如果没有,多半是分桶字段与join字段不一致,或者桶数不满足整数倍条件。
第三是分桶裁剪。分桶字段也常常出现在WHERE过滤条件里,比如按user_id查询某个用户的数据,Hive可以直接定位到该用户对应的桶,只扫几十个文件中的一个。这和分区裁剪的思路一脉相承,只不过裁剪粒度更细,细化到了桶级别。
3. 分区+分桶组合设计:扫描瘦身与分布控制的平衡
3.1 什么场景适合组合使用
实际数仓建模里,分区和分桶很少是二选一,更多是配合使用。我看到比较典型的场景是用户行为日志表:几十亿行、每天新增好几个TB,查询经常要按日期过滤,又要按用户维度做关联分析。
这种表最适合的方式就是“日期分区 + 用户ID分桶”。分区把查询范围收敛到几天甚至一天,分桶再把桶内的数据按用户ID组织好,抽样、join、用户级过滤全部受益。
组合设计的决策逻辑并不复杂:分区负责处理“时间维度”的裁剪,分桶负责处理“业务维度”的均匀散列。分桶字段不要求跟分区字段一致,你可以按日期分区、按用户ID分桶;但如果分桶字段和分区字段是同一个,比如按天分区再按天分桶,那就没什么意义了,桶的粒度落在单分区内,等于做了一层没有业务价值的二次切分。
3.2 组合建表与写入完整示例
我拿一个用户事件表来演示,建表语句如下:
sql复制CREATE TABLE dwd_user_event (
event_id STRING,
user_id BIGINT,
event_type STRING,
event_time STRING
)
PARTITIONED BY (dt STRING)
CLUSTERED BY (user_id) INTO 64 BUCKETS
STORED AS ORC;
写入数据时,动态分区和强制分桶的开关最好同时打开:
sql复制SET hive.exec.dynamic.partition=true;
SET hive.exec.dynamic.partition.mode=nonstrict;
SET hive.enforce.bucketing=true;
INSERT OVERWRITE TABLE dwd_user_event PARTITION(dt)
SELECT event_id, user_id, event_type, event_time, event_date
FROM tmp_event_log;
写完可以在HDFS上验证一下表目录的结构。分区目录会依然存在,但每个分区目录下面不再是一堆随机文件名,而是固定数量的part-00000到part-00063。这意味着什么?意味着你在查询某个分区时,文件数量和大小是基本可控的。64个桶如果对应一天50GB数据,单文件大约800MB,虽然比128MB块大,但还在合理范围,如果还想控制得更小,可以加桶数或换执行引擎。
3.3 组合设计里最容易翻车的地方
先说一个我以为大家都知道、结果经常有人踩的坑:动态分区字段在SELECT里必须放最后,而且分区字段的取值如果为NULL,会被放入一个默认分区__HIVE_DEFAULT_PARTITION__。你辛辛苦苦按天分区,结果发现一批脏数据全聚到这个默认分区里,查询时要么多扫一个目录,要么忘记过滤导致数据算重。所以在导入前把非空校验做在前面。
其次是动态分区数量阈值问题。Hive默认对动态分区数量有限制,如果你一下子补一个月数据,按天分区只有30个分区,通常没事;但如果是“日期+省份”两级动态分区,30天乘以34个省份,上千个分区,可能直接顶到上限,报错信息类似Too many dynamic partitions。解决办法是作业前调高限制,或者分批跑:
sql复制SET hive.exec.max.dynamic.partitions=10000;
SET hive.exec.max.dynamic.partitions.pernode=10000;
最后是分桶字段本身的分布问题。我前面说过分桶能让多数情况分布均匀,但前提是分桶字段本身不离谱。比如按user_id分桶,绝大多数用户数据量接近,很均匀;但按city_id分桶,一线城市数据量可能是小城市的几十倍,那你选它做分桶字段就是自找麻烦。分桶能优化“随机散列场景”,但它救不了“天然倾斜的业务字段”,识别这种字段靠的是对业务的敏感度。
4. 配套优化:小文件治理与数据倾斜这两个老大难
4.1 数据倾斜在分区场景中的体现
数据倾斜这个问题在Hive里几乎绕不开,核心症状是“明明分配了几百个reducer,但有个别reducer跑了半天,其他reducer早就结束了”。倾斜的根源是某些key的数据量远超平均,比如按省份聚合订单金额,北京上海的数据量天然比其他省份大一个量级,这些大key所在的reducer就变成短板。
在分区场景里,倾斜经常表现为某个分区的数据量异常大。比如按省份分区,某个省数据量占40%,做跨分区join时,这个分区的计算压力就会明显偏大。分桶能不能解决倾斜?不能。分桶解决的是数据文件分布,不是shuffle阶段的key倾斜。所以你该用常规的去倾斜手段:
- 对空值或异常值先做过滤或填充,避免大量数据集中到默认分区或同一key。
- 对热点key做加盐(salting),把一个大key拆成多个随机子key,先做一轮局部聚合,再去掉盐值做第二轮全局聚合。
- 两阶段聚合或者用
MAPJOIN把小表直接分发到map端,减少reduce阶段的连接压力。
这里顺便说一个很容易搞混的概念:分桶表的桶内数据也有可能倾斜。比如分桶字段选择不当,某个桶里落进去60%的数据,那么这个桶对应的文件特别大,查询扫描时不均匀,join时同样会有短板。所以分桶字段的选择不是看“能不能分”,而是看“分了之后均不均匀”。
4.2 小文件治理:给HDFS减负
前面说过分区过细会引起小文件问题。这里再展开说一说,因为小文件问题不完全是分区粒度造成的,写入任务的reducer数量也会直接影响文件数量。如果一个任务有1000个reducer,每个reducer写一个文件,即使分区粒度合理,某个分区目录下也可能铺满了大量几十MB的小文件。
常见合并参数的组合可以从两个角度下手,一是从写入源头控制reducer数量,二是对已经产生的小文件再做合并。
先看源头控制。Hive计算reducer数量的重要依据是hive.exec.reducers.bytes.per.reducer,默认值通常在256MB到1GB之间,根据执行引擎不同有差异。调大这个值,单个reducer处理的数据量变大,reducer数量变少,写入文件数自然变少。
再看事后合并。对已经碎成一堆小文件的分区,可以设置以下几个参数,让Hive在任务结束后自动合并小文件:
| 参数 | 建议值 | 作用 |
|---|---|---|
| hive.merge.mapfiles | true | Map-only任务结束时合并小文件 |
| hive.merge.mapredfiles | true | 带Reduce阶段的任务结束时合并小文件 |
| hive.merge.size.per.task | 256000000 | 合并后期望文件大小,默认值约256MB |
| hive.merge.smallfiles.avgsize | 16000000 | 目录下平均文件大小小于该值时触发合并 |
这些参数在不同Hive版本里可能默认值不同,建议根据自己集群实际测试。我个人的判断标准是:如果一张表某个分区目录下文件数量超过几百个,且单文件平均小于32MB,就该考虑合并了;如果是ODS层的原始日志表,可能允许稍微碎一点,因为还要做增量回溯。DWD、DWS层尽量保持整洁。
4.3 常用优化参数速查
这里把做Hive优化时最常用的几个参数汇总成一张表,方便直接抄作业:
| 场景 | 参数 | 建议值 |
|---|---|---|
| 动态分区 | hive.exec.dynamic.partition | true |
| 动态分区宽松模式 | hive.exec.dynamic.partition.mode | nonstrict |
| 动态分区数量上限 | hive.exec.max.dynamic.partitions | 10000 |
| 强制分桶 | hive.enforce.bucketing | true |
| 分桶MapJoin | hive.optimize.bucketmapjoin | true |
| 小文件合并 | hive.merge.mapredfiles | true |
| 合并后目标大小 | hive.merge.size.per.task | 256000000 |
| 控制Reducer数据量 | hive.exec.reducers.bytes.per.reducer | 256000000 |
注意参数不是越多越好,更不是一口吃成胖子。改一个参数跑一次,多看看执行计划和日志,比盲目堆参数靠谱得多。
5. 实战复盘:从MySQL到Hive分区分桶表的一次完整改造
5.1 项目背景与设计思路
假设这样一个业务场景:业务库是MySQL,订单表一天新增几百万条记录,数仓这边要对历史订单做分析,常用过滤条件是日期范围,并且经常要和用户维度表做join。最初数仓里就是两张“大宽表”,没有分区也没有分桶,每次跑日报都要全表扫描,数据量过了千万级之后,跑一个聚合查询要十几分钟,IT那边已经开始抱怨了。
改造思路很简单:订单事实表按日期分区,把每天的扫描量砍下去;用户维度表按用户ID分桶,让join和抽样都有更好的文件组织。这样既利用了分区裁剪减少查询数据量,又利用了分桶提升join效率。
5.2 操作步骤与关键SQL
先把MySQL数据导入到Hive。如果你有Sqoop,可以直接拉取到临时目录,再用Hive外部表指向目录;没有Sqoop的话,先把数据文件放到HDFS,再建外部表读取。总之先落到一张临时ODS表,这部分不要做太多计算,保持原样:
sql复制CREATE EXTERNAL TABLE tmp_order_ods (
order_id BIGINT,
user_id BIGINT,
amount DECIMAL(10,2),
order_status TINYINT,
order_date STRING
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
LOCATION '/data/tmp_ods/orders';
然后从临时表往分区表做一次动态分区写入:
sql复制SET hive.exec.dynamic.partition=true;
SET hive.exec.dynamic.partition.mode=nonstrict;
INSERT OVERWRITE TABLE dwd_order_detail PARTITION(dt)
SELECT order_id, user_id, amount, order_status, order_date
FROM tmp_order_ods;
用户维度表则改成分桶表,先建新表再写入:
sql复制CREATE TABLE dim_user_new (
user_id BIGINT,
user_name STRING,
level TINYINT
)
CLUSTERED BY (user_id) INTO 64 BUCKETS
STORED AS ORC;
SET hive.enforce.bucketing=true;
INSERT OVERWRITE TABLE dim_user_new
SELECT user_id, user_name, level
FROM dim_user_old;
确认数据量和抽样结果没问题之后,做表切换,把应用层查询指向新表。
5.3 如何验证优化效果
优化效果不能靠“感觉变快了”,要用可量化的指标验证。
第一看扫描量。在改造前后分别跑:
sql复制EXPLAIN SELECT COUNT(*) FROM dwd_order_detail WHERE dt = '2024-06-01';
看Partition部分,如果改造前是Total partitions几十个甚至全表,改造后应该只出现1个分区。再对比实际运行时间,通常有数量级的提升。
第二看文件数和单文件大小。用HDFS命令直接数文件:
bash复制hdfs dfs -count /user/hive/warehouse/dwd.db/dwd_order_detail/dt=2024-06-01/
改造前如果几百个小文件,改造后应该是几十个大小均匀的文件。分桶表的桶数应该在目录文件里也能体现,比如64个桶就看到64个part-*文件。
第三看join执行计划。改造后重新跑用户维度表join订单表的SQL,用EXPLAIN看关键信息。如果分桶map join生效,计划里会有对应算子,mr任务量也会明显下降。
整个过程我做过很多次,总结下来记住一条主线:先梳理查询模式,再决定分区键和分桶键,最后用参数和文件指标去验证,而不是一上来就堆参数。
6. 常见问题排查与避坑实录
6.1 高频问题速查表
把这些年踩过以及帮同事排查过的高频问题整理成一张速查表,建议收藏:
| 现象 | 常见原因 | 解决办法 |
|---|---|---|
| 动态分区插入报Too many dynamic partitions | 分区数量超过限制 | 调大hive.exec.max.dynamic.partitions,或拆分任务 |
| 分区表查询仍然很慢 | WHERE条件没写分区字段,或字段类型隐式转换 | 检查SQL,确保过滤条件直接落在分区字段上 |
| 分桶表文件数不等于桶数 | 用LOAD DATA写入,或没开hive.enforce.bucketing | 改用INSERT OVERWRITE,并显式开启强制分桶 |
| 分桶表join没走BucketMapJoin | join字段不是分桶字段,或桶数不满足整数倍条件 | 统一join字段与分桶字段,重建分桶表 |
| 增量导入后出现大量小文件 | reducer数量过多,或分区粒度太细 | 调大bytes.per.reducer,开启合并参数 |
| 某一天查询特别慢,其他天正常 | 该日期数据量异常大或分区数据倾斜 | 检查数据分布,考虑对异常分区单独处理 |
| 查询结果出现重复数据 | 外部表删除后重新建表,或动态分区空值落入默认分区 | 检查元数据,过滤NULL分区值 |
| 表目录下出现__HIVE_DEFAULT_PARTITION__目录 | 分区字段值为NULL | 数据清洗时填充默认值,避免NULL进分区 |
6.2 三个值得留意的经验
第一个经验是别把分区和分桶当成银弹。分区能让查询少读数据,分桶能让文件分布更规整,但表结构一旦设计错,后续要改的代价很大。建表前先问自己三个问题:这张表最常用的过滤条件是什么?最频繁的join字段是什么?数据量大概在什么级别?答案清楚了再动手。
第二个经验是桶数不要拍脑袋。桶数太少了,单桶文件过大会影响并行度;桶数太多了,又会有一堆空桶和大量小文件。我的做法是先看单分区数据量,再估算单文件期望大小,反推桶数。比如单分区100GB,期望单文件256MB,桶数就在400左右,但实际会折中选256或512这种整桶数,避免文件过于零碎。
第三个经验是定期做文件健康检查。Hive表跑时间长了,垃圾文件、小文件、重复数据一定会冒出来。可以写个定时脚本,按月扫描关键表的目录文件数和平均大小,超过阈值就触发合并任务。这和给硬盘做碎片整理是一个道理,平时不觉得,等集群响应变慢再去处理就晚了。
做SQL查询开发和数仓调优这几年,我最大的体会是:分区和分桶本身不难,难的是理解每种优化手段的适用边界。分区是为了“少读数据”,分桶是为了“数据分布更可预期”,两者配合好了,查询性能和存储效率的提升是实实在在的。早些年我总喜欢堆参数,觉得什么优化开关都打开就是高手,其实不如静下心来把表结构设计对。如果你正准备优化Hive表,我建议从重新梳理分区键和分桶键入手,这一步是性价比最高的开始。
