性能优化

一、前言

1.1 概述

性能优化是指在不影响系统运行正确性的前提下,使之运行地更快,完成特定功能所需的时间更短。达梦数据库性能优化主要包含以下内容:

  • 数据库架构优化
  • 数据库参数优化
  • SQL 优化
  • 统计信息

1.2 工具与术语

数据库性能优化中可能使用到的相关工具:

  • 达梦 SQL 日志分析工具 DMLOG:通过分析数据库的 SQL 日志文件,直观地反映 SQL 执行情况。
  • 命令行调试工具 dmdbg:DM 数据库安装目录的“bin”子目录下可找到 dmdbg 执行程序,可调试直接执行的 DMSQL 程序或非 DDL 的 SQL 语句。
  • DM 性能监控工具 Monitor:Monitor 是 DM 系统管理员用来监视服务器的活动和性能情况的客户端工具。它允许系统管理员在本机或远程监控服务器的运行状况,并根据系统情况对系统参数进行调整,以提高系统效率。

数据库性能优化中可能使用到的相关术语:

  • 通配符:通配符是一种特殊语句,主要有星号 (*) 和问号 (?),用来模糊搜索文件。
  • 回滚段 (rollback segments):用于临时存储数据库还原信息。
  • 统计信息:对象统计信息描述了对象数据的分布特征。统计信息是优化器的代价计算的依据,可以帮助优化器较精确地估算成本,对执行计划的选择起着至关重要的作用。
  • CBO(基于代价的优化器):它是看语句的代价,这里的代价主要指 cpu 和内存。优化器在判断是否用这种方式时,主要参照的是表及索引的统计信息,统计信息给出表的大小、多少行、每行的长度等信息。
  • 执行计划:执行计划是一条 SQL 语句在数据库中的执行过程或访问路径的描述。

1.3 适用范围

本文中所涉及内容适用于 DM7 及 DM8 版本数据库产品。

二、数据库架构优化

达梦数据库提供多种数据库架构,用于解决多种场景的数据库安全、性能等问题。根据业务的特性,选择合适的数据库架构非常必要。

数据库架构 架构介绍 架构特性
数据守护集群
DMDataWatch
DMDataWatch 是一种高可用数据库解决方案,主备节点间通过日志同步来保证数据的同步,可以实现数据库快速切换与灾难性恢复,满足用户对数据安全性和高可用性的需求,提供不间断的数据库服务。 实时保证数据完全一致,备库支持临时表,故障秒级切换。
读写分离集群
DMRWC
DMRWC 在保障主库和备库事务强一致的前提下,开创性地在接口层(JDBC、DPI 等)将只读操作自动分流到备库,有效降低主库的负载,提升系统吞吐量,适用于读多写少的业务场景。 事务自动分发,OA 办公系统的最佳选择,高可用性。
数据共享集群
DMDSC
DMDSC 是一个多实例、单数据库的系统。主要由数据库和数据库实例、共享存储、本地存储、通信网络、以及集群控制软件 DMCSS 组成,允许多个数据库实例同时访问、获得完整的数据库服务。 金融级高可用,自动负载均衡,应用高效迁移,高性能存储管理,企业级容灾,全面支持国产平台。
新一代分布式集群
DMDPC
DMDPC 同时支持在线分析处理和在线事务处理,具备高可用、高扩展、高性能、高吞吐量、继承了 DM8 良好的兼容性,应用无需改造即可迁移到 DMDPC。 高可用,高可扩展,高性能,高吞吐量,透明易用。

三、数据库参数优化

3.1 INI 参数配置说明

参数 含义 优化建议
MEMORY_POOL 共享内存池大小,以 M 为单位。 高并发时应调大,避免频繁向 OS 申请内存。
MEMORY_N_POOLS 共享内存池个数,减少内存临界区冲突。 设置较大会导致启动时报错申请内存失败。
BUFFER 系统缓冲区大小,以 M 为单位。 如果数据量小于内存,则设置为数据量大小;否则设置为总内存的 2/3 比较合适。
BUFFER_POOLS BUFFER 系统分区数,每个 BUFFER 分区的大小为 BUFFER/BUFFER_POOLS。取值范围 1~10000。 并发较大的系统需要配置该参数,减少数据缓冲区并发冲突。
RECYCLE RECYCLE 缓冲区大小,以 M 为单位。 高并发或大量使用 with、临时表、排序等时,可以将值调大。
DICT_BUF_SIZE 字典缓冲区大小,以 M 为单位。 如果数据库中对象数量较多,或者存在大量分区表,可适当调大。
HJ_BUF_GLOBAL_SIZE HASH 连接操作符的数据总缓存大小(>= HJ_BUF_SIZE),系统级参数,以 M 为单位。 内存足够的情况下,可以适当调大。实际使用大小,由包含 HASH JOIN 操作符的 SQL 并发数决定。
HJ_BUF_SIZE 单个 HASH 连接操作符的数据总缓存大小,以 M 为单位。 在 OLTP 环境中,建议采用默认值。在 OLAP 环境下,可以根据参与 HASH JOIN 的数据量调大。
HAGR_BUF_GLOBAL_SIZE HAGR、DIST、集合操作、SPL2、NTTS2 以及 HTAB 操作符的数据总缓存大小(>= HAGR_BUF_SIZE),系统级参数,以 M 为单位。 高并发、大量的聚集操作如 sum 等,可适当调大。
HAGR_BUF_SIZE 单个 HAGR、DIST、集合操作、SPL2、NTTS2 以及 HTAB 操作符的数据总缓存大小,以 M 为单位。 监控 V$SORT_HISTORY,判断是否需要调整,有大表的 hash 分组应调大。
WORKER_THREADS 工作线程的数目。有效值范围(1~64)。 建议设置为 cpu 核数或其两倍。
ENABLE_MONITOR 用于打开或者关闭系统的监控功能。1:打开;0:关闭。 性能优化是设置为 3,运行时设置为 2。
OLAP_FLAG 启用联机分析处理。0:不启用;1:启用;2:不启用,同时倾向于使用索引范围扫描。 该参数会影响到计划的生成。在 OLTP 环境下,通常保持默认值 2。
SORT_BUF_SIZE 原排序机制下,排序缓存区最大值,以 M 为单位。 建索引时可以适当调大,通常不超过 20M。
TOP_ORDER_OPT_FLAG 优化带有 TOP 和 ORDER BY 子句的查询,使得 SORT 操作符可以省略。 优化的效果是尽量使得 ORDER BY 的排序列所对应的基表可以使用包含排序列的索引,从而可以移除排序 SORT 操作符,减少排序操作。如果排序列不属于同一个基表,或者排序列不是基表列,则无法进行优化。
MAX_OPT_N_TABLES 优化器在处理连接时,一次能优化的最大表连接个数。默认 6,取值范围 3~8 。 在表关联过多但结果集返回较少的场景可能有作用,减少该值可能会调整表关联之间顺序。
MAX_OPT_N_OR_BEXPS 能参与优化的最大 OR 分支个数,取值范围 7~64 。 当查询 or 分支过多时可以考虑调大概值,确保 or 的条件能够根据实际情况正确访问。
SUBQ_CVT_SPL_FLAG 控制相关子查询的实现方式,0:不优化;1:使用 SPL2 方式实现相关子查询;2:DBLINK 相关子查询是否转换为函数,由参数 ENABLE_DBLINK_TO_INV 取值决定;4:将多列 IN 转换为 EXISTS;8:将引用列转换为变量 VAR;16:用临时函数替代查询项中的相关查询表达式;32:存储过程、语句块中的多列表达式过滤条件含有非相关子查询时转换为连接。支持使用上述有效值的组合值,如 5 表示同时进行 1 和 4 的优化。 /
OPTIMIZER_OR_NBEXP OR 表达式的优化方式。0:不优化;1:生成 UNION_FOR_OR 操作符时,优化为无 KEY 比较方式;2:OR 表达式优先考虑整体处理方式;4:相关子查询的 OR 表达也优先考虑整体处理方式;8:OR 布尔表达式的范围合并优化;16:同一列上同时存在常量范围过滤和 IS NULL 过滤时的优化,如 C1 > 5 OR C1 IS NULL。支持使用上述有效值的组合值,如 7 表示同时进行 1、2、4 的优化。 在 WHERE 后有 OR 条件的情形使用,可以将多个条件过滤合并成一次过滤,减少分支数从而降低表扫描行数。
ENABLE_RQ_TO_NONREF_SPL 0:不启用该优化;1:对查询项中出现的相关子查询表达式进行优化处理;2:对查询项和 WHERE 表达式中出现的相关子查询表达式进行优化处理;4:相关查询采用 SPL 方式去相关性后,可以作为单表过滤条件。支持使用上述有效值的组合值,如 3 表示同时进行 1 和 2 的优化。 相关查询表达式转化为非相关查询表达式,目的在于相关查询表达式的执行处理由之前的平坦化方式转化为一行一行处理。
VIEW_PULLUP_FLAG 是否对视图进行上拉优化,把视图转换为其原始定义,消除视图。 优化的原理是直接将过滤条件下推到表上过滤。
FILTER_PUSH_DOWN 对单表条件是否下放的不同处理方式。0:表示条件不下放;2:表示在新优化器下对外连接、半连接进行下放条件优化处理;4:表示语义分析阶段考虑单表过滤条件的选择率,超过 0.5 则不下放,由后面进行代价计算选择是否下放,参数值 4 仅在参数取值包含 2 时有效,即将参数值设为 6 时有效;8:表示尝试将包含非相关子查询的布尔表达式进行下放;16:表示 where 条件尽可能下放,即便 where 之后还有 rownum、order 等操作。支持使用上述有效值的组合值,如 6 表示同时进行 2 和 4 的优化。 /
ENABLE_HASH_JOIN 是否允许使用哈希连接,0:不允许;1:允许。 尽可能使得执行计划只能选择走嵌套循环方式。
ENABLE_INDEX_JOIN 是否允许使用索引连接,0:不允许;1:允许。 有些场景走索引连接反而效果更差,直接走 NEST LOOP INNER JOIN2,多见存在子查询场景。

