第九章:性能优化实战

最后更新: 2026-04-15 作者: Oracle Team
页面目录

第九章:性能优化实战

文档信息
  • 适用版本:Oracle 12c / 18c / 19c / 21c
  • 阅读时间:约 60 分钟
  • 前置知识:第八章事务与并发控制

9.1 SQL 执行计划

9.1.1 执行计划基础

-- 1. EXPLAIN PLAN
EXPLAIN PLAN FOR
SELECT e.employee_id, e.first_name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.salary > 5000
ORDER BY e.salary DESC;

-- 查看执行计划
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

-- 2. AUTOTRACE
SET AUTOTRACE ON
SELECT * FROM employees WHERE department_id = 50;
SET AUTOTRACE OFF

-- AUTOTRACE 选项
SET AUTOTRACE ON;              -- 显示执行计划和统计
SET AUTOTRACE ON EXPLAIN;     -- 只显示执行计划
SET AUTOTRACE ON STATISTICS;  -- 只显示统计信息
SET AUTOTRACE TRACEONLY;      -- 不输出查询结果
SET AUTOTRACE TRACEONLY EXPLAIN;  -- 不执行,只看计划

-- 3. DBMS_XPLAN.DISPLAY_CURSOR
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(null, null, 'ALLSTATS LAST'));

-- 4. SQL Developer 执行计划
-- F10 或 工具菜单 → 监视执行计划

9.1.2 执行计划解读

┌─────────────────────────────────────────────────────────────────┐
│                      执行计划输出示例                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Plan hash value: 1234567890                                    │
│                                                                 │
│  ┌──────┬────────────────┬──────┬──────┬───────────┬──────────┐│
│  │ Id   │ Operation      │ Name │ Rows │ Cost (%CPU)│ Time     ││
│  ├──────┼────────────────┼──────┼──────┼───────────┼──────────┤│
│  │   0  │ SELECT STATEMENT│      │  100 │ 5 (20)    │ 00:00:01 ││
│  │   1  │  SORT ORDER BY │      │  100 │ 5 (20)    │ 00:00:01 ││
│  │   2  │   NESTED LOOPS │      │  100 │ 4 (0)     │ 00:00:01 ││
│  │   3  │    TABLE ACCESS│ EMP  │  100 │ 2 (0)     │ 00:00:01 ││
│  │*  4  │     INDEX RANGE│ IDX  │   50 │ 1 (0)     │ 00:00:01 ││
│  │   5  │    TABLE ACCESS│ DEPT │    1 │ 2 (0)     │ 00:00:01 ││
│  └──────┴────────────────┴──────┴──────┴───────────┴──────────┘│
│                                                                 │
│  Predicate Information (identified by operation id):            │
│  ──────────────────────────────────────────────────────────── │
│     4 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")        │
│     4 - filter("E"."SALARY">5000)                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

关键列说明:
• Id: 操作步骤编号
• Operation: 操作类型
• Name: 对象名称
• Rows: 估算返回行数
• Cost: 估算成本
• Time: 估算时间
• Bytes: 估算字节数

9.1.3 常见访问路径

操作 说明 性能
TABLE ACCESS FULL 全表扫描 慢
TABLE ACCESS BY ROWID 通过 ROWID 访问 快
INDEX UNIQUE SCAN 唯一索引扫描 快
INDEX RANGE SCAN 索引范围扫描 中
INDEX FULL SCAN 索引全扫描 中
INDEX FAST FULL SCAN 索引快速全扫描 快
INDEX SKIP SCAN 跳跃索引扫描 中

9.2 SQL 调优技巧

9.2.1 避免全表扫描

-- ❌ 错误:全表扫描
SELECT * FROM employees WHERE TO_CHAR(hire_date, 'YYYY') = '2024';

-- ✅ 正确:使用函数索引
CREATE INDEX idx_hire_year ON employees(EXTRACT(YEAR FROM hire_date));
SELECT * FROM employees WHERE EXTRACT(YEAR FROM hire_date) = 2024;

