Oracle 数据仓库 ETL 详解
Oracle 数据仓库 ETL 详解
适用版本:Oracle Database 10g / 11g / 12c / 19c / 23ai 文档版本:v1.0 / 2026-07
1. 概述
数据仓库 ETL(Extract-Transform-Load)[1]:
详细见:Oracle 数据仓库 ETL。
2. 架构
2.1 流程
源系统 → Extract → Staging → Transform → Load → 数据仓库
2.2 组件
- ODS(操作数据存储)
- Staging(暂存)
- Data Warehouse(仓库)
- Data Mart(数据集市)
3. Extract
3.1 全量
INSERT INTO stg_emp
SELECT * FROM source_emp@source_db;
3.2 增量
-- 基于时间
INSERT INTO stg_emp
SELECT * FROM source_emp@source_db
WHERE updated_at > :last_extract;
-- 基于 CDC
-- LogMiner / GoldenGate
3.3 外部表
CREATE TABLE ext_sales (
id NUMBER,
amount NUMBER,
sale_date DATE
)
ORGANIZATION EXTERNAL (
TYPE ORACLE_LOADER
DEFAULT DIRECTORY data_dir
ACCESS PARAMETERS (
RECORDS DELIMITED BY NEWLINE
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
)
LOCATION ('sales_2025.csv')
);
SELECT * FROM ext_sales;
4. Transform
4.1 清洗
-- 去重
DELETE FROM stg_emp WHERE ROWID IN (
SELECT rid FROM (
SELECT ROWID rid, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) rn
FROM stg_emp
) WHERE rn > 1
);
-- 标准化
UPDATE stg_emp SET name = TRIM(UPPER(name));
UPDATE stg_emp SET phone = REGEXP_REPLACE(phone, '[^0-9]', '');
-- 默认值
UPDATE stg_emp SET status = NVL(status, 'ACTIVE');
4.2 转换
-- 类型转换
UPDATE stg_emp SET salary = TO_NUMBER(salary_str);
-- 日期
UPDATE stg_emp SET hire_date = TO_DATE(hire_str, 'YYYY-MM-DD');
-- 拆分
INSERT INTO emp_addr (emp_id, address)
SELECT id, address FROM stg_emp WHERE address IS NOT NULL;
-- 合并
INSERT INTO emp_full (id, name, addr, phone)
SELECT e.id, e.name, a.address, p.phone
FROM stg_emp e
LEFT JOIN emp_addr a ON e.id = a.emp_id
LEFT JOIN emp_phone p ON e.id = p.emp_id;
4.3 查找
-- 维度查找
UPDATE fact_sales s
SET dept_id = (
SELECT d.id FROM dim_dept d
WHERE d.name = s.dept_name
);
-- MERGE
MERGE INTO fact_sales f
USING dim_dept d
ON (f.dept_name = d.name)
WHEN MATCHED THEN UPDATE SET f.dept_id = d.id;
详细见:Oracle MERGE 语句详解。
4.4 聚合
INSERT INTO agg_sales_monthly (year, month, dept_id, total)
SELECT EXTRACT(YEAR FROM sale_date),
EXTRACT(MONTH FROM sale_date),
dept_id,
SUM(amount)
FROM fact_sales
GROUP BY EXTRACT(YEAR FROM sale_date), EXTRACT(MONTH FROM sale_date), dept_id;
5. Load
5.1 直接路径
-- INSERT /*+ APPEND */
INSERT /*+ APPEND */ INTO sales
SELECT * FROM stg_sales;
-- SQL*Loader
-- DIRECT=TRUE
5.2 分区交换
-- 准备表
CREATE TABLE sales_2025_07 (...) AS SELECT * FROM sales WHERE 1=0;
-- 加载
INSERT /*+ APPEND */ INTO sales_2025_07 SELECT * FROM stg_2025_07;
-- 交换
ALTER TABLE sales EXCHANGE PARTITION p2025_07 WITH TABLE sales_2025_07;
5.3 批量
-- BULK
DECLARE
TYPE sales_tab IS TABLE OF sales%ROWTYPE;
v_sales sales_tab;
BEGIN
SELECT * BULK COLLECT INTO v_sales FROM stg_sales LIMIT 10000;
FORALL i IN 1..v_sales.COUNT
INSERT INTO sales VALUES v_sales(i);
COMMIT;
END;
/
详细见:Oracle BULK COLLECT 与 FORALL 详解。
6. Data Pump
6.1 Export
expdp scott/tiger DIRECTORY=dp_dir DUMPFILE=sales.dmp TABLES=sales
expdp scott/tiger DIRECTORY=dp_dir DUMPFILE=full.dmp FULL=Y
expdp scott/tiger DIRECTORY=dp_dir DUMPFILE=schema.dmp SCHEMAS=scott
6.2 Import
impdp scott/tiger DIRECTORY=dp_dir DUMPFILE=sales.dmp TABLES=sales
impdp scott/tiger DIRECTORY=dp_dir DUMPFILE=sales.dmp TABLE_EXISTS_ACTION=APPEND
impdp scott/tiger DIRECTORY=dp_dir DUMPFILE=full.dmp REMAP_SCHEMA=scott:hr
详细见:Oracle Data Pump 全集。
7. SQL*Loader
7.1 控制文件
LOAD DATA
INFILE 'sales.csv'
BADFILE 'sales.bad'
DISCARDFILE 'sales.dsc'
APPEND INTO TABLE sales
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
TRAILING NULLCOLS
(
id INTEGER,
amount DECIMAL,
sale_date DATE 'YYYY-MM-DD',
status CONSTANT 'NEW'
)
7.2 命令
sqlldr scott/tiger control=sales.ctl log=sales.log
sqlldr scott/tiger control=sales.ctl direct=true -- 直接路径
8. External Table
8.1 创建
CREATE TABLE ext_sales (
id NUMBER,
amount NUMBER,
sale_date DATE
)
ORGANIZATION EXTERNAL (
TYPE ORACLE_LOADER
DEFAULT DIRECTORY data_dir
ACCESS PARAMETERS (
RECORDS DELIMITED BY NEWLINE
FIELDS TERMINATED BY ','
MISSING FIELD VALUES ARE NULL
)
LOCATION ('sales_2025.csv')
)
REJECT LIMIT UNLIMITED;
8.2 使用
-- 查询
SELECT * FROM ext_sales;
-- 加载
INSERT /*+ APPEND */ INTO sales SELECT * FROM ext_sales;
9. 物化视图
9.1 聚合
CREATE MATERIALIZED VIEW mv_sales_monthly
REFRESH COMPLETE ON DEMAND
ENABLE QUERY REWRITE
AS
SELECT EXTRACT(YEAR FROM sale_date) AS yr,
EXTRACT(MONTH FROM sale_date) AS mon,
dept_id,
SUM(amount) AS total
FROM sales
GROUP BY EXTRACT(YEAR FROM sale_date), EXTRACT(MONTH FROM sale_date), dept_id;
9.2 查询重写
ALTER SESSION SET query_rewrite_enabled = TRUE;
-- 自动使用物化视图
SELECT dept_id, SUM(amount) FROM sales GROUP BY dept_id;
详细见:Oracle 视图与物化视图详解。
10. Dimension
CREATE DIMENSION time_dim
LEVEL day IS time.day
LEVEL month IS time.month
LEVEL quarter IS time.quarter
LEVEL year IS time.year
HIERARCHY time_rollup (
day CHILD OF month CHILD OF quarter CHILD OF year
)
ATTRIBUTE month DETERMINES month_name;
11. Partition
11.1 Range
CREATE TABLE sales (
id NUMBER,
sale_date DATE,
amount NUMBER
)
PARTITION BY RANGE (sale_date) (
PARTITION p2024 VALUES LESS THAN (TO_DATE('2025-01-01', 'YYYY-MM-DD')),
PARTITION p2025 VALUES LESS THAN (TO_DATE('2026-01-01', 'YYYY-MM-DD')),
PARTITION p2026 VALUES LESS THAN (TO_DATE('2027-01-01', 'YYYY-MM-DD'))
);
11.2 交换
ALTER TABLE sales EXCHANGE PARTITION p2025 WITH TABLE sales_stg;
详细见:Oracle 表分区策略详解。
12. 压缩
12.1 压缩类型
-- 仓库压缩
CREATE TABLE sales (...) COMPRESS FOR QUERY HIGH;
-- 归档
CREATE TABLE sales_archive (...) COMPRESS FOR ARCHIVE HIGH;
详细见:Oracle 表压缩技术详解。
13. 调度
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name => 'etl_daily',
job_type => 'PLSQL_BLOCK',
job_action => 'BEGIN etl_pkg.run_daily; END;',
start_date => SYSTIMESTAMP,
repeat_interval => 'FREQ=DAILY; BYHOUR=2',
enabled => TRUE
);
END;
/
14. 错误处理
14.1 日志表
CREATE TABLE etl_log (
id NUMBER GENERATED ALWAYS AS IDENTITY,
job_name VARCHAR2(100),
step VARCHAR2(100),
status VARCHAR2(20),
error_msg VARCHAR2(4000),
rows_processed NUMBER,
start_time TIMESTAMP,
end_time TIMESTAMP
);
14.2 SAVE EXCEPTIONS
FORALL i IN 1..v_data.COUNT SAVE EXCEPTIONS
INSERT INTO target VALUES v_data(i);
EXCEPTION
WHEN OTHERS THEN
FOR i IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP
INSERT INTO etl_log (...) VALUES (...);
END LOOP;
详细见:Oracle BULK COLLECT 与 FORALL 详解。
15. 性能
15.1 并行
ALTER SESSION ENABLE PARALLEL DML;
INSERT /*+ PARALLEL(s 8) APPEND */ INTO sales s SELECT * FROM stg_sales;
15.2 直接路径
INSERT /*+ APPEND */ INTO sales SELECT * FROM stg_sales;
15.3 分区
- 分区交换
- 局部索引
- 增量
详细见:Oracle 并行查询详解。
16. 监控
16.1 进度
SELECT sid, serial#, opname, sofar, totalwork, ROUND(sofar/totalwork*100, 2) AS pct
FROM v$session_longops
WHERE opname LIKE 'ETL%';
16.2 统计
SELECT * FROM etl_log WHERE job_name = 'etl_daily' ORDER BY start_time DESC;
17. 常见坑与排错
17.1 数据倾斜
- 并行不均
- 分区
- Skew
17.2 约束违反
- 检查数据
- 异常表
- 清洗
17.3 性能慢
- 索引过多
- 触发器
- 直接路径
18. 最佳实践
- 外部表:灵活
- 直接路径:性能
- 分区交换:高效
- 物化视图:聚合
- 并行:吞吐
- 压缩:空间
- MERGE:UPSERT
- BULK:批量
- 错误处理:完整
- 监控:进度
19. 参考资料
[1] Oracle Database Data Warehousing Guide 19c https://docs.oracle.com/en/database/oracle/oracle-database/19/dwh/