ETL 数据工程

多源数据的 ETL 清洗与汇总 — 给中际旭创数据可视化工程师的实战手册

01 · ETL 是什么?

ETL 是数据处理的核心流程,几乎所有数据项目都离不开它。三个字母分别代表:

📥
Extract
从多个数据源
抽取原始数据
🔧
Transform
清洗、转换、计算
变成可用数据
📤
Load
写入目标数据库
供报表/分析使用
📦 核心概念:ETL = 数据流水线

E(Extract 抽取):从多个数据源读取原始数据 — MySQL 数据库、Excel 文件、MES 接口、传感器日志
T(Transform 转换):清洗过滤、格式统一、字段映射、聚合计算
L(Load 加载):写入目标存储 — 数据仓库、报表数据库、ECharts 可视化用的中间表

原始数据不清洗直接用 = 从垃圾桶里翻东西,根本找不到你要的!

📊 你已经在用 Excel 做 ETL 了!

回忆一下你每次做月度报表的过程:

ETL 步骤你的 Excel 操作
Extract从 3 个 Excel 文件里复制数据
Transform删掉空行、修正格式、VLOOKUP 关联
Load粘贴到最终的汇总表

区别:Excel 手动操作 → ETL 自动化、可重复、处理百万行数据

🏭 制造业 ETL 场景

中际旭创的光模块生产线,数据分散在多个系统中:

数据源数据内容格式
MES 系统OEE、产量、良率SQL Server
设备日志停机记录、报警CSV 文件
质检系统测试参数、不良品Excel
ERP 系统订单、工单信息Oracle API

ETL 把这些数据整合到 一个数据库,才能做出完整的生产看板。

02 · 数据抽取 (Extract)

从各种数据源把数据"搬出来"。制造业常见的数据源和对应的抽取方式:

从 SQL Server 抽取

SQL — 从 MES 数据库抽取 OEE 数据
-- 从 MES 系统的 production_log 表抽取昨天的 OEE 数据
SELECT
    machine_id,
    shift_date,
    availability_rate   -- 可用率
    performance_rate    -- 性能率
    quality_rate        -- 良品率
    FROM mes_database.dbo.production_log
WHERE shift_date = CAST(GETDATE() - 1 AS DATE);

从 CSV 文件抽取

Python — 读取设备日志 CSV
# 读取设备停机日志 CSV 文件
import pandas as pd

df_downtime = pd.read_csv(
    'equipment_downtime_2024.csv',
    encoding='utf-8',          # 中文文件注意编码
    parse_dates=['downtime_start', 'downtime_end']
)
print(df_downtime.head())

从 Excel 文件抽取

Python — 读取质检 Excel 报表
# 读取质检系统导出的 Excel,指定 sheet
df_qc = pd.read_excel(
    'quality_report_jan.xlsx',
    sheet_name='测试数据',
    header=1                  # 第2行是表头
)

从 API 抽取

Python — 调用 ERP 系统 API 获取工单
import requests

response = requests.get(
    'http://erp.company.com/api/workorders',
    params={'date': '2024-01-15'},
    headers={'Authorization': 'Bearer xxx'}
)
df_orders = pd.DataFrame(response.json()['data'])
🏭 制造业场景:多源抽取汇总

假设你每天要从 3 个地方抽数据做生产日报:

步骤数据源方法数据量
1. 抽取 OEEMES (SQL Server)SQL 查询~2000 行/天
2. 抽取停机设备日志 (CSV)pandas read_csv~500 行/天
3. 抽取工单ERP (API)requests + JSON~100 行/天

03 · 数据清洗 (Transform) — ETL 最重要的一步

抽取出来的数据通常是"脏"的。数据清洗占据了 ETL 70% 的工作量。

3.1 处理缺失值 (NULL / 空值)

BEFORE — 原始数据(有空值)
machine_idoeetemp
M-001NULL42.1
M-00288.5NULL
M-00391.240.3
M-004NULLNULL
AFTER — 填充后
machine_idoeetemp
M-00189.9 ← 平均值42.1
M-00288.541.2 ← 前值填充
M-00391.240.3
M-00489.9 ← 平均值41.2 ← 前值填充
SQL — 处理缺失值
-- 方法1:用全局平均值填充 NULL
SELECT
    machine_id,
    COALESCE(oee, (SELECT AVG(oee) FROM production_log WHERE oee IS NOT NULL)) AS oee
