性能优化实战

最后更新: 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:活动会话历史,实时性能分析
✅ 统计信息:收集、锁定、导出导入

📖 下章预告:日志管理

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