视图中子查询不缓存,每次调用均内联展开,ETL中相关子查询触发嵌套循环(如10万行×500行=5000万行扫描),NOT IN需改LEFT JOIN IS NULL并确保复合索引,标量子查询应预聚合再JOIN,物化视图可固化高频聚合结果。

视图里的子查询不会缓存,每次调用都原样展开——ETL跑得慢,八成卡在这儿。
为什么ETL里视图子查询特别拖后腿
数据仓库ETL通常按天/小时批量执行,而视图中嵌套的子查询(尤其是相关子查询)会在每次INSERT/UPDATE前被完整内联展开。优化器没法预估代价,常退化为嵌套循环:外层10万行 × 子查询扫500行 = 5000万行扫描。更糟的是,如果子查询含IN、NOT IN或EXISTS且没索引支撑,还会触发临时表+磁盘排序,I/O直接拉满。
- 典型现象:
EXPLAIN里出现type=ALL或rows列远超实际值,PREDICATE列显示filter而非access - 分区表上尤其明显:子查询里写
SUBSTR(dt, 1, 6),整个分区段全扫,PARTITION RANGE ALL只是假象 - Oracle 9i+或MySQL 8.0+虽支持函数索引,但
TO_CHAR(dt, 'YYYYMMDD')这类写法仍阻断分区裁剪
用LEFT JOIN IS NULL替代NOT IN子查询
这是ETL脚本里最常见也最该优先改写的模式。原始写法如WHERE user_id NOT IN (SELECT user_id FROM user_logins WHERE login_time > ...),在大表上基本等于自毁。
- 必须把过滤条件写进
ON子句:LEFT JOIN user_logins l ON u.user_id = l.user_id AND l.login_time > DATE_SUB(NOW(), INTERVAL 30 DAY),否则WHERE l.user_id IS NULL会变成INNER JOIN语义 - 确保
user_logins(user_id, login_time)有复合索引,否则JOIN还是全表扫 - 该写法在Oracle、MySQL、PostgreSQL中都能被识别为哈希连接,避免逐行判断
用预聚合+JOIN替代EXISTS标量子查询
ETL中常需标记“是否存在某类记录”,比如(SELECT 1 FROM order_items i WHERE i.order_id = o.order_id AND i.status = 'shipped')。这种写法让优化器无法下推status = 'shipped'谓词,常导致order_items全表扫描。
- 先聚合再关联:
SELECT o.order_id, COALESCE(s.has_shipped, 0) AS has_shipped FROM orders o LEFT JOIN (SELECT order_id, 1 AS has_shipped FROM order_items WHERE status = 'shipped' GROUP BY order_id) s ON o.order_id = s.order_id - 聚合子查询加
WHERE条件,比EXISTS更容易触发索引范围扫描 - 如果
order_items是分区表,确保status字段不参与分区键表达式,否则分区裁剪失效
拆独立子查询为物化视图,而非普通视图
ETL中反复使用的聚合逻辑(如“每日用户活跃度”)若放在普通视图里,每次调度都重算;但直接抽成物化视图,就能固化结果。
- Oracle用
CREATE MATERIALIZED VIEW,MySQL 8.0+可用CREATE TABLE ... AS SELECT配合定时刷新 - 物化视图要建日志(
MATERIALIZED VIEW LOG)才能支持FAST REFRESH,否则全量重建成本更高 - 注意权限:物化视图依赖基表的
SELECT权,且刷新作业账户需有ON COMMIT或DBMS_MVIEW.REFRESH权限
真正卡点不在语法多难,而在你是否检查过执行计划里那一行VIEW节点背后,到底展开了多少层子查询、扫了多少行——这些数字不会说谎,但容易被忽略。

