-- ❌ 错误:隐式类型转换
SELECT * FROM employees WHERE employee_id = '100';  -- NUMBER 列用字符串

-- ✅ 正确:类型匹配
SELECT * FROM employees WHERE employee_id = 100;

-- ❌ 错误:使用函数
SELECT * FROM employees WHERE UPPER(last_name) = 'SMITH';

-- ✅ 正确:函数索引
CREATE INDEX idx_emp_upper_name ON employees(UPPER(last_name));
SELECT * FROM employees WHERE UPPER(last_name) = 'SMITH';

-- ❌ 错误:NOT IN
SELECT * FROM employees WHERE department_id NOT IN (10, 20);

-- ✅ 正确:NOT EXISTS 或外部连接
SELECT * FROM employees e
WHERE NOT EXISTS (SELECT 1 FROM departments d WHERE d.dept_id = e.department_id);

9.2.2 优化 JOIN

-- 小表驱动大表(NL 循环时)
-- Oracle 自动优化,无需手动指定

-- 避免不必要的 JOIN
-- ❌ 过度 JOIN
SELECT e.employee_id, e.first_name, 
       d.department_name, l.city, c.country_name
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
JOIN locations l ON d.loc_id = l.loc_id
JOIN countries c ON l.country_id = c.country_id  -- 可能不需要
WHERE e.employee_id = 100;

-- ✅ 按需 JOIN
SELECT e.employee_id, e.first_name, d.department_name
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.employee_id = 100;

-- WHERE 条件位置
-- ❌ WHERE 在 JOIN 之后
SELECT * FROM employees e, departments d
WHERE e.dept_id = d.dept_id AND e.salary > 5000;

-- ✅ Oracle 自动优化,但保持清晰
SELECT * FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.salary > 5000;

9.2.3 分页查询优化

-- ❌ 低效分页
SELECT * FROM (
    SELECT a.*, ROWNUM rn FROM (
        SELECT * FROM large_table ORDER BY create_time DESC
    ) a
) WHERE rn BETWEEN 1001 AND 1020;

-- ✅ 优化:使用 ROWID 分页
SELECT * FROM large_table
WHERE rowid IN (
    SELECT rowid FROM large_table 
    WHERE create_time > '2024-01-01'
    ORDER BY create_time DESC
)
AND ROWNUM <= 20;

-- ✅ 使用 Keyset 分页(游标分页,更高效)
SELECT * FROM large_table
WHERE create_time < :last_time  -- 使用上一页最后一条的时间
ORDER BY create_time DESC
FETCH FIRST 20 ROWS ONLY;

-- ✅ Oracle 12c+ 优化
SELECT * FROM large_table
ORDER BY create_time DESC
OFFSET 1000 ROWS FETCH NEXT 20 ROWS ONLY;

9.2.4 批量操作优化

-- FORALL 批量插入
DECLARE
    TYPE emp_tab IS TABLE OF employees%ROWTYPE;
    v_emps emp_tab;
BEGIN
    SELECT * BULK COLLECT INTO v_emps FROM employees WHERE department_id = 50;
    
    FORALL i IN 1..v_emps.COUNT
        INSERT INTO employees_backup VALUES v_emps(i);
    
    COMMIT;
END;
/

-- BULK COLLECT 批量查询
DECLARE
    TYPE id_tab IS TABLE OF NUMBER;
    TYPE name_tab IS TABLE OF VARCHAR2(100);
    v_ids id_tab;
    v_names name_tab;
BEGIN
    SELECT employee_id, first_name
    BULK COLLECT INTO v_ids, v_names
    FROM employees
    WHERE department_id = 50;
    
    FOR i IN 1..v_ids.COUNT LOOP
        DBMS_OUTPUT.PUT_LINE(v_ids(i) || ': ' || v_names(i));
    END LOOP;
