Skip to content

Oracle SQL语句优化

SQL优化基础

SQL执行过程

  1. 解析:将SQL语句转换为解析树
  2. 优化:生成执行计划
  3. 执行:执行SQL语句
  4. 获取结果:返回查询结果

优化器类型

  • 基于规则的优化器(RBO):根据预定义的规则生成执行计划
  • 基于成本的优化器(CBO):根据统计信息计算成本,选择最优执行计划
  • 自适应优化器:结合CBO和运行时反馈,动态调整执行计划

优化目标

  • 响应时间:最小化单个查询的执行时间
  • 吞吐量:最大化单位时间内处理的查询数量
  • 资源利用率:最小化CPU、内存和I/O资源的使用

执行计划分析

查看执行计划

使用EXPLAIN PLAN

sql
-- 生成执行计划
EXPLAIN PLAN FOR
SELECT * FROM employees WHERE department_id = 50;

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

-- 查看详细执行计划
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(NULL, NULL, 'ALL'));

使用SQL*Plus AUTOTRACE

sql
-- 启用AUTOTRACE
SET AUTOTRACE ON;

-- 执行SQL并查看执行计划
SELECT * FROM employees WHERE department_id = 50;

-- 启用AUTOTRACE但只显示执行计划
SET AUTOTRACE TRACEONLY;

使用V$SQL_PLAN

sql
-- 查看已执行SQL的执行计划
SELECT * FROM v$sql_plan
WHERE sql_id = '7t4b3d2x4y5z6'
ORDER BY child_number, position;

-- 查看执行计划和统计信息
SELECT * FROM v$sql_plan_statistics_all
WHERE sql_id = '7t4b3d2x4y5z6'
ORDER BY child_number, position;

执行计划解读

访问路径

  • 全表扫描(TABLE ACCESS FULL):扫描表的所有数据块
  • 索引扫描(INDEX SCAN)
    • 索引唯一扫描(INDEX UNIQUE SCAN)
    • 索引范围扫描(INDEX RANGE SCAN)
    • 索引全扫描(INDEX FULL SCAN)
    • 索引快速全扫描(INDEX FAST FULL SCAN)
  • 索引跳跃扫描(INDEX SKIP SCAN):适用于复合索引

连接方法

  • 嵌套循环连接(NESTED LOOPS):适用于小结果集
  • 哈希连接(HASH JOIN):适用于大结果集
  • 排序合并连接(SORT MERGE JOIN):适用于有序数据
  • 笛卡尔积连接(CARTESIAN PRODUCT):应避免使用

操作类型

  • 排序(SORT):ORDER BY、GROUP BY等操作
  • 聚合(AGGREGATE):SUM、COUNT、AVG等聚合函数
  • 过滤(FILTER):WHERE子句过滤
  • 分区操作(PARTITION):分区表的操作

执行计划异常识别

  • 全表扫描:对于大表应考虑使用索引
  • 排序操作:检查是否必要,考虑索引排序
  • 哈希连接:对于小表考虑使用嵌套循环连接
  • 笛卡尔积:检查连接条件是否正确
  • 临时表:检查是否可以避免
  • 大量的I/O:考虑增加内存或优化SQL

索引优化

索引类型

B树索引

  • 标准B树索引:最常用的索引类型
  • 唯一索引:确保列值唯一
  • 复合索引:包含多个列
  • 反向键索引:减少索引叶块竞争
  • 降序索引:支持降序排序

位图索引

  • 位图索引:适用于低基数列
  • 位图连接索引:基于表连接的索引

其他索引类型

  • 函数索引:基于函数或表达式的索引
  • 分区索引:与分区表配合使用
  • 域索引:用于文本、空间等特殊数据类型

索引设计原则

选择合适的列

  • 高选择性列:列值分布均匀,选择性高
  • 频繁查询的列:WHERE子句中经常使用的列
  • 连接条件列:表连接时使用的列
  • 排序和分组列:ORDER BY、GROUP BY子句中的列

