05车间工人 Spark SQL目录
一个 SQL 节点只干三件事:从上游表把料读出来、照模具算一遍、把结果整桶倒进目标表的某一个格子。算不清"哪天"、找不准"哪格",SQL 写得再花也是白干。
水厂是站在厂长视角看整条链。这一关把镜头推到车间里:一个工人(Spark)拿着一副模具(SQL),干完活把料倒进身后那排格子柜(分区)。模具决定料被压成什么形状,格子柜决定这桶料倒进哪个格口。倒错格口,就把别人那一格盖掉了。

一、先说人话

"Spark SQL 加工"这几个字听着玄,落到具体活上就是车间:湖里那四层 SDI / DWI / DWR / DWS / ADS,不是一个抽象概念,是几百上千个这样的工位排在一起。生产湖 dm_gk_hudi 里就有 2,763 张表(落地库 dm_gk_hudi_sit 3,027 张),每一张背后都至少有一个工位在往里倒料。

一个工位就是一个 SQL 节点,跑在 Spark 上。它不聪明,也不看业务,只认三件事:读哪张表、算什么、写哪一格。所以看懂一个 SQL 节点,你不需要懂大数据——只要在它开跑之前问清三句:算哪天?写哪格?会不会跟别人抢同一格?

这三句答不上来,任务照样会跑成功。它真的会成功,只是把别人的数据盖掉了。这一关八成的内容,都是在讲"怎么会盖掉"和"怎么不盖掉"。

工人 = Spark 模具(SQL) 定形状 只倒一格 格子柜 = 分区 这一天 别的天 别的公司 ··· ··· 抢同一格 = 打架 两把铲子同时倒 → 后倒的盖掉先倒的

图上三个位置要认准:模具(SELECT 里的字段和条件)决定这桶料的形状;箭头落点(PARTITION(...))决定它进第几格;左下角那两把铲子是反面教材——它们倒的是同一格。工人本身不决定任何东西,Spark 只是那个使力气的。

二、拆开看:每个零件是什么

按"进车间 → 领模具 → 干活 → 倒料 → 别抢格"的顺序,一共六样东西,最后再补两件收尾的事(省料、出货)。看完全部,你就能读懂任何一个 SQL 节点。

先说清一个前提:这一关讲的是写。读的部分(贴源数据怎么进来的)在第 3 关,出去的部分(怎么同步到 Doris)在第 7 关。

2.1 总闸:所有 SQL 从同一个门进去

🚰 1/6 Kyuubi(车间总闸 / 统一插座)

像什么:车间墙上只有一个总闸,所有机床都插这一个插座。不是因为别的插座不好,是因为只有这一个口能查到谁在用电、用了多少。

其实是什么: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 才算接对
会出事DEV 账号不带 kyuubi.engine.share.level.subdomain=<子域>,这是平台明写的准入要求。会不会因此被 KILL?目前没拿到证据,别当结论用;但它是准入条件,照带不误——真出事时你连排查方向都找不到。
注意两种糊法都不认——在 SQL 里写注释冒充 tag、连上之后用 SET 补一个 subdomain,都不行。平台在开引擎那一步就要知道这个值,等你开始执行 SQL,早就过了判断的时点。

2.2 一个工位:SQL 节点到底在干什么

🧑‍🏭 2/6 一个 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 定形状,日期从外面拧进来

🧱 3/6 模具(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)
注意SQL 里别用 now() / current_date() / current_timestamp() 取"今天"。规范把这类函数列进"ETL 开发一般不使用,如需使用要评估"。原因很实在:补数、重跑、跨零点执行,这三种情况下"今天"都不是你要算的那一天。

2.4 格子柜:分区就是一个格口,倒错就把别人的盖掉

🗄 4/6 格子柜的格口(分区)

像什么:一排格子柜,每格贴一张标签:哪天、哪个公司。今天这桶只倒进"今天 + 这个公司"那一格。柜子不关心你倒了什么,它只按标签分格。

其实是什么:分区的本质是"按值把表切成一份一份"。写的时候把分区值指定死,Spark 就只动那一格对应的数据,其余格口碰都不碰——这也是 INSERT OVERWRITE 敢说"换掉整格"的底气:它换的只是一格,不是一张表。

真名
分区(partition),写入时写死在 PARTITION(分区键=分区值)
常见的键
partition_key、cdc_partition_key、system_code 这几个是平台里常用的分区/主键字段名,你的表用哪个,看建表语句
格口划多细
越细,单次写的量越小、越快;太细,小文件就多,查询反而慢
会出事格口写错:该写 20260831,写成 20260830;或者公司代码写串了。结果是原来那一格的数据被整格覆盖,而任务状态是成功。等业务第二天问"昨天的数怎么少了一天",翻任务日志什么都看不出来,只能一层层往回查。
注意分区值为空也算事故。《数据实施规范》4.4.5 说得很直白:字段值为 NULL 一般不转换,但"组织 id 不存在或者分区字段为空"属于异常。分区字段为空,等于把这桶料倒进了"没贴标签的那一格",谁也找不到它。

2.5 两把铲子:同一张表不许拆并行分支

🥄 5/6 一把铲子一个格(同表禁并行)

像什么:两个人同时往同一格里倒料,谁后倒谁的留在上面。他们各自都觉得自己干完了,收工时都签了字。

其实是什么:INSERT OVERWRITE 的语义是"这一格清空再写"。两个分支同时写同一张表(甚至同一格),后提交的那次会把先提交的整格盖掉,两个任务都报成功。

