外观
Oracle SQL语句优化
SQL优化基础
SQL执行过程
- 解析:将SQL语句转换为解析树
- 优化:生成执行计划
- 执行:执行SQL语句
- 获取结果:返回查询结果
优化器类型
- 基于规则的优化器(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优化最佳实践
开发阶段优化
- 尽早优化:在开发阶段就考虑SQL性能
- 使用绑定变量:避免硬解析
- 合理使用索引:根据查询模式设计索引
- 优化JOIN操作:选择合适的JOIN类型和顺序
- 避免复杂SQL:将复杂SQL拆分为多个简单SQL
- 使用视图谨慎:避免多层嵌套视图
- 限制结果集:使用ROWNUM或FETCH FIRST限制返回行数
- **避免使用SELECT ***:只选择必要的列
生产环境优化
- 定期收集统计信息:确保优化器有准确的统计信息
- 监控SQL性能:识别性能差的SQL
- 使用SQL计划管理:稳定执行计划
- 考虑分区表:对于大表使用分区
- 调整内存参数:合理设置SGA和PGA
- 使用并行处理:对于大型操作使用并行
- 定期维护索引:重建碎片化的索引
- 实施访问控制:限制用户执行的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;