复合索引设计

  • 列顺序:选择性高的列放在前面
  • 列数量:一般不超过3-4列
  • 前缀使用:确保查询能使用索引前缀
  • 避免冗余:避免创建冗余索引

索引使用技巧

强制索引使用

sql
-- 提示Oracle使用指定索引
SELECT /*+ INDEX(employees emp_department_ix) */ *
FROM employees
WHERE department_id = 50;

-- 提示Oracle不使用索引
SELECT /*+ FULL(employees) */ *
FROM employees
WHERE department_id = 50;

索引监控

sql
-- 启用索引监控
ALTER INDEX emp_department_ix MONITORING USAGE;

-- 查看索引使用情况
SELECT index_name, table_name, monitoring, used
FROM v$object_usage
WHERE index_name = 'EMP_DEPARTMENT_IX';

-- 禁用索引监控
ALTER INDEX emp_department_ix NOMONITORING USAGE;

索引维护

sql
-- 重建索引
ALTER INDEX emp_department_ix REBUILD;

-- 重建索引并并行执行
ALTER INDEX emp_department_ix REBUILD PARALLEL 4;

-- 收集索引统计信息
EXEC DBMS_STATS.GATHER_INDEX_STATS('HR', 'EMP_DEPARTMENT_IX');

-- 分析索引
ANALYZE INDEX emp_department_ix VALIDATE STRUCTURE;

SQL查询重写

WHERE子句优化

避免使用函数

sql
-- 不好的写法
SELECT * FROM employees WHERE UPPER(last_name) = 'SMITH';

-- 好的写法
SELECT * FROM employees WHERE last_name = 'SMITH';

-- 或者创建函数索引
CREATE INDEX emp_last_name_upper_ix ON employees(UPPER(last_name));

避免使用不等于

sql
-- 不好的写法
SELECT * FROM employees WHERE department_id != 50;

-- 好的写法(如果值分布不均匀)
SELECT * FROM employees WHERE department_id < 50 OR department_id > 50;

避免使用IS NULL

sql
-- 不好的写法
SELECT * FROM employees WHERE commission_pct IS NULL;

-- 好的写法
SELECT * FROM employees WHERE commission_pct = 0;

-- 或者创建位图索引(如果列基数低)
CREATE BITMAP INDEX emp_commission_null_ix ON employees(commission_pct);

使用绑定变量

sql
-- 不好的写法
SELECT * FROM employees WHERE employee_id = 100;

-- 好的写法
SELECT * FROM employees WHERE employee_id = :emp_id;

JOIN优化

JOIN顺序

  • 小表驱动大表:将结果集小的表放在前面
  • 使用STRAIGHT_JOIN:强制指定连接顺序
sql
-- 提示Oracle连接顺序
SELECT /*+ ORDERED */ *
FROM departments d, employees e
WHERE d.department_id = e.department_id;

JOIN类型选择

  • 嵌套循环连接:适用于小结果集,有索引的情况
  • 哈希连接:适用于大结果集,无索引的情况
  • 排序合并连接:适用于有序数据的连接
sql
-- 提示使用嵌套循环连接
SELECT /*+ USE_NL(e d) */ *
FROM employees e, departments d
WHERE e.department_id = d.department_id;

-- 提示使用哈希连接
SELECT /*+ USE_HASH(e d) */ *
FROM employees e, departments d
WHERE e.department_id = d.department_id;

子查询优化

相关子查询

sql
-- 不好的写法(相关子查询)
SELECT *
FROM employees e
WHERE salary > (
  SELECT AVG(salary)
  FROM employees
  WHERE department_id = e.department_id
);

-- 好的写法(使用连接)
SELECT e.*
FROM employees e,
     (SELECT department_id, AVG(salary) avg_sal
      FROM employees
      GROUP BY department_id) d
WHERE e.department_id = d.department_id
AND e.salary > d.avg_sal;

EXISTS vs IN

sql
-- 当子查询结果集大时,使用EXISTS
SELECT *
FROM employees e
WHERE EXISTS (
  SELECT 1
  FROM departments d
  WHERE d.department_id = e.department_id
  AND d.location_id = 1700
);