END;
/

-- FORALL with SAVE EXCEPTIONS
DECLARE
    TYPE num_tab IS TABLE OF NUMBER;
    v_ids num_tab := num_tab(1, 2, 3, 4, 5);
    v_errors NUMBER;
BEGIN
    FORALL i IN 1..v_ids.COUNT SAVE EXCEPTIONS
        UPDATE employees SET salary = salary * 1.1 WHERE employee_id = v_ids(i);
EXCEPTION
    WHEN OTHERS THEN
        v_errors := SQL%BULK_EXCEPTIONS.COUNT;
        FOR i IN 1..v_errors LOOP
            DBMS_OUTPUT.PUT_LINE('Error ' || i || ': Row ' || 
                SQL%BULK_EXCEPTIONS(i).ERROR_INDEX || 
                ' - ' || SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE));
        END LOOP;
END;
/

9.3 提示词 (Hints)

9.3.1 常用提示词

提示词 说明
/*+ FULL(t) */ 全表扫描
/*+ INDEX(t idx) */ 使用索引
/*+ INDEX_FFS(t idx) */ 索引快速全扫描
/*+ USE_NL(t1 t2) */ 使用嵌套循环
/*+ USE_HASH(t1 t2) */ 使用 Hash 连接
/*+ USE_MERGE(t1 t2) */ 使用排序合并
/*+ PARALLEL(t, 4) */ 并行执行
/*+ LEADING(t1) */ 指定驱动表
/*+ ORDERED */ 按 FROM 顺序连接

9.3.2 提示词使用

-- 强制使用索引
SELECT /*+ INDEX(e idx_emp_dept) */ *
FROM employees e
WHERE department_id = 50;

-- 强制嵌套循环
SELECT /*+ USE_NL(e d) */ *
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id;

-- 强制 Hash 连接
SELECT /*+ USE_HASH(e d) */ *
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id;

-- 并行查询
SELECT /*+ PARALLEL(employees, 4) */ COUNT(*)
FROM employees;

-- 指定驱动表
SELECT /*+ LEADING(d e) */ *
FROM departments d
JOIN employees e ON d.dept_id = e.dept_id;

-- 优化器参数提示
SELECT /*+ OPT_PARAM('optimizer_index_caching', 80) */ *
FROM employees WHERE department_id = 50;

9.4 AWR 与性能诊断

9.4.1 AWR 基础

┌─────────────────────────────────────────────────────────────────┐
│                      AWR 工作原理                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  MMON 进程每 60 分钟自动采集快照                                  │
│                                                                 │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐                     │
│  │ 快照 1  │ →  │ 快照 2  │ →  │ 快照 3  │  →  ...             │
│  └─────────┘    └─────────┘    └─────────┘                     │
│      ↓              ↓              ↓                            │
│    10:00          11:00          12:00                           │
│                                                                 │
│  AWR 存储:                                                      │
│  • 默认保留 8 天(可调整)                                        │
│  • SYSAUX 表空间                                                 │
│  • 快照数据可通过 DBA_HIST_* 视图查询                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

9.4.2 AWR 操作

-- 查看 AWR 配置
SELECT 
    snap_interval,
    retention,
    topnsql
FROM dba_hist_wr_control;

-- 修改 AWR 设置
BEGIN
    DBMS_WORKLOAD_REPOSITORY.MODIFY_SNAPSHOT_SETTINGS(
        retention => 43200,    -- 30 天(分钟)
        interval  => 3600,      -- 1 小时
        topnsql   => 100        -- Top SQL 数量
    );
END;
/

-- 手动创建快照
EXEC DBMS_WORKLOAD_REPOSITORY.CREATE_SNAPSHOT;

-- 查看快照
SELECT 
    snap_id,
    to_char(begin_interval_time, 'YYYY-MM-DD HH24:MI') as begin_time,
    to_char(end_interval_time, 'YYYY-MM-DD HH24:MI') as end_time