3.2 参数修改方法

参数分为:静态、动态、手动。如下表所示:

参数 描述
静态 可以被动态修改,需重启服务器生效。
动态 可以被动态修改,修改后即时生效;动态分为会话级和系统级;会话级:新参数值只影响新创建的会话,之前的会话不受影响;系统级:修改后会影响所有会话。
手动 不能动态修改,只能修改 dm.ini 然后重启。

3.2.1 调用系统过程

1. 参数查询

(1)查询数值类型参数值。

--语法格式:
select SF_GET_PARA_VALUE (scope int, paraname varchar(256));
--SCOPE 参数为 1 表示获取 INI 文件中配置参数的值
--SCOPE 参数为 2 表示获取内存中配置参数的值

--例如:获取 DM.INI 文件中动态参数 HFS_CACHE_SIZE 的当前值
select SF_GET_PARA_VALUE (1,'HFS_CACHE_SIZE');

(2)查询浮点型参数值。

--语法格式
select SF_GET_PARA_DOUBLE_VALUE(scope int, paraname varchar(8187));
--SCOPE 参数为 1 表示获取 INI 文件中配置参数的值
--SCOPE 参数为 2 表示获取内存中配置参数的值

--例如:获取内存中 参数SEL_RATE_EQU 的当前值
select SF_GET_PARA_DOUBLE_VALUE(2,'SEL_RATE_EQU');

(3)查询字符串类型参数值。

--语法格式
select SF_GET_PARA_STRING_VALUE(scope int, paraname varchar(8187));
--SCOPE 参数为 1 表示获取 INI 文件中配置参数的值
--SCOPE 参数为 2 表示获取内存中配置参数的值

--例如:获取 DM.INI 文件中动态参数 SQL_TRACE_MASK 的当前值
select SF_GET_PARA_STRING_VALUE(1,'SQL_TRACE_MASK');

(4)获得当前会话的某个会话级 INI 参数的值。

--语法格式
select SF_GET_SESSION_PARA_VALUE (paraname varchar(8187));
--例如:获取当前会话 USE_HAGR_FLA 参数的值
select SF_GET_SESSION_PARA_VALUE ('USE_HAGR_FLAG');

2. 参数修改

(1)修改整型静态配置参数和动态配置参数。

--语法格式
SP_SET_PARA_VALUE (scope int, paraname varchar(256), value int64);
--SCOPE 参数为 1 表示在内存和 INI 文件中都修改参数值,此时只能修改动态的配置参数。当 SCOPE 等于 1,试图修改静态配置参数时服务器会返回错误信息
--SCOPE 参数为 2 表示只在 INI 文件中修改配置参数,此时可用来修改静态配置参数和动态配置参数

--例如:将 DM.INI 文件中动态参数 HFS_CACHE_SIZE 设置为 320,在 disql 中执行以下命令即可立即生效
SP_SET_PARA_VALUE (1,'HFS_CACHE_SIZE',320);

(2)修改浮点型静态配置参数和动态配置参数。

--语法格式
SP_SET_PARA_DOUBLE_VALUE(scope int,paraname varchar(8187),value double);
--SCOPE 参数为 1 表示在内存和 INI 文件中都修改参数值,此时只能修改动态的配置参数。当 SCOPE 等于 1,试图修改静态配置参数时服务器会返回错误信息
--SCOPE参数为 2 表示只在 INI 文件中修改配置参数,此时可用来修改静态配置参数和动态配置参数

--例如:将 DM.INI 文件中动态参数 SEL_RATE_EQU 设置为 0.3,在 disql 中执行以下命令即可立即生效
SP_SET_PARA_DOUBLE_VALUE(1, 'SEL_RATE_EQU', 0.3);

(3)修改系统整型、double、 varchar 的静态配置参数或动态配置参数。

--语法格式
SF_SET_SYSTEM_PARA_VALUE(paraname varchar(256),value int64\double\varchar(256),deferred int,scope int64);
--DEFERRED 参数为 0 表示当前 session 修改的参数立即生效,默认为 0
--DEFERRED 参数为 1 表示当前 session 不生效,后续再生效
--SCOPE 参数为 1 表示在内存和 INI 文件中都修改参数值,此时只能修改动态的配置参数
--SCOPE 参数为 2 表示只在 INI 文件中修改配置参数,此时可用来修改静态配置参数和动态配置参数

--例如:将动态参数 ENABLE_DDL_ANY_PRIV 设置为 1,且当前 session 立即生效
select SF_SET_SYSTEM_PARA_VALUE('ENABLE_DDL_ANY_PRIV',1,0,1);

(4)修改某个会话级 INI 参数的值,设置的参数值只对本会话有效。

--语法格式
SF_SET_SESSION_PARA_VALUE (paraname varchar(8187), value bigint);
--例如:将 USE_HAGR_FLAG 设置为 1,且只对本会话有效
select SF_SET_SESSION_PARA_VALUE ('USE_HAGR_FLAG',1); 

(5)重置某个会话级 INI 参数的值,使得这个 INI 参数的值和系统 INI 参数的值保持一致。

--语法格式
SP_RESET_SESSION_PARA_VALUE (paraname varchar(8187));
--例如:重置 USE_HAGR_FLAG
select SP_RESET_SESSION_PARA_VALUE ('USE_HAGR_FLAG');

3.2.2 ALTER 命令修改

修改系统参数:

--语法格式
ALTER SYSTEM SET ‘<参数名称>’ =<参数值> [DEFERRED] [MEMORY|BOTH|SPFILE];

--静态参数修改
ALTER SYSTEM SET ‘MTAB_MEM_SIZE’ =1200 spfile;
--PURGE关键字指是否清理执行计划
ALTER SESSION SET ‘<参数名称>’ =<参数值> [PURGE];
--修改当前会话参数
ALTER SESSION SET ‘HAGR_HASH_SIZE’ =2000000;

3.2.3 修改 INI 文件

dm.ini 文件一般情况下位于数据库实例路径下,可以通过 vi dm.ini 命令修改。修改完成后,可以通过 v$dm_ini 或者 v$parameter 查询参数值。例如:

--查询v$dm_ini
select * from v$dm_ini where para_name LIKE 'PK_WITH%';
--查询v$parameter
select * from v$parameter where name LIKE 'PK_WITH%'

四、SQL 优化

4.1 定位慢 SQL

定位执行效率低的 SQL 语句是 SQL 优化的第一步。待优化的 SQL 可大致分为两类:

  • SQL 执行时间在十几秒到数十秒之间,但执行频率不高,此类 SQL 对数据库整体性能影响并不大,可以放到最后进行优化。
  • SQL 单独执行时间可能很快,在几百毫秒到几秒之间,但执行频率非常高,甚至达到每秒上百次,高并发下执行效率降低,很可能导致系统瘫痪,此类 SQL 是优化的首要对象。

以下介绍两种定位慢 SQL 的方法,可记录下具体 SQL 语句以及对应执行时间,为后续 SQL 优化工作奠定基础。

4.1.1 开启跟踪日志记录

跟踪日志文件是一个纯文本文件,以”dmsql_实例名_日期_时间命名.log”,默认生成在 DM 安装目录的 log 子目录下。跟踪日志内容包含系统各会话执行的 SQL 语句、参数信息、错误信息、执行时间等。跟踪日志主要用于分析错误和分析性能问题,基于跟踪日志可以对系统运行状态进行分析。

1. 跟踪日志记录配置

(1)配置 dm.ini 文件,设置 SVR_LOG = 1 以启用 sqllog.ini 配置,该参数为动态参数,可通过调用数据库函数直接修改,如下所示:

SP_SET_PARA_VALUE(1,'SVR_LOG',1);

(2)配置数据文件目录下的 sqllog.ini 文件。

[dmdba@localhost DAMENG]$ cat sqllog.ini 
BUF_TOTAL_SIZE  = 10240  #SQLs Log Buffer Total Size(K)(1024~1024000)
BUF_SIZE        = 1024   #SQLs Log Buffer Size(K)(50~409600)
BUF_KEEP_CNT    = 6      #SQLs Log buffer keeped count(1~100)

[SLOG_ALL]
    FILE_PATH       = ../log
    PART_STOR       = 0
    SWITCH_MODE     = 1
    SWITCH_LIMIT    = 100000
    ASYNC_FLUSH     = 0
    FILE_NUM        = 200
    ITEMS           = 0
    SQL_TRACE_MASK  = 2:3:23:24:25
    MIN_EXEC_TIME   = 0
    USER_MODE       = 0
    USERS           =
注意

为避免记录 SQL log 对服务器产生较大的影响,可以配置异步日志刷新(参数 ASYNC_FLUSH 设置为 1)。

(3)如果对 sqllog.ini 进行了修改,可通过调用以下函数即时生效,无需重启数据库,如下所示:

SP_REFRESH_SVR_LOG_CONFIG();

(4)各配置项详细说明如下表所示:

参数名 缺省值 说明
SQL_TRACE_MASK 1 LOG 记录的语句类型掩码,是一个格式化的字符串,表示一个 32 位整数上哪一位将被置为 1,置为指定 SQL 日志中需要被记录的语句类型。指定方式为 SQL_TRACE_MASK=位号:位号:位号……。
如:3:5:7 表示第 3,第 5,第 7 位号代表的类型需要被记录在 SQL 日志中。
以下位号均可以单独使用,也可以搭配使用,其中位号 2~17 与 23、24、25、26、28 搭配使用时,表示取其交集。
例如:SQL_TRACE_MASK=2 表示记录 DML 语句;SQL_TRACE_MASK=24 表示记录执行语句;SQL_TRACE_MASK=29 表示记录事务相关语句;SQL_TRACE_MASK=2:3:24:29 表示记录 DML 和 DDL 的执行语句以及事务相关语句。
位号的含义如下所示:
1 全部记录(全部记录并不包含原始语句)
2 全部 DML 类型语句
3 全部 DDL 类型语句 
4 UPDATE 类型语句(更新)
5 DELETE 类型语句(删除)
6 INSERT 类型语句(插入)
7 SELECT 类型语句(查询)
8 COMMIT 类型语句(提交)
9 ROLLBACK 类型语句(回滚)
10 CALL 类型语句(过程调用)
11 BACKUP 类型语句(备分)
12 RESTORE 类型语句(恢复)
13 创建对象操作 (CREATE DDL)
14 修改对象操作 (ALTER DDL)
15 删除对象操作 (DROP DDL)
16 授权操作 (GRANT DDL)
17 回收操作 (REVOKE DDL)
22 绑定参数
23 存在错误的语句(语法错误,语义分析错误等)
24 是否需要记录执行语句
25 是否需要打印计划和语句和执行的时间
26 是否需要记录执行语句的时间
27 原始语句(服务器从客户端收到的未加分析的语句)
28 是否记录参数信息,包括参数的序号、数据类型和值
29 是否记录事务相关事件
FILE_NUM 0 总共记录多少个日志文件,当日志文件达到这个设定值以后,
再生成新的文件时,会删除最早的那个日志文件,
日志文件的命令格式为 dmsql_实例名_日期时间.log。
当这个参数配置成 0 时,只会生成两个日志相互切换着记录。
有效值范围(0~1024)。
例如,当 FILE_NUM=0,实例名为 PDM 时,根据当时的日期时间,
生成的日志名称为:DMSQL_PDM_20180719_163701.LOG,
DMSQL_PDM_20180719_163702.LOG
SWITCH_MODE 0 表示 SQL 日志文件切换的模式:
0:不切换
1:按文件中记录数量切换
2:按文件大小切换
3:按时间间隔切换
SWITCH_LIMIT 100000 不同切换模式 SWITCH_MODE 下,意义不同:
按数量切换时,一个日志文件中的 SQL 记录条数达到多少条之后
系统会自动将日志切换到另一个文件中。一个日志文件中的 SQL
记录条数达到多少条之后系统会自动将日志切换到另一个文件中。
有效值范围(1000-10000000)
按文件大小切换时,一个日志文件达到该大小后,
系统自动将日志切换到另一个文件中,单位为 MB。
有效值范围(1-2000)按时间间隔切换时,每个指定的时间间隔,
按文件新建时间进行文件切换,单位为分钟。有效值范围(1-30000)。
ASYNC_FLUSH 0 是否打开异步 SQL 日志功能。
0:表示关闭
1:表示打开
MIN_EXEC_TIME 0 详细模式下,记录的最小语句执行时间,单位为毫秒。
执行时间小于该值的语句不记录在日志文件中。
有效值范围(0-4294967294)。
FILE_PATH ../log 日志文件所在的文件夹路径
BUF_TOTAL_SIZE 10240 SQL 日志 BUFFER 占用空间的上限,单位为 KB,取值范围(1024-1024000)
BUF_SIZE 1024 一块 SQL 日志 BUFFER 的空间大小,单位为 KB,取值范围(50-09600)
BUF_KEEP_CNT 6 系统保留的 SQL 日志缓存的个数, 有效值范围(1-100)
PART_STOR 0 SQL 日志分区存储,表示 SQL 日志进行分区存储的划分条件。
0 表示不划分;
1 表示 USER:根据不同用户分布存储
ITEMS 0 配置 SQL 日志记录中的那些列要被记录。
该参数是一个格式化的字符串,表示一个记录中的那些项目要被记录,
格式为:列号:列号:列号。
如:3:5:7 表示第 3,第 5,第 7 列要被记录。0 表示记录所有的列。
1 TIME 执行的时间
2 SEQNO 服务器的站点号
3 SESS 操作的 SESS 地址
4 USER 执行的用户
5 TRXID 事务 ID
6 STMT 语句地址
7 APPNAME 客户端工具
8 IP 客户端 IP
9 STMT_TYPE 语句类型
10 INFO 记录内容
11 RESULT 运行结果,包括运行用时和影响行数(可能没有)
USER_MODE 0 SQL 日志按用户过滤时的过滤模式,取值:
0:关闭用户过滤
1:白名单模式,只记录列出的用户操作的 SQL 日志
2:黑名单模式,列出的用户不记录 SQL 日志
USERS 空串 打开 USER_MODE 时指定的用户列表。
格式为:用户名:用户名:用户名

2. 查询方法

sqllog.ini 文件配置成功后可在 dmsql 指定目录下生成 dmsql 开头的 log 日志文件。日志内容如下所示:

image.png

上图中选中记录执行 SQL 语句如下所示,SQL 语句执行时间为 33.815 秒。

select * from t1 left join t2 on t1.c1=t2.c1 and t1.c1=999933;

可以通过正则表达式在 dmsql 日志文件中查找执行时间超过一定阈值的 SQL 语句。例如:查找执行时间超过 10 秒的 SQL 语句。

[1-9][0-9][0-9][0-9][0-9](ms)

如需进行更为系统全面的分析,可使用 DMLOG 工具 进行分类汇总。

4.1.2 通过系统视图查看

DM 数据库提供系统动态视图,可自动记录执行时间超过设定阈值的 SQL 语句。

1. SQL 记录配置

当 INI 参数 ENABLE_MONITOR=1 打开时,显示系统最近 1000 条执行时间超过预定值的 SQL 语句,默认预定值为 1000 毫秒。

以上两个参数可通过 SP_SET_PARA_VALUE 系统函数修改,通过 SF_GET_PARA_VALUE 系统函数查看当前值。

--修改参数值
SP_SET_PARA_VALUE(1,'ENABLE_MONITOR',1);

--查看参数值
select SF_GET_PARA_VALUE(1,'ENABLE_MONITOR');
注意

该参数为动态参数,可直接调用系统函数进行修改,无须重启数据库实例服务;
通过 SP_SET_PARA_VALUE 方式修改的参数值仅对当前会话以及新建会话生效,对其它已建立会话不生效。

2. 查询方式

(1)查询当前正在执行的会话信息。

SELECT * FROM (
SELECT 'SP_CLOSE_SESSION('||SESS_ID||');' AS CLOSE_SESSION,
       DATEDIFF(SS,LAST_SEND_TIME,SYSDATE) sql_exectime,
       TRX_ID,
       CLNT_IP,
       B.IO_WAIT_TIME AS IO_WAIT_TIME,
       SF_GET_SESSION_SQL(SESS_ID) FULLSQL,
       A.SQL_TEXT
 FROM V$SESSIONS a,V$SQL_STAT B WHERE STATE IN ('ACTIVE','WAIT') 
 AND A.SESS_ID = B.SESSID
 )

SQL_TEXT 列记录的是部分 SQL 语句;FULLSQL 列存储了完整的执行 SQL 语句。

(2)查询超过执行时间阈值的 SQL 语句。

可通过查询 V$LONG_EXEC_SQLS 系统视图获取结果:

SELECT * FROM V$LONG_EXEC_SQLS;

image.png

查询结果字段详细信息介绍如下表所示:

列名 说明
SESS_ID 会话 ID,会话唯一标识
SQL_ID 语句 ID,语句唯一标识
SQL_TEXT SQL 文本
EXEC_TIME 执行时间(毫秒)
FINISH_TIME 执行结束时间
N_RUNS 执行次数
SEQNO 编号
TRX_ID 事务号

4.2 SQL 分析方法

4.2.1 执行计划

1. 概述

简单来说,执行计划就是一条 SQL 语句在数据库中的执行过程或访问路径的描述。SQL 语言是种功能强大且非过程性的编程语言,比如以下这条 SQL 语句:

SELECT * FROM T1, T2 WHERE T1.ID = T2.ID AND T1.ID = 6;

开发人员只关心 SQL 语句能否返回 T1 与 T2 表的关联查询结果,不需要指定该 SQL 如何执行,也就是说不关心该 SQL 是先访问 T1 表还是先访问 T2 表。对于 SQL 来说,两种访问方式就是两个执行计划,查询优化器 (CBO) 将根据代价也就是开销来选择最优的执行计划。以如下 SQL 语句执行计划为例:

SELECT * FROM SYSOBJECTS;

1   #NSET2: [0, 1282, 396]
2     #PRJT2: [0, 1282, 396]; exp_num(17), is_atom(FALSE)
3       #CSCN2: [0, 1282, 396]; SYSINDEXSYSOBJECTS(SYSOBJECTS as SYSOBJECTS)