-- 当子查询结果集小时,使用IN
SELECT *
FROM employees e
WHERE department_id IN (
  SELECT department_id
  FROM departments
  WHERE location_id = 1700
);

NOT EXISTS vs NOT IN

sql
-- 推荐使用NOT EXISTS
SELECT *
FROM employees e
WHERE NOT EXISTS (
  SELECT 1
  FROM departments d
  WHERE d.department_id = e.department_id
);

-- 避免使用NOT IN(如果子查询可能返回NULL)
SELECT *
FROM employees e
WHERE department_id NOT IN (
  SELECT department_id
  FROM departments
);

聚合查询优化

避免在WHERE子句中使用聚合函数

sql
-- 不好的写法
SELECT department_id, AVG(salary)
FROM employees
WHERE AVG(salary) > 5000
GROUP BY department_id;

-- 好的写法
SELECT department_id, AVG(salary)
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 5000;

使用分析函数替代自连接

sql
-- 不好的写法(使用自连接)
SELECT e1.employee_id, e1.last_name, e1.salary,
       (SELECT AVG(salary)
        FROM employees e2
        WHERE e2.department_id = e1.department_id)
FROM employees e1;

-- 好的写法(使用分析函数)
SELECT employee_id, last_name, salary,
       AVG(salary) OVER (PARTITION BY department_id)
FROM employees;

高级SQL优化技术

分区表优化

分区裁剪

sql
-- 利用分区裁剪
SELECT *
FROM sales_partitioned
WHERE sale_date BETWEEN '2023-01-01' AND '2023-01-31';

-- 避免全分区扫描
SELECT *
FROM sales_partitioned
WHERE EXTRACT(YEAR FROM sale_date) = 2023;

分区连接

sql
-- 使用分区连接
SELECT /*+ USE_HASH_PARTITION(s c) */
       s.sale_id, s.customer_id, c.customer_name
FROM sales_partitioned s,
     customers_partitioned c
WHERE s.customer_id = c.customer_id;

并行查询优化

启用并行查询

sql
-- 为语句启用并行
SELECT /*+ PARALLEL(employees, 4) */ *
FROM employees;

-- 为表启用并行
ALTER TABLE employees PARALLEL 4;

-- 为语句禁用并行
SELECT /*+ NO_PARALLEL(employees) */ *
FROM employees;

并行度选择

  • CPU数量:一般不超过CPU核心数的2倍
  • I/O系统:考虑存储系统的并行处理能力
  • 内存:确保有足够的内存支持并行操作
  • 系统负载:在系统负载低时使用更高的并行度

结果缓存

启用结果缓存

sql
-- 启用结果缓存
ALTER SYSTEM SET result_cache_mode = FORCE;

-- 为语句启用结果缓存
SELECT /*+ RESULT_CACHE */ *
FROM employees
WHERE department_id = 50;

-- 为函数启用结果缓存
CREATE OR REPLACE FUNCTION get_employee_name(
  p_employee_id IN NUMBER
) RETURN VARCHAR2
RESULT_CACHE
IS
  l_name VARCHAR2(100);
BEGIN
  SELECT last_name INTO l_name
  FROM employees
  WHERE employee_id = p_employee_id;
  RETURN l_name;
END;
/

监控结果缓存

sql
-- 查看结果缓存状态
SELECT status, block_size, block_count_max, block_count_used
FROM v$result_cache_status;

-- 查看结果缓存统计信息
SELECT name, value
FROM v$result_cache_statistics;

-- 查看缓存的SQL语句
SELECT sql_id, buffer_gets, executions
FROM v$sql
WHERE result_cache = 'YES';

绑定变量窥探

绑定变量窥探问题

  • 问题:优化器使用第一次执行时的绑定变量值生成执行计划
  • 影响:当绑定变量值分布不均匀时,可能生成不是最优的执行计划

解决方案

sql
-- 使用绑定变量提示
SELECT /*+ BIND_AWARE */ *
FROM employees
WHERE department_id = :dept_id;

