一、先说人话
"Spark SQL 加工"这几个字听着玄,落到具体活上就是车间:湖里那四层 SDI / DWI / DWR / DWS / ADS,不是一个抽象概念,是几百上千个这样的工位排在一起。生产湖 dm_gk_hudi 里就有 2,763 张表(落地库 dm_gk_hudi_sit 3,027 张),每一张背后都至少有一个工位在往里倒料。
一个工位就是一个 SQL 节点,跑在 Spark 上。它不聪明,也不看业务,只认三件事:读哪张表、算什么、写哪一格。所以看懂一个 SQL 节点,你不需要懂大数据——只要在它开跑之前问清三句:算哪天?写哪格?会不会跟别人抢同一格?
这三句答不上来,任务照样会跑成功。它真的会成功,只是把别人的数据盖掉了。这一关八成的内容,都是在讲"怎么会盖掉"和"怎么不盖掉"。
图上三个位置要认准:模具(SELECT 里的字段和条件)决定这桶料的形状;箭头落点(PARTITION(...))决定它进第几格;左下角那两把铲子是反面教材——它们倒的是同一格。工人本身不决定任何东西,Spark 只是那个使力气的。
二、拆开看:每个零件是什么
按"进车间 → 领模具 → 干活 → 倒料 → 别抢格"的顺序,一共六样东西,最后再补两件收尾的事(省料、出货)。看完全部,你就能读懂任何一个 SQL 节点。
先说清一个前提:这一关讲的是写。读的部分(贴源数据怎么进来的)在第 3 关,出去的部分(怎么同步到 Doris)在第 7 关。
2.1 总闸:所有 SQL 从同一个门进去
像什么:车间墙上只有一个总闸,所有机床都插这一个插座。不是因为别的插座不好,是因为只有这一个口能查到谁在用电、用了多少。
其实是什么:Kyuubi 是 Spark SQL 的 JDBC 网关。你要跑的每一句 SQL 都从这一个口进去,由它分配引擎、跑完再把结果还给你。数据湖里所有层的写入和查询,都走它。(第 2 关里管它叫总水闸,是同一个东西。)
- 真名
Kyuubi,入口jdbc:kyuubi://…:10009- 办公室
10.160.4.50或10.160.4.236,端口10009- VPN / 家里
10.160.4.100:10009,同一条线的外网入口- DEV 必带
kyuubi.engine.share.level.subdomain=<子域>- 连上先验
show databases,能看到dwi_gk_hudi/dwr_gk_hudi/dm_gk_hudi才算接对
kyuubi.engine.share.level.subdomain=<子域>,这是平台明写的准入要求。会不会因此被 KILL?目前没拿到证据,别当结论用;但它是准入条件,照带不误——真出事时你连排查方向都找不到。SET 补一个 subdomain,都不行。平台在开引擎那一步就要知道这个值,等你开始执行 SQL,早就过了判断的时点。2.2 一个工位:SQL 节点到底在干什么
像什么:车间里每张工艺卡只写一件事:从哪个料架取料、怎么压、压完放到哪个格口。工序里没人问"这张卡有没有意义",卡上写的就是要干的活。
其实是什么:一个节点 = 一句 INSERT OVERWRITE:把上游表读出来(可能还要关联维表),按 SELECT 算成目标表的字段结构,再整格写进目标表的一个分区。读、算、写,就这三步。
-- 骨架:表名、字段名都要换成你自己的
INSERT OVERWRITE TABLE dwi_gk_hudi.dwi_场景_xxx_mid
PARTITION (partition_key = '${biz_date}')
SELECT
a.system_code,
a.biz_id,
...
FROM dwr_gk_hudi.dwr_场景_xxx a
WHERE a.partition_key = '${biz_date}';
四行对应四件事:写哪张表、写哪一格、取哪些字段、从哪张表取(并且只取这一天)。分区键到底叫 partition_key 还是别的,以目标表的建表语句为准,别照抄。
- 真名
INSERT OVERWRITE TABLE 库.表 PARTITION(分区键=分区值) SELECT …- 节点类型
- 海豚里选 SQL 组件,数据源类型选 KYUUBI
- 脚本头
- 要写清谁写的、什么时候写的、这张表干什么用(《数据实施规范》4.1)
- 中间表
- 中间过程和目标表建在同一层,表名后面加
_mid,多个再加_mid_sale这种后缀
truncate 再 insert into",也别"先 drop 再 insert into"。规范里明令禁止:前半句成功了、后半句失败,这张表就空一段时间,下游全在算空集。要整格换新,就用 INSERT OVERWRITE 一句做完。2.3 模具:SQL 定形状,日期从外面拧进来
SELECT 的形状)+ biz_date像什么:模具是一块刻好槽的钢板,料倒进去按槽成形。模具上不能刻"今天几号"——刻死了,明天还得重做一块。
其实是什么:模具就是 SELECT 那一段:选哪些字段、怎么算、跟谁关联。日期不写死在 SQL 里,而是从外面当参数递进来。这个参数叫 biz_date,含义是本次算哪一天的业务。
- 真名
biz_date- 谁传进来
- 海豚的全局参数,SQL 里用
${biz_date}引用 - 常见写法
$[month_last_day(yyyyMMdd,-1)]:取上个月最后一天,格式yyyyMMdd(偏移量的含义以项目级参数的实际定义为准)- 长什么样
- 分区字段不加间隔符,是
20260831这种,不是2026-08-31(《数据实施规范》4.4.11) - 参数放哪
- SQL 依赖的参数一律在项目级参数里定义,同义的不许重复定义(《数据实施规范》3.2.1)
now() / current_date() / current_timestamp() 取"今天"。规范把这类函数列进"ETL 开发一般不使用,如需使用要评估"。原因很实在:补数、重跑、跨零点执行,这三种情况下"今天"都不是你要算的那一天。2.4 格子柜:分区就是一个格口,倒错就把别人的盖掉
像什么:一排格子柜,每格贴一张标签:哪天、哪个公司。今天这桶只倒进"今天 + 这个公司"那一格。柜子不关心你倒了什么,它只按标签分格。
其实是什么:分区的本质是"按值把表切成一份一份"。写的时候把分区值指定死,Spark 就只动那一格对应的数据,其余格口碰都不碰——这也是 INSERT OVERWRITE 敢说"换掉整格"的底气:它换的只是一格,不是一张表。
- 真名
- 分区(partition),写入时写死在
PARTITION(分区键=分区值) - 常见的键
partition_key、cdc_partition_key、system_code这几个是平台里常用的分区/主键字段名,你的表用哪个,看建表语句- 格口划多细
- 越细,单次写的量越小、越快;太细,小文件就多,查询反而慢
20260831,写成 20260830;或者公司代码写串了。结果是原来那一格的数据被整格覆盖,而任务状态是成功。等业务第二天问"昨天的数怎么少了一天",翻任务日志什么都看不出来,只能一层层往回查。2.5 两把铲子:同一张表不许拆并行分支
像什么:两个人同时往同一格里倒料,谁后倒谁的留在上面。他们各自都觉得自己干完了,收工时都签了字。
其实是什么:INSERT OVERWRITE 的语义是"这一格清空再写"。两个分支同时写同一张表(甚至同一格),后提交的那次会把先提交的整格盖掉,两个任务都报成功。
- 真名
- 红线第 4 条:同一张表不能拆并行分支——
INSERT OVERWRITE会互相覆盖 - 怎么排
- 要么串成一条链(分支 2 依赖分支 1),要么各自写进不同的目标表(
_mid后缀分开),最后再合并
2.6 进料:伪增量和真增量,差的是"扫多少"
这一节是本页最值钱的一段。同样叫"只算增量",两种写法在 SQL 里看起来差不多,底下的力气差一个量级。
像什么:你想只拿今天新到的那几箱货,但仓库没有到货单,只能把整个仓库翻一遍,翻完再挑出今天的。
其实是什么:拿 _hoodie_commit_time 或 additiondate 这类字段在 WHERE 里过滤。SQL 读起来很干净,结果也只有增量——但底层还是把全表扫一遍再筛。资源按全表申请,跑得慢,表越大越明显。
- 真名
WHERE _hoodie_commit_time > '…'或WHERE additiondate >= '…'- 代价
- 扫描量 = 全表;资源也按全表申请
像什么:这次仓库有到货单了:告诉管理员"从这单到那单之间的货",他直接把那几箱推出来,仓库其余部分不动。
其实是什么:Hudi 自己记着每一次提交(commit)的时间点。用 call copy_to_temp_view(...) 把"某两个提交时间之间"的变更做成一张临时视图,再 select 它,读的就只有这一段。
真实调用长这样(原文的例子,表是 data_quality.test_hudi_cow_1):
call copy_to_temp_view(
table => 'data_quality.test_hudi_cow_1',
view_name => 'test_hudi_cow_1_view',
query_type => 'incremental',
begin_instance_time => '20230629172809616',
end_instance_time => '20230629172809619'
);
select * from test_hudi_cow_1_view;
- 真名
copy_to_temp_view,参数table/view_name/query_type/begin_instance_time/end_instance_time- 两个时间是什么
- 是 Hudi 的实例时间,
20230629172809618这种 17 位串,不是2026-08-31这种日期。边界含不含、怎么从前一步取到,按你表上实际的 commit 时间点核
| 怎么读增量 | SQL 长什么样 | 底层扫多少 | 什么时候用它 |
|---|---|---|---|
| 伪增量 | WHERE _hoodie_commit_time > … 或 additiondate … | 全表扫一遍再筛 | 数据量小、偶尔跑一次 |
| 真增量 | call copy_to_temp_view(…) + select * from 视图名 | 只读 begin ~ end 之间的变更 | 每天的小增量,省资源、跑得快 |
怎么选很简单:日常那点小增量走真增量,资源申请少、跑得快;跨很多天、或者口径改了要重算整段,就别玩增量了,整段重跑更省心——增量读完还得回头补历史,两头对账更费劲。
还有一个容易混的点:读进来用什么方式,跟写出去用什么语句是两件事。平台里的习惯是——离线计算用 insert overwrite(没有延迟数据,整格覆盖最干净);Hudi 增量计算、动态分区写用 insert into(怕延迟数据到了,把已有数据覆盖掉)。这一条按表上的实际场景选,别一刀切。
2.7 省料四招:同样的活,别用双倍的电
这四条出自《数据实施规范》的查询规范(原文是对着仓库侧写的,湖里同一个道理)。每条一句人话、一句代价。
| 招 | 人话 | 不这么干的代价 |
|---|---|---|
| 先缩范围,再排序 | 5 亿行以上还要排序取前几条的,先用 WHERE 把范围砍小,再排。 | 先把 5 亿行排完再扔掉 99%,排序那一步把内存和磁盘全占了,任务卡在那儿转圈。 |
| 大表 JOIN 用对策略 | 两张都超过 3 亿的表 JOIN,用 Colocate JOIN;能用 INNER JOIN 就别 CROSS JOIN。 | 数据要在节点之间来回搬,shuffle 成了瓶颈,一个 JOIN 顶平时十个任务。 |
别 select * | 亿级大表必须点名字段,并且带上分区分桶条件。 | 不需要的列也读,还要跨全部分区扫,IO 全浪费在你不看的字段上。 |
| 倾斜字段别硬 Hash | 分桶字段有 30% 以上的数据挤在同一个值上,就改 Random,别用 Hash。 | 一个桶扛了半张表,其余桶干等着——十个工人,一个在搬砖,九个在看。 |
dwi / dwr / dim / dws / ads),第二段是主题域(比如 ret)。名字对了才进对应的资源池,比如 ads_ret 会落到 ads-ret-0 / ads-ret-1 / ads-ret-2 其中一个;名字不合规,就按原来的策略随便分,跟你该用的队列对不上。前提还有一条:得用 etl 这个数据源账号。2.8 出货口:湖里的大数据量怎么取出来
像什么:客户要 20 箱货,你不能把整个仓库推给他——先说清要哪几列、哪一段,再一批一批往外搬。
其实是什么:查询平台导出有上限(最大 5 万行)。超过这个量,常规路子是提一个 SeaTunnel 导出任务,把查询结果直接写成 CSV 落到文件服务器,再去下载。要带格式的表格,另有 Excel 模版工具。
- 真名
SeaTunnel导出任务,提交接口http://seatunnel.sinopharmholding.com:10001/submit-job- 要 CSV
- 提交 SeaTunnel 任务:source 用
HUDI_GK_LINK带上你的query,sink 用FTP_GK_TEST并设path(如/libo),跑完到http://ftp.sinopharmholding.com:10001/libo/下载 - 要 Excel
- 走数据文件模版工具
http://dpf.sinopharmholding.com:10001/dmt/dataCheck/template,按模版填数据集,生成 xlsx - 共同前提
- 先点名字段、带上分区条件把范围缩小,再决定走哪个出口;
select *全表导,等于把整个仓库推给客户
三、串起来:一次完整的流程
把上面这些零件拼起来,就是一个 SQL 节点从开工到收工的全过程。照这个顺序走,出事的概率能砍掉大半。
- 连总闸。连
jdbc:kyuubi://…:10009:办公室走10.160.4.50/10.160.4.236,在家走 VPN 的10.160.4.100:10009。DEV 账号把kyuubi.engine.share.level.subdomain=<子域>带上,别省。 - 先看格口。看目标表的建表语句:分区键叫什么、有几级。这一步不花时间,但决定了后面写的那一行对不对。
- 确定 biz_date。这次算哪天?日常跑就是 T-1(或上月末,看任务周期),由海豚的全局参数传进来,测的时候手动给小一号的日期,别拿"今天"凑。
- 取料。整段重算就
FROM整张上游表加日期过滤;小增量就走真增量,先call copy_to_temp_view(...)把变更做成视图,再select * from 视图名。 - 压模具。按目标表字段顺序写
SELECT,维表放在最后关联(维表属性不参与计算,只用主键)。能INNER JOIN的别CROSS JOIN。 - 倒一格。
INSERT OVERWRITE TABLE 库.表 PARTITION(分区键='${biz_date}') SELECT …,一句话写完,中间不夹truncate/drop。 - 自检。跑完先看两件事:目标格口的行数对不对得上(跟上游同日期比)、有没有空的分区值。对不上就别急着往上游发布,先怀疑格口写错了。
这七步里,第 2、3、5 步是纯人工判断,机器不会帮你。剩下几步基本能照模板抄。真正的差别就在这三步上——老手和新手写的 SQL,长度可能一模一样,差的就是这三步想没想。
四、搞错了会怎样
这一关的坑,两个会让数据直接不对,两个是慢慢把集群拖垮。四个都有明确的出处,不是吓唬人。
CREATE / DROP——平台在保存那一步就拦下来。同样被拦的还有:读写 _sit / _uat / temp_db / default 这几个库,其中 import_db 只读。所以"先在测试库跑通再改生产"这件事,得靠换连接 / 换参数,不是靠改 SQL 里的库名——改库名这条路,存都存不进去。INSERT OVERWRITE 互相覆盖,后跑的赢,两个任务都是绿的。查起来最费劲:日志干净、状态成功、数据少一天。SET 引擎参数。这条路是封死的:参数要么在连接层给(subdomain 这种必须建会话前就带的),要么在平台 / 项目级参数里配。别在脚本里 SET 一个 spark. 开头的参数就当调了资源——一次性的资源需求走平台配置,不走 SQL。四条里只有第一条会在保存时就被挡住,剩下三条全都是"跑得成功"的。所以别指望拿任务状态当报警器:调度面板一片绿勾,也可能已经少了一天数据。这一关的坑,只能靠写之前那三句问出来。
五、一句话记住
答得上来,这个节点基本不会出事:biz_date 说清算哪天,PARTITION(...) 说清写哪格,串行依赖说清没人抢。答不上来就先别按提交——跑成功只说明它跑完了,不说明它没盖掉别人的数据。
至于跑得慢,分两件事查:先看是伪增量还是真增量(全表扫 vs 只读一段),再看那四招(先缩范围、JOIN 用对、别 select *、倾斜别硬 Hash)。这两层分开看,比一上来就调参数快得多。