首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >数据工程的专业底线:分层建模、增量合并、质量断言与调度闭环,附可运行代码

数据工程的专业底线:分层建模、增量合并、质量断言与调度闭环,附可运行代码

原创
作者头像
用户12339161
发布于 2026-10-17 15:23:33
发布于 2026-10-17 15:23:33
970
举报

数据工程不是"写SQL取数"。它的专业内核是:可分层的数据架构、幂等的增量管道、可断言的数据质量、可编排的调度依赖、可追溯的血缘治理。下面按生产级数仓流水线拆解,每环给可运行代码。

一、分层建模:ODS→DWD→DWS→ADS,别把数仓做成SQL堆

分层不是为了好看,是为了隔离变更:ODS贴源、DWD做明细清洗与维度关联、DWS做轻度聚合、ADS面向应用。维度建模的核心是事实表+维度表,事实表只存外键和度量,维度表存描述属性。

代码语言:javascript
复制
-- DWD层:订单明细事实表(只存外键+度量)
CREATE TABLE dwd_order_detail (
  order_id     STRING,
  user_sk      BIGINT,   -- 维度代理键,不是业务ID
  product_sk   BIGINT,
  order_amt    DECIMAL(18,2),
  qty          INT,
  dt           STRING
) PARTITIONED BY (dt);

-- 维度表:用户维(SCD2缓慢变化维)
CREATE TABLE dim_user (
  user_sk      BIGINT,   -- 代理键
  user_id      STRING,   -- 业务键
  city         STRING,
  level        STRING,
  start_dt     STRING,
  end_dt       STRING,
  is_current   BOOLEAN
);

关键约束:事实表绝不冗余用户名、城市等描述字段,否则维度变更要回刷全量历史事实。代理键user_sk让同一用户的历史订单能关联到当时的维度版本。

二、SCD2缓慢变化维:维度变更不能覆盖历史

用户从"普通"升"VIP",历史订单必须关联当时的等级。SCD2用起止时间和当前标志实现版本化。

代码语言:javascript
复制
# scd2_merge.py  —— PySpark实现SCD2
from pyspark.sql import functions as F

def scd2_merge(spark, dim_table, inc_df):
    cur = spark.table(dim_table)
    # 1. 关闭已变化的当前版本
    changed = inc_df.alias("i").join(
        cur.filter("is_current=true").alias("c"), "user_id"
    ).where("i.level <> c.level or i.city <> c.city") \
     .select("c.user_sk", "c.user_id")

    closed = cur.join(changed, "user_sk", "left_anti") \
        .union(changed.join(cur, "user_sk")
               .withColumn("end_dt", F.current_date().cast("string"))
               .withColumn("is_current", F.lit(False)))

    # 2. 插入新版本(代理键用row_number+偏移生成,生产用sequence)
    new_ver = inc_df.join(cur.filter("is_current=true").select("user_id"), "user_id", "left_anti") \
        .withColumn("user_sk", F.monotonically_increasing_id()) \
        .withColumn("start_dt", F.current_date().cast("string")) \
        .withColumn("end_dt", F.lit("9999-12-31")) \
        .withColumn("is_current", F.lit(True))

    closed.union(new_ver).write.mode("overwrite").saveAsTable(dim_table)

工程约束:SCD2必须保证同一业务键同一时刻只有一个is_current=true,用唯一约束或断言兜底,否则维度关联会膨胀。

三、增量合并:幂等upsert是管道底线

全量重刷在大表上不可接受。增量管道用MERGE INTO做幂等upsert,重复跑同一批次结果不变。

代码语言:javascript
复制
MERGE INTO dwd_order_detail t
USING (SELECT * FROM stg_order WHERE dt = '${biz_date}') s
ON t.order_id = s.order_id AND t.dt = s.dt
WHEN MATCHED AND t.order_amt <> s.order_amt THEN UPDATE SET
  order_amt = s.order_amt, qty = s.qty