-- 使用自适应游标共享
ALTER SYSTEM SET cursor_sharing = FORCE;

-- 使用SQL计划管理
EXEC DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(sql_id => '7t4b3d2x4y5z6');

SQL优化工具

自动SQL优化

SQL Tuning Advisor

sql
-- 创建SQL调优任务
DECLARE
  l_task_id VARCHAR2(100);
BEGIN
  l_task_id := DBMS_SQLTUNE.CREATE_TUNING_TASK(
    sql_id => '7t4b3d2x4y5z6',
    scope => 'COMPREHENSIVE',
    time_limit => 60,
    task_name => 'tune_emp_query',
    description => 'Tune query for employees');
  
  -- 执行调优任务
  DBMS_SQLTUNE.EXECUTE_TUNING_TASK(task_name => l_task_id);
END;
/

-- 查看调优建议
SELECT DBMS_SQLTUNE.REPORT_TUNING_TASK('tune_emp_query') FROM DUAL;

SQL Access Advisor

sql
-- 创建SQL访问顾问任务
DECLARE
  l_task_id VARCHAR2(100);
BEGIN
  l_task_id := DBMS_ADVISOR.CREATE_TASK('SQL Access Advisor');
  DBMS_ADVISOR.CREATE_FILE(
    file_name => 'emp_workload.sql',
    directory_name => 'ADVISOR_DIR',
    content => 'SELECT * FROM employees WHERE department_id = 50;');
  
  DBMS_ADVISOR.SET_TASK_PARAMETER(
    task_id => l_task_id,
    parameter => 'ANALYSIS_SCOPE',
    value => 'ALL');
  
  DBMS_ADVISOR.EXECUTE_TASK(task_id => l_task_id);
END;
/

-- 查看访问建议
SELECT DBMS_ADVISOR.REPORT_TASK('SQL Access Advisor') FROM DUAL;

性能监控工具

AWR报告

sql
-- 生成AWR报告
SELECT output FROM TABLE(
  DBMS_WORKLOAD_REPOSITORY.AWR_REPORT_HTML(
    l_dbid => (SELECT dbid FROM v$database),
    l_inst_num => 1,
    l_bid => 100,
    l_eid => 110
  )
);

-- 查看Top SQL
SELECT * FROM TABLE(
  DBMS_WORKLOAD_REPOSITORY.TOPSQL_REPORT_HTML(
    l_dbid => (SELECT dbid FROM v$database),
    l_inst_num => 1,
    l_bid => 100,
    l_eid => 110,
    l_options => 1
  )
);

ASH报告

sql
-- 生成ASH报告
SELECT output FROM TABLE(
  DBMS_WORKLOAD_REPOSITORY.ASH_REPORT_HTML(
    l_dbid => (SELECT dbid FROM v$database),
    l_inst_num => 1,
    l_begin_time => SYSDATE - 1/24,
    l_end_time => SYSDATE
  )
);

SQL Monitor

sql
-- 启用SQL监控
ALTER SESSION SET statistics_level = ALL;

-- 执行SQL
SELECT /*+ MONITOR */ *
FROM employees e, departments d
WHERE e.department_id = d.department_id;

-- 查看SQL监控报告
SELECT DBMS_SQL_MONITOR.REPORT_SQL_MONITOR() FROM DUAL;

SQL优化最佳实践

开发阶段优化

  1. 尽早优化:在开发阶段就考虑SQL性能
  2. 使用绑定变量:避免硬解析
  3. 合理使用索引:根据查询模式设计索引
  4. 优化JOIN操作:选择合适的JOIN类型和顺序
  5. 避免复杂SQL:将复杂SQL拆分为多个简单SQL
  6. 使用视图谨慎:避免多层嵌套视图
  7. 限制结果集:使用ROWNUM或FETCH FIRST限制返回行数
  8. **避免使用SELECT ***:只选择必要的列