执行计划的每行即为一个计划节点,主要包含三部分信息。

  • 第一部分 NEST2、PRJT2、CSCN2 为操作符及数据库具体执行了什么操作。
  • 第二部分的三元组为该计划节点的执行代价,具体含义为[代价,记录行数,字节数]。
  • 第三部分为操作符的补充信息。

例如:第三个计划节点表示操作符是 CSCN2(即全表扫描),代价估算是 0 ms,扫描的记录行数是 1282 行,输出字节数是 396 个。

各计划节点的执行顺序为:缩进越多的越先执行,同样缩进的上面的先执行,下面的后执行,上下的优先级高于内外。缩进最深的,最先执行;缩进深度相同的,先上后下。口诀:最右最上先执行。

#CSCN2: [1, 2, 12]; INDEX33555496(TEST)
 操作符,[代价,行数,字节数] 描述

2. 查看执行计划

达梦数据库可通过两种方式查看执行计划。

方式一:通过 DM 数据库配套管理工具查看。

方式二:使用 explain 命令查看。

以下对两种查看方式进行介绍。

(1)管理工具查看执行计划

在 DM 配套管理工具中,选中待查看执行计划的 SQL 语句,点击工具栏中的按钮,或使用快捷键 F9,即可查看执行计划。

(2)使用 explain 命令查看执行计划

在待查看执行计划的 SQL 语句前加 explain 执行 SQL 语句即可查看预估的执行计划:

explain select * from sysobjects;

--执行计划
1   #NSET2: [1, 986, 396] 
2     #PRJT2: [1, 986, 396]; exp_num(17), is_atom(FALSE) 
3       #CSCN2: [1, 986, 396]; SYSINDEXSYSOBJECTS(SYSOBJECTS as SYSOBJECTS)

(3)使用 disql 命令行查真实执行计划

SQL> set autotrace traceonly
SQL> select * from sysobjects;
2348 rows got
1   #NSET2: [1, 2348->2348, 397]
2     #PRJT2: [1, 2348->2348, 397]; exp_num(17), is_atom(FALSE)
3       #CSCN2: [1, 2348->2348, 397]; SYSINDEXSYSOBJECTS(SYSOBJECTS)
Statistics
-----------------------------------------------------------------
        0           data pages changed
        0           undo pages changed
        67          logical reads
        0           physical reads
        0           redo size
        364191      bytes sent to client
        155         bytes received from client
        2           roundtrips to/from client
        0           sorts (memory)
        0           sorts (disk)
        2348        rows processed
        0           io wait time(ms)
        2           exec time(ms)

重点关注 logical reads(逻辑读)和 physical reads(物理读)相应的指标值,并结合 rows processed 返回处理行数多少来分析。如果返回行数少(并且 bytes sent to client 总量不大),应尽可能减少 IO 开销,让执行计划选择正确的索引路径。

Sort(disk) 一般因排序( hash join 发生归并、order by、group by 场景)区内存不足,如果数据库服务器物理内存充足,可以适当上调排序区内存,尽量避免操作刷盘,否则会影响执行性能。

4.2.2 常见操作符解读

下面通过几个例子来介绍一些常见操作符。准备测试表及数据如下:

DROP TABLE T1;
DROP TABLE T2;
CREATE TABLE T1(C1 INT ,C2 CHAR(1),C3 VARCHAR(10) ,C4 VARCHAR(10) );
CREATE TABLE T2(C1 INT ,C2 CHAR(1),C3 VARCHAR(10) ,C4 VARCHAR(10) );
INSERT INTO T1
SELECT LEVEL C1,CHR(65+MOD(LEVEL,57)) C2,'TEST',NULL FROM DUAL
CONNECT BY LEVEL<=10000;
INSERT INTO T2
SELECT LEVEL C1,CHR(65+MOD(LEVEL,57)) C2,'TEST',NULL  FROM DUAL
CONNECT BY LEVEL<=10000;
CREATE INDEX IDX_C1_T1 ON T1(C1);
SP_INDEX_STAT_INIT(USER,'IDX_C1_T1');

1. NSET:结果集收集

EXPLAIN SELECT * FROM T1;

1   #NSET2: [1, 10000, 156]
2     #PRJT2: [1, 10000, 156]; exp_num(5), is_atom(FALSE)
3       #CSCN2: [1, 10000, 156]; INDEX33556710(T1)

NSET 是用于结果集收集的操作符,一般是查询计划的顶层节点,优化工作中无需对该操作符过多关注,一般没有优化空间。

2. PRJT:投影

EXPLAIN SELECT * FROM T1;

1   #NSET2: [1, 10000, 156]
2     #PRJT2: [1, 10000, 156]; exp_num(5), is_atom(FALSE)
3       #CSCN2: [1, 10000, 156]; INDEX33556710(T1)

PRJT 是关系的【投影】 (project) 运算,用于选择表达式项的计算。广泛用于查询,排序,函数索引创建等。优化工作中无需对该操作符过多关注,一般没有优化空间。

3. SLCT:选择

EXPLAIN SELECT * FROM T1 WHERE C2='TEST';

1   #NSET2: [1, 250, 156]
2     #PRJT2: [1, 250, 156]; exp_num(5), is_atom(FALSE)
3       #SLCT2: [1, 250, 156]; T1.C2 = TEST
4         #CSCN2: [1, 10000, 156]; INDEX33556717(T1)

SLCT 是关系的【选择】运算,用于查询条件的过滤。可比较返回结果集与代价估算中是否接近,如相差较大可考虑收集统计信息。若该过滤条件过滤性较好,可考虑在条件列增加索引。

4. AAGR:简单聚集

EXPLAIN SELECT COUNT(*) FROM T1 WHERE C1 = 10;

1   #NSET2: [0, 1, 4]
2     #PRJT2: [0, 1, 4]; exp_num(1), is_atom(FALSE)
3       #AAGR2: [0, 1, 4]; grp_num(0), sfun_num(1)
4         #SSEK2: [0, 1, 4]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]

AAGR 用于没有 GROUP BY 的 COUNT、SUM、AGE、MAX、MIN 等聚集函数的计算。

5. FAGR:快速聚集

EXPLAIN  SELECT MAX(C1) FROM T1;

1   #NSET2: [1, 1, 0]
2     #PRJT2: [1, 1, 0]; exp_num(1), is_atom(FALSE)
3       #FAGR2: [1, 1, 0]; sfun_num(1)

FAGR 用于没有过滤条件时,从表或索引快速获取 MAX、MIN、COUNT 值。

6. HAGR:HASH 分组聚集

EXPLAIN SELECT COUNT(*) FROM T1 GROUP BY C2;

1   #NSET2: [1, 100, 48]
2     #PRJT2: [1, 100, 48]; exp_num(1), is_atom(FALSE)
3       #HAGR2: [1, 100, 48]; grp_num(1), sfun_num(1)
4         #CSCN2: [1, 10000, 48]; INDEX33556717(T1)

HAGR 用于分组列没有索引只能走全表扫描的分组聚集,该示例中 C2 列没有创建索引。

7. SAGR:流分组聚集

EXPLAIN SELECT COUNT(*) FROM T1 GROUP BY C1;

1   #NSET2: [1, 100, 4]
2     #PRJT2: [1, 100, 4]; exp_num(1), is_atom(FALSE)
3       #SAGR2: [1, 100, 4]; grp_num(1), sfun_num(1)
4         #SSCN: [1, 10000, 4]; IDX_C1_T1(T1)

SAGR 用于分组列是有序的情况下,可以使用流分组聚集,C1 列上已经创建了索引,SAGR2 性能优于 HAGR2。

8. BLKUP:二次扫描 (回表)

EXPLAIN SELECT * FROM T1 WHERE C1=10;

1   #NSET2: [0, 1, 156]
2     #PRJT2: [0, 1, 156]; exp_num(5), is_atom(FALSE)
3       #BLKUP2: [0, 1, 156]; IDX_C1_T1(T1)
4         #SSEK2: [0, 1, 156]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]

BLKUP 先使用二级索引索引定位 rowid,再根据表的主键、聚集索引、rowid 等信息获取数据行中其它列。

9. CSCN:全表扫描

EXPLAIN SELECT * FROM T1;

1   #NSET2: [1, 10000, 156]
2     #PRJT2: [1, 10000, 156]; exp_num(5), is_atom(FALSE)
3       #CSCN2: [1, 10000, 156]; INDEX33556710(T1)

CSCN2 是 CLUSTER INDEX SCAN 的缩写即通过聚集索引扫描全表,全表扫描是最简单的查询,如果没有选择谓词,或者没有索引可以利用,则系统一般只能做全表扫描。全表扫描 I/O 开销较大,在一个高并发的系统中应尽量避免全表扫描。

10. SSEK、CSEK、SSCN:索引扫描

-- 创建所需索引
CREATE CLUSTER INDEX IDX_C1_T2  ON T2(C1);
CREATE  INDEX IDX_C1_C2_T1  ON T1(C1,C2);

(1)SSEK

EXPLAIN SELECT * FROM T1 WHERE C1=10;

1   #NSET2: [0, 1, 156]
2     #PRJT2: [0, 1, 156]; exp_num(5), is_atom(FALSE)
3       #BLKUP2: [0, 1, 156]; IDX_C1_T1(T1)
4         #SSEK2: [0, 1, 156]; scan_type(ASC), IDX_C1_T1(T1), scan_range[10,10]

