分类 其它 下的文章

概述

使用 explain 输出 SELECT 语句执行的详细信息,包括以下信息:

  • 表的加载顺序
  • sql 的查询类型
  • 可能用到哪些索引,实际上用到哪些索引
  • 读取的行数

Explain 执行计划包含字段信息如下:分别是 id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered、Extra 12个字段。

通过explain extended + show warnings可以在原本explain的基础上额外提供一些查询优化的信息,得到优化以后的可能的查询语句(不一定是最终优化的结果)。

测试环境:

CREATE TABLE `blog` (
  `blog_id` int NOT NULL AUTO_INCREMENT COMMENT '唯一博文id--主键',
  `blog_title` varchar(255) NOT NULL COMMENT '博文标题',
  `blog_body` text NOT NULL COMMENT '博文内容',
  `blog_time` datetime NOT NULL COMMENT '博文发布时间',
  `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `blog_state` int NOT NULL COMMENT '博文状态--0 删除 1正常',
  `user_id` int NOT NULL COMMENT '用户id',
  PRIMARY KEY (`blog_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8

CREATE TABLE `user` (
  `user_id` int NOT NULL AUTO_INCREMENT COMMENT '用户唯一id--主键',
  `user_name` varchar(30) NOT NULL COMMENT '用户名--不能重复',
  `user_password` varchar(255) NOT NULL COMMENT '用户密码',
  PRIMARY KEY (`user_id`),
  KEY `name` (`user_name`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8

CREATE TABLE `discuss` (
  `discuss_id` int NOT NULL AUTO_INCREMENT COMMENT '评论唯一id',
  `discuss_body` varchar(255) NOT NULL COMMENT '评论内容',
  `discuss_time` datetime NOT NULL COMMENT '评论时间',
  `user_id` int NOT NULL COMMENT '用户id',
  `blog_id` int NOT NULL COMMENT '博文id',
  PRIMARY KEY (`discuss_id`)
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8

id

表示查询中执行select子句或者操作表的顺序,id的值越大,代表优先级越高,越先执行

explain select discuss_body 
from discuss 
where blog_id = (
    select blog_id from blog where user_id = (
        select user_id from user where user_name = 'admin'));

三个表依次嵌套,发现最里层的子查询 id最大,最先执行。

select_type

表示 select 查询的类型,主要是用于区分各种复杂的查询,例如:普通查询、联合查询、子查询等。

  • SIMPLE:表示最简单的 select 查询语句,在查询中不包含子查询或者交并差集等操作。
  • PRIMARY:查询中最外层的SELECT(存在子查询的外层的表操作为PRIMARY)。
  • SUBQUERY:子查询中首个SELECT。
  • DERIVED:被驱动的SELECT子查询(子查询位于FROM子句)。
  • UNION:在SELECT之后使用了UNION

table

查询的表名,并不一定是真实存在的表,有别名显示别名,也可能为临时表。当from子句中有子查询时,table列是<derivenN>的格式,表示当前查询依赖 id为N的查询,会先执行 id为N的查询。

partitions

查询时匹配到的分区信息,对于非分区表值为NULL,当查询的是分区表时,partitions显示分区表命中的分区情况。

type

查询使用了何种类型,它在 SQL优化中是一个非常重要的指标

访问效率:const > eq_ref > ref > range > index > ALL

system

当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘IO,速度非常快。比如,Mysql系统表proxies_priv在Mysql服务启动时候已经加载在内存中,对这个表进行查询不需要进行磁盘 IO。

const

单表操作的时候,查询使用了主键或者唯一索引。

eq_ref

多表关联查询的时候,主键和唯一索引作为关联条件。如下图的sql,对于user表(外循环)的每一行,user_role表(内循环)只有一行满足join条件,只要查找到这行记录,就会跳出内循环,继续外循环的下一轮查询。

ref

查找条件列使用了索引而且不为主键和唯一索引。虽然使用了索引,但该索引列的值并不唯一,这样即使使用索引查找到了第一条数据,仍然不能停止,要在目标值附近进行小范围扫描。但它的好处是不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内做扫描。

ref_or_null

类似 ref,会额外搜索包含NULL值的行

index_merge

使用了索引合并优化方法,查询使用了两个以上的索引。新建comment表,id为主键,value_id为非唯一索引,执行explain select content from comment where value_id = 1181000 and id > 1000;,执行结果显示查询同时使用了id和value_id索引,type列的值为index_merge。

range

有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。像between、and、>、<、in和or都是范围索引扫描。

index

index包括select索引列,order by主键两种情况。

order by主键。这种情况会按照索引顺序全表扫描数据,拿到的数据是按照主键排好序的,不需要额外进行排序。

select索引列。type为index,而且extra字段为using index,也称这种情况为索引覆盖。所需要取的数据都在索引列,无需回表查询。

all

全表扫描,查询没有用到索引,性能最差。

possible_keys

此次查询中可能选用的索引。但这个索引并不定一会是最终查询数据时所被用到的索引。

key

此次查询中确切使用到的索引

ref

ref 列显示使用哪个列或常数与key一起从表中选择数据行。常见的值有const、func、NULL、具体字段名。当 key 列为 NULL,即不使用索引时。如果值是func,则使用的值是某个函数的结果。

以下SQL的执行计划ref为const,因为使用了组合索引(user_id, blog_id),where user_id = 13中13为常量

mysql> explain select blog_id from user_like where user_id = 13;
+----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | user_like | NULL       | ref  | ul1,ul2       | ul1  | 4       | const |    2 |   100.00 | Using index |
+----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+

而下面这个SQL的执行计划ref值为NULL,因为key为NULL,查询没有用到索引。

mysql> explain select user_id from user_like where status = 1;
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | user_like | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    6 |    16.67 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+

rows

估算要找到所需的记录,需要读取的行数。评估SQL 性能的一个比较重要的数据,mysql需要扫描的行数,很直观的显示 SQL 性能的好坏,一般情况下 rows 值越小越好

filtered

存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例

extra

表示额外的信息说明。为了方便测试,这里新建两张表。

CREATE TABLE `t_order` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int DEFAULT NULL,
  `order_id` int DEFAULT NULL,
  `order_status` tinyint DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_userid_order_id_createdate` (`user_id`,`order_id`,`create_date`)
) ENGINE=InnoDB AUTO_INCREMENT=99 DEFAULT CHARSET=utf8

CREATE TABLE `t_orderdetail` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_id` int DEFAULT NULL,
  `product_name` varchar(100) DEFAULT NULL,
  `cnt` int DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_orderid_productname` (`order_id`,`product_name`)
) ENGINE=InnoDB AUTO_INCREMENT=152 DEFAULT CHARSET=utf8

using where

表示在查询过程中使用了WHERE条件进行数据过滤。当一 个查询中包含WHERE条件时,MySQL会根据该条件过滤出满足条件的数据行,然后再进行后续的操作。这个过程 就被称为"Using Where”。

表示查询的列未被索引覆盖,,且where筛选条件是索引列前导列的一个范围,或者是索引列的非前导列,或者是非索引列。对存储引擎返回的结果进行过滤(Post-filter,后过滤),一般发生在MySQL服务器,而不是存储引擎层,因此需要回表查询数据。

using index

查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过索引查找就能直接找到符合条件的数据,
不需要回表
查询数据。

Using where&Using index

查询的列被索引覆盖,但无法通过索引查找找到符合条件的数据,不过可以通过索引扫描找到符合条件的数据,也不需要回表查询数据。

包括两种情况(组合索引为(user_id, orde)):

where筛选条件不符合最左前缀原则

where筛选条件是索引列前导列的一个范围

null

查询的列未被索引覆盖,并且where筛选条件是索引的前导列,也就是用到了索引,但是部分字段未被索引覆盖,必须回表查询这些字段,Extra中为NULL。

using index condition

索引下推(index condition pushdown,ICP),先使用where条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。

对于联合索引(a, b),在执行 select * from table where a > 1 and b = 2 语句的时候,只有 a 字段能用到索引,那在联合索引的 B+Tree 找到第一个满足条件的主键值(ID 为 2)后,还需要判断其他条件是否满足(看 b 是否等于 2),那是在联合索引里判断?还是回主键索引去判断呢?
MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

不使用ICP的情况(set optimizer_switch='index_condition_pushdown=off'),如下图,在步骤4中,没有使用where条件过滤索引:

使用ICP的情况(set optimizer_switch='index_condition_pushdown=on'):

下面的例子使用了ICP:

explain select user_id, order_id, order_status from t_order where user_id > 1 and user_id < 5\G;

关掉ICP之后(set optimizer_switch='index_condition_pushdown=off'),可以看到extra列为using where,不会使用索引下推。

using temporary

使用了临时表保存中间结果,常见于 order by 和 group by 中。典型的,当group by和order by同时存在,且作用于不同的字段时,就会建立临时表,以便计算出最终的结果集

filesort

文件排序。表示无法利用索引完成排序操作,以下情况会导致filesort:

  • order by 的字段不是索引字段
  • select 查询字段不全是索引字段
  • select 查询字段都是索引字段,但是 order by 字段和索引字段的顺序不一致

using join buffer

Block Nested Loop,需要进行嵌套循环计算。两个关联表join,关联字段均未建立索引,就会出现这种情况。比如内层和外层的type均为ALL,rows均为4,需要循环进行
4*4
次计算。常见的优化方案是,在关联字段上添加索引,避免每次嵌套循环计算。

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

iOS
动态链接器
dyld
中有一个神秘的变量
__dso_handle
:

// dyld/dyldMain.cpp
static const MachOAnalyzer* getDyldMH()
{
#if __LP64__
    // 声明 __dso_handle
    extern const MachOAnalyzer __dso_handle;
    return &__dso_handle;
#else
    ...
#endif // __LP64__
}

这个函数内部声明了一个变量
__dso_handle
,其类型是
struct MachOAnalyzer

查看
struct MachOAnalyzer
的定义,它继承自
struct mach_header
:

image

struct mach_header
正是
XNU
内核里面,定义的
Mach-O
文件头:

// EXTENERL_HEADERS/mach-o/loader.h
struct mach_header {
	uint32_t	magic;		/* mach magic number identifier */
	cpu_type_t	cputype;	/* cpu specifier */
	cpu_subtype_t	cpusubtype;	/* machine specifier */
	uint32_t	filetype;	/* type of file */
	uint32_t	ncmds;		/* number of load commands */
	uint32_t	sizeofcmds;	/* the size of all the load commands */
	uint32_t	flags;		/* flags */
};

从上面函数
getDyldMH
的名字来看,它返回
dyld
这个
Mach-O
文件的文件头,而这确实也符合变量
__dso_handle
的类型定义。

但是奇怪的事情发生了,搜遍整个
dyld
源码库,都无法找到变量
__dso_handle
的定义。所有能搜到的地方,都只是对这个变量
__dso_handle
的声明。

众所周知,动态连接器
dyld
本身是静态链接的。

也就是说,动态连接器
dyld
本身是不依赖任何其他动态库的。

因此,这个变量
__dso_handle
不可能定义在其他动态库。

既然这样,动态链接器
dyld
本身是如何静态链接通过的呢?

答案只可能是静态链接器
ld
在链接过程中做了手脚。

查看静态链接器
ld
的源码,也就是
llvm
的源码,可以找到如下代码:

// lld/MachO/SyntheticSections.cpp
void macho::createSyntheticSymbols() {
  // addHeaderSymbol 的 lamba 表达式
  auto addHeaderSymbol = [](const char *name) {
    symtab->addSynthetic(name, in.header->isec, /*value=*/0,
                         /*isPrivateExtern=*/true, /*includeInSymtab=*/false,
                         /*referencedDynamically=*/false);
  };

  ...

  // The Itanium C++ ABI requires dylibs to pass a pointer to __cxa_atexit
  // which does e.g. cleanup of static global variables. The ABI document
  // says that the pointer can point to any address in one of the dylib's
  // segments, but in practice ld64 seems to set it to point to the header,
  // so that's what's implemented here.
  addHeaderSymbol("___dso_handle");
}

上面代码定义了一个
addHeaderSymbol

lamda
表达式,然后使用它添加了一个符号,这个符号正是
__dso_handle

调用
addHeaderSymbol
上方的注释使用
chatGPT
翻译过来如下:

Itanium C++ ABI 要求动态库传递一个指向 __cxa_atexit 的指针,该函数负责例如静态全局变量的清理。ABI 文档指出,指针可以指向动态库的某个段中的任意地址,但实际上,ld64(苹果的链接器)似乎将其设置为指向头部,所以这里实现了这种做法。

注释中提到的
Itanium C++ ABI
最初是为英特尔和惠普联合开发的
Itanium
处理器架构设计的。

但其影响已经超过了最初设计的架构范围,并被广泛用于其他架构,比如
x86

x86-64
上的多种编译器,包括
GCC

Clang

而且,注释中还提到,
__dso_handle
在苹果的实现里,是指向了
Mach-O
的头部。

至此,谜底解开~。

在PDF中绘制图形可以增强文档的视觉效果。通过添加不同类型的形状,如实线、虚线、矩形、圆形等,可以使文档更加生动有趣,提高读者的阅读兴趣。这对于制作报告、演示文稿或是教材特别有用。本文将通过以下几个示例介绍如何
使用Python 在PDF中绘制不同的图形

  • Python 在PDF中绘制实线、虚线
  • Python 在PDF中绘制矩形
  • Python 在 PDF 中绘制椭圆形

所需Python库
-
Spire.PDF for Python
。可以通过下面的pip 命令直接安装:

pip install Spire.Pdf

Python 在PDF中绘制实线、虚线

Spire.PDF for Python库提供了
PdfPageBase.Canvas.DrawLine()
方法用于在PDF页面指定位置绘制线条。通过设置画笔PdfPen的样式,可以绘制实线或虚线。

Python 代码:

from spire.pdf.common import *
from spire.pdf import *
 
#创建PDF文档
pdf =PdfDocument()#添加页面
page =pdf.Pages.Add()#保存当前绘图状态
state =page.Canvas.Save()#指定线条起始点的X和Y坐标
x = 100.0y= 50.0   
 
#制定线条长度
width = 300.0  
 
#创建指定颜色和粗细的画笔
pen = PdfPen(PdfRGBColor(Color.get_Blue()), 2.0)#用画笔在页面上绘制实线
page.Canvas.DrawLine(pen, x, y, x +width, y)#设置画笔样式为虚线
pen.DashStyle =PdfDashStyle.Dash#设置虚线样式为[1, 4, 1]
pen.DashPattern = [1, 4, 1]#指定虚线起始点 y 坐标
y = 80.0  
 
#用画笔页面上绘制虚线
page.Canvas.DrawLine(pen, x, y, x +width, y)#恢复之前保存的绘图状态
page.Canvas.Restore(state)#保存PDF文档
pdf.SaveToFile("绘制线条.pdf")
pdf.Close()
pdf.Dispose()

Python 在PDF中
绘制矩形

PdfPageBase.Canvas.DrawRectangle()
方法可用于在PDF页面指定位置绘制矩形。通过向该方法中传递不同的参数,可以指定矩形的大小、填充色等。

Python 代码:

from spire.pdf.common import *
from spire.pdf import *
 
#创建PDF文档
pdf =PdfDocument()#添加页面
page =pdf.Pages.Add()#保存当前绘图状态
state =page.Canvas.Save()#创建指定颜色和粗细的画笔
pen = PdfPen(PdfRGBColor(Color.get_Blue()), 1.5)#用画笔在页面上绘制一个矩形
page.Canvas.DrawRectangle(pen, RectangleF(PointF(20.0, 60.0), SizeF(150.0, 90.0)))#创建一个线性渐变笔刷
linearGradientBrush = PdfLinearGradientBrush(PointF(220.0, 60.0), PointF(350.0, 180.0), PdfRGBColor(Color.get_Green()), PdfRGBColor(Color.get_Pink()))#用线性渐变笔刷绘制一个填充式矩形
page.Canvas.DrawRectangle (linearGradientBrush, RectangleF(PointF(220.0, 60.0), SizeF(150.0, 90.0)))#恢复之前保存的绘图状态
page.Canvas.Restore(state)#保存PDF文档
pdf.SaveToFile("绘制矩形.pdf")
pdf.Close()
pdf.Dispose()

Python 在 PDF 中绘制椭圆形

在PDF页面指定位置绘制椭圆形可以使用
PdfPageBase.Canvas.DrawEllipse
()

方法。通过指定不同的PDF画笔或画刷,可以绘制不同样式的椭圆形。

Python 代码:

from spire.pdf.common import *
from spire.pdf import *
 
#创建PDF文档
pdf =PdfDocument()#添加页面
page =pdf.Pages.Add()#保存当前绘图状态
state =page.Canvas.Save()#创建画笔
pen =PdfPens.get_Violet()#用画笔在页面上绘制椭圆形
page.Canvas.DrawEllipse(pen, 30.0, 60.0, 150.0, 100.0)#创建填充画刷对象
brush =PdfSolidBrush(PdfRGBColor(Color.get_Violet()))#绘制填充的椭圆形状
page.Canvas.DrawEllipse(brush, 220.0, 60.0, 150.0, 100.0)#恢复之前保存的绘图状态
page.Canvas.Restore(state)#保存PDF文档
pdf.SaveToFile("绘制椭圆形.pdf")
pdf.Close()
pdf.Dispose()


生成文档中的红色水印,可以通过点击以下链接,申请一个月免费授权来去除:

https://www.e-iceblue.cn/misc/temporary-license.html

本文分享自华为云社区
《【GaussTech技术专栏】GaussDB性能调优》
,作者:GaussDB 数据库。

数据库性能调优是一项复杂且系统性的工作,需要综合考虑多方面的因素。因此,调优人员应对系统软件架构、软硬件配置、数据库配置参数、并发控制、查询处理和数据库应用拥有广泛而深刻的理解。

本文旨在剖析GaussDB性能调优的总体思路,探讨系统整体性能问题,以及对锁阻塞问题进行分析和优化。

1. 性能调优思路

GaussDB总体性能调优的思路是:先进行性能瓶颈点分析,找到相应的瓶颈点之后,再针对性地进行优化,直到系统性能到达业务可接受的范围内。

调优思路,如图1所示:

图1 GaussDB总体性能调优思路

首先,应该确认应用压力是否传递到数据库,可以通过分析数据库节点的资源使用情况,如CPU、I/O、内存以及数据库线程池、活跃会话等信息来辅助判断。GaussDB数据库的管控平台提供了丰富的监控指标体系,便于性能分析人员查看数据库的实时或者历史资源使用情况。

登录管控平台后,进入监控巡检菜单,选择监控大盘,即可查看对应实例的CPU/内存使用率,如图2所示:

图2 对应实例的CPU/内存使用率

点击磁盘/存储菜单,可以查看磁盘I/O使用率,重点关注磁盘读写速率以及时延是否符合预期,如图3所示:

图3 磁盘读写速率以及时延情况

点击网络菜单,可以查看网络传输速率及网卡是否有丢包、错包等情况,如图4所示:

图4 网络传输速率及网卡发送速率

选择连接菜单,可以查看数据库的连接及会话状态,如图5所示:

图5 连接及会话状态

图5中,如果活跃会话的占比远低于应用的并发数,说明数据库中大量会话处于空闲状态。同时,如果CPU使用率也很低,那么,就可以判断压力没到达数据库,此时需要排查应用端是否存在瓶颈。

导致应用侧瓶颈的问题比较常见的原因有:

  • 应用服务器资源瓶颈。比如,应用服务器的CPU满载,应用程序内存分配不足等;

  • 应用到数据库网络问题。比如,网络时延高,带宽满,存在丢包现象等;

  • 应用自身逻辑处理速度慢;

  • 应用配置不优,比如连接池参数、内存相关配置等设置不当。

例如,某个客户通过 jmeter 做大并发压测,性能不及业务预期。经过分析,发现是 jmeter 工具分配的最大可用内存不足,导致压力没有到达数据库。通过修改如下配置,问题得到了解决。

编辑jmeter.sh文件:set HEAP=-Xms1g -Xmx4g

确认压力到达数据库后,再针对相应的瓶颈点进行分析优化。主要从以下两个方面进行:

1)排查数据库中是否存在性能不优的业务SQL语句,并对性能不优的SQL进行优化。通过如下语句,查看数据库中耗时高的TOP SQL语句,并对那些执行性能不符合预期的SQL语句逐一进行分析与调优。

select unique_sql_id,substr(query,1,50) as query ,n_calls,round(total_elapse_time/n_calls/1000,2) avg_time,round(total_elapse_time/1000,2) as total_time from dbe_perf.summary_statement t where  n_calls>10 and avg_time>3  and user_name='root' order by total_time desc;

如图6所示,n_calls 表示该SQL语句在数据库中的执行次数,avg_time 为该SQL 语句的平均执行时间,total_time 为该SQL语句的总耗时。对于平均执行时间超过阈值的SQL语句,重点进行分析与优化。

图6 SQL语句指标及对应数据展示

针对执行性能不优的SQL语句,通过unique_sql_id可以查看该SQL语句的执行详情,帮助分析SQL语句的性能瓶颈点。

select * from dbe_perf.statement where unique_sql_id=3508314654;

如图7所示,该视图记录了SQL语句在数据库的详细执行情况,比如,总执行次数(n_calls)和总耗时(total_elapse_time),便于获取该SQL的总耗时以及平均耗时。

图7 SQL语句在数据库中的详细执行情况视图

行活动,
包括随机扫描、顺序扫描行数、返回的行数、插入/更新/删除的行数以及buffer命中的页面数等信息。此外,还记录了软解析(n_soft_parse)、硬解析(n_hard_parse)的次数,比如SQL大量硬解析导致的数据库CPU飚高,可以通过该指标进行分析定位。

时间模型
,包含db_time、cpu_time、execution_time、plan_time、data_io_time、net_send_info、net_recv_info、sort_time以及hash_time等指标,有助于判断SQL在数据库中的时间消耗在哪个阶段。例如,若某环境磁盘性能不佳,则data_io_time的耗时占比就会比较高。

如果需要进一步分析SQL本身的性能问题,比如执行计划是否最优、索引是否最优等性能问题,可以借助SQL的执行计划进行分析。

通过如下方式,可查看SQL的执行计划:

explain analyze SELECT c_id     FROM bmsql_customer     WHERE c_w_id = 1 AND c_d_id = 1 AND c_last = 'ABLEABLEABLE'     ORDER BY c_first;

结合SQL的执行计划,分析SQL性能的瓶颈点,再进行性能优化,如图8所示:

图8 SQL性能优化过程

2)从系统层面进行操作系统级和数据库系统级的调优,充分利用机器的CPU、内存、I/O和网络资源,避免资源冲突,从而提升整个系统查询的吞吐量。

2. 系统级性能问题分析

2.1 CPU使用率高

数据库的CPU使用率高,通常是由业务SQL语句引起的,我们可以通过如下方式,获取数据库中消耗CPU资源高的SQL语句,并对相应的业务SQL语句进行优化。

select unique_sql_id,substr(query,1,50) as query ,n_calls,round(total_elapse_time/n_calls/1000,2) avg_time,round(total_elapse_time/1000,2) as total_time,round(cpu_time/1000,2) as cup_time from dbe_perf.statement t where  n_calls>10 and avg_time>3  and user_name='root'  order by cpu_time desc limit 5;

常见的导致CPU资源消耗高的原因有:

  • SQL语句大量使用了全表扫描,这可能是由索引缺失、索引失效、执行计划不优等因素所导致。

  • SQL语句大量进行硬解析,通常是因为应用逻辑未使用PBE(Prepare Bind Execute)。

  • SQL语句扫描了大量的元组,比如,分区表分区剪枝失效,扫描了全分区,表中存在大量的死元组,导致扫描了大量无用页面等。

如果CPU使用率高是由非业务SQL语句引起的,可以借助火焰图来进行分析定位。通过火焰图,可以直观地了解程序中哪些函数占用了大量的 CPU 时间或资源,并且可以追踪函数调用路径。

GaussDB在内核505版本中内置了火焰图工具,默认每5分钟会自动采集一次,保存在$GAUSSLOG/gs_flamegraph/{datanode}路径下,详细信息可参考GaussDB产品文档《内置perf工具》章节。

例如,某客户在压测过程中发现数据库服务器的CPU SYS占用率超过70%,通过抓取压测期间的火焰图进行分析,如图9所示,发现数据库加载时,区文件的线程占比超过40%。

图9 某客户压测期间的火焰图

经分析,原因是在高并发频繁建立连接时,数据库每次建连都需要读取时区文件以获取时区信息,而应用未使用长连接,导致CPU SYS使用率飙升。

2.2 内存不足

内存资源,也是影响数据库性能的关键因素之一。在分析内存问题之前,我们先了解一下GaussDB的内存管理机制。

如图10所示,GaussDB的内存管理采用动态内存与静态内存相结合的方式,由参数 max_process_memory 控制数据库可用的最大内存。其中,静态内存区域主要用作数据库的共享缓冲区,用于缓存数据页面,由shared_buffers参数控制。动态内存区域,则由数据库根据需要进行动态分配,主要包括元数据的缓存、执行计划的缓存、用户建连以及内部线程的内存消耗等。

图10 GaussDB的内存管理机制

内存导致的性能问题,通常分为以下几个方面:

1)共享缓存区不足,导致SQL的buffer命中率低。为了查看相应的性能指标,可以借助GaussDB的管控平台或者WDR报告。通常情况下,TP数据库的buffer命中率应该在99%以上。如果数据库的buffer命中率较低,建议排查数据库的shared_buffers参数设置是否合理(如图11所示)。

图11 数据库的buffer命中率

2)在GaussDB中,SQL的hash join或者sort算子存在数据落盘操作,work_mem参数控制可下盘算子可用的物理内存空间。如果work_mem所限定的物理内存不够,算子运算的数据将被写入临时表空间,会带来5-10倍的性能下降。为了优化性能,可以查看SQL的执行计划,如果算子存在落盘的情况(如图12所示),可适当调整work_mem参数值。

图12 算子落盘情况

3)数据库动态内存不足,导致业务执行报错(ERROR:memory is temporarily unavailable )或者性能不足。当动态内存不足时,可以通过如下SQL语句找出内存消耗高的SQL语句,以便排查是否存在不优的SQL 语句。借助SQL的执行计划分析,可以检查是否有不合理的join顺序,或者是否存在非必要的排序操作,从而避免消耗大量内存。

select unique_sql_id,substr(query,1,50) as query ,n_calls,round(total_elapse_time/n_calls/1000,2) avg_time,round(total_elapse_time/1000,2) as total_time,hash_mem_used,sort_mem_used from dbe_perf.statement t where  n_calls>10 and avg_time>3  and user_name='root' order by (hash_mem_used+sort_mem_used) desc;

如果需要排查由非业务SQL语句导致的异常的内存消耗问题,比如内存堆积、内存泄露等,GaussDB提供了丰富的内存相关的监控视图,可以通过下面的视图(如图13所示),查看数据库节点的内存消耗情况。

图13 GaussDB内存相关的监控视图

基于上面的查询结果,如果dynamic_used_shrctx的占用率高,说明是全局共享动态内存的占用高。可以通过如下SQL语句,查看全局共享动态内存上下文的消耗情况。

select contextname, sum(totalsize)/1024/1024 totalsize, sum(freesize)/1024/1024 freesize, count(*) count from gs_shared_memory_detail group by contextname order by totalsize desc limit 10;

如果max_dynamic_memory的占用率高,但是dynamic_used_shrctx的占用率低,那么说明是线程或者会话占用的内存多。可以通过如下SQL语句,查询数据库线程的内存上下文消耗情况。

select contextname, sum(totalsize)/1024/1024 totalsize, sum(freesize)/1024/1024 freesize, count(*) sum from gs_thread_memory_context group by contextname order by sum desc limit 10;

查询结果如下图所示,可以看出,当前数据库中内存占用最高的为元数据的缓存(LocalSysCacheShareMemory)。结合图14中的查询结果,排查是否存在不合理的内存占用情况。

图14 数据库线程的内存上下文消耗情况

2.3 IO瓶颈

通过 iostat 命令,可以查看数据库节点 I/O 的繁忙度和吞吐量,分析是否存在由于 I/O 导致的性能瓶颈。如图15所示:

图15 数据库节点 I/O 的繁忙度和吞吐量

重点关注磁盘的读写吞吐量和读写时延。通常情况下,SSD盘的读写时延在2ms以下,单盘带宽在300MB以上。如果磁盘性能存在异常,优先排查硬件是否存在故障,如磁盘存在坏盘、慢盘、RAID卡故障或磁盘读写策略不正确等。如果磁盘硬件性能正常,而I/O 压力大,可以适当调整数据库I/O 相关的参数,以降低数据的I/O 消耗,从而优化数据库的整体性能。I/O 相关的关键参数链接如下:

后端写进程:
https://support.huaweicloud.com/distributed-devg-v2-gaussdb/gaussdb-12-1124.html

异步I/O:
https://support.huaweicloud.com/distributed-devg-v2-gaussdb/gaussdb-12-1125.html

2.4 网络异常

在传统集中式数据库环境下,应用服务器与数据库服务器通常部署在同一个机房内,从而确保应用与数据库间的网络开销较小。然而,在云+分布式数据库环境下,应用服务器到数据库服务器的网络链路较长,网络耗时对交易性能至关重要。在此情境下,我们不仅需要关注应用与数据库之间的网络状况(通常应该小于0.2ms),还需考虑数据库内部节点之间的网络情况,也会对性能产生较大的影响。

GaussDB要求AZ内网络时延小于0.2ms,AZ间的网络时延小于2ms,region间网络时延小于100ms。可以通过linux的ping命令,排查两个服务器之间的网络时延及丢包等情况,如图16所示:

图16 ping命令,排查两个服务器之间的网络时延及丢包等情况

通过 sar -n DEV 1 命令,查看网络的传输情况。

如图17所示,“rxkB/s”为每秒接收的千字节数,“txkB/s”为每秒发送的千字节数,主要关注每个网卡的传输量是否达到传输上限。

图17 sar -n DEV 1 命令,网络传输情况

3. 锁阻塞问题分析

数据库锁机制是一种用于管理并发访问的技术。它通过对数据库中的数据进行锁定,来确保在多个用户并发访问数据库时,数据的一致性和完整性。

在并发访问的场景下,经常会遇到因为锁冲突导致的性能问题。下面我们看一下在GaussDB中应该如何定位和分析锁冲突的问题。

如果应用正在运行,可以通过下面的SQL语句,查看当前数据库中正在执行的会话是否存在锁阻塞。

集中式场景:

SELECT a.pid as w_pid,a.query as w_query,a.state,d.query as locking_query,d.state as l_state,d.pid as l_pid,d.sessionid as l_sessionid
FROM pg_stat_activity AS a
JOIN pg_thread_wait_status b ON b.query_id = a.query_id
JOIN pg_thread_wait_status c 
ON c.sessionid = b.block_sessionid and c.node_name=b.node_name
JOIN pg_stat_activity d
on d.sessionid=c.sessionid
;

分布式场景:

SELECT a.pid as w_pid,a.query as w_query,a.state as w_state, a.datname, a.usename,d.query as lock_query,d.state as l_state,d.pid as l_pid,d.sessionid as l_sessionid
FROM pgxc_stat_activity AS a
JOIN pgxc_thread_wait_status b ON b.query_id = a.query_id
JOIN pgxc_thread_wait_status c ON c.sessionid = b.block_sessionid and c.node_name=b.node_name
JOIN pgxc_stat_activity d
on substring(d.global_sessionid,0,instr(d.global_sessionid,'#')) ilike substring(c.global_sessionid,0,instr(c.global_sessionid,'#'))
;

查询结果如图18所示,可以获取当前库中存在锁阻塞的SQL语句,同时获取到阻塞它的会话ID、线程ID以及对应的查询。

图18 锁阻塞查询结果展示

要找到并结束阻塞当前查询的会话,可以使用以下语句。

SELECT PG_TERMINATE_BACKEND(pid);

如果是历史的锁阻塞导致的性能问题,可以通过下面语句查询指定时间段内的数据库等待事件。如果发现有大量的acquire lock(包括transaction ID、relation、tuple)事件,表示该时间段内数据库存在锁阻塞问题。

select wait_status,event,count(*) from gs_asp where sample_time>='20241016 18:45:00' and sample_time <='20241016 19:00:00' group by 1,2 order by 3 desc;

ASP(Active Session Profile,活跃会话概要信息),通过采样实例中活跃会话的状态信息,以低成本的方式复现过去一段时间内的系统活动,主要包含会话基本信息、会话事务、执行的语句,等待事件,会话状态(如active、idle等)、当前正阻塞在哪个事件上、正在等待哪个锁或被哪个会话阻塞。

如图19所示,该时间段数据库占比最高的两个等待事件,一个是等待dn_6004_6005_6006分片返回执行结果,这需要进一步排查该分片上性能瓶颈的原因;另外一个等待事件是acquire lock(relation),表示存在大量的表级锁等待。

图19 特定事件内数据库占比最高的两个等待事件

结合数据库的归一化视图,可以获取数据库中存在锁等待的SQL语句,如图20所示:

图20 获取数据库中存在锁等待的SQL语句

通过该语句的Unique_query_id,获得查询阻塞该语句的query_id。

execute direct on datanodes $$select t1.unique_query_id,t1.thread_id,t1.sessionid,t1.wait_status,t1.event,t1.state,t2.query_id as lock_query_id from gs_asp t1,gs_asp t2 where t1.block_sessionid=t2.sessionid and  t1.unique_query_id=168353725$$;

如图21所示,lock_query_id 为阻塞该SQL语句的query_id。

图21 获取阻塞锁等待SQL语句的query_id

利用上一步查询出来的query_id,并结合gs_asp视图,可以通过如下语句获取该SQL语句的详情。查询结果如图22所示,可以看到,阻塞该语句的也是同一张表的update语句,这表明是由于并发更新同一行数据所导致的锁冲突。

图22 锁等待的SQL语句查询结果

通常情况下,解决并发更新锁冲突问题的解决思路需要从业务角度出发,审视存在并发更新同一行的情况是否符合业务场景。如果业务中不存在这样的场景,那应该从业务逻辑或者业务数据上进行优化,以避免并发更新同一行的情况发生。

4. 总结

数据库性能调优涉及硬件、操作系统、数据库、应用等多个层面,因此,在性能调优过程中,需要综合考虑各方面因素的影响。本文介绍了在GaussDB中分析性能问题时常见的手段和思路,帮助大家熟悉GaussDB数据库性能诊断常用的工具及使用方法。


华为开发者空间,汇聚鸿蒙、昇腾、鲲鹏、GaussDB、欧拉等各项根技术的开发资源及工具,致力于为每位开发者提供一台云主机、一套开发工具及云上存储空间,让开发者基于华为根生态创新。
点击链接
,免费领取您的专属云主机。

点击关注,第一时间了解华为云新鲜技术~

如果多个实体类都有
isDelete
字段,并且你希望在插入时为它们统一设置默认值,可以采取以下几种方法来减少代码重复:

1. 使用基类(抽象类)

创建一个基类,其中包含
isDelete
字段和
@PrePersist
方法。然后让所有需要这个字段的实体类继承这个基类。

示例代码:

import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;

@MappedSuperclass
public abstract class BaseEntity {

    protected Integer isDelete;

    @PrePersist
    public void prePersist() {
        if (isDelete == null) {
            isDelete = 0; // 设置默认值为0
        }
    }

    // Getter 和 Setter
    public Integer getIsDelete() {
        return isDelete;
    }

    public void setIsDelete(Integer isDelete) {
        this.isDelete = isDelete;
    }
}

然后在其他实体类中继承
BaseEntity

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class MyEntity extends BaseEntity {

    @Id
    private Long id;

    // 其他字段、getter 和 setter
}

2. 使用 AOP(面向切面编程)

通过 Spring AOP 创建一个切面,在插入操作时检查并设置
isDelete
的默认值。这种方式不需要修改每个实体类,适合大规模应用。

示例代码:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.lang.reflect.Field;

@Aspect
@Component
public class DefaultValueAspect {

    @PersistenceContext
    private EntityManager entityManager;

    @Before("execution(* com.example.repository.*.save(..))") // 根据你的仓库路径调整
    public void setDefaultValues(Object entity) throws IllegalAccessException {
        Field[] fields = entity.getClass().getDeclaredFields();
        for (Field field : fields) {
            if ("isDelete".equals(field.getName())) { // 检查字段名
                field.setAccessible(true);
                if (field.get(entity) == null) {
                    field.set(entity, 0); // 设置默认值为0
                }
            }
        }
    }
}

3. 使用 JPA 审计功能

使用 Spring Data JPA 的审计功能,通过实现
AuditorAware
接口来统一处理审计字段,包括 createdBy,createdTime,updatedBy,updatedTime等,而
isDelete
这种状态字段在审计注释中并没有实现。

4. 使用事件监听@EntityListeners

JPA 提供了事件监听器的功能,你可以定义一个事件监听器来处理所有需要设置默认值的实体类。

示例代码:

import javax.persistence.PostLoad;
import javax.persistence.PrePersist;
import javax.persistence.EntityListeners;

public interface DeletedField {

  	Integer getDeletedFlag();

	void setDeletedFlag(Integer deletedFlag);
}

public class DeleteDefaultValueListener {

	@PrePersist
	public void setDefaultValues(DeletedFlagField deletedFlagField) {
		if (deletedFlagField.getDeletedFlag() == null) {
			deletedFlagField.setDeletedFlag(0); // 设置默认值为0
		}
	}

}

@EntityListeners(DefaultValueListener.class)
@Entity
public class TableUserAccount extends EntityBase implements DeletedFlagField {

  	/**
	 * 删除标识(逻辑删除),1删除 0未删除
	 */
	@Column(name = "deleted_flag")
	private Integer deletedFlag;
}

5. 扩展JPA,对审计字段建立者和更新者的扩展

  • CreatedByField 建立者字段接口
  • UpdatedByField 更新者字段接口
  • CreatedByDefaultValueListener 建立者字段监听器
  • UpdatedByDefaultValueListener 更新者字段监听器
  • AuditorAwareImpl 审计接口,返回当前用户

CreatedByField

public interface CreatedByField {

	String getCreatedBy();

	void setCreatedBy(String createdBy);

}

扩展EntityBase实体,不使用默认的
CreatedBy

LastModifiedBy

@Getter
@Setter
@MappedSuperclass
@EntityListeners({ AuditingEntityListener.class, UpdatedByDefaultValueListener.class,
		CreatedByDefaultValueListener.class })
public abstract class EntityBase implements Serializable, CreatedByField, UpdatedByField {

	/**
	 * 创建人
	 */
	@Column(name = "created_by")
	private String createdBy;

	/**
	 * 修改人
	 */
	@Column(name = "updated_by")
	private String updatedBy;
	}

CreatedByDefaultValueListener

public class CreatedByDefaultValueListener implements ApplicationContextAware {

	private ApplicationContext applicationContext;

	@PrePersist
	public void setDefaultValues(CreatedByField createdByField) {
		if (createdByField.getCreatedBy() == null) {
			if (this.applicationContext.getBean(AuditorAwareImpl.class) != null) {
				createdByField.setCreatedBy(
						this.applicationContext.getBean(AuditorAwareImpl.class).getCurrentAuditor().orElse(""));

			}
		}
	}

	/**
	 * @param applicationContext
	 * @throws BeansException
	 */
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

}