生产环境优化

  1. 定期收集统计信息:确保优化器有准确的统计信息
  2. 监控SQL性能:识别性能差的SQL
  3. 使用SQL计划管理:稳定执行计划
  4. 考虑分区表:对于大表使用分区
  5. 调整内存参数:合理设置SGA和PGA
  6. 使用并行处理:对于大型操作使用并行
  7. 定期维护索引:重建碎片化的索引
  8. 实施访问控制:限制用户执行的SQL类型

常见问题处理

全表扫描

  • 原因:没有合适的索引,或者索引不可用
  • 解决方案:创建合适的索引,或者使用索引提示

索引失效

  • 原因:在索引列上使用函数,或者使用不等于操作符
  • 解决方案:使用函数索引,或者修改查询条件

绑定变量窥探

  • 原因:优化器使用第一次执行时的绑定变量值生成执行计划
  • 解决方案:使用绑定变量提示,或者使用SQL计划管理

执行计划不稳定

  • 原因:统计信息过时,或者绑定变量值分布不均匀
  • 解决方案:定期收集统计信息,使用SQL计划管理

排序操作过多

  • 原因:查询中包含ORDER BY、GROUP BY等操作
  • 解决方案:创建合适的索引,或者修改排序方式

常见问题(FAQ)

Q1: 如何识别性能差的SQL语句?

A1: 识别性能差的SQL语句的方法:

  • 使用AWR报告:查看Top SQL部分
  • 查询V$SQL视图:按执行时间、逻辑读等排序
  • 使用SQL Monitor:监控正在执行的SQL
  • 设置SQL_TRACE:跟踪SQL执行细节
  • 使用第三方工具:如TOAD、SQL Developer等

示例SQL:

sql
-- 查找执行时间长的SQL
SELECT sql_id, elapsed_time/1000000 AS elapsed_seconds,
       executions, elapsed_time/executions/1000000 AS avg_elapsed_seconds
FROM v$sql
WHERE executions > 5
ORDER BY elapsed_time DESC
FETCH FIRST 10 ROWS ONLY;

-- 查找逻辑读多的SQL
SELECT sql_id, buffer_gets, executions,
       buffer_gets/executions AS avg_buffer_gets
FROM v$sql
WHERE executions > 5
ORDER BY buffer_gets DESC
FETCH FIRST 10 ROWS ONLY;

Q2: 为什么我的SQL语句有时快有时慢?

A2: SQL语句执行时间不稳定的原因:

  • 绑定变量窥探:优化器使用不同的绑定变量值生成不同的执行计划
  • 执行计划变化:统计信息更新或参数变化导致执行计划改变
  • 系统负载:系统负载高时SQL执行变慢
  • 资源竞争:与其他会话竞争CPU、内存或I/O资源
  • 锁竞争:遇到锁等待
  • 缓存状态:第一次执行时缓存未命中

解决方案:

  • 使用绑定变量提示:/*+ BIND_AWARE */
  • 稳定执行计划:使用SQL计划管理
  • 监控系统负载:避开高峰期执行大查询
  • 优化SQL:减少资源使用
  • 检查锁情况:SELECT * FROM v$lock;

Q3: 如何优化包含大量数据的表的查询?

A3: 优化大表查询的方法:

  • 使用分区表:按时间、范围等分区
  • 创建合适的索引:基于查询条件创建索引
  • 使用并行查询:对于大型操作使用并行
  • 限制结果集:只返回必要的数据
  • 使用物化视图:预计算汇总数据
  • 数据归档:将历史数据归档到分区或其他表
  • 使用索引组织表:对于频繁通过主键访问的表
  • 表压缩:减少存储空间和I/O

Q4: 如何优化JOIN操作?

A4: 优化JOIN操作的方法:

  • 选择合适的JOIN类型:小结果集使用嵌套循环连接,大结果集使用哈希连接
  • 优化JOIN顺序:小表驱动大表
  • 确保连接列有索引:特别是嵌套循环连接
  • 使用等值连接:避免非等值连接
  • 减少连接的表数量:只连接必要的表
  • 使用视图:将复杂的JOIN逻辑封装在视图中
  • 考虑使用并行JOIN:对于大型JOIN操作