SSEK2 是二级索引扫描即先扫描索引,再通过主键、聚集索引、rowid 等信息去扫描表。

(2)CSEK

EXPLAIN SELECT * FROM T2 WHERE C1=10;

1   #NSET2: [0, 250, 156]
2     #PRJT2: [0, 250, 156]; exp_num(5), is_atom(FALSE)
3       #CSEK2: [0, 250, 156]; scan_type(ASC), IDX_C1_T2(T2), scan_range[10,10]

CSEK2 是聚集索引扫描只需要扫描索引,不需要扫描表,即无需 BLKUP 操作,如果 BLKUP 开销较大时,可考虑创建聚集索引。

(3)SSCN

EXPLAIN SELECT C1,C2 FROM T1;

1   #NSET2: [1, 10000, 60]
2     #PRJT2: [1, 10000, 60]; exp_num(3), is_atom(FALSE)
3       #SSCN: [1, 10000, 60]; IDX_C1_C2_T1(T1)

SSCN 是索引全扫描,不需要扫描表。

11. NEST LOOP:嵌套循环连接

嵌套循环连接是最基础的连接方式,将一张表(驱动表)的每一个值与另一张表(被驱动表)的所有值拼接,形成一个大结果集,再从大结果集中过滤出满足条件的行。驱动表的行数就是循环的次数,将在很大程度上影响执行效率。

连接列是否有索引,都可以走 NEST LOOP,但没有索引,执行效率会很差,语句如下所示:

select /*+use_nl(t1,t2)*/* from t1 inner join t2 on t1.c1=t2.c1 where t1.c2='A';
1   #NSET2: [17862, 24725, 296] 
2     #PRJT2: [17862, 24725, 296]; exp_num(8), is_atom(FALSE) 
3       #SLCT2: [17862, 24725, 296]; T1.C1 = T2.C1
4         #NEST LOOP INNER JOIN2: [17862, 24725, 296]; 
5           #SLCT2: [1, 250, 148]; T1.C2 = 'A'
6             #CSCN2: [1, 10000, 148]; INDEX33555594(T1)
7           #CSCN2: [1, 10000, 148]; INDEX33555595(T2)

可针对 T1 和 T2 的连接列创建索引,并收集统计信息,语句如下所示:

CREATE INDEX IDX_T1_C2  ON T1(C2);
CREATE INDEX IDX_T2_C1  ON T2(C1);
DBMS_STATS.GATHER_INDEX_STATS(USER,'IDX_T1_C2');
DBMS_STATS.GATHER_INDEX_STATS(USER,'IDX_T2_C1');

再次查看执行计划可看出效率明显改善,代价有显著下降,语句如下所示:

select /*+use_nl(t1,t2)*/* from t1 inner join t2 on t1.c1=t2.c1 where t1.c2='A';
1   #NSET2: [9805, 17151, 296] 
2     #PRJT2: [9805, 17151, 296]; exp_num(8), is_atom(FALSE) 
3       #SLCT2: [9805, 17151, 296]; T1.C1 = T2.C1
4         #NEST LOOP INNER JOIN2: [9805, 17151, 296]; 
5           #BLKUP2: [1, 175, 148]; IDX_T1_C2(T1)
6             #SSEK2: [1, 175, 148]; scan_type(ASC), IDX_T1_C2(T1), scan_range['A','A']
7           #CSCN2: [1, 10000, 148]; INDEX33555585(T2)

适用场景:

  • 驱动表有很好的过滤条件
  • 表连接条件能使用索引
  • 结果集比较小

12. HASH JOIN:哈希连接

哈希连接是在没有索引或索引无法使用情况下大多数连接的处理方式。哈希连接使用关联列去重后结果集较小的表做成 HASH 表,另一张表的连接列在 HASH 后向 HASH 表进行匹配,这种情况下匹配速度极快,主要开销在于对连接表的全表扫描以及 HASH 运算。

select * from t1 inner join t2 on t1.c1=t2.c1 where t1.c2='A';
1   #NSET2: [4, 24502, 296] 
2     #PRJT2: [4, 24502, 296]; exp_num(8), is_atom(FALSE) 
3       #HASH2 INNER JOIN: [4, 24502, 296];  KEY_NUM(1); KEY(T1.C1=T2.C1) KEY_NULL_EQU(0)
4         #SLCT2: [1, 250, 148]; T1.C2 = 'A'
5           #CSCN2: [1, 10000, 148]; INDEX33555599(T1)
6         #CSCN2: [1, 10000, 148]; INDEX33555600(T2)

哈希连接比较消耗内存如果系统有很多这种连接时,需调整以下 3 个参数:

参数名 说明
HJ_BUF_GLOBAL_SIZE HASH 连接操作符的数据总缓存大小 ()>=HJ_BUF_SIZE),系统级参数,以兆为单位。有效值范围(10~500000)
HJ_BUF_SIZE 单个哈希连接操作符的数据总缓存大小,以兆为单位。有效值范围(2~100000)
HJ_BLK_SIZE 哈希连接操作符每次分配缓存( BLK )大小,以兆为单位,必须小于 HJ_BUF_SIZE。有效值范围(1~50)

13. MERGE JOIN:归并排序连接

归并排序连接需要两张表的连接列都有索引,对两张表扫描索引后按照索引顺序进行归并。

-- 对连接列创建索引
CREATE INDEX IDX_T1_C1C2 ON T1(C1,C2);
select /*+use_merge(t1 t2)*/t1.c1,t2.c1 from t1 inner join t2 on t1.c1=t2.c1 where t2.c2='b';
1   #NSET2: [13, 24725, 56] 
2     #PRJT2: [13, 24725, 56]; exp_num(2), is_atom(FALSE) 
3       #SLCT2: [13, 24725, 56]; T2.C2 = 'b'
4         #MERGE INNER JOIN3: [13, 24725, 56]; KEY_NUM(1); KEY(COL_0 = COL_0) KEY_NULL_EQU(0)
5           #SSCN: [1, 10000, 4]; IDX_C1_T1(T1)
6           #BLKUP2: [1, 10000, 52]; IDX_T2_C1(T2)
7             #SSCN: [1, 10000, 52]; IDX_T2_C1(T2)

4.2.3 ET 工具

ET 工具是 DM 数据库自带的 SQL 性能分析工具,能够统计 SQL 语句执行过程中每个操作符的实际开销,为 SQL 优化提供依据以及指导。

1. 功能的开启/关闭

ET 功能默认关闭,可通过配置 INI 参数中的 ENABLE_MONITOR=1、MONITOR_SQL_EXEC=1 开启该功能。

--两个参数均为动态参数,可直接调用系统函数进行修改
SP_SET_PARA_VALUE(1,'ENABLE_MONITOR',1);
SP_SET_PARA_VALUE(1,'MONITOR_SQL_EXEC',1);

--会话级别修改只在当前会话生效
SF_SET_SESSION_PARA_VALUE('MONITOR_SQL_EXEC',1);

--关闭 ET
SP_SET_PARA_VALUE(1,'ENABLE_MONITOR',0);
SP_SET_PARA_VALUE(1,'MONITOR_SQL_EXEC',0);
注意

ET 功能的开启将对数据库整体性能造成一定影响,优化工作结束后尽量关闭该功能以提升数据库整体运行效率;DM 数据库新版本的开启 ET 功能时需要设置: ENABLE_MONITOR = 1(默认打开) MONITOR_SQL_EXEC = 1(设置成1)。

部分 DM 数据库版本的开启方式可能存在一定区别,详细内容请参照安装目录 /doc 下《系统管理员手册》。

2. 查看方式

执行 SQL 语句后,客户端会返回 SQL 语句的执行号。单击执行号即可查看 SQL 语句对应的 ET 结果。

image.png

如果没有图形界面,调用存储过程可返回相同结果。

CALL ET(55);

image.png

ET 结果说明:

  • OP: 操作符
  • TIME(us): 时间开销,单位为微秒
  • PERCENT: 执行时间占总时间百分比
  • RANK: 执行时间耗时排序
  • SEQ: 执行计划节点号
  • N_ENTER: 进入次数

以 SORT3 操作符为例,时间开销为 2.8 ms,占总执行时间的 59.13%,可作为优化的重点对象。对 T2 表的 C1 字段建二级索引,消除排序操作符。

CREATE INDEX IDX_T2_C1 ON T2(C1);

image.png

与之前 ET 结果相比较,可看出 SORT3 操作符由于 C1 列已有序被消除,总耗时明显减少。

4.2.4 dbms_sqltune 工具

DBMS_SQLTUNE 包提供一系列实时 SQL 监控的方法。当 SQL 监控功能开启后,DBMS_SQLTUNE 包可以实时监控 SQL 执行过程中的信息,包括:执行时间、执行代价、执行用户、统计信息等情况。

使用前提:建议会话级开启参数 MONITOR_SQL_EXEC=1,而 MONITOR_SQL_EXEC 在达梦数据库中一般默认是 1,无需调整。

ALTER SESSION SET 'MONITOR_SQL_EXEC' = 1;
<执行待优化SQL>
select DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_EXEC_ID=>1213701) from dual;

例如:

image.png

image.png

dbms_sqltune 系统包相比 ET 功能更强大,能够获取 IO 操作量,查看真实执行计划,每个操作符消耗占比和相应的花费时间,还能看出每个操作符执行的次数,非常便于了解执行计划中瓶颈位置。

