很高兴你能来阅读,过去一年多,我主要从事天猫国际商品以及订单相关数仓开发与数据分析工作。接下来会陆续分享这段经历中的实战问题、对应解决思路,以及数仓基础的进阶学习总结,希望能给有需要的朋友带来参考和帮助~
一、序言
有段时间我们阿里数仓dataworks任务调度老是在凌晨堆积任务,除了本身资源内存等可以拓展外,我们发现,有很多大表,比如两千万以上的数据表,都是全量抽数,基本上抽一次大约一个小时多。在我们优化成增量抽数后,每次抽数时间大约只要10-20分钟,明显任务堆积以及产出延迟的问题得到一定程度的解决。
我们最初也是希望服务器可以扩容,性能可以更好,但是在有限的情况下,我们作为研发,避免自己代码的原因导致无效的资源浪费,尽可能的要将服务器本身的性能发挥到最近。
1. 增量抽数的业务触发场景
增量抽数并非适用于所有业务场景,其核心触发条件是“数据持续新增/变更、数据量较大、存在明确增量标识”,在电商业务中,以下三类场景是增量抽数的典型应用场景:
-
高频交易类场景:如订单交易、支付记录等核心业务数据。这类数据具有“实时新增、总量庞大”的特点,以订单表为例,电商平台日均订单量可达数十万至数千万级,双11等大促峰值更是每秒新增数百单,若采用全量抽数,不仅会占用大量数据库读写资源,还会导致抽数延迟超过数小时,无法满足后续实时监控、库存更新等业务需求,因此必须通过增量抽数获取每段时间的新增/状态变更订单。
-
用户行为类场景:如用户浏览日志、点击记录、收藏/加购操作等。这类数据属于“海量高频写入”类型,单用户每日就可能产生数十条行为记录,全量抽数会造成巨大的网络传输和存储成本,而通过增量抽数(基于时间戳或日志偏移量)仅获取新增行为数据,能大幅提升数据同步效率,支撑个性化推荐、用户画像等实时业务。
-
状态流转类场景:如商品库存变更、物流状态更新、订单售后进度等。这类数据的核心特点是“存量数据会动态变更”,例如商品库存会随下单、退款实时变化,物流状态会从“待发货”流转至“已签收”,若采用全量抽数,会重复同步大量未变更数据,而增量抽数可精准捕捉状态变更记录,确保数据同步的时效性和准确性。
总结来说,当业务数据满足“数据量级大(通常万级以上)、更新频率高、存在可识别的增量标识(如时间戳、自增ID、状态字段)”三个条件时,增量抽数是优于全量抽数的最优解。
真实场景:
- 作为一个数仓开发,我们的订单表和商品表都是通过增量抽数来实现的。全量抽,主要是占用大量系统的copy资源,任务资源不足的情况下,会导致其余任务延迟。
- 如上面说的物流状态或者历史部分数据不会发生变化的场景,重复抽数多余!不如根据更新时间来进行增量抽数。
2. 业务背景
比如我们电商系统中,我们订单表的抽数汇总就是使用增量抽数来实现的, 学习思想即可
电商数仓中:
-
增量源表:
ods.ods_t_order_incr(每日抽取的新增/更新订单,2025-08-01分区,含订单ID、用户ID、金额、更新时间等,order_id为主键); -
老全量表:
dwd.dwd_fact_order(订单历史全量数据,含所有历史订单); -
合并规则:
✅ 增量表中存在的订单(新增/更新)→ 优先用增量表数据(覆盖老数据);
✅ 增量表中不存在的老订单 → 保留老数据;
✅ 过滤无效数据(order_id为空、订单金额≤0)。
3. FULL OUTER JOIN 合并逻辑说明
FULL OUTER JOIN 会返回左表(老全量表)和右表(增量表)的所有记录:
-
两边
order_id匹配 → 取增量表数据(更新老数据); -
增量表有、老表无 → 取增量表数据(新增订单);
-
老表有、增量表无 → 取老表数据(未变更订单);
-
最终通过
COALESCE函数优先取增量表字段,实现“增量覆盖、老数据保留”。
二、具体实现步骤(Hive SQL)
步骤1:增量表去重(前置处理,避免重复数据)
增量抽数可能因源库重复写入导致同order_id多条记录,需先按update_time取最新一条:
-- 创建增量去重临时表
CREATE TEMPORARY TABLE tmp.tmp_order_incr_dedup AS
SELECT
order_id,
user_id,
goods_id,
order_amount,
pay_time,
create_time,
update_time,
dt
FROM (
SELECT
*,
-- 按订单ID分组,取更新时间最新的一条
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY update_time DESC) AS rn
FROM ods.ods_t_order_incr
WHERE dt = '2025-08-01' -- 仅处理2025-08-01增量数据
AND order_id IS NOT NULL
AND order_amount > 0 -- 过滤无效数据
) t
WHERE rn = 1;
步骤2:FULL OUTER JOIN 合并增量与老数据
核心是通过FULL JOIN关联老表和去重后的增量表,用COALESCE优先取增量字段:
-- 创建合并后临时表
CREATE TEMPORARY TABLE tmp.tmp_order_full_merge AS
SELECT
-- 优先取增量表的订单ID(无则取老表,保证主键非空)
COALESCE(source.order_id, target.order_id) AS order_id,
-- 增量表有数据则取增量,无则取老表(更新/保留逻辑)
COALESCE(source.user_id, target.user_id) AS user_id,
COALESCE(source.goods_id, target.goods_id) AS goods_id,
COALESCE(source.order_amount, target.order_amount) AS order_amount,
COALESCE(source.pay_time, target.pay_time) AS pay_time,
COALESCE(source.create_time, target.create_time) AS create_time,
-- 更新时间优先取增量表(即使是老订单,更新后时间也最新)
COALESCE(source.update_time, target.update_time) AS update_time,
-- 分区字段:增量数据标当日,老数据保留原分区
COALESCE(source.dt, target.dt) AS dt
FROM dwd.dwd_fact_order target -- 左表:老全量表(历史数据)
FULL OUTER JOIN tmp.tmp_order_incr_dedup source -- 右表:去重后的增量表
ON target.order_id = source.order_id; -- 按订单ID关联
步骤3:覆盖更新DWD层全量表
将合并后的临时表数据覆盖写入目标全量表,完成最终合并:
-- 覆盖DWD层订单全量表(保留所有历史+最新增量)
INSERT OVERWRITE TABLE dwd.dwd_fact_order
SELECT
order_id,
user_id,
goods_id,
order_amount,
pay_time,
create_time,
update_time,
dt
FROM tmp.tmp_order_full_merge
-- 最终过滤:防止极端情况(如JOIN后order_id为空)
WHERE order_id IS NOT NULL;
-- 清理临时表(可选)
DROP TABLE tmp.tmp_order_incr_dedup;
DROP TABLE tmp.tmp_order_full_merge;
三、FULL OUTER JOIN 合并逻辑拆解(关键说明)
| 场景 | 老表(target) | 增量表(source) | COALESCE取值逻辑 | 最终处理结果 |
|---|---|---|---|---|
| 老订单状态更新 | 有记录 | 有记录 | 取source字段(增量数据) | 覆盖老数据 |
| 全新订单 | 无记录 | 有记录 | 取source字段 | 新增订单 |
| 老订单无变更 | 有记录 | 无记录 | 取target字段 | 保留老数据 |
| 异常:两边都无记录 | 无记录 | 无记录 | 被WHERE过滤(order_id空) | 剔除 |
四、核心优势与注意事项
1. FULL OUTER JOIN 优势
-
相比传统“LEFT JOIN + UNION ALL”,只需一次关联即可完成“新增+更新+保留老数据”,逻辑更简洁;
-
天然支持“双向匹配”,无需拆分“老数据非增量部分”和“增量部分”,减少子查询嵌套。
2. 关键注意事项
-
去重前置:增量表必须先去重,否则FULL JOIN会因同
order_id多条增量记录导致结果重复; -
COALESCE优先级:必须将增量表字段放在前面(
COALESCE(source.字段, target.字段)),保证增量数据覆盖老数据; -
分区优化:若老全量表按
dt分区,建议按分区执行FULL JOIN(如仅关联老表2025-08-01前的分区),避免全表扫描; -
数据校验:合并后校验总行数 = 老表行数 - 老表中被更新的行数 + 增量表行数,确保无漏数/重复。
📣非常感谢你阅读到这里,如果这篇文章对你有帮助,希望能留下你的点赞👍 关注❤️ 分享👥 留言💬thanks!!!