FROM dba_hist_snapshot
ORDER BY snap_id DESC;

-- 生成 AWR 报告(SQL*Plus)
@?/rdbms/admin/awrrpt.sql

-- 生成 AWR 比较报告
@?/rdbms/admin/awrddrpt.sql

-- 生成单实例 AWR HTML 报告
SELECT OUTPUT FROM TABLE(DBMS_WORKLOAD_REPOSITORY.AWR_REPORT_HTML(
    DBID => 1234567890,
    INST_NUM => 1,
    START_SNAP_ID => 100,
    END_SNAP_ID => 110
));

9.4.3 AWR 报告解读

-- 生成指定时间段的 AWR
SELECT DBMS_WORKLOAD_REPOSITORY.AWR_REPORT_HTML(
    DBID      => (SELECT DBID FROM V$DATABASE),
    INST_NUM  => 1,
    START_SNAP_ID => 150,
    END_SNAP_ID   => 160
) FROM DUAL;

-- 关键指标:
-- Load Profile
-- ┌─────────────────┬──────────┬──────────┐
-- │ Per Second      │ Per Trans│ Redo Size│
-- ├─────────────────┼──────────┼──────────┤
-- │ DB Time(s): 1.5 │ 15.2     │ 125KB    │
-- │ DB CPU(s): 1.0  │ 10.0     │          │
-- │ Buffer Gets:150 │ 1500     │          │
-- └─────────────────┴──────────┴──────────┘

-- Top 5 Timed Events
-- ┌─────────────────────────┬──────────┬───────────┬───────────┐
-- │ Event                   │ Waits    │ Time(s)   │ %DB Time  │
-- ├─────────────────────────┼──────────┼───────────┼───────────┤
-- │ db file sequential read │ 1,500    │ 50        │ 55%       │
-- │ log file sync           │ 200      │ 20        │ 22%       │
-- │ CPU time                │ -        │ 15        │ 17%       │
-- └─────────────────────────┴──────────┴───────────┴───────────┘

-- SQL Statistics
-- Top SQL by Elapsed Time
-- Top SQL by CPU Time
-- Top SQL by Buffer Gets
-- Top SQL by Row Processed

9.4.4 ADDM 分析

-- 运行 ADDM 分析
@?/rdbms/admin/addmrpt.sql

-- 查看 ADDM 建议
SELECT 
    task_name,
    TO_CHAR(execution_start, 'YYYY-MM-DD HH24:MI') as start_time,
    STATUS
FROM dba_advisor_tasks
WHERE task_type = 'ADDM'
ORDER BY execution_start DESC;

-- 查看 ADDM 发现
SELECT 
    finding_id,
    message
FROM dba_advisor_finding_names
WHERE task_name = 'MY_ADDM_TASK';

-- 查看 ADDM 推荐
SELECT 
    r.rank,
    r.type,
    r.message,
    r.benefit_type,
    r.benefit
FROM dba_advisor_recommendations r
WHERE r.task_name = 'MY_ADDM_TASK'
ORDER BY r.rank;

9.4.5 ASH 实时分析

-- 查看当前活动的会话
SELECT 
    s.sid,
    s.serial#,
    s.username,
    s.program,
    s.status,
    s.event,
    s.seconds_in_wait,
    sql.sql_text
FROM v$session s
LEFT JOIN v$sql sql ON s.sql_id = sql.sql_id
WHERE s.status = 'ACTIVE'
AND s.type = 'USER';

-- 查看等待事件
SELECT 
    event,
    COUNT(*) as wait_count,
    SUM(seconds_in_wait) as total_wait_sec
FROM v$session
WHERE status = 'ACTIVE'
AND wait_class != 'Idle'
GROUP BY event
ORDER BY SUM(seconds_in_wait) DESC;

-- ASH 样本数据
SELECT 
    session_id,
    session_serial#,
    event,
    wait_class,
    sql_id,
    program