dbms_sqltune 功能远不止定位执行计划瓶颈,还拥有调优助手功能(建议性提示建某索引和收集某统计信息),具体使用参考《DM8 系统包使用手册》。

4.2.5 存储过程调试

在 DM 数据库中,我们可以利用 DM PL/SQL 进行存储过程及函数的编写。有时,我们编写的程序块中可能会有 BUG 导致编译失败,这种情况下就需要用到相关工具来进行调试。本章节主要介绍两种存储过程的调试方法。

1. 图形化界面工具进行调试

在有图形化界面的情况下,我们可以利用管理工具来进行匿名块的调试。

(1)使用匿名块调试

  • 开启系统包 DEBUG SYSTEM。【工具包】->DEBUG SYSTEM-> 右键->【启用】;
  • 点击上方的【调试】按钮进入调试。

(2)创建测试存储过程,并进行调试

  • 创建测试存储过程。
--当i到3的时候就会报除0错误
DECLARE
	--变量
	aa int;
BEGIN
	--调试语句
	for i in 1..3
	loop
		aa = 1 / (3-i);
		print aa;
	end loop;

END
  • 进入调试,点击【进入】按钮,会依照代码逻辑进行分步调试,显示区域会显示当前变量执行结果。若遇到报错会终止调试,并显示报错内容,可根据报错内容进行代码修改。

2. DMDBG 进行调试

当我们没有图形界面,或者通过远程连接服务器的时候,我们也可以利用 DM 提供的命令行调试工具 dmdbg ,来完成同样的任务。

(1)登录 dmdbg。dmdbg 与 disql 同级目录,都在 dmdbms/bin 目录下,登录方式也与 disql 类似:

cd /dmdbms/bin
./dmdbg SYSDBA/*****@LOCALHOST:5236

--可以使用 help 命令查看参数
DEG> help

(2)引用存储过程。在 disql 中创建如下测试存储过程,然后在 DBG 中把 call TEST_DMDBG; 放到 SQL 内执行,如下所示:

--注意调试前确保已经开启系统包 DEBUG SYSTEM
--登录disql,在disql中创建如下测试存储过程
create or replace procedure TEST_DEBUG
as
aa int;
begin
	for i in 1..3
	loop
		aa = 1 / (3-i);
		print aa;
	end loop;
end;

--登录dmdbg,把 call TEST_DMDBG; 放到 SQL 内执行
DBG> sql call TEST_DMDBG;

(3)添加断点。从头开始调试,将断点放到最开始的位置,可以在需要的行数打上断点,如下所示:

DBG> B 0
Breakpoint 1 at @dbg_main, line: 1@{call TEST_DMDBG;}

(4)开始调试。

DBG> r
Breakpoint 1,  line: 1@{call TEST_DMDBG;}

(5)进入循环里调试。

DBG> s
SYSDBA.TEST_DMDBG line: 5       @{   for i in 1..3}
DBG> s
SYSDBA.TEST_DMDBG line: 7       @{     aa = 1 / (3-i);}

(6)查看当前的堆栈。

DBG> bt
\#0      SYSDBA.TEST_DMDBG()     line: 7@{     aa = 1 / (3-i);}
\#1      @dbg_main       line: 1@{call TEST_DMDBG;}

(7)查看当前变量的数值。

--可以通过 P 变量名的方式输出打印
DBG> p aa
$3 = 0
--当 i 到3的时候就会报除0错误
DBG> s
[TEST_DMDBG] 除0错误.error code=-6103
--报错停止

4.3 SQL 语句优化

关于查询语句,有以下几点特征:

  • 返回数据越多,语句执行时间越长;
  • 分页是一个优化重点,order by 排序大小由结果集大小决定,过大会在临时表空间排序,性能降低;
  • 一些语句会隐式排序,比如 uinon group by;
  • buffer 过小,数据页频繁的换入换出。

关于优化 sql 语句:

  • 通过各种手段减少 sql 执行过程中的 IO 代价,内存中的计算,临时表使用等;
  • 表与表之间的关系,即关联条件之间的数据对应关系;
  • 表数据量大小,对于 OLTP 是否满足小表驱动大表;
  • sql 返回结果集多少,如果返回结果集少,sql 优化余地较大;
  • 合理利用索引(组合索引)的特点,虽然维护索引也需要代价,但是对于查询来说,很多时候效果立竿见影。

关于访问和连接方法:

  • 全表扫描访问;
  • 索引扫描访问;
  • 嵌套循环连接;
  • 哈希连接;
  • 归并连接;
  • 半连接、反连接;
  • n 张表至少有 n-1 次连接。

4.3.1 索引

索引是一种特殊的数据库结构,由数据表中的一列或多列组合而成,可以用来快速查询数据表中有某一特定值的记录。

索引结构:最常见的索引结构为 B*树索引,存储结构如下图所示:

image.png

最顶层的为根节点,最底层的为叶子节点,中间层为内节点。实际使用当中一般不止 3 层(取决于数据量大小),除根节点以及叶子节点以外仅为内节点。对于一个 m 阶(本例中 m=2)的 B*树存储结构有以下几个特点:

  • 每个结点最多有 m 个子结点。
  • 除了根结点和叶子结点外,每个结点最少有 m/2(向上取整)个子结点。
  • 如果根结点不是叶子结点,那根结点至少包含两个子结点。
  • 所有的叶子结点都位于同一层。
  • 每个结点都包含 k 个元素,这里 m/2 ≤ k < m,这里 m/2 向下取整。
  • 每个节点中的元素从小到大排列。
  • 每个元素左结点的值都小于或等于该元素,右结点的值都大于或等于该元素。
  • 所有的非叶子节点只存储关键字信息。
  • 所有的叶子结点中包含了全部元素的信息。
  • 所有叶子节点之间都有一个链指针。

可以看出在该存储结构中查找特定数据的算法复杂度为 O(log2N),查找速度仅与树高度有关。
对于聚集索引叶子节点存储的元素是数据块即为整行数据,对于非聚集索引叶子节点存储的元素是索引字段的所对应的聚集索引的值或 rowid,如果需要获取其它字段信息需要根据聚集索引的值或 rowid 回表 (BLKUP) 进行查询。

索引适用范围:

在以下场景下可考虑创建索引:

  • 仅当要通过索引访问表中很少的一部分行(1%~20%)。
  • 索引可覆盖查询所需的所有列,不需额外去访问表。
注意

对于一个表来说索引并非越多越好,过多的索引将影响该表的 DML 效率。

存在下列情况将导致无法使用索引:

  • 组合索引中,条件列中没有组合索引的首列。
  • 条件列带有函数或计算。
  • 索引排序是按照字段值进行排序的,字段值通过函数或计算后的值索引无法获取。
  • 索引过滤性能不好时。

例如对一张 10 万条记录的表进行条件查询,获取 5 万条数据,通过索引进行查找效率低于全表扫描,将放弃使用索引。

建立索引的原则:

  • 建立唯一索引。唯一索引能够更快速地帮助我们进行数据定位;
  • 为经常需要进行查询操作的字段建立索引;
  • 对经常需要进行排序、分组以及联合操作的字段建立索引;
  • 在建立索引的时候,要考虑索引的最左匹配原则(在使用 SQL 语句时,如果 where 部分的条件不符合最左匹配原则,可能导致索引失效,或者不能完全发挥建立的索引的功效);
  • 不要建立过多的索引。因为索引本身会占用存储空间;
  • 如果建立的单个索引查询数据很多,查询得到的数据的区分度不大,则考虑建立合适的联合索引;
  • 尽量考虑字段值长度较短的字段建立索引,如果字段值太长,会降低索引的效率。

4.3.2 SQL 语句改写

DM 数据库针对 SQL 语句有以下常见几种改写方法:

1. 优化 GROUP BY

提高 GROUP BY 语句的效率,可以在 GROUP BY 之前过滤掉不需要的内容。

--优化前
SELECT JOB,AVG(AGE) FROM TEMP 
GROUP BY JOB HAVING JOB = 'STUDENT' OR JOB = 'MANAGER';
--优化后
SELECT JOB,AVG(AGE) FROM TEMP 
WHERE JOB = 'STUDENT' OR JOB = 'MANAGER' GROUP BY JOB;

2. 用 UNION ALL 替换 UNION

当 SQL 语句需要 UNION 两个查询结果集合时,这两个结果集合会以 UNION ALL 的方式被合并,在输出最终结果前进行排序。用 UNION ALL 替代 UNION, 这样排序就不必要了,效率就会因此得到提高。

注意

UNION 将对结果集合排序,这个操作会使用到 SORT_AREA_SIZE 这块内存,对于这块内存的优化也很重要;UNION ALL 将重复输出两个结果集合中相同记录,要从业务需求判断使用 UNION ALL 的可行性。

--优化前
SELECT USER_ID,BILL_ID FROM USER_TAB1 WHERE AGE = '20' 
UNION 
SELECT USER_ID,BILL_ID FROM USER_TAB2 WHERE AGE = '20'; 
--优化后
SELECT USER_ID,BILL_ID FROM USER_TAB1 WHERE AGE = '20' 
UNION ALL 
SELECT USER_ID,BILL_ID FROM USER_TAB2 WHERE AGE = '20';

3. 用 EXISTS 替换 DISTINCT

当 SQL 包含一对多表查询时,避免在 SELECT 子句中使用 DISTINCT,一般用 EXISTS 替换 DISTINCT 查询更为迅速。

--优化前
SELECT DISTINCT USER_ID,BILL_ID FROM USER_TAB1 D,USER_TAB2 E 
WHERE D.USER_ID= E.USER_ID;
--优化后
SELECT USER_ID,BILL_ID FROM USER_TAB1 D WHERE EXISTS(SELECT 1 FROM USER_TAB2 E WHERE E.USER_ID= D.USER_ID);

4. 多使用 COMMIT

可以在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。
COMMIT 所释放的资源:

  • 回滚段上用于恢复数据的信息;
  • 被程序语句获得的锁;
  • redo log buffer 中的空间;
  • 为管理上述 3 种资源中的内部花销。

5. 用 WHERE 子句替换 HAVING 子句

避免使用 HAVING 子句,HAVING 只会在检索出所有记录之后才对结果集进行过滤,这个处理需要排序、总计等操作,可以通过 WHERE 子句限制记录的数目。on、where、having 三个都可以加条件子句,其中,on 是最先执行,where 次之,having 最后。

  • on 是先把不符合条件的记录过滤后才进行统计,在两个表联接时才用 on;
  • 在单表查询统计的情况下,如果要过滤的条件没有涉及到要计算字段,where 和 having 结果是一样的,但 where 比 having 快;
  • 如果涉及到计算字段,where 的作用时间是在计算之前完成,而 having 是在计算后才起作用,两者的结果会不同;
  • 在多表联接查询时,on 比 where 更早起作用。首先会根据各个表之间的关联条件,把多个表合成一个临时表后,由 where 进行过滤再计算,计算完再由 having 进行过滤。

6. 用 TRUNCATE 替换 DELETE

当删除表中的记录时,在通常情况下, 回滚段用来存放可以被恢复的信息。如果没有 COMMIT 事务,会将数据恢复到执行删除命令之前的状况;而当运用 TRUNCATE 时,回滚段不再存放任何可被恢复的信息。当命令运行后,数据不能被恢复。因此很少的资源被调用,执行时间也会很短。

注意

TRUNCATE 只在删除全表适用,TRUNCATE 是 DDL 不是 DML。

7. 用 EXISTS 替换 IN、用 NOT EXISTS 替换 NOT IN

在基于基础表的查询中可能会需要对另一个表进行联接。在这种情况下, 使用 EXISTS (或 NOT EXISTS )通常将提高查询的效率。在子查询中,NOT IN 子句将执行一个内部的排序和合并。无论在哪种情况下,NOT IN 都是最低效的(要对子查询中的表执行一个全表遍历),所以尽量将 NOT IN 改写成外连接( Outer Joins )或 NOT EXISTS。

--优化前
SELECT A.* FROM TEMP(基础表) A WHERE AGE > 0 
AND A.ID IN(SELECT ID FROM TEMP1 WHERE NAME ='TOM'); 
--优化后
SELECT A.* FROM TEMP(基础表) A WHERE AGE > 0 
AND EXISTS(SELECT 1 FROM TEMP1 WHERE A.ID= ID AND NAME='TOM');

8.半连接优化

半连接也是子查询的一种,查询只返回主表数据,子查询作为条件过滤使用。exists 关注是否有返回行,取决于关联列,in 关注是否存在过滤数据,在半连接改写中理解这点很重要。

优化改写:半连接改 join 。

--改写前,已下两种写法特征就是执行计划出现 semi 关键字
--写法一:
select EMPNO, ENAME, JOB, MGR, HIREDATE
  from emp2
 where deptno in (select deptno from dept2)
--写法二:
 select EMPNO, ENAME, JOB, MGR, HIREDATE
  from emp2
 where exists (select deptno from dept2 where dept2.deptno = emp2.deptno)

--改写优化
--当子查询中部门表中部门编号不存在重复改写如下:
    select emp2.EMPNO, 
           emp2.ENAME, 
           emp2.JOB, 
           emp2.MGR, 
           emp2.HIREDATE 
      from emp2
inner join dept2 
        on dept2.deptno = emp2.deptno

--若存在数据重复先根据关联列去重再关联
select dept2.*
  from (select distinct deptno from emp2) emp2
 inner join dept2
on dept2.deptno = emp2.deptno

9.反连接优化

同半连接一样,查询也只返回主表数据,通过 not in 和 not exists 过滤,再改写的过程中特别要注意反连接 not in 对空值敏感。

--ept2 deptno 列不存在空值时,以下两种写法等价,当 not in 存在空时,无数据行返回,因此 not exists 改写 not in 需要加上 not is null
select * from emp2 where deptno not in (select deptno from dept2);
select * from emp2 e where not exists (select * from dept2 d where d.deptno = e.deptno)
--not in、 not exists 改写 left join 
select * from emp2 E  where deptno not in (select deptno from dept2 D)
--反连接驱动是 E 表,被驱动是 D 表,所以改写 left join ,not in 表示不在此范围,即 emp2 有的部门编号,dept2 没有
--左连接会将右表没有的内容用 NULL 表示,所以关联后取 d.deptno is null 过滤
select e.*
  from emp2 e
  left join dept2 d
    on d.deptno = e.deptno
 where d.deptno is null

4.3.3 表设计优化

表设计优化可以从三个方面入手:选择合适的表类型、设置分区表、设置全局临时表。

1. 表类型选择

达梦数据库提供了三种表类型:行存储表、列存储表(HUGE)和堆表。运维人员可根据实际需求选择合适的表类型。

表类型 描述 主要特征 适用场景
行存储表 行存储是以记录为单位进行存储的,数据页面中存储的是完整的若干条记录 1.按行存储 2.每个表都创建一个 B 树,并在叶子上存放数据 适用于高并发 OLTP 场景。
列存储表(HUGE) 列存储是以列为单位进行存储的,每一个列的所有行数据都存储在一起,而且一个指定的页面中存储的都是某一个列的连续数据。 1.按列存储 2.非事务型 HUGE 表:LOG NONE、LOG LAST、LOG ALL3.事务型 HUGE 表 适用于海量数据分析场景
堆表 堆表是指采用了物理 ROWID 形式的表,即使用文件号、页号和页内偏移而得到 ROWID 值,这样就不需要存储 ROWID 值,可以节省空间 1.数据页都是通过链表形式存储 2.可设置并发分支 并发插入性能较高

2. 水平分区表

(1)分区类型

  • 范围(range)水平分区:对表中的某些列上值的范围进行分区,根据某个值的范围,决定将该数据存储在哪个分区上;
  • 哈希(hash)水平分区:通过指定分区编号来均匀分布数据的一种分区类型,通过在 I/O 设备上进行散列分区,使得这些分区大小基本一致;
  • 列表(list)水平分区:通过指定表中的某个列的离散值集,来确定应当存储在一起的数据。例如,可以对表上的 status 列的值在('A','H','O')放在一个分区,值在('B','I','P')放在另一个分区,以此类推;
  • 多级分区表:按上述三种分区方法进行任意组合,将表进行多次分区,称为多级分区表。

(2)分区优势

  • 减少访问数据
  • 操作灵活:可以操作分区 truncate、分区 drop、分区 add、分区 exchange

(3)举例说明

select *
	from range_part_tab
	where deal_date >= TO_DATE('2019-08-04','YYYY-MM-DD')
	and deal_date <= TO_DATE('2019-08-07','YYYY-MM-DD');

执行计划:

1 #NSET2:[24,18750,158]
2  #PRJT2:[24,18750,158];exp_num(6),is_atom(FALSE)
3   #PARALLEL:[24,18750,158];scan_type(GE_LE),key_num(0,1,1)
4    #SLCT2:[24,18750,158];[(RANGE_PART_TAB.DEAL_DATE >= var2 AND RANGE_PART_TAB.DEAL_DATE <= var4)]
5      #CSCN2:[73,500000,158];INDEX33555933(RANGE_PART_TAB)

--#PARALLEL:控制水平分区子表的扫描
  • 对主表和所有子表都收集统计信息
  • 对索引收集统计信息
注意

如果 SQL 中有可利用的索引,普通表也可能比分区表性能高。

3. 全局临时表

当处理复杂的查询或事务时,由于在数据写入永久表之前需要暂时存储一些行信息或需要保存查询的中间结果,可能需要一些表来临时存储这些数据。DM 允许创建临时表来保存会话甚至事务中的数据。在会话或事务结束时,这些表上的数据将会被自动清除。

(1)全局临时表类型

  • 事务级-ON COMMIT DELETE ROWS
  • 会话级-ON COMMIT PRESERVE ROWS

(2)全局临时表优势

  • 不同 session 数据独立
  • 自动清理

(3)举例说明

第一步:原始语句如下:

--T_1 视图(与 oracle 的 dblink 全表查询)
--T_1 视图的结构为
--(INIT_DATE int , BRANCH_NO int , FUND_ACCOUNT int , BUSINESS_FLAG int , remark varchar(32))

--T_2 表
--T_2 表的结构为
--(BRANCH_NO int,FUND_ACCOUNT int , prodta_no int,v_config_4662 varchar(32))

select a.init_date as oc_date,a.BRANCH_NO,a.FUND_ACCOUNT,a.BUSINESS_FLAG,a.remark,b.BRANCH_NO,b.FUND_ACCOUNT,b.prodta_no
	from T_1 a,T_2 b
	where init_date = 20181120
	AND a.BRANCH_NO = b.BRANCH_NO
	AND a.FUND_ACCOUNT = b.FUND_ACCOUNT
	and instr(v_config_4662, ',' || b.prodta_no || ',')>0
	and a.BUSINESS_FLAG in (2629,2630)
	and nvl(a.remark,' ')not like '%实时TA%';

第二步:创建临时表 T1_20181122,将 T_1 视图中部分数据插入临时表中。

CREATE GLOBAL TEMPORARY TABLE "T1_20181122"
(init_date int,  BRANCH_NO int, FUND_ACCOUNT int,BUSINESS_FLAG int,remark varchar(32));

--插入dblink获取的数据到临时表
	insert into T1_20181122
		select *
			from T_1 a
			where init_date = 20181120
			and a.BUSINESS_FLAG in (2629,2630)
			and nvl(a.remark,' ')not like '%实时TA%';

第三步:语句改写。

select a.init_date as oc_date,a.BRANCH_NO,a.FUND_ACCOUNT,a.BUSINESS_FLAG,a.remark,b.BRANCH_NO,b.FUND_ACCOUNT,b.prodta_no
			from T1_20181122 a, T_2 b
	where a.BRANCH_NO = b.BRANCH_NO
		AND a.FUND_ACCOUNT = b.FUND_ACCOUNT
	and instr(v_config_4662, ',' || b.prodta_no || ',')>0;

执行计划:50 分钟 >>1 分钟。

--原语句执行计划
1 #NSET2:[11,1,1644]
2	#PRJT2:[11,1,1644];exp_num(41),is_atom(FALSE)
3 		#HASH2 INNER JOIN:[11,1,1644];KEY_NUM(2);
4 			#SLCT2:[0,1,270];exp11>0
5 				#CSCN2:[0,1,270];INDEX33560908(T_HSOTCPRODCASHACCT as B)
6 			#HASH RIGHT SEMI JOIN2:[10,380,1374];n_keys(1)
7 				#CONST VALUE LIST:[0,2,30];row_num(2),col_num(1),
8 				#SLCT2:[10,380,1374];(A.INIT_DATE = var4 AND NOT(exp11 LIKE '%实时TA%'))
9 					#PRJT2:[10,1000,1374];exp_num(13),is_atom(FALSE)
10 						#REMOTE SCAN:[0,0,0] HIS_FUNDJOUR@HS08HIS

--改写后执行计划
1   #NSET2: [1, 1, 124] 
2     #PRJT2: [1, 1, 124]; exp_num(8), is_atom(FALSE) 
3       #HASH2 INNER JOIN: [1, 1, 124];  KEY_NUM(2); KEY(B.BRANCH_NO=A.BRANCH_NO AND B.FUND_ACCOUNT=A.FUND_ACCOUNT) KEY_NULL_EQU(0, 0)
4         #SLCT2: [1, 1, 60]; exp11 > 0
5           #CSCN2: [1, 1, 60]; INDEX33555476(T_2 as B)
6         #CSCN2: [1, 1, 64]; INDEX33555478(T1_20181122 as A)

4.3.4 hint 优化 sql

当统计信息已收集,且索引也按照需求建立,sql 执行效率仍然不符合预期,可以考虑添加 hint 方式来进行优化。

--例如:多个单表查询通过 union 连接后组成一个视图,然后视图通过 where 过滤,过滤条件在每个单表中过滤性较好,且存在索引,通过视图过滤无法使用索引,执行时间55s。
select * from dms.view_da_base where ahdm = '00005fe8-b171-4292-b776-b53c3fd65923';

image.png

使用 hint 修改参数 VIEW_FILTER_MERGING 的值对视图条件进行优化,当参数值取 1 时表示尽可能地进行视图条件合并。

select    /*+ VIEW_FILTER_MERGING(1) */  * from dms.view_da_base where ahdm = '00005fe8-b171-4292-b776-b53c3fd65923';

