1 基本步骤是什么
慢sql分析是在已经知道了慢sql,trace计划,ET信息的前提下,对慢sql进行分析以及优化的过程。下面是总结
基本步骤大概可以总结为3段:查看ET和trace找到瓶颈的执行计划,分析出性能瓶颈对应的sql,对照sql以及执行计划进行慢sql的优化。
2 怎么样用ET和trace找到性能瓶颈
查看ET,先关注代价占比比较大的操作符,如果这些操作符,根据行号(蓝框所在)定位到对应的执行计划。
根据蓝框定位到对应的行号之后,根据操作符的不同进行具体的分析。比如图中的PRJT2,代表的是投影,他的性能慢,常常是因为其字段中带有子查询或者函数一类的。
后面这两个是相关子查询的回表操作。跟刚刚一样,具体操作符具体分析。回表常常是和SSEK等绑定出现的,当我们往前看时,发现SSEK拿到了50多万行数据,这就是回表慢的根因。
SPL2是临时表的意思,在达梦执行计划中,在末尾的SPL2常常是相关子查询的执行计划。这种现象由DM_INI中的两个参数来决定,第二张图是参数SUBQ_CVT_SPL_FLAG和参数ENABLE_RQ_TO_SPL
3 怎么样查找到性能瓶颈对应的sql
需要对操作符的作用有一定的了解,详情可见DBA手册附录4——执行计划操作符。
以上面的例子为例,首先第一个,PRJT2,作用如下图所示。通过附加信息,我们知道了映射列数是9列,加上之前总结出的其字段必定带有子查询或者复杂的函数,来去sql语句中查找对应的部分。
在sql语句中查看每一个select后面所带有的字段数量以及是否复杂,这样就可以判断出到底是哪一个了。可以看到,下面图片中的,刚好有9个字段并且带有子查询。
对于这个查询语句,刚刚说过,投影之所以代价大,是因为它里面的子查询或者复杂函数导致的,所以我们具体去看它的子查询。对于这个子查询,首先他是一个相关子查询,那么很有可能就是在下面这三个临时表这一块。
根据子查询里面的表名,条件以及字段等可以进行验证以及搜寻。格局这些信息,可以判断出下面两个SPL2对应两个子查询。
到这里其实可以总结了,因为我们已经找到了所有ET中比较慢的操作符所对应的SQL部分。也就是下图中的这一段。
技巧:如果单单根据表名之类的信息没有办法判断出在哪里,还可以通过执行计划的前后逻辑来判断。下面举一个例子。
下图红框处为要找sql的操作符。先根据表名去找,可以找到好几处地方,仅凭现有信息没办法判断到底是哪一处。
这个时候我们阅读一下前后的逻辑,可以看到。进行了一个连接,并且另外一边是一个层次查询。根据这些条件,其实我们就可以判断出对应的部分在哪了。
所以,总的来说,根据操作符含义,附加信息,以及前后逻辑来寻找对应的sql所在。
4 怎么样优化
优化是最重要的一部分,本文章不可能尽善尽美。经过本次的学习,我重点学习到的是索引以及改写表结构这两种思路。
4.1 索引
我们大家都学过索引,知道索引的目的是为了避免全表扫描提高效率,当使用索引时,可以进行B+树的树形检索,其时间复杂度来到了O(log2n),而全表扫描则为O(n),并且我们知道,如果不是覆盖索引,那么为了拿到数据,数据库会进行一次回表,这是一种额外的成本。
所以,索引并非万能。是否使用索引,其实还是得根据具体的情况而论。
对过滤性进行分析。索引是否有用,具体取决于条件的过滤性,条件的过滤性好并且用了索引,那么就可以大幅度提高效率;条件过滤性差用了索引,反而因为回表的代价导致了效率降低。所以最佳的索引,就是用到了查询条件中全部过滤性好的条件来进行过滤,而不使用任何过滤性差的条件。
就上面的例子,我们进行分析。我们可以发现,一共有5个条件,第一个条件走了索引,过滤完之后有529756条数据;第二个条件接受第一条返回,过滤后i就有529756条数据,他几乎没有过滤性;第三,第四,第五条件组合在一起,过滤完之后还有230064条数据。
这个时候,如果使用最佳的过滤条件,把第一,第三,第四,第五条件都做成索引,那么确实可以提高效率,但是提高有限。如果表中已经有了这种索引,我们可以通过一些手段来使用它。但是,如果表中并没有这条索引,那么我们新建索引是非常不划算的。
这就是过滤性对于是否使用索引的判断。重点在于知道每个条件的过滤效果。
假如评估起来有过滤性好的列,但是没有用上索引,那么我们在索引的角度来优化它。
用过滤性好的列建立索引,减少回表。
如果优化器不按预期,使用hint来指定需要使用的索引。
如果hint也失效(当索引过多,会出现这种问题),这时我们使用在待查找的表名后面+index 索引名的方式来强制指定。如果带别名,注意语法为
表名 + index +索引名 +别名
4.2 改写sql
改写sql是另外一种思路。在实际生产中,因为改写一般周期较长,应用不可能立马改立马重新部署,所以应急时就不是改写而是hint注入。但是,它同样是一种非常重要的优化方法。
先讲一下改写sql的思想所在。我们都知道,从北京到上海,可以走很多种路,有的路快,有的路慢,有的路堵车,有的路不堵车。Sql也是一样,要实现同样的查询,可以有多种不同的写法,不同的写法也许会带来不同的运行效率。
以上面的例子来作为示范。当我们已经知道了问题所在并且要进行改写语句时。首先第一步,就是要读懂我们的sql语句,重点需要知道的信息有,业务查询的需求和涉及到的表中有哪些字段。
阅读下面的sql语句,我们可以知道,但从语句的角度,它的执行顺序为两个表先左连接,然后映射字段,到子查询的对应字段时进行子查询,然后一起返回给上层。从业务逻辑的角度,在知道了这几个表和字段的含义之后,我们分析出。它的目的是要算出每一个id,从最开始到现在的每一天的统计信息,两个子查询分别是从开始到某天的出账金额,以及从开始到某天的入账金额。
对sql语句有了充分的了解之后,进行改写。首先我们观察到,两个子查询的效率都非常低,并且两个子查询的大部分都是一样的,仅仅字段DEBITCREDITFLAG的值有所不同。所以,我们想到,对于这两个效率非常低的子查询,我们可以使用in来覆盖这个DEBITCREDITFLAG='1'和DEBITCREDITFLAG='2'。
其他逻辑不变,我们就写出了最终的结果。这里我们使用OUTER APPLY的写法,因为J要使用到b的列。如果使用left join无法达成这种目的,使用outer apply则可以。对于表j,用in来覆盖DEBITCREDITFLAG='1'和DEBITCREDITFLAG='2'。
4.3 层次查询
改写依靠的是经验的积累,下面将举一个例子来讲述本次学到的层次查询的效果和用法。
首先,构造一个测试所需要的表和数据。
CREATE TABLE org (
id INT PRIMARY KEY,
pid INT,
name VARCHAR(50)
);
INSERT INTO org VALUES
(0, NULL, '总公司'),
(1, 0, '技术中心'),
(2, 0, '市场部'),
(3, 1, '前端组'),
(4, 1, '后端组'),
(5, 2, '华北区'),
(6, 3, '组件库小组'),
(7, 4, '数据库小组');
插入成功后,可以到manager中查看,通过右键相关的表,然后点击浏览数据的按钮来直接浏览数据。我们通过数据,可以发现有两个字段,id和pid,他们的类型是完全一样的。他们就是本次实验中带有层次关系的两个字段了。
方式一:CONNECT BY。达梦完整支持 START WITH ... CONNECT BY 语法,并提供了 LEVEL、CONNECT_BY_ISLEAF 等伪列,以及 SYS_CONNECT_BY_PATH 函数
SELECT LEVEL AS lv,
RPAD(' ', (LEVEL - 1) * 4) || name AS 树形,
id,
pid,
SYS_CONNECT_BY_PATH(name, '/') AS 路径,
CONNECT_BY_ISLEAF AS 是否叶子
FROM org
START WITH pid IS NULL
CONNECT BY PRIOR id = pid
ORDER SIBLINGS BY id;
查看输出,可以发现,输出根据路径展示了一个树形的关系,这就是层次查询的作用,在一个表中得到树形结构的关系,可以根据层次进行其他多种操作。
第二种:递归CTE。达梦同样支持 WITH RECURSIVE 递归 CTE,写法与 MySQL 8 / PostgreSQL 一致
WITH RECURSIVE tree (id, pid, name, lv, sort_path, name_path) AS (
-- 锚点:根节点
SELECT id, pid, name,
1,
LPAD(id, 4, '0'),
CAST(name AS VARCHAR(500))
FROM org
WHERE pid IS NULL
UNION ALL
-- 递归:挂到上一层
SELECT o.id, o.pid, o.name,
t.lv + 1,
t.sort_path || '-' || LPAD(o.id, 4, '0'),
t.name_path || '/' || o.name
FROM org o
JOIN tree t ON o.pid = t.id
)
SELECT lv,
REPEAT(' ', lv - 1) || name AS 树形,
id, pid, name_path AS 路径
FROM tree
ORDER BY sort_path;
查看上面的语句可以发现,这是一种原始的写法,也就是不断地使用递归来实现层次查询的效果。这里只需要能认得到这种写法就好。
常用变体:
从上到下查询祖先链:这里需要注意的是语法中的PRIOR,PRIOR 的作用是:把上一轮已经取到的行,作为"已知值",去匹配原表里的下一行。所以,PRIOR指向谁,就代表从谁出发。然后根据层次关系就很好判断了。
SELECT LEVEL, id, name
FROM org
START WITH id = 6
CONNECT BY PRIOR pid = id
ORDER BY LEVEL;
观察输出,可以发现,他实际上已经输出了一条从下往上的链条。
处理循环数据:当数据存在环形引用时,达梦默认会报错 -4030: 用户数据中的CONNECT BY循环。加上 NOCYCLE 关键字即可跳过循环继续遍历,同时可用 CONNECT_BY_ISCYCLE 伪列标记出导致循环的行。
加了NOCYCLE后,达梦在递归过程中会记录已经走过的节点。一旦发现"下一个节点已经出现过了",就不再往下展开,直接跳过,继续处理其他分支。这就是它的作用,记住就行。
CONNECT_BY_ISCYCLE 是一个伪列,专门配合 NOCYCLE 使用。值为 1:这一行"本来会导致循环",被 NOCYCLE 拦下了。值为 0:正常行。它的作用是标记出问题数据,方便你去排查哪一行的父子关系配错了。这也是一种语法知识,记住就行。
SELECT id, name, CONNECT_BY_ISCYCLE AS 是否循环
FROM org
START WITH pid IS NULL
CONNECT BY NOCYCLE PRIOR id = pid;
只取叶子节点:只取叶子节点一共有两种方式,他们的原理非常的简单,第一种方法,就是加一个判断,如果CONNECT_BY_ISLEAF = 1就留下。第二种方式对应第二种写法,做法是先查出叶子节点,然后在递归过程中用not exists判断是否在其中。
SELECT *
FROM (
SELECT LEVEL AS lv,
RPAD(' ', (LEVEL - 1) * 4) || name AS 树形,
id,
pid,
name,
CONNECT_BY_ISLEAF AS 是否叶子
FROM org
START WITH pid IS NULL
CONNECT BY PRIOR id = pid
)
WHERE 是否叶子 = 1
ORDER BY lv, id;
WITH RECURSIVE tree (id, pid, name, lv, sort_path) AS (
SELECT id, pid, name,
1,
LPAD(id, 4, '0')
FROM org
WHERE pid IS NULL
UNION ALL
SELECT o.id, o.pid, o.name,
t.lv + 1,
t.sort_path || '-' || LPAD(o.id, 4, '0')
FROM org o
JOIN tree t ON o.pid = t.id
)
SELECT lv,
REPEAT(' ', lv - 1) || name AS 树形,
id, pid, name
FROM tree t
WHERE NOT EXISTS (
SELECT 1
FROM org c
WHERE c.pid = t.id
)
ORDER BY sort_path;
文章
阅读量
获赞