真名
红线第 4 条:同一张表不能拆并行分支——INSERT OVERWRITE 会互相覆盖
怎么排
要么串成一条链(分支 2 依赖分支 1),要么各自写进不同的目标表(_mid 后缀分开),最后再合并
会出事这是真事故级的红线,不是理论风险。表现还特别阴——调度面板上一排绿勾,数据里少一天、或少一个公司。要等业务发现数不对,才会有人回头看这两个分支的执行时间是不是重叠的。

2.6 进料:伪增量和真增量,差的是"扫多少"

这一节是本页最值钱的一段。同样叫"只算增量",两种写法在 SQL 里看起来差不多,底下的力气差一个量级。

🕳 6/6-A 伪增量(看着像增量,底层全表扫)

像什么:你想只拿今天新到的那几箱货,但仓库没有到货单,只能把整个仓库翻一遍,翻完再挑出今天的。

其实是什么:拿 _hoodie_commit_time 或 additiondate 这类字段在 WHERE 里过滤。SQL 读起来很干净,结果也只有增量——但底层还是把全表扫一遍再筛。资源按全表申请,跑得慢,表越大越明显。

真名
WHERE _hoodie_commit_time > '…' 或 WHERE additiondate >= '…'
代价
扫描量 = 全表;资源也按全表申请
✅ 6/6-B 真增量(Hudi 原生,只读那一段)

像什么:这次仓库有到货单了:告诉管理员"从这单到那单之间的货",他直接把那几箱推出来,仓库其余部分不动。

其实是什么: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。一个桶扛了半张表,其余桶干等着——十个工人,一个在搬砖,九个在看。
小窍门还有一招不写在 SQL 里:作业流名字。资源是按作业流名的前两段分的——第一段是分层(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 节点从开工到收工的全过程。照这个顺序走,出事的概率能砍掉大半。

  1. 连总闸。连 jdbc:kyuubi://…:10009:办公室走 10.160.4.50 / 10.160.4.236,在家走 VPN 的 10.160.4.100:10009。DEV 账号把 kyuubi.engine.share.level.subdomain=<子域> 带上,别省。
  2. 先看格口。看目标表的建表语句:分区键叫什么、有几级。这一步不花时间,但决定了后面写的那一行对不对。
  3. 确定 biz_date。这次算哪天?日常跑就是 T-1(或上月末,看任务周期),由海豚的全局参数传进来,测的时候手动给小一号的日期,别拿"今天"凑。
  4. 取料。整段重算就 FROM 整张上游表加日期过滤;小增量就走真增量,先 call copy_to_temp_view(...) 把变更做成视图,再 select * from 视图名。
  5. 压模具。按目标表字段顺序写 SELECT,维表放在最后关联(维表属性不参与计算,只用主键)。能 INNER JOIN 的别 CROSS JOIN。
  6. 倒一格。INSERT OVERWRITE TABLE 库.表 PARTITION(分区键='${biz_date}') SELECT …,一句话写完,中间不夹 truncate / drop。
  7. 自检。跑完先看两件事:目标格口的行数对不对得上(跟上游同日期比)、有没有空的分区值。对不上就别急着往上游发布,先怀疑格口写错了。

这七步里,第 2、3、5 步是纯人工判断,机器不会帮你。剩下几步基本能照模板抄。真正的差别就在这三步上——老手和新手写的 SQL,长度可能一模一样,差的就是这三步想没想。

四、搞错了会怎样

这一关的坑,两个会让数据直接不对,两个是慢慢把集群拖垮。四个都有明确的出处,不是吓唬人。

会出事生产 SQL 里写 CREATE / DROP——平台在保存那一步就拦下来。同样被拦的还有:读写 _sit / _uat / temp_db / default 这几个库,其中 import_db 只读。所以"先在测试库跑通再改生产"这件事,得靠换连接 / 换参数,不是靠改 SQL 里的库名——改库名这条路,存都存不进去。
会出事同一张表拆两个并行分支(第 2.5 节)。两个 INSERT OVERWRITE 互相覆盖,后跑的赢,两个任务都是绿的。查起来最费劲:日志干净、状态成功、数据少一天。
注意忘带分区条件。没有分区过滤,Spark 就把这张表所有格口全扫一遍;一张几亿行的表这么扫,资源申请一大片,同队列里排队的任务全跟着等。集群的负载不均不是玄学——单项目 756 个任务分在 10 台 worker 上,其中一台承接 218 个、排队 3.8 分钟,另外九台基本空闲。你多申请的那一份,就是别人排队的那几分钟。
注意在 SQL 里 SET 引擎参数。这条路是封死的:参数要么在连接层给(subdomain 这种必须建会话前就带的),要么在平台 / 项目级参数里配。别在脚本里 SET 一个 spark. 开头的参数就当调了资源——一次性的资源需求走平台配置,不走 SQL。

四条里只有第一条会在保存时就被挡住,剩下三条全都是"跑得成功"的。所以别指望拿任务状态当报警器:调度面板一片绿勾,也可能已经少了一天数据。这一关的坑,只能靠写之前那三句问出来。

五、一句话记住

做对了写 SQL 之前先问三句:算哪天?写哪格?会不会和别人抢同一格?

答得上来,这个节点基本不会出事:biz_date 说清算哪天,PARTITION(...) 说清写哪格,串行依赖说清没人抢。答不上来就先别按提交——跑成功只说明它跑完了,不说明它没盖掉别人的数据。

至于跑得慢,分两件事查:先看是伪增量还是真增量(全表扫 vs 只读一段),再看那四招(先缩范围、JOIN 用对、别 select *、倾斜别硬 Hash)。这两层分开看,比一上来就调参数快得多。

事实来源:基于Hudi的增量能力 · 数据实施规范第 4 节 · SQL资源分配及问题排查这一关只管"一道工序"下一关:调度值班室