Q5: 索引越多越好吗?

A5: 不是,索引过多会带来以下问题:

  • 存储空间增加:每个索引都需要存储空间
  • 插入/更新/删除性能下降:每次修改数据时需要维护索引
  • 优化器选择困难:索引过多时,优化器可能选择错误的索引
  • 统计信息收集时间增加:需要收集更多索引的统计信息

合理的索引策略:

  • 只创建必要的索引:基于实际查询模式
  • 避免冗余索引:如同时创建(A)和(A,B)索引
  • 定期审查索引:删除未使用的索引
  • 监控索引使用情况:使用ALTER INDEX ... MONITORING USAGE

Q6: 如何处理执行计划突然变坏的情况?

A6: 处理执行计划变坏的方法:

  • 查看执行计划变化:比较前后执行计划的差异
  • 检查统计信息:确认统计信息是否准确
  • 使用SQL计划管理:从历史计划中选择好的执行计划
  • 使用提示:强制使用特定的执行计划
  • 回滚最近的变更:如果是由于变更导致的
  • 收集系统统计信息:确保优化器有准确的系统信息
  • 检查绑定变量值:确认是否是绑定变量窥探导致的

Q7: 如何优化分组和聚合操作?

A7: 优化分组和聚合操作的方法:

  • 创建合适的索引:包含GROUP BY列
  • 使用并行聚合:对于大型聚合操作
  • 避免在HAVING子句中使用复杂条件:尽量在WHERE子句中过滤
  • 使用ROLLUP或CUBE:替代多个GROUP BY查询
  • 考虑使用分析函数:对于需要同时返回详细数据和聚合结果的情况
  • 使用物化视图:预计算聚合结果
  • 限制分组的列数量:减少GROUP BY子句中的列数量

Q8: 如何优化ORDER BY操作?

A8: 优化ORDER BY操作的方法:

  • 创建排序索引:包含ORDER BY列
  • 使用索引排序:避免排序操作
  • 限制排序的数据量:使用ROWNUM或FETCH FIRST
  • 使用并行排序:对于大型排序操作
  • 考虑存储排序结果:对于频繁执行的相同排序
  • 避免在ORDER BY中使用函数:或者创建函数索引
  • 使用降序索引:对于降序排序

Q9: 如何优化子查询?

A9: 优化子查询的方法:

  • 使用连接替代相关子查询:相关子查询效率较低
  • 合理选择IN和EXISTS:大结果集使用EXISTS,小结果集使用IN
  • 使用WITH子句:提高可读性和性能
  • 避免多层嵌套子查询:尽量减少子查询的嵌套层数
  • 使用标量子查询缓存:对于重复执行的标量子查询
  • 考虑使用临时表:对于复杂的子查询

Q10: 如何监控SQL语句的执行情况?

A10: 监控SQL语句执行情况的方法:

  • 使用V$SQL视图:查看SQL的执行统计信息
  • 使用SQL_TRACE:跟踪SQL执行的详细信息
  • 使用SQL Monitor:实时监控SQL执行情况
  • 使用AWR报告:查看历史SQL性能
  • 使用ASH报告:查看SQL的等待事件
  • 设置会话级别的统计信息ALTER SESSION SET statistics_level = ALL;
  • 使用第三方工具:如TOAD、SQL Developer等

示例监控SQL:

sql
-- 查看SQL执行统计信息
SELECT sql_id, sql_text, executions, elapsed_time/1000000 AS elapsed_seconds,
       buffer_gets, disk_reads, rows_processed
FROM v$sql
WHERE sql_text LIKE '%employees%'
ORDER BY elapsed_time DESC;

-- 查看SQL的等待事件
SELECT sql_id, event, total_waits, time_waited_micro/1000000 AS time_waited_seconds
FROM v$active_session_history
WHERE sql_id = '7t4b3d2x4y5z6'
GROUP BY sql_id, event, total_waits, time_waited_micro
ORDER BY time_waited_micro DESC;