暂无图片
暂无图片
9
暂无图片
暂无图片
暂无图片

SQL优化分析思路方法【实用干货】

原创 lu9up的数据库笔记 2025-01-07
1149

1 说明

值班的时候接收到某核心库节点连接数较高告警,上去查看等待事件挺正常,顺便查一下topsql,其中一个语句占用了大量cpu资源。
image.png

常用脚本都放在了 5 常用脚本 ,如需,自取。

下面开始进行分析,由于是生产数据,截图经过了脱敏处理,重点在分析方法。

2 分析思路

2.1 基本思路

cpu资源占用比较厉害的语句一般有以下几种:

  1. 全表扫描
  2. 低效sql语句
  3. 排序
  4. 重复解析
  5. 高并发

最常见的就是前面两种了,与之再配合上第5项高并发,通常会导致的cpu资源占用较高。

2.2 全表扫描

全表扫描其实挺正常的,不要觉得说走了全表就是错的,还要根据返回的数据量考虑,当返回的数据量比较多,走全表扫描多块读的效率是比走索引单块读高的

因此,看到全表扫描,就要分析一下全表扫描步骤数据集占整个表的比例,一般建议返回表中总数5%以内的数据,走索引扫描,超过5%走全表扫描

不过,5%只是一个参考值,适用于大部分场景,具体问题具体分析。

2.3 低效sql

低效sql这个概念听起来比较空旷,因为没有一个具体的解释去定义什么是慢sql,一般是应用测反馈变慢了,或者本来就慢。
一般来说最常见的就是走错执行计划,常见原因有:

  1. 统计信息过期
  2. 索引失效或缺少合适的索引
  3. 表连接顺序不对
  4. sql写法

3 分析过程

3.1 查看sql基本信息

一、sqltext

根据sql_id查gv$sql视图,获取执行实例id、sql_text和plan_hash_value等。

可以看到,此sql是个查询语句,比较简单的单表查询,使用了绑定变量。

image.png

下面是经过脱敏后的sql_text:

select t.* from lu9up.tab01 t where :"SYS_B_0" = :"SYS_B_1" and hand_notes = :"SYS_B_2" and mod(PS_ID, :"SYS_B_3") = :"SYS_B_4" and rownum <= :"SYS_B_5" order by t.create_date, t.ps_id
复制

注意:实例5上执行的,后面步骤登录实例5排查,要不然执行计划查不到。

二、当前会话和等待事件

gv$session,获取sid、执行时间、当前等待事件和会话状态。

有14个会话同时在执行,上一次执行时间是2秒前,并发量不少。当前等待事件正常:SQL* NET message to client

image.png

三、每个快照内 sql执行统计

dba_hist_sqlstat,统计每个快照时间内cpu耗时、执行次数、平均返回行数、平均执行时间等。

语句执行次数较多,每分钟300+次,cpu占用较高,但是返回行数为0?怀疑是统计信息过期或者表为空。

image.png

四、历史等待事件汇总

查看gv$active_session_history视图,有严重的gc等待,当一个实例需要访问另一个实例缓冲区中的数据块时,如果该数据块正在被其他进程使用(例如,正在被修改或读取),也就是数据块争用,则会触发 “gc buffer busy acquire” 等待事件。

目前来看,判断是并发量高以及sql执行缓慢引起的等待。

image.png

五、sql执行性能汇总

查看gv$sql视图,汇总平均执行时间、平均cpu占用时间、平均返回行数、总执行次数等。

语句平均执行时间2秒,平均cpu占用0.642秒,总执行次数436879,每天执行2万次。。。

image.png

3.2 查看执行计划

使用dbms_xplan包和sql monitoring report查看真实的执行计划。走了全表扫描,返回数据预估1条,实际0条,开销比较大54141,执行时间是37s,其中io等待占6.95秒,集群等待占28秒,时间基本都花在这两个上面了。

dbms_xplan包

image.png

sql_monitor

image.png

根据monitor report将变量值代入:

select t.* from lu9up.tab01 t where 1 = 1 and hand_notes = 782 and mod(PS_ID, 1) = 0 and rownum <= 100 order by t.create_date, t.ps_id
复制

3.3 查看统计信息

一、表

统计信息未过期,空表。

image.png

二、索引

索引的统计信息也没过期,索引字段是ps_id,不少唯一索引。