image.png

参数 VIEW_FILTER_MERGING 详细说明可参考数据库安装目录下 doc 目录中《DM8 系统管理员手册》。

当系统在运行过程中,出现慢 sql 需要紧急进行优化处理时,可以在数据库端通过 SF_INJECT_HINT 函数将 hint 与 sql 进行绑定对慢 SQL 进行优化。

--设置 INI 参数 ENABLE_INJECT_HINT 为 1
SP_SET_PARA_VALUE(1,'ENABLE_INJECT_HINT',1);   
 
--计划添加 hint 后的 SQL
select    /*+ VIEW_FILTER_MERGING(1) */  * from dms.view_da_base where ahdm = '00005fe8-b171-4292-b776-b53c3fd65923';

---模糊匹配 sql 进行 hint 绑定
 SF_INJECT_HINT('select * from dms.view_da_base where    ,', 'VIEW_FILTER_MERGING(1)', 'VIEW_HINT', 'to testfunction of injecting hint', TRUE, TRUE);

SF_INJECT_HINT 函数详细使用方法可参考数据库安装目录下 doc 目录中《DM8_SQL 语言使用手册》。

注意

此优化方式不推荐作为常规优化方法使用,特定场景优化或应急处理时使用。

五、统计信息

5.1 统计信息概述

统计信息主要是描述数据库中表和索引的大小数以及数据分布状况等的一类信息。比如:表的行数、块数、平均每行的大小、索引的高度、叶子节点数以及索引字段的行数等。

统计信息对于 CBO(基于代价的优化器)生成执行计划具有直接影响。例如在嵌套循环连接(链接)中需要选择小表作为驱动表,两个关联表哪个是小表完全取决于统计信息中记录的数据量信息。此外,访问一个表是否要走索引,关联查询能否采用其它关联方式等都是 CBO 基于统计信息确定的。因此,统计信息的准确是生成最优执行计划的必要前提。

5.2 收集统计信息

DM 收集统计信息的方法分为手动收集和自动收集。

5.2.1 手动收集

--收集指定用户下所有表所有列的统计信息:
DBMS_STATS.GATHER_SCHEMA_STATS('username',100,TRUE,'FOR ALL COLUMNS SIZE AUTO');

--收集指定用户下所有索引的统计信息:
DBMS_STATS.GATHER_SCHEMA_STATS('usename',1.0,TRUE,'FOR ALL INDEXED SIZE AUTO');
--或 收集单个索引统计信息:
DBMS_STATS.GATHER_INDEX_STATS('username','IDX_T2_X');

--收集指定用户下某表统计信息:
DBMS_STATS.GATHER_TABLE_STATS('username','table_name',null,100,TRUE,'FOR ALL COLUMNS SIZE AUTO');

--收集某表某列的统计信息:
STAT 100 ON table_name(column_name);
注意

统计信息收集过程中将对数据库性能造成一定影响,避免在业务高峰期收集统计信息。

5.2.2 自动收集

DM 数据库支持统计信息的自动收集,当全表数据量变化超过设定阈值后可自动更新统计信息。

--打开表数据量监控开关,参数值为 1 时监控所有表,2 时仅监控配置表
SP_SET_PARA_VALUE(1,'AUTO_STAT_OBJ',2);

--设置 SYSDBA.T 表数据变化率超过 15% 时触发自动更新统计信息
DBMS_STATS.SET_TABLE_PREFS('SYSDBA','T','STALE_PERCENT',15);

--配置自动收集统计信息触发时机
SP_CREATE_AUTO_STAT_TRIGGER(1, 1, 1, 1,'14:36', '2020/3/31',60,1);

/*
函数各参数介绍
SP_CREATE_AUTO_STAT_TRIGGER(
    TYPE                    INT,    --间隔类型,默认为天
    FREQ_INTERVAL         INT,    --间隔频率,默认 1
    FREQ_SUB_INTERVAL    INT,    --间隔频率,与 FREQ_INTERVAL 配合使用
    FREQ_MINUTE_INTERVAL INT,    --间隔分钟,默认为 1440
    STARTTIME              VARCHAR(128), --开始时间,默认为 22:00
    DURING_START_DATE    VARCHAR(128), --重复执行的起始时间,默认 1900/1/1
    MAX_RUN_DURATION    INT,    --允许的最长执行时间(秒),默认不限制
    ENABLE                  INT     --0 关闭,1 启用  --默认为 1
);
*/

5.3 查看统计信息

--用于经过 GATHER_TABLE_STATS、GATHER_INDEX_STATS 或 GATHER_SCHEMA_STATS 收集之后展示。
dbms_stats.table_stats_show('模式名','表名');

--用于经过 GATHER_TABLE_STATS、GATHER_INDEX_STATS 或 GATHER_SCHEMA_STATS 收集之后展示。 返回两个结果集:一个是索引的统计信息;另一个是直方图的统计信息。
dbms_stats.index_stats_show('模式名','索引名');

--用于经过 GATHER_TABLE_STATS、GATHER_INDEX_STATS 或 GATHER_SCHEMA_STATS 收集之后展示。
dbms_stats.COLUMN_STATS_SHOW('模式名','表名','列名');

5.4 更新统计信息

--更新已有统计信息
DBMS_STATS.UPDATE_ALL_STATS();

5.5 删除统计信息

--表
DBMS_STATS.DELETE_TABLE_STATS('模式名','表名','分区名',...);

--模式
DBMS_STATS.DELETE_SCHMA_STATS('模式名','','',...);

--索引
DBMS_STATS.DELETE_INDEX_STATS('模式名','索引名','分区表名',...);

--字段
DBMS_STATS.DELETE_COLUMN_STATS('模式名','表名','列名','分区表名',...);

六、参考

若以上内容无法解决您的问题,可以在达梦技术社区提问交流。

微信扫码
分享文档
扫一扫
联系客服