spark引擎跑数后查询慢
sql
insert overwrite table tmp.tmp1 partition(etl_date=${last_date})
select * from tmp.tmp2 where etl_date=${last_date};
输入参数为20260211引擎为spark,插到结果表,用hive引擎去count的时间为93s,如果输入参数为20260211引擎为hive,插到结果表,再用hive引擎去count的时间为1s。
差异的根源不是count阶段的引擎,而是Spark和 Hive写入时生成的底层文件格式/数量/元数据完全不同 ------Hive写入会生成带「行数元数据」的列式文件,而Spark默认写入的文件要么是Text格式、要么是元数据缺失的列式文件,导致Hive count时无法走元数据优化,只能全量扫描。
一、核心原因:Spark vs Hive 写入的文件差异(关键!)
用Hive引擎写入后count快,本质是Hive默认会对输出文件做「列式存储+元数据统计」;而Spark默认写入的文件不满足这个条件,具体差异如下:
a、文件格式:
Hive默认将结果表存储为Orc格式(数仓主流列式存储),且会自动统计每个文件的「行数、字段统计信息」并写入文件Footer;Hive count时直接读Footer的行数,无需扫描数据。
Spark默认写入的是TextFile格式(行式存储),即使表定义是Orc,Spark也可能因配置缺失以Text格式写入;即使是Orc格式,Spark默认不开启「文件元数据统计」,Footer中无行数信息,Hive count时只能逐行扫描。
b、文件数量:
Hive写入时默认合并小文件,一个分区仅生成1-2个大文件,扫描开销低。
Spark按Executor数量拆分输出,一个分区生成数十/数百个小文件,Hive count时要打开成百上千个文件,IO开销暴增。
c、元数据同步:
Hive写入后会立即更新分区元数据(如文件行数、大小),count时直接用元数据。
Spark写入后仅更新表分区的文件列表,不统计文件行数等元数据,Hive无法直接复用。
二、验证:查看两种写入方式的文件格式/数量
-- 1.查看表的存储格式(确认表定义是否为Orc)
sql
desc formatted mid.dwd_mkt_user_behavior_analyse_datas_dd;
--重点看:Storage Format →若显示ORC,则表定义是列式存储;若显示TextFile,说明表定义本身有问题。
-- 2.查看Hive引擎写入后的文件(etl_date=20260211)
sql
dfs -ls /user/hive/warehouse/mid.db/dwd_mkt_user_behavior_analyse_datas_dd/etl_date=20260211;
--正常结果:1-2个.orc文件,大小几十/上百MB。
-- 3.查看Spark引擎写入后的文件
sql
dfs -ls /user/hive/warehouse/mid.db/dwd_mkt_user_behavior_analyse_datas_dd/etl_date=20260211;
--异常结果:数十/数百个.txt或.orc文件,每个文件几KB(小文件)。
三、解决方案:让Spark写入「带元数据的 Orc 文件」
核心思路是:修改Spark的写入配置,让Spark和Hive一样生成「Orc格式+元数据统计+合并小文件」的文件,这样Hive count时就能走元数据优化,耗时降到1秒级。
步骤1:修改表定义(确保表是Orc格式,若已定义可跳过)
如果表定义是TextFile,先修改为Orc格式(数仓必须的优化):
sql
ALTER TABLE mid.dwd_mkt_user_behavior_analyse_datas_dd SET FILEFORMAT ORC;
--同时开启Orc元数据统计
ALTER TABLE mid.dwd_mkt_user_behavior_analyse_datas_dd SET TBLPROPERTIES (
'orc.compress'='SNAPPY', --压缩格式,减少文件大小
'orc.stats.autogather'='true' --自动收集元数据
);
步骤2:在Spark引擎执行SQL时,添加以下配置(核心!)
新增Spark专属配置,强制Spark以Orc格式写入,并开启元数据统计、合并小文件:
sql
--Spark写入Orc格式的核心配置(必须加在SQL最前面)
SET spark.sql.execution.arrow.enabled=true;
SET spark.sql.parquet.writeLegacyFormat=false;
--强制输出Orc格式,开启元数据统计
SET spark.sql.files.format=orc;
SET spark.sql.orc.impl=native;
SET spark.sql.orc.statistics.enabled=true;
SET spark.sql.orc.filesystem.cache.enabled=true;
--合并小文件(关键:减少文件数量)
SET spark.sql.adaptive.enabled=true;
SET spark.sql.adaptive.coalescePartitions.enabled=true;
SET spark.sql.adaptive.coalescePartitions.minPartitionNum=1;
SET spark.sql.adaptive.maxPartitionBytes=128MB; --每个文件128MB,合并小文件
--减少Executor开销(适配轻量任务)
SET spark.executor.instances=1;
SET spark.executor.cores=2;
SET spark.executor.memory=2g;
SET spark.driver.memory=1g;
insert overwrite table tmp.tmp1 partition(etl_date=${last_date})
select * from tmp.tmp2 where etl_date=${last_date};
(适配Parquet格式的Spark配置)
sql
-- ===================== Spark Parquet 核心优化配置 =====================
-- 1. 强制Spark按表定义的Parquet格式写入(关键)
SET spark.sql.files.format=parquet;
SET spark.sql.parquet.writeLegacyFormat=true; -- 兼容Hive MR的Parquet格式(必开!)
SET spark.sql.parquet.enableVectorizedReader=true; -- 开启Parquet矢量化读取/写入,提升效率
-- 2. 开启Parquet元数据统计(让Footer包含行数)
SET spark.sql.parquet.statistics.enabled=true; -- 生成行数等元数据
SET spark.sql.parquet.filesystem.cache.enabled=true; -- 缓存Parquet文件元数据
-- 3. 合并小文件(核心:减少文件数量,从数百个→1-2个)
SET spark.sql.adaptive.enabled=true; -- 开启自适应执行
SET spark.sql.adaptive.coalescePartitions.enabled=true; -- 自动合并小分区
SET spark.sql.adaptive.coalescePartitions.minPartitionNum=1; -- 最少保留1个分区(文件)
SET spark.sql.adaptive.maxPartitionBytes=128MB; -- 每个Parquet文件128MB(数仓最佳大小)
SET spark.sql.adaptive.forceApply=true; -- 强制生效合并规则
-- 4. 减少Spark启动/资源开销(适配轻量任务)
SET spark.executor.instances=1; -- 仅启动1个Executor(足够处理单分区数据)
SET spark.executor.cores=2; -- 每个Executor 2核
SET spark.executor.memory=2g; -- 2G内存(避免OOM,足够解析JSON)
SET spark.driver.memory=1g; -- Driver 1G内存(轻量调度)
SET spark.task.cpus=1; -- 每个Task 1核
SET spark.sql.shuffle.partitions=2; -- 减少Shuffle分区(无Shuffle但兜底)
insert overwrite table tmp.tmp1 partition(etl_date=${last_date})
select * from tmp.tmp2 where etl_date=${last_date};
必须开启 writeLegacyFormat=true:Spark默认生成的Parquet格式和Hive MR不兼容,开启该配置后,Spark写入的Parquet文件才能被Hive正确读取元数据;
步骤3:写入后手动补全元数据(兜底方案)
若Spark写入后count仍慢,执行以下命令手动更新Parquet文件的统计信息,Hive会立即识别行数:
sql
-- 手动计算分区的元数据(行数、大小等)
ANALYZE TABLE mid.dwd_mkt_user_behavior_analyse_datas_dd
PARTITION (etl_date='20260211')
COMPUTE STATISTICS;
四、验证优化效果
执行优化后的 Spark SQL写入后,再执行count:
sql
--用Hive引擎count,验证耗时
SET hive.execution.engine=mr;
SELECT COUNT(*) FROM mid.dwd_mkt_user_behavior_analyse_datas_dd WHERE etl_date='20260211';
五、生产级最佳实践(避免后续踩坑)
统一表存储格式:数仓所有表默认用Orc格式,开启元数据统计(orc.stats.autogather=true);
Spark写入必加配置:所有Spark写入Hive表的SQL,都要加「Orc格式+小文件合并」配置,避免生成无效文件;
sql
分区后优化:写入分区后,执行ANALYZE TABLE 表名 PARTITION (etl_date='20260211') COMPUTE STATISTICS;,手动补全元数据(兜底方案);
避免Text格式:绝对不要用TextFile格式存储数仓表,列式存储(Orc/Parquet)是count/聚合查询快的核心。
总结
核心原因:Spark默认写入的文件是「Text格式/小文件/元数据缺失」,Hive count时无法走元数据优化,只能全量扫描;Hive写入的是「Orc格式+大文件+元数据完整」,count直接读元数据;
解决关键:给Spark添加「强制Orc格式+元数据统计+小文件合并」配置,让Spark写入的文件和Hive一致;