WHEN NOT MATCHED THEN INSERT (order_id,user_sk,product_sk,order_amt,qty,dt)
VALUES (s.order_id,s.user_sk,s.product_sk,s.order_amt,s.qty,s.dt);

工程约束:MERGE的匹配键必须能唯一定位一行;分区字段dt要进匹配条件,避免跨分区误更新。CDC场景下还要处理op类型(I/U/D),删除用软删标记而非物理删,保留回溯能力。

四、数据质量断言:不通过就不发布

质量校验要作为管道的门禁,而不是事后报表。用断言+阈值,失败阻断下游。

代码语言:javascript
复制
# quality.py
from great_expectations.dataset import PandasDataset

def validate(df, biz_date):
    ds = PandasDataset(df)
    ds.expect_column_values_to_not_be_null("order_id")
    ds.expect_column_values_to_be_unique("order_id")
    ds.expect_column_values_to_be_between("order_amt", 0, 1_000_000)
    # 主键唯一率、空值率、金额分布偏离
    res = ds.validate()
    if not res["success"]:
        raise ValueError(f"[{biz_date}] 质量门禁失败: {res['statistics']}")
    return True

专业点:除了单表断言,还要做跨表一致性(订单总额=支付总额-退款)和同比波动监控(当日GMV偏离7日均值超30%告警)。质量规则要版本化,随表结构演进而更新。

五、调度编排:依赖、重试、回填是一体的

代码语言:javascript
复制
# dag_order_dwd.py —— Airflow DAG
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta

default = {"retries": 3, "retry_delay": timedelta(minutes=5),
           "depends_on_past": False, "max_active_runs": 1}

with DAG("order_dwd", start_date=datetime(2024,1,1),
         schedule="0 2 * * *", default_args=default,
         catchup=True) as dag:
    extract = PythonOperator(task_id="ods_extract", python_callable=run_ods,
                             op_kwargs={"ds": "{{ ds }}"})
    merge   = PythonOperator(task_id="dwd_merge", python_callable=run_dwd,
                             op_kwargs={"ds": "{{ ds }}"})
    check   = PythonOperator(task_id="quality_gate", python_callable=run_check,
                             op_kwargs={"ds": "{{ ds }}"})
    extract >> merge >> check

工程约束:catchup=True支持历史回填,但回填必须幂等(靠MERGE和分区覆盖);max_active_runs=1防止同分区并发写冲突;重试要区分可重试错误(网络)和不可重试错误(数据契约失败),后者重试无意义。

六、血缘与治理:能反查才算工程化

每张表的字段级血缘、任务上下游、数据版本要能一键反查。生产上用OpenLineage/Atlas采集,最小可用版本可以在调度回调里落元数据:

代码语言:javascript
复制
def log_lineage(task_id, inputs, outputs, ds):
    db.lineage.insert_one({"task": task_id, "inputs": inputs,
                           "outputs": outputs, "ds": ds,
                           "run_at": datetime.utcnow()})

线上指标异常,第一件事是沿血缘反查上游哪张表、哪个任务、哪批数据变了。没有血缘,排查就是人肉翻SQL。

总结:数据工程的专业度不在SQL写得多花,而在分层隔离变更、SCD2保留历史、MERGE保证幂等、断言守住门禁、调度支撑回填、血缘支撑排查这六件事。能给别人讲清楚"为什么事实表不能冗余维度描述字段""回填为什么必须幂等",你就站稳了数据工程的底线。觉得有用点赞收藏,需要完整工程目录留言。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、分层建模:ODS→DWD→DWS→ADS,别把数仓做成SQL堆
  • 二、SCD2缓慢变化维:维度变更不能覆盖历史
  • 三、增量合并:幂等upsert是管道底线
  • 四、数据质量断言:不通过就不发布
  • 五、调度编排:依赖、重试、回填是一体的
  • 六、血缘与治理:能反查才算工程化
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档