这个索引对于此sql无效,因为相关谓词条件是mod(PS_ID, :“SYS_B_3”) = :“SYS_B_4”,这种写法本身返回的数据量就会很大,并且函数内部使用了绑定变量,索引并不好创建。

image.png

image.png

image.png

三、字段选择性

空表,无有效信息。

image.png

3.4 查看高水位

高水位未下降,这就是为什么空表但是查询性能缓慢的原因了,是一个优化项。

image.png

4 优化方法

根据前面的分析数据,此sql虽然是空表,但由于高水位的原因,执行效率比较低,且高并发,导致不必要的buffer被请求访问,增加了buffer busy的机会,造成了有严重的gc等待,数据块争用占用了比较多的cpu资源。

因此,需要想办法提升sql的执行效率,减少数据块争用,降低gc等待,减轻系统负担,降低占用资源。

4.1 降低高水位

一、空表

空表直接用truncate table的方式能够快速降低高水位。本次案例的表为空,因此可以采取此办法:

truncate table lu9up.tab01;
复制

二、非空表

如果遇到了非空表,则需要将表move来降低水位线:

--在原来表空间内move alter table lu9up.tab01 move; --move到其他表空间(tbs02为其他表空间) alter table lu9up.tab01 move tablespace tbs02 parallel 8;
复制

需要注意的地方:

  • move 需要额外(一倍)的空间。
  • move 过程中会锁表,其他用户不能在该表上做 DML 或 DDL 操作。
  • move 之后,相关索引都不可用了,表上的索引需要重建。

索引重建:

alter index lu9up.ind_ps_id rebuild online parallel 8 nologging; alter index lu9up.ind_ps_id noparallel;
复制

4.2 创建合适索引

由于表是空的,不知道具体的数据是怎么样的,当有新数据进来后,可以尝试创建以下组合索引:

create index lu9up.ind_hand_notes on lu9up.tab01(hand_notes,create_date,ps_id) parallel 8 nologging; alter index lu9up.ind_hand_notes noparallel;
复制

如果没有效果,删了便是。。。

索引测试小技巧可以先在我们个人用户下ctas创建相同的测试表,然后基于此测试表创建索引去验证性能。因为在生产环境,不能随便给业务表创建索引,我们经过充分测试后,再交由应用测验证后,才能执行索引的创建,生产创建索引的动作最好也是应用测执行。

4.3 应用优化

gc等待在RAC数据库,同一数据在不同数据库实例上被请求访问。建议不同的应用功能/模块数据分布在不同的数据库实例上被访问,避免同一数据被多个实例交叉访问,可以减少buffer的争用,避免gc等待。

即在rac集群中多创建几个service(服务),每个service就单独配置在一个实例上,不要配置多个实例,要不然依旧解决不了gc的问题。这样,某个应用程序就会集中访问某个实例,可以避免跨实例的buffer的争用。但是也要注意service的分配均衡的问题。

5 常用脚本

