ETL 数据工程
多源数据的 ETL 清洗与汇总 — 给中际旭创数据可视化工程师的实战手册
01 · ETL 是什么?
ETL 是数据处理的核心流程,几乎所有数据项目都离不开它。三个字母分别代表:
抽取原始数据
变成可用数据
供报表/分析使用
E(Extract 抽取):从多个数据源读取原始数据 — MySQL 数据库、Excel 文件、MES 接口、传感器日志
T(Transform 转换):清洗过滤、格式统一、字段映射、聚合计算
L(Load 加载):写入目标存储 — 数据仓库、报表数据库、ECharts 可视化用的中间表
原始数据不清洗直接用 = 从垃圾桶里翻东西,根本找不到你要的!
回忆一下你每次做月度报表的过程:
| ETL 步骤 | 你的 Excel 操作 |
|---|---|
| Extract | 从 3 个 Excel 文件里复制数据 |
| Transform | 删掉空行、修正格式、VLOOKUP 关联 |
| Load | 粘贴到最终的汇总表 |
区别:Excel 手动操作 → ETL 自动化、可重复、处理百万行数据
中际旭创的光模块生产线,数据分散在多个系统中:
| 数据源 | 数据内容 | 格式 |
|---|---|---|
| MES 系统 | OEE、产量、良率 | SQL Server |
| 设备日志 | 停机记录、报警 | CSV 文件 |
| 质检系统 | 测试参数、不良品 | Excel |
| ERP 系统 | 订单、工单信息 | Oracle API |
ETL 把这些数据整合到 一个数据库,才能做出完整的生产看板。
02 · 数据抽取 (Extract)
从各种数据源把数据"搬出来"。制造业常见的数据源和对应的抽取方式:
从 SQL Server 抽取
-- 从 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 文件抽取
# 读取设备停机日志 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 文件抽取
# 读取质检系统导出的 Excel,指定 sheet
df_qc = pd.read_excel(
'quality_report_jan.xlsx',
sheet_name='测试数据',
header=1 # 第2行是表头
)
从 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. 抽取 OEE | MES (SQL Server) | SQL 查询 | ~2000 行/天 |
| 2. 抽取停机 | 设备日志 (CSV) | pandas read_csv | ~500 行/天 |
| 3. 抽取工单 | ERP (API) | requests + JSON | ~100 行/天 |
03 · 数据清洗 (Transform) — ETL 最重要的一步
抽取出来的数据通常是"脏"的。数据清洗占据了 ETL 70% 的工作量。
3.1 处理缺失值 (NULL / 空值)
| machine_id | oee | temp |
|---|---|---|
| M-001 | NULL | 42.1 |
| M-002 | 88.5 | NULL |
| M-003 | 91.2 | 40.3 |
| M-004 | NULL | NULL |
| machine_id | oee | temp |
|---|---|---|
| M-001 | 89.9 ← 平均值 | 42.1 |
| M-002 | 88.5 | 41.2 ← 前值填充 |
| M-003 | 91.2 | 40.3 |
| M-004 | 89.9 ← 平均值 | 41.2 ← 前值填充 |
-- 方法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;
# 查看缺失情况
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 处理异常值
| machine_id | temp |
|---|---|
| M-001 | 42.1 |
| M-002 | 999.0 |
| M-003 | -1.0 |
| M-004 | 39.8 |
| machine_id | temp |
|---|---|
| M-001 | 42.1 |
| M-002 | 41.0 ← 平均值替换 |
| M-003 | 41.0 ← 平均值替换 |
| M-004 | 39.8 |
# 温度传感器:正常范围 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 数据类型转换
| machine_id | oee_str |
|---|---|
| M-001 | "85.6%" |
| M-002 | "92.1%" |
| M-003 | "78%" |
| machine_id | oee (float) |
|---|---|
| M-001 | 85.6 |
| M-002 | 92.1 |
| M-003 | 78.0 |
# "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 时间格式统一
| 来源 | date 值 |
|---|---|
| MES | "2024-01-15" |
| Excel | "01/15/2024" |
| 设备日志 | "20240115" |
| API | "Jan 15, 2024" |
| 来源 | date (统一) |
|---|---|
| MES | 2024-01-15 |
| Excel | 2024-01-15 |
| 设备日志 | 2024-01-15 |
| API | 2024-01-15 |
# 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 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 去重
| machine_id | date | oee |
|---|---|---|
| M-001 | 2024-01-15 | 88.5 |
| M-001 | 2024-01-15 | 88.5 |
| M-002 | 2024-01-15 | 91.2 |
| M-002 | 2024-01-15 | 91.0 |
| machine_id | date | oee |
|---|---|---|
| M-001 | 2024-01-15 | 88.5 |
| M-002 | 2024-01-15 | 91.2 ← 保留最新 |
-- 使用 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;
# 按 machine_id + date 去重,保留最后一条(最新的)
df = df.drop_duplicates(
subset=['machine_id', 'date'],
keep='last'
)
3.6 字段映射
| 系统 A (MES) | |
|---|---|
| 设备号 | 班次日期 |
| 系统 B (ERP) | |
| machine_id | shift_date |
| machine_id | shift_date |
|---|---|
| M-001 | 2024-01-15 |
| M-002 | 2024-01-15 |
# 系统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 — 简单插入
-- 全量加载:先清空,再插入
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 — 存在则更新,不存在则插入
-- 增量更新:有的更新,没有的新增
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());
几乎每个 ETL 岗位都会问 MERGE / UPSERT。
记住口诀:有则更新,无则插入。相当于 INSERT + UPDATE 的合体。
05 · SQL Server ETL 实战
中际旭创岗位明确提到 SQL Server,这些是必须掌握的技能。
5.1 窗口函数 — 聚合计算利器
-- 每台设备的 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_id | shift_date | oee | oee_rank | oee_7day_avg |
|---|---|---|---|---|
| M-003 | 2024-01-15 | 95.2 | 1 | 93.8 |
| M-001 | 2024-01-15 | 88.5 | 3 | 87.1 |
| M-002 | 2024-01-15 | 91.2 | 2 | 90.5 |
5.2 MERGE 语句 — 增量更新核心
已在第 04 节详细介绍。核心模式:
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 逻辑
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 — 定时调度
步骤:
- 打开 SSMS → SQL Server Agent → Jobs → 右键 "New Job"
- General:Job 名称 =
Daily_OEE_ETL - Steps:新建步骤 → 输入
EXEC sp_daily_etl; - Schedules:设置每天早上 6:00 执行
- Alerts:失败时发邮件通知
-- 创建作业
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 文件 → 清洗 → 合并 → 写入数据库。
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 表达式
# 编辑定时任务
# 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 — 复杂任务编排
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(*) 对比 |
| 空值检查 | 关键字段不能为 NULL | WHERE key_col IS NULL |
| 范围检查 | OEE 在 0~100,温度在 20~80 | WHERE oee > 100 OR temp < 0 |
| 唯一性 | 主键没有重复 | GROUP BY HAVING COUNT(*) > 1 |
| 一致性 | 两个表的关联字段能匹配上 | LEFT JOIN ... WHERE IS NULL |
| 时效性 | 数据是最新的(不超过 24 小时) | WHERE update_time < GETDATE()-1 |
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
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
| ETL | ELT | |
|---|---|---|
| 顺序 | Extract → Transform → Load | Extract → Load → Transform |
| 转换位置 | 在加载前转换(中间层) | 先加载到目标库,在库里转换 |
| 适合场景 | 传统数据仓库、数据量中等 | 大数据平台(Snowflake、BigQuery) |
| 工具 | SSIS、Informatica、Kettle | dbt、SQL 直接在数仓里做 |
面试回答要点:ETL 是先洗后存,适合 SQL Server 这类传统环境;ELT 是先存后洗,利用数仓本身的算力。现在趋势是 ELT。
| 全量加载 | 增量加载 | |
|---|---|---|
| 做法 | 每次删掉目标表,重新插入全部 | 只插入新增/修改的行 |
| 速度 | 慢(数据量大时) | 快 |
| 复杂度 | 简单 | 需要追踪变更(时间戳、CDC) |
| 适用 | 维度表(设备列表、工艺代码) | 事实表(每日产量、传感器日志) |
增量加载实现方式:① 时间戳字段(WHERE create_time > @last_run)② CDC(Change Data Capture)③ MERGE / UPSERT
场景:设备 M-001 原来在 A 车间,后来搬到了 B 车间。历史报表需要正确显示"当时在哪个车间"。
| 类型 | 做法 | 优缺点 |
|---|---|---|
| SCD Type 1 | 直接覆盖旧值 | 简单,但丢失历史 |
| SCD Type 2 最常用 | 新增一行,加有效期 | 保留完整历史,但行数增加 |
| SCD Type 3 | 加一列存旧值 | 只保留一次历史 |
-- 新记录: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());
三层防护:
- 入口检查:抽取时验证源数据格式、行数
- 清洗规则:NULL 填充、异常值过滤、类型转换
- 出口校验:加载后做行数对比、范围检查、唯一性检查(见第 08 节)
关键指标:完整性(行数一致)、准确性(范围合理)、一致性(无冲突)、时效性(数据新鲜度)。
| 优化方向 | 具体方法 |
|---|---|
| 减少数据量 | 增量加载、分区表、WHERE 过滤尽早下推 |
| 并行处理 | 多线程抽取、SQL 并行查询、分区切换 |
| 索引优化 | 临时表不加索引、目标表 LOAD 时禁用索引 |
| 批量操作 | 用 BULK INSERT 代替逐行 INSERT |
| 缓存中间结果 | 用临时表存中间数据,避免重复计算 |
| 场景 | 推荐 | 原因 |
|---|---|---|
| 表到表的清洗转换 | SQL | 数据库内操作,不搬数据,最快 |
| 多表 JOIN + 聚合 | SQL | SQL 天生擅长 |
| 读 Excel/CSV 文件 | Python | SQL 做不到 |
| 复杂清洗逻辑 | Python | pandas 灵活强大 |
| 调用外部 API | Python | 需要 requests 库 |
| 定时存储过程 | SQL + Agent | 纯数据库内搞定 |
实际项目:通常是混合使用 — Python 负责抽取和复杂清洗,SQL 负责库内转换和加载。
制造业的传感器数据(温度、压力、振动)是时序数据,特点:
- 数据量极大:每秒数千个采样点
- 写入频繁、读取按时间范围
- 传统关系型数据库扛不住
| 数据库 | 类型 | 适合场景 |
|---|---|---|
| SQL Server | 关系型 | OEE、产量、质检等业务数据 |
| InfluxDB | 时序型 | 传感器高频采样数据 |
| TimescaleDB | 时序型(PostgreSQL扩展) | 既有时序能力又能用 SQL |
ETL 架构:传感器 → 时序数据库 → 定时聚合 → SQL Server → 可视化看板
| 工具 | 类型 | 优点 | 缺点 | 适合 |
|---|---|---|---|---|
| SSIS | 微软 ETL 工具 | SQL Server 深度集成 拖拽式开发 | 只能 Windows 版本管理困难 | 纯 SQL Server 环境 |
| Airflow | 任务调度平台 | Python 编写 DAG 强大的依赖管理 | 学习曲线陡 需要搭建环境 | 多任务复杂编排 |
| 自研 Python | 自己写脚本 | 最灵活 无额外依赖 | 缺少监控 UI 需自己处理重试 | 简单场景 快速起步 |
| Kettle | 开源 ETL | 可视化拖拽 免费 | 界面老旧 性能一般 | 传统数据仓库 |
| dbt | ELT 工具 | SQL-only 版本控制友好 | 不做 Extract 需要数仓支持 | 现代数仓项目 |
面试建议:说清楚你用过哪个、解决了什么问题,比堆工具名更有说服力。
| Extract | SQL Server / CSV / Excel / API,用 SQL 或 pandas |
| Transform | NULL处理 → 异常值 → 类型转换 → 日期统一 → 去重 → 字段映射 |
| Load | 全量(TRUNCATE+INSERT) vs 增量(MERGE/UPSERT) |
| 调度 | SQL Server Agent / cron / Airflow |
| 质量 | 行数、空值、范围、唯一性、一致性、时效性 |