2. 多表连接与数据合并:JOIN、UNION等
特征工程里有个基本认知——绝大多数有用变量都不是从单张表里直接长出来的。你想想看,用户画像在 A 表,交易行为在 B 表,点击日志在 C 表,你要怎么把它们捏到一起?这就是本章要讲的核心:JOIN 和 UNION。
说白了,这是特征衍生最基础也最关键的一步。数据都拼不对,后面算再多特征也是白搭。
2.1 连接哲学:横向扩展还是纵向扩展
很多新人会混淆这两个概念。我简单解释下:JOIN 是横向扩展,给你原表增加列;UNION 是纵向扩展,给你原表增加行。
- 横向(JOIN): 用户表 + 交易统计 → 用户特征宽表
- 纵向(UNION): 线上订单 + 线下订单 → 全渠道订单表
我做特征工程时,90% 的时间都在和 LEFT JOIN 打交道。为什么?因为业务主体通常只有一个(比如用户),其他表都只是用来给它“贴”特征的。
我的习惯:开始写 SQL 前,先问自己一句——我的主体是谁,我要给它加什么信息?想清楚横向还是纵向,再动笔。
2.2 JOIN 实战:聚合前的一步到位
实战中最常见的场景:从交易表里衍生用户特征。比如算近 30 天交易频次、总额、最近一次交易时间。我个人习惯用 LEFT JOIN 保留所有用户,然后提前在子查询里做聚合。
SELECT
u.user_id,
COALESCE(t.order_freq_30d, 0) AS order_freq_30d,
COALESCE(t.total_amount_30d, 0) AS total_amount_30d,
t.last_order_time
FROM dim_user u
LEFT JOIN (
SELECT
user_id,
COUNT(order_id) AS order_freq_30d,
SUM(amount) AS total_amount_30d,
MAX(order_time) AS last_order_time
FROM fact_order
WHERE order_time >= DATEADD(day, -30, GETDATE())
GROUP BY user_id
) t ON u.user_id = t.user_id
嗯,这里要注意一个核心问题。我早期犯过一个错——直接用 LEFT JOIN 把交易明细贴到用户表里,没做聚合。结果一个用户有 100 笔交易,用户表就被撑大了 100 倍。那个模型跑出来一塌糊涂,分布全变了。
避坑指南:我曾经因为表关联逻辑没写对,导致一版特征全部重跑。Join 之前,一定确认好“一对多”关系。如果你需要的是用户级特征,那必须在 Join 前完成聚合。记住:特征工程宁可信息少一点(先聚合),也不能让数据膨胀失真。
2.3 UNION 应用场景:合并多源数据
UNION 更多用于合并多源数据。比如公司有 App 端和 Web 端的行为数据,要拉到一起做分析,就会用 UNION ALL。
SELECT user_id, page, action, event_time, 'app' AS source
FROM app_events
WHERE event_time >= '2024-01-01'
UNION ALL
SELECT user_id, page, action, event_time, 'web' AS source
FROM web_events
WHERE event_time >= '2024-01-01'
性能关键点:
UNION 默认会去重,这非常耗费性能。如果你能确定两个子集没有重叠,或者重叠不影响业务逻辑,直接用 UNION ALL,效率能翻好几倍。我在处理亿级日志合并时,ALL 跑 5 分钟,DISTINCT 跑 40 分钟还没出结果。
另外,UNION 要求上下两个查询的列数一致,列类型要兼容。我个人习惯在每段子查询末尾加一个常量字段(如 'app' AS source 来标记数据来源),这样后期排查问题会方便很多。
技巧:如果你合并后需要区分数据来源,一定要加 source 字段。这是我早期踩过坑才养成的习惯,不然数据混在一起根本查不出问题。
2.4 数据合并的避坑指南
这块是经验活,光看书不够,得动手踩坑才能记住。我把自己遇到过的几个典型问题列出来,你遇到了能少走弯路。
| 常见错误 | 场景 | 后果 | 解决方案 |
|---|---|---|---|
| Join 键重复 | 右表存在多条记录 | 左表行数膨胀,特征分布失真 | 连接前去重,或先聚合再连接 |
| 误用 INNER JOIN | 需要保留全量主体 | 丢失无交易用户信息 | 用 LEFT JOIN 保底 |
| UNION 性能差 | 亿级数据合并 | Job 跑挂或超时 | 用 UNION ALL + 分区裁剪 |
| 空值处理不当 | 左表无匹配项 | 聚合函数结果异常 | 用 COALESCE 或 NVL 填充默认值 |
我的切身体会:连接前,必须明确“多”在哪一边。特征工程宁可信息少一点,也不能让数据膨胀失真。我每次写连接 SQL 前,都会加一句
SELECT COUNT(*) 做行数校验,左右对比没问题再继续向下跑。这习惯救过我不少次。