FROM production_log;

-- 方法2:用前一行填充 (SQL Server)
SELECT
    machine_id,
    LAST_VALUE(temp IGNORE NULLS)
        OVER (ORDER BY machine_id
              ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS temp
FROM production_log;
Python (pandas) — 处理缺失值
# 查看缺失情况
print(df.isnull().sum())

# 方法1:用平均值填充
df['oee'].fillna(df['oee'].mean(), inplace=True)

# 方法2:用前一个有效值填充(适合时序传感器数据)
df['temp'].fillna(method='ffill', inplace=True)

# 方法3:直接删掉整行(缺失太多时)
df.dropna(subset=['machine_id'], inplace=True)
💡 怎么选填充策略?

传感器温度 → 前值填充(温度变化平缓)
OEE 指标 → 平均值填充(统计有意义)
设备ID 缺失 → 直接删除(无法补全)
质检结果 → 标记为"未检测"(不能伪造数据)

3.2 处理异常值

BEFORE — 传感器异常值
machine_idtemp
M-00142.1
M-002999.0
M-003-1.0
M-00439.8
AFTER — 过滤后
machine_idtemp
M-00142.1
M-00241.0 ← 平均值替换
M-00341.0 ← 平均值替换
M-00439.8
Python — 异常值检测与处理
# 温度传感器:正常范围 20~80°C
valid_mask = (df['temp'] >= 20) & (df['temp'] <= 80)
mean_temp = df.loc[valid_mask, 'temp'].mean()

# 将异常值替换为正常数据的平均值
df.loc[~valid_mask, 'temp'] = mean_temp

# 或者直接过滤掉异常行
df = df[valid_mask].copy()

3.3 数据类型转换

BEFORE — 混合类型
machine_idoee_str
M-001"85.6%"
M-002"92.1%"
M-003"78%"
AFTER — 统一数值类型
machine_idoee (float)
M-00185.6
M-00292.1
M-00378.0
Python — 字符串转数值
# "85.6%" → 85.6
df['oee'] = df['oee_str'].str.replace('%', '').astype(float)

# "1,234" → 1234
df['quantity'] = df['quantity'].str.replace(',', '').astype(int)

3.4 时间格式统一

BEFORE — 各种日期格式
来源date 值
MES"2024-01-15"
Excel"01/15/2024"
设备日志"20240115"
API"Jan 15, 2024"
AFTER — 统一为 DATE
来源date (统一)
MES2024-01-15
Excel2024-01-15
设备日志2024-01-15
API2024-01-15
Python — 统一日期格式
# pandas 自动解析多种日期格式
df['date'] = pd.to_datetime(df['date'], errors='coerce')

# 统一输出为 "2024-01-15" 格式
df['date'] = df['date'].dt.strftime('%Y-%m-%d')

# errors='coerce':解析失败的变为 NaT(而不是报错)
SQL — 日期格式转换
-- SQL Server: 各种格式统一转 DATE
SELECT
    CAST(CASE
        WHEN ISDATE(raw_date) = 1 THEN raw_date
        ELSE NULL
    END AS DATE) AS clean_date
FROM staging_table;

-- 格式化输出
SELECT FORMAT(clean_date, 'yyyy-MM-dd') AS date_str;

3.5 去重

BEFORE — 重复记录
machine_iddateoee
M-0012024-01-1588.5
M-0012024-01-1588.5
M-0022024-01-1591.2
M-0022024-01-1591.0
AFTER — 去重后
machine_iddateoee
M-0012024-01-1588.5
M-0022024-01-1591.2 ← 保留最新
SQL — 去重
-- 使用 ROW_NUMBER() 保留最新的一条
WITH ranked AS (
    SELECT *,
        ROW_NUMBER() OVER (
            PARTITION BY machine_id, shift_date
            ORDER BY update_time DESC
        ) AS rn
    FROM production_log
)
SELECT * FROM ranked WHERE rn = 1;
Python — 去重
# 按 machine_id + date 去重,保留最后一条(最新的)
df = df.drop_duplicates(
    subset=['machine_id', 'date'],
    keep='last'
)

3.6 字段映射

BEFORE — 两个系统字段名不同
系统 A (MES)
设备号班次日期
系统 B (ERP)
machine_idshift_date
AFTER — 统一字段名
machine_idshift_date
M-0012024-01-15
M-0022024-01-15
Python — 字段映射(重命名列)
# 系统A 的中文列名 → 统一英文列名
df_mes = df_mes.rename(columns={
    '设备号': 'machine_id',
    '班次日期': 'shift_date',
    '可用率': 'availability_rate',
})

# 现在两个 DataFrame 列名一致,可以 merge 了
df_merged = pd.merge(df_mes, df_erp, on=['machine_id', 'shift_date'])
⚠️ 制造业数据清洗的常见坑

1. 编码问题:CSV 中文乱码 → 用 encoding='gbk'utf-8-sig
2. 时区不一致:MES 用北京时间,设备日志用 UTC → 统一转一个时区
3. 精度丢失float 存良率 0.856 → 显示 85.6%,计算时注意精度
4. 隐式空值:不是 NULL 而是 "N/A"、"-"、空字符串 → 都要识别为空值

04 · 数据加载 (Load)

清洗好的数据写入目标数据库,供报表和看板使用。

全量加载 vs 增量加载

方式做法适用场景风险
全量加载先 DELETE 再 INSERT 全部数据数据量小 (<1万行)
维度表(设备清单等)
数据量大时很慢
增量加载
推荐
只插入新增/修改的行每日新增数据
事实表(产量日志等)
需要记录"上次加载到哪"

INSERT — 简单插入

SQL — INSERT INTO
-- 全量加载:先清空,再插入
TRUNCATE TABLE oee_summary;
INSERT INTO oee_summary (machine_id, shift_date, oee)
SELECT machine_id, shift_date, oee
FROM stg_production_clean;

-- 增量加载:只插入不存在的新数据
INSERT INTO oee_summary (machine_id, shift_date, oee)
SELECT s.machine_id, s.shift_date, s.oee
FROM stg_production_clean s
LEFT JOIN oee_summary t
    ON s.machine_id = t.machine_id AND s.shift_date = t.shift_date
WHERE t.machine_id IS NULL;  -- 目标表里没有的

MERGE / UPSERT — 存在则更新,不存在则插入

SQL Server — MERGE 语句(核心)
-- 增量更新:有的更新,没有的新增
MERGE INTO oee_summary AS target
USING stg_production_clean AS source
    ON target.machine_id = source.machine_id
    AND target.shift_date = source.shift_date

-- 匹配到了 → 更新
WHEN MATCHED THEN
    UPDATE SET
        target.oee = source.oee,
        target.update_time = GETDATE()

-- 没匹配到 → 插入
WHEN NOT MATCHED THEN
    INSERT (machine_id, shift_date, oee, update_time)
    VALUES (source.machine_id, source.shift_date, source.oee, GETDATE());
💡 MERGE 是面试高频考点

几乎每个 ETL 岗位都会问 MERGE / UPSERT。
记住口诀:有则更新,无则插入。相当于 INSERT + UPDATE 的合体。

05 · SQL Server ETL 实战

中际旭创岗位明确提到 SQL Server,这些是必须掌握的技能。

5.1 窗口函数 — 聚合计算利器

SQL — 窗口函数计算各设备排名和移动平均
-- 每台设备的 OEE 在所有设备中的排名
SELECT
    machine_id,
    shift_date,
    oee,
    RANK() OVER (ORDER BY oee DESC) AS oee_rank,
    -- 7天移动平均 OEE(看趋势)
    AVG(oee) OVER (
        PARTITION BY machine_id
        ORDER BY shift_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS oee_7day_avg,
    -- 当天在同设备中的 OEE 占比
    oee * 1.0 / SUM(oee) OVER (PARTITION BY shift_date) AS oee_ratio
FROM production_log
WHERE shift_date >= '2024-01-01';
输出结果
machine_idshift_dateoeeoee_rankoee_7day_avg
M-0032024-01-1595.2193.8
M-0012024-01-1588.5387.1
M-0022024-01-1591.2290.5

5.2 MERGE 语句 — 增量更新核心

已在第 04 节详细介绍。核心模式:

SQL — MERGE 模板
MERGE INTO <目标表> AS T
USING <源表/子查询> AS S
    ON T.主键 = S.主键
WHEN MATCHED THEN UPDATE SET ...
WHEN NOT MATCHED THEN INSERT ...
WHEN NOT MATCHED BY SOURCE THEN DELETE;  -- 可选:删掉源里没有的

5.3 存储过程 — 封装 ETL 逻辑

SQL — 存储过程:每日 ETL 作业
CREATE PROCEDURE sp_daily_etl
    @target_date DATE = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- 默认处理昨天
    IF @target_date IS NULL
        SET @target_date = CAST(GETDATE() - 1 AS DATE);

    -- Step 1: 抽取到临时表
    SELECT machine_id, shift_date, oee, temp
    INTO #staging
    FROM mes_database.dbo.production_log
    WHERE shift_date = @target_date;

    -- Step 2: 清洗(去NULL、去异常)
    DELETE FROM #staging WHERE oee IS NULL OR oee > 100;

    -- Step 3: 增量写入目标表
    MERGE INTO oee_summary AS T
    USING #staging AS S
        ON T.machine_id = S.machine_id AND T.shift_date = S.shift_date
    WHEN MATCHED THEN UPDATE SET
        T.oee = S.oee, T.temp = S.temp, T.update_time = GETDATE()
    WHEN NOT MATCHED THEN INSERT
        (machine_id, shift_date, oee, temp, update_time)
        VALUES (S.machine_id, S.shift_date, S.oee, S.temp, GETDATE());

    -- 记录执行日志
    INSERT INTO etl_log (job_name, run_date, rows_processed, status)
    VALUES ('daily_oee_etl', @target_date, @@ROWCOUNT, 'SUCCESS');

    DROP TABLE #staging;
END
执行存储过程
-- 处理昨天的数据
EXEC sp_daily_etl;

-- 处理指定日期
EXEC sp_daily_etl @target_date = '2024-01-15';

5.4 SQL Server Agent — 定时调度

⏰ 用 SQL Server Agent 设置定时任务

步骤:

  1. 打开 SSMS → SQL Server Agent → Jobs → 右键 "New Job"
  2. General:Job 名称 = Daily_OEE_ETL
  3. Steps:新建步骤 → 输入 EXEC sp_daily_etl;
  4. Schedules:设置每天早上 6:00 执行
  5. Alerts:失败时发邮件通知
SQL — 用 T-SQL 创建 Agent Job
-- 创建作业
EXEC msdb.dbo.sp_add_job
    @job_name = N'Daily_OEE_ETL';

-- 添加步骤
EXEC msdb.dbo.sp_add_jobstep
    @job_name = N'Daily_OEE_ETL',
    @step_name = N'Run ETL',
    @subsystem = N'TSQL',
    @command = N'EXEC sp_daily_etl;';

-- 设置调度:每天早上6:00
EXEC msdb.dbo.sp_add_jobschedule
    @job_name = N'Daily_OEE_ETL',
    @name = N'Daily 6AM',
    @freq_type = 4,          -- 每天
    @active_start_time = 60000; -- 06:00:00

06 · Python ETL 实战 (pandas)

完整示例:读取 3 个 Excel 文件 → 清洗 → 合并 → 写入数据库。

读取 3 个 Excel
清洗数据
合并关联
写入 SQL Server
Python — 完整 ETL 脚本
import pandas as pd
from sqlalchemy import create_engine

# =============================================
# EXTRACT — 从 3 个数据源抽取
# =============================================

# 1. MES 系统导出的 OEE Excel
df_oee = pd.read_excel('mes_oee_jan.xlsx')

# 2. 设备停机日志 CSV
df_downtime = pd.read_csv(
    'downtime_log_jan.csv',
    encoding='gbk',
    parse_dates=['start_time', 'end_time']
)

# 3. 质检报表 Excel(指定 sheet)
df_qc = pd.read_excel('quality_jan.xlsx', sheet_name='成品测试')

# =============================================
# TRANSFORM — 清洗 + 转换
# =============================================

# --- 清洗 OEE 数据 ---
# 列名映射:中文 → 英文
df_oee = df_oee.rename(columns={
    '设备编号': 'machine_id',
    '日期': 'shift_date',
    'OEE': 'oee'
})

# 去掉 OEE 中的 "%",转 float
df_oee['oee'] = df_oee['oee'].astype(str).str.replace('%', '')
df_oee['oee'] = pd.to_numeric(df_oee['oee'], errors='coerce')

# 过滤异常值(OEE > 100 或 < 0 的不正常)
df_oee = df_oee[(df_oee['oee'] >= 0) & (df_oee['oee'] <= 100)]

# 填充空值
df_oee['oee'].fillna(df_oee['oee'].mean(), inplace=True)

# 统一日期格式
df_oee['shift_date'] = pd.to_datetime(df_oee['shift_date']).dt.date

# --- 清洗停机数据 ---
df_downtime = df_downtime.rename(columns={
    'machine_id': 'machine_id',
    'down_minutes': 'downtime_min'
})
# 按设备+日期汇总停机时长
df_downtime['shift_date'] = df_downtime['start_time'].dt.date
df_down_sum = df_downtime.groupby(['machine_id', 'shift_date'])
    ['downtime_min'].sum().reset_index()

# --- 清洗质检数据 ---
df_qc = df_qc.rename(columns={
    '设备': 'machine_id',
     '检测日期': 'shift_date',
    '不良率': 'defect_rate'
})
df_qc['shift_date'] = pd.to_datetime(df_qc['shift_date']).dt.date

# =============================================
# MERGE — 关联合并
# =============================================

# 三张表按 machine_id + shift_date 关联
df_merged = df_oee.merge(df_down_sum, on=['machine_id', 'shift_date'], how='left')
df_merged = df_merged.merge(df_qc[['machine_id', 'shift_date', 'defect_rate']],
                      on=['machine_id', 'shift_date'], how='left')

# 去重
df_merged = df_merged.drop_duplicates(subset=['machine_id', 'shift_date'])

# =============================================
# LOAD — 写入 SQL Server
# =============================================

engine = create_engine(
    'mssql+pyodbc://user:pass@server/db?driver=ODBC+Driver+17+for+SQL+Server'
)

# 写入目标表(有则替换)
df_merged.to_sql(
    'production_daily',
    engine,
    if_exists='append',    # 追加模式
    index=False
)

print(f'ETL 完成!共写入 {len(df_merged)} 行')

07 · ETL 调度 — 让数据自动流动

ETL 脚本写好后,不能每天手动执行。需要设置定时调度

调度工具适合场景难度特点
SQL Server Agent纯 SQL Server 环境SSMS 自带,图形化配置
Windows 任务计划运行 Python 脚本系统自带,简单粗暴
cron (Linux)Linux 服务器一行命令搞定
Airflow复杂多任务 DAG任务依赖、重试、监控

cron 表达式

bash — crontab 定时任务
# 编辑定时任务
# crontab -e

# 格式: 分 时 日 月 星期  命令
# ┌─────── 分钟 (0-59)
# │ ┌───── 小时 (0-23)
# │ │ ┌─── 日 (1-31)
# │ │ │ ┌─ 月 (1-12)
# │ │ │ │ ┌ 星期 (0-6)

# 每天早上 6:00 执行 ETL
0 6 * * * /usr/bin/python3 /opt/etl/daily_etl.py >> /var/log/etl.log 2>&1

# 每30分钟执行一次
*/30 * * * * /usr/bin/python3 /opt/etl/realtime_sync.py

# 工作日早上 7:00
0 7 * * 1-5 /usr/bin/python3 /opt/etl/weekday_report.py

Airflow DAG — 复杂任务编排

Python — Airflow DAG 定义
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime

with DAG(
    dag_id='manufacturing_daily_etl',
    schedule='0 6 * * *',       # 每天 6:00
    start_date=datetime(2024, 1, 1),
    catchup=False
) as dag:

    # 定义 3 个任务,按顺序执行
    extract = PythonOperator(task_id='extract', python_callable=run_extract)
    transform = PythonOperator(task_id='transform', python_callable=run_transform)
    load = PythonOperator(task_id='load', python_callable=run_load)

    # 任务依赖:extract → transform → load
    extract >> transform >> load
🏭 制造业调度场景

每天 06:00 → 执行每日 ETL,汇总前一天生产数据
每天 06:30 → 数据就绪,管理层打开看板查看
每小时 → 同步设备实时状态到报表库
每周一 08:00 → 生成周报数据

关键原则:ETL 必须在看板使用前完成!

08 · 数据质量 — ETL 有没有出错?

ETL 执行完了不等于对了。必须有数据质量检查

✅ 数据质量检查清单
检查类型检查什么SQL / Python 方法
行数校验源表行数 ≈ 目标表行数COUNT(*) 对比
空值检查关键字段不能为 NULLWHERE key_col IS NULL
范围检查OEE 在 0~100,温度在 20~80WHERE oee > 100 OR temp < 0
唯一性主键没有重复GROUP BY HAVING COUNT(*) > 1
一致性两个表的关联字段能匹配上LEFT JOIN ... WHERE IS NULL
时效性数据是最新的(不超过 24 小时)WHERE update_time < GETDATE()-1
SQL — 数据质量检查存储过程
CREATE PROCEDURE sp_data_quality_check
AS
BEGIN
    -- 1. 行数对比
    DECLARE @src_count INT, @tgt_count INT;
    SELECT @src_count = COUNT(*) FROM production_log WHERE shift_date = CAST(GETDATE()-1 AS DATE);
    SELECT @tgt_count = COUNT(*) FROM oee_summary WHERE shift_date = CAST(GETDATE()-1 AS DATE);

    IF ABS(@src_count - @tgt_count) > @src_count * 0.05  -- 差异超过5%
        INSERT INTO dq_alerts VALUES ('ROW_COUNT_MISMATCH', GETDATE());

    -- 2. 范围检查
    IF EXISTS (SELECT 1 FROM oee_summary WHERE oee > 100 OR oee < 0)
        INSERT INTO dq_alerts VALUES ('OEE_OUT_OF_RANGE', GETDATE());

    -- 3. 空值检查
    IF EXISTS (SELECT 1 FROM oee_summary WHERE machine_id IS NULL)
        INSERT INTO dq_alerts VALUES ('NULL_MACHINE_ID', GETDATE());

    -- 4. 唯一性检查
    IF EXISTS (
        SELECT machine_id, shift_date FROM oee_summary
        GROUP BY machine_id, shift_date HAVING COUNT(*) > 1
    )
        INSERT INTO dq_alerts VALUES ('DUPLICATE_KEY', GETDATE());
END
Python — 数据质量检查
def quality_check(df, table_name):
    issues = []

    # 空值检查
    null_counts = df.isnull().sum()
    for col, cnt in null_counts.items():
        if cnt > 0:
            issues.append(f"[{table_name}] {col} 有 {cnt} 个空值")

    # 范围检查
    if 'oee' in df.columns:
        bad_oee = df[(df['oee'] < 0) | (df['oee'] > 100)]
        if len(bad_oee) > 0:
            issues.append(f"[{table_name}] {len(bad_oee)} 条 OEE 超出范围")

    # 唯一性检查
    if 'machine_id' in df.columns:
        dupes = df.duplicated(subset=['machine_id', 'shift_date']).sum()
        if dupes > 0:
            issues.append(f"[{table_name}] {dupes} 条重复记录")

    return issues

# 使用
problems = quality_check(df_merged, 'production_daily')
if problems:
    for p in problems:
        print(f"⚠️ {p}")
else:
    print("✅ 数据质量检查通过")

09 · 面试考点 — ETL Top 8

Q1
ETL 和 ELT 有什么区别?
ETLELT
顺序Extract → Transform → LoadExtract → Load → Transform
转换位置在加载转换(中间层)先加载到目标库,在库里转换
适合场景传统数据仓库、数据量中等大数据平台(Snowflake、BigQuery)
工具SSIS、Informatica、Kettledbt、SQL 直接在数仓里做

面试回答要点:ETL 是先洗后存,适合 SQL Server 这类传统环境;ELT 是先存后洗,利用数仓本身的算力。现在趋势是 ELT。

Q2
增量加载 vs 全量加载?什么时候用哪个?
全量加载增量加载
做法每次删掉目标表,重新插入全部只插入新增/修改的行
速度慢(数据量大时)
复杂度简单需要追踪变更(时间戳、CDC)
适用维度表(设备列表、工艺代码)事实表(每日产量、传感器日志)

增量加载实现方式:① 时间戳字段(WHERE create_time > @last_run)② CDC(Change Data Capture)③ MERGE / UPSERT

Q3
什么是缓慢变化维 (SCD)?怎么处理?

场景:设备 M-001 原来在 A 车间,后来搬到了 B 车间。历史报表需要正确显示"当时在哪个车间"。

类型做法优缺点
SCD Type 1直接覆盖旧值简单,但丢失历史
SCD Type 2
最常用
新增一行,加有效期保留完整历史,但行数增加
SCD Type 3加一列存旧值只保留一次历史
SQL — SCD Type 2 实现(MERGE)
-- 新记录:INSERT,有效期从今天开始
-- 旧记录:UPDATE 设置 is_current = 0, end_date = 今天
MERGE INTO dim_machine AS T
USING stg_machine AS S
    ON T.machine_id = S.machine_id AND T.is_current = 1
WHEN MATCHED AND T.workshop <> S.workshop THEN
    UPDATE SET is_current = 0, end_date = GETDATE()
WHEN NOT MATCHED THEN
    INSERT (machine_id, workshop, is_current, start_date)
    VALUES (S.machine_id, S.workshop, 1, GETDATE());
Q4
数据质量怎么保证?

三层防护:

  1. 入口检查:抽取时验证源数据格式、行数
  2. 清洗规则:NULL 填充、异常值过滤、类型转换
  3. 出口校验:加载后做行数对比、范围检查、唯一性检查(见第 08 节)

关键指标:完整性(行数一致)、准确性(范围合理)、一致性(无冲突)、时效性(数据新鲜度)。

Q5
ETL 性能怎么优化?
优化方向具体方法
减少数据量增量加载、分区表、WHERE 过滤尽早下推
并行处理多线程抽取、SQL 并行查询、分区切换
索引优化临时表不加索引、目标表 LOAD 时禁用索引
批量操作用 BULK INSERT 代替逐行 INSERT
缓存中间结果用临时表存中间数据,避免重复计算
Q6
什么时候用 SQL 做 ETL,什么时候用 Python?
场景推荐原因
表到表的清洗转换SQL数据库内操作,不搬数据,最快
多表 JOIN + 聚合SQLSQL 天生擅长
读 Excel/CSV 文件PythonSQL 做不到
复杂清洗逻辑Pythonpandas 灵活强大
调用外部 APIPython需要 requests 库
定时存储过程SQL + Agent纯数据库内搞定

实际项目:通常是混合使用 — Python 负责抽取和复杂清洗,SQL 负责库内转换和加载。

Q7
时序数据库在 ETL 中的角色?

制造业的传感器数据(温度、压力、振动)是时序数据,特点:

  • 数据量极大:每秒数千个采样点
  • 写入频繁、读取按时间范围
  • 传统关系型数据库扛不住
数据库类型适合场景
SQL Server关系型OEE、产量、质检等业务数据
InfluxDB时序型传感器高频采样数据
TimescaleDB时序型(PostgreSQL扩展)既有时序能力又能用 SQL

ETL 架构:传感器 → 时序数据库 → 定时聚合 → SQL Server → 可视化看板

Q8
常见 ETL 工具对比?
工具类型优点缺点适合
SSIS微软 ETL 工具SQL Server 深度集成
拖拽式开发
只能 Windows
版本管理困难
纯 SQL Server 环境
Airflow任务调度平台Python 编写 DAG
强大的依赖管理
学习曲线陡
需要搭建环境
多任务复杂编排
自研 Python自己写脚本最灵活
无额外依赖
缺少监控 UI
需自己处理重试
简单场景
快速起步
Kettle开源 ETL可视化拖拽
免费
界面老旧
性能一般
传统数据仓库
dbtELT 工具SQL-only
版本控制友好
不做 Extract
需要数仓支持
现代数仓项目

面试建议:说清楚你用过哪个、解决了什么问题,比堆工具名更有说服力。

📌 总结:ETL 核心要点速记
Extract SQL Server / CSV / Excel / API,用 SQL 或 pandas
Transform NULL处理 → 异常值 → 类型转换 → 日期统一 → 去重 → 字段映射
Load 全量(TRUNCATE+INSERT) vs 增量(MERGE/UPSERT)
调度 SQL Server Agent / cron / Airflow
质量 行数、空值、范围、唯一性、一致性、时效性