--0 topsql 分钟 select ash.sql_id, sum(decode(ash.session_state,'on cpu',1,0)) "cpu", sum(decode(ash.session_state,'waiting',1,0)) - sum(decode(ash.session_state,'waiting',decode(en.wait_class,'user i/o',1,0),0)) "wait", sum(decode(ash.session_state,'waiting',decode(en.wait_class,'user i/o',1,0),0)) "io", sum(decode(ash.session_state,'on cpu',1,1)) "total" from v$active_session_history ash,v$event_name en where sql_id is not null and en.event# = ash.event# and ash.sample_time > sysdate -&min/(24*60) group by ash.sql_id order by total desc; --1 sql_text select * from gv$sql where sql_id = '&sql_id'; --2 当前会话和等待事件 select inst_id,sid,sql_exec_start,prev_exec_start,event from gv$session where sql_id = '&sql_id' order by sid; --3 每个快照内 sql执行统计 select a.snap_id, to_char(b.end_interval_time, 'yyyymmdd hh24:mi') as snap_date, a.instance_number as inst, sql_id, plan_hash_value as phv, round((cpu_time_delta) / 1e6) as cpu_s, (executions_delta) as execs, round((BUFFER_GETS_DELTA) / greatest((executions_delta), 1)) as Buf_pe, round((ROWS_PROCESSED_DELTA) / greatest((executions_delta), 1), 1) as Row_pe, case when (ELAPSED_TIME_DELTA) / 1e3 / greatest((executions_delta), 1) < 1 then round((ELAPSED_TIME_DELTA) / 1e3 / greatest((executions_delta), 1), 2) else round((ELAPSED_TIME_DELTA) / 1e3 / greatest((executions_delta), 1)) end as ms_pe, round((DISK_READS_DELTA)) as DISK_R, round((IOWAIT_DELTA) / 1e6) as IO_s, round((APWAIT_DELTA) / 1e3) as App_ms from dba_hist_sqlstat a, dba_hist_snapshot b where a.snap_id = b.snap_id and a.instance_number = b.instance_number and b.end_interval_time > sysdate - &min/(24*60) and sql_id = '&sql_id' and a.instance_number = 6 order by snap_id desc; --4 历史等待事件汇总 select * from (select inst_id, event, count(*) cnt from gv$active_session_history where sql_id = '&sql_id' group by inst_id, event order by cnt desc) where rownum <= 5; --5 历史执行计划,需要在sql执行的实例查看 select * from table(dbms_xplan.display_awr('&sql_id',format=>'all')); --advanced all typical --6 当前执行计划,需要在sql执行的实例查看 select * from table(dbms_xplan.display_cursor('&sql_id',null,'all')); --advanced all typical --7 sql monitoring report,需要在sql执行的实例查看 select dbms_sqltune.report_sql_monitor(sql_id => '&sql_id',type => 'html') from dual; --8 历史sql执行性能汇总 SELECT ROWNUM as rn, t.plan_hash_value, TO_CHAR(t.avg_elapsed_time_secs, '9999990D990') as avg_etime_s, TO_CHAR(t.avg_cpu_time_secs, '9999990D990') as avg_cpu_s, TO_CHAR(t.avg_user_io_wait_time_secs, '99999999999990D990') as avg_iowait_s, t.avg_buffer_gets as avg_buffers, t.avg_disk_reads as avg_reads, t.avg_direct_writes, t.avg_rows_processed as avg_rows, t.delta_executions as execs, t.min_optimizer_cost as min_cost, DECODE(t.min_optimizer_cost, t.max_optimizer_cost, NULL, t.max_optimizer_cost) as max_cost, TO_CHAR(t.first_snap_time, 'YYYY-MM-DD HH24:MI:SS') as first_snap, TO_CHAR(t.last_snap_time, 'YYYY-MM-DD HH24:MI:SS') as last_snap FROM ( SELECT /*+ NO_MERGE */ h.plan_hash_value, ROUND((SUM(h.elapsed_time_delta)/SUM(GREATEST(h.executions_delta, 1))) / 1e6, 3) avg_elapsed_time_secs, ROUND((SUM(h.cpu_time_delta)/SUM(GREATEST(h.executions_delta, 1))) / 1e6, 3) avg_cpu_time_secs, ROUND((SUM(h.iowait_delta)/SUM(GREATEST(h.executions_delta, 1))) / 1e6, 3) avg_user_io_wait_time_secs, ROUND(SUM(h.buffer_gets_delta)/SUM(GREATEST(h.executions_delta, 1))) avg_buffer_gets, ROUND(SUM(h.disk_reads_delta)/SUM(GREATEST(h.executions_delta, 1))) avg_disk_reads, ROUND(SUM(h.direct_writes_delta)/SUM(GREATEST(h.executions_delta, 1))) avg_direct_writes, ROUND(SUM(h.rows_processed_delta)/SUM(GREATEST(h.executions_delta, 1))) avg_rows_processed, SUM(GREATEST(h.executions_delta, 1)) delta_executions, MIN(h.optimizer_cost) min_optimizer_cost, MAX(h.optimizer_cost) max_optimizer_cost, MIN(s.end_interval_time) first_snap_time, MAX(s.end_interval_time) last_snap_time FROM dba_hist_sqlstat h, dba_hist_snapshot s WHERE h.sql_id = '&sql_id' AND h.snap_id = s.snap_id AND h.dbid = s.dbid AND h.instance_number = s.instance_number GROUP BY h.plan_hash_value ORDER BY 2) t; --9 当前sql执行性能汇总 SELECT ROWNUM as rn, t.plan_hash_value, TO_CHAR(t.avg_elapsed_time_secs, '9999990D990') as avg_etime_s, TO_CHAR(t.avg_cpu_time_secs, '9999990D990') as avg_cpu_s, TO_CHAR(t.avg_user_io_wait_time_secs, '99999999999990D990') as avg_iowait_s, t.avg_buffer_gets as avg_buffers, t.avg_disk_reads as avg_reads, t.avg_direct_writes, t.avg_rows_processed as avg_rows, t.total_executions as total_exec, DECODE(t.min_optimizer_cost, t.max_optimizer_cost, NULL, t.max_optimizer_cost) as max_cost, t.first_load_time, t.last_load_time, TO_CHAR(t.last_active_time, 'YYYY-MM-DD HH24:MI:SS') as last_active FROM ( SELECT /*+ NO_MERGE */ plan_hash_value, ROUND((SUM(elapsed_time)/SUM(GREATEST(executions, 1))) / 1e6, 3) avg_elapsed_time_secs, ROUND((SUM(cpu_time)/SUM(GREATEST(executions, 1))) / 1e6, 3) avg_cpu_time_secs, ROUND((SUM(user_io_wait_time)/SUM(GREATEST(executions, 1))) / 1e6, 3) avg_user_io_wait_time_secs, ROUND(SUM(buffer_gets)/SUM(GREATEST(executions, 1))) avg_buffer_gets, ROUND(SUM(disk_reads)/SUM(GREATEST(executions, 1))) avg_disk_reads, ROUND(SUM(direct_writes)/SUM(GREATEST(executions, 1))) avg_direct_writes, ROUND(SUM(rows_processed)/SUM(GREATEST(executions, 1))) avg_rows_processed, SUM(GREATEST(executions, 1)) total_executions, MIN(optimizer_cost) min_optimizer_cost, MAX(optimizer_cost) max_optimizer_cost, MIN(first_load_time) first_load_time, MAX(last_load_time) last_load_time, MAX(last_active_time) last_active_time FROM gv$sql WHERE sql_id = '&sql_id' GROUP BY plan_hash_value ORDER BY 2) t; --10 表统计信息 select * from dba_tab_statistics where owner = upper('&table_owner') and table_name = upper('&table_name') --表数据变化情况 select * from dba_tab_modifications where table_owner = upper('&table_owner') and table_name = upper('&table_name'); --重新收集表统计信息 /* begin dbms_stats.gather_table_stats(ownname => '&table_owner', tabname => '&table_name', estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE, method_opt => 'for all columns size repeat', --auto/repeat no_invalidate => FALSE, degree => 4, granularity => 'AUTO', cascade => true); end; */ --11 索引统计信息 select * from dba_ind_statistics where owner = upper('&table_owner') and table_name = upper('&table_name') --索引字段 select * from dba_ind_columns where table_owner = upper('&table_owner') and table_name = upper('&table_name') --12 字段统计信息(字段选择性、直方图类型直方图桶数) select b.owner,b.table_name,a.column_name,a.num_distinct,b.num_rows, case when b.num_rows = 0 then 'num_rows = 0' else round(a.num_distinct / b.num_rows * 100, 2) || '%' end selectivity, a.histogram,a.num_buckets from dba_tab_col_statistics a, dba_tables b where a.owner = b.owner and a.table_name = b.table_name and b.owner = upper('&table_owner') and b.table_name = upper('&table_name'); --13 高水位 select a.OWNER,a.TABLE_NAME,a.TABLESPACE_NAME,a.NUM_ROWS,a.BLOCKS,a.EMPTY_BLOCKS,a.NUM_ROWS * a.AVG_ROW_LEN real_bytes,b.BYTES from dba_tables a,dba_segments b where a.OWNER = b.owner and a.TABLE_NAME = b.segment_name and a.OWNER = upper('&table_owner') and a.TABLE_NAME = upper('&table_name');
复制

6 总结

虽然本篇是关于空表sql分析,但是方法都是通用的,适用于其他topsql场景。

最后修改时间:2025-01-07 09:50:01
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

文章被以下合辑收录

评论

目录
  • 1 说明
  • 2 分析思路
  • 2.1 基本思路
  • 2.2 全表扫描
  • 2.3 低效sql
  • 3 分析过程
    • 3.1 查看sql基本信息
    • 3.2 查看执行计划
    • 3.3 查看统计信息
    • 3.4 查看高水位
  • 4 优化方法
    • 4.1 降低高水位
    • 4.2 创建合适索引
    • 4.3 应用优化
  • 5 常用脚本
  • 6 总结