FROM v$active_session_history
WHERE sample_time >= SYSDATE - INTERVAL '5' MINUTE
ORDER BY sample_time DESC;

-- Top SQL 正在执行
SELECT 
    sql_id,
    COUNT(*) as sample_count,
    SUM(CASE WHEN session_state = 'ON CPU' THEN 1 ELSE 0 END) as on_cpu,
    SUM(CASE WHEN session_state = 'WAITING' THEN 1 ELSE 0 END) as waiting
FROM v$active_session_history
WHERE sample_time >= SYSDATE - INTERVAL '5' MINUTE
GROUP BY sql_id
ORDER BY COUNT(*) DESC;

9.5 统计信息管理

9.5.1 表与索引统计

-- 收集表统计
BEGIN
    DBMS_STATS.GATHER_TABLE_STATS(
        ownname          => 'HR',
        tabname          => 'EMPLOYEES',
        estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
        method_opt       => 'FOR ALL COLUMNS SIZE AUTO',
        degree           => DBMS_STATS.AUTO_DEGREE,
        cascade          => TRUE,  -- 同时收集索引统计
        no_invalidate    => FALSE
    );
END;
/

-- 收集模式统计
BEGIN
    DBMS_STATS.GATHER_SCHEMA_STATS(
        ownname          => 'HR',
        estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
        method_opt       => 'FOR ALL COLUMNS SIZE AUTO',
        degree           => DBMS_STATS.AUTO_DEGREE,
        cascade          => TRUE,
        options          => 'GATHER AUTO'  -- 只收集脏数据
    );
END;
/

-- 收集系统统计
BEGIN
    DBMS_STATS.GATHER_SYSTEM_STATS(
        stattab     => 'SYS_STATS',
        statid      => 'PROD_STATS',
        statown     => 'SYS'
    );
END;
/

-- 查看统计信息
SELECT 
    table_name,
    num_rows,
    blocks,
    sample_size,
    last_analyzed
FROM user_tab_statistics
WHERE table_name = 'EMPLOYEES';

SELECT 
    index_name,
    num_rows,
    distinct_keys,
    leaf_blocks,
    clustering_factor,
    last_analyzed
FROM user_ind_statistics
WHERE index_name = 'IDX_EMP_DEPT';

9.5.2 统计信息锁定

-- 锁定统计信息(生产环境常用)
BEGIN
    DBMS_STATS.LOCK_TABLE_STATS('HR', 'EMPLOYEES');
END;
/

-- 锁定模式统计
BEGIN
    DBMS_STATS.LOCK_SCHEMA_STATS('HR');
END;
/

-- 解锁
BEGIN
    DBMS_STATS.UNLOCK_TABLE_STATS('HR', 'EMPLOYEES');
END;
/

-- 导出/导入统计信息
-- 导出
BEGIN
    DBMS_STATS.EXPORT_TABLE_STATS(
        ownname  => 'HR',
        tabname  => 'EMPLOYEES',
        statown  => 'HR',
        stattab  => 'STATS_TABLE'
    );
END;
/

-- 导入
BEGIN
    DBMS_STATS.IMPORT_TABLE_STATS(
        ownname  => 'HR',
        tabname  => 'EMPLOYEES',
        statown  => 'HR',
        stattab  => 'STATS_TABLE'
    );
END;
/

9.6 本章小结

✅ 执行计划分析:TABLE ACCESS FULL、INDEX SCAN、JOIN 类型
✅ SQL 调优技巧:避免全表扫描、优化 JOIN、分页优化
✅ 提示词:强制执行计划、提示优化器行为
✅ AWR:自动工作负载仓库,快照管理,性能报告
✅ ADDM:自动诊断推荐,分析性能问题
✅ ASH:活动会话历史,实时性能分析
✅ 统计信息:收集、锁定、导出导入

📖 下章预告:日志管理

下一步:学习 第十章:日志管理