mysql 有哪些常见的优化策略

mysql 有哪些常见的优化策略,第1张

在开始演示之前,我们先介绍下两个概念。

概念一,数据的可选择性基数,也就是常说的cardinality值。

查询优化器在生成各种执行计划之前,得先从统计信息中取得相关数据,这样才能估算每步 *** 作所涉及到的记录数,而这个相关数据就是cardinality。简单来说,就是每个值在每个字段中的唯一值分布状态。

比如表t1有100行记录,其中一列为f1。f1中唯一值的个数可以是100个,也可以是1个,当然也可以是1到100之间的任何一个数字。这里唯一值越的多少,就是这个列的可选择基数。

那看到这里我们就明白了,为什么要在基数高的字段上建立索引,而基数低的的字段建立索引反而没有全表扫描来的快。当然这个只是一方面,至于更深入的探讨就不在我这篇探讨的范围了。

概念二,关于HINT的使用。

这里我来说下HINT是什么,在什么时候用。

HINT简单来说就是在某些特定的场景下人工协助MySQL优化器的工作,使她生成最优的执行计划。一般来说,优化器的执行计划都是最优化的,不过在某些特定场景下,执行计划可能不是最优化。

比如:表t1经过大量的频繁更新 *** 作,(UPDATE,DELETE,INSERT),cardinality已经很不准确了,这时候刚好执行了一条SQL,那么有可能这条SQL的执行计划就不是最优的。为什么说有可能呢?

来看下具体演示

譬如,以下两条SQL,

A:

select * from t1 where f1 = 20

B:

select * from t1 where f1 = 30

如果f1的值刚好频繁更新的值为30,并且没有达到MySQL自动更新cardinality值的临界值或者说用户设置了手动更新又或者用户减少了sample page等等,那么对这两条语句来说,可能不准确的就是B了。

这里顺带说下,MySQL提供了自动更新和手动更新表cardinality值的方法,因篇幅有限,需要的可以查阅手册。

那回到正题上,MySQL 8.0 带来了几个HINT,我今天就举个index_merge的例子。

示例表结构:

mysql>desc t1+------------+--------------+------+-----+---------+----------------+| Field      | Type         | Null | Key | Default | Extra          |+------------+--------------+------+-----+---------+----------------+| id         | int(11)      | NO   | PRI | NULL    | auto_increment || rank1      | int(11)      | YES  | MUL | NULL    |                || rank2      | int(11)      | YES  | MUL | NULL    |                || log_time   | datetime     | YES  | MUL | NULL    |                || prefix_uid | varchar(100) | YES  |     | NULL    |                || desc1      | text         | YES  |     | NULL    |                || rank3      | int(11)      | YES  | MUL | NULL    |                |+------------+--------------+------+-----+---------+----------------+7 rows in set (0.00 sec)

表记录数:

mysql>select count(*) from t1+----------+| count(*) |+----------+|    32768 |+----------+1 row in set (0.01 sec)

这里我们两条经典的SQL:

SQL C:

select * from t1 where rank1 = 1 or rank2 = 2 or rank3 = 2

SQL D:

select * from t1 where rank1 =100  and rank2 =100  and rank3 =100

表t1实际上在rank1,rank2,rank3三列上分别有一个二级索引。

那我们来看SQL C的查询计划。

显然,没有用到任何索引,扫描的行数为32034,cost为3243.65。

mysql>explain  format=json select * from t1  where rank1 =1 or rank2 = 2 or rank3 = 2\G*************************** 1. row ***************************EXPLAIN: {  "query_block": {    "select_id": 1,    "cost_info": {      "query_cost": "3243.65"    },    "table": {      "table_name": "t1",      "access_type": "ALL",      "possible_keys": [        "idx_rank1",        "idx_rank2",        "idx_rank3"      ],      "rows_examined_per_scan": 32034,      "rows_produced_per_join": 115,      "filtered": "0.36",      "cost_info": {        "read_cost": "3232.07",        "eval_cost": "11.58",        "prefix_cost": "3243.65",        "data_read_per_join": "49K"      },      "used_columns": [        "id",        "rank1",        "rank2",        "log_time",        "prefix_uid",        "desc1",        "rank3"      ],      "attached_condition": "((`ytt`.`t1`.`rank1` = 1) or (`ytt`.`t1`.`rank2` = 2) or (`ytt`.`t1`.`rank3` = 2))"    }  }}1 row in set, 1 warning (0.00 sec)

我们加上hint给相同的查询,再次看看查询计划。

这个时候用到了index_merge,union了三个列。扫描的行数为1103,cost为441.09,明显比之前的快了好几倍。

mysql>explain  format=json select /*+ index_merge(t1) */ * from t1  where rank1 =1 or rank2 = 2 or rank3 = 2\G*************************** 1. row ***************************EXPLAIN: {  "query_block": {    "select_id": 1,    "cost_info": {      "query_cost": "441.09"    },    "table": {      "table_name": "t1",      "access_type": "index_merge",      "possible_keys": [        "idx_rank1",        "idx_rank2",        "idx_rank3"      ],      "key": "union(idx_rank1,idx_rank2,idx_rank3)",      "key_length": "5,5,5",      "rows_examined_per_scan": 1103,      "rows_produced_per_join": 1103,      "filtered": "100.00",      "cost_info": {        "read_cost": "330.79",        "eval_cost": "110.30",        "prefix_cost": "441.09",        "data_read_per_join": "473K"      },      "used_columns": [        "id",        "rank1",        "rank2",        "log_time",        "prefix_uid",        "desc1",        "rank3"      ],      "attached_condition": "((`ytt`.`t1`.`rank1` = 1) or (`ytt`.`t1`.`rank2` = 2) or (`ytt`.`t1`.`rank3` = 2))"    }  }}1 row in set, 1 warning (0.00 sec)

我们再看下SQL D的计划:

不加HINT,

mysql>explain format=json select * from t1 where rank1 =100 and rank2 =100 and rank3 =100\G*************************** 1. row ***************************EXPLAIN: {  "query_block": {    "select_id": 1,    "cost_info": {      "query_cost": "534.34"    },    "table": {      "table_name": "t1",      "access_type": "ref",      "possible_keys": [        "idx_rank1",        "idx_rank2",        "idx_rank3"      ],      "key": "idx_rank1",      "used_key_parts": [        "rank1"      ],      "key_length": "5",      "ref": [        "const"      ],      "rows_examined_per_scan": 555,      "rows_produced_per_join": 0,      "filtered": "0.07",      "cost_info": {        "read_cost": "478.84",        "eval_cost": "0.04",        "prefix_cost": "534.34",        "data_read_per_join": "176"      },      "used_columns": [        "id",        "rank1",        "rank2",        "log_time",        "prefix_uid",        "desc1",        "rank3"      ],      "attached_condition": "((`ytt`.`t1`.`rank3` = 100) and (`ytt`.`t1`.`rank2` = 100))"    }  }}1 row in set, 1 warning (0.00 sec)

加了HINT,

mysql>explain format=json select /*+ index_merge(t1)*/ * from t1 where rank1 =100 and rank2 =100 and rank3 =100\G*************************** 1. row ***************************EXPLAIN: {  "query_block": {    "select_id": 1,    "cost_info": {      "query_cost": "5.23"    },    "table": {      "table_name": "t1",      "access_type": "index_merge",      "possible_keys": [        "idx_rank1",        "idx_rank2",        "idx_rank3"      ],      "key": "intersect(idx_rank1,idx_rank2,idx_rank3)",      "key_length": "5,5,5",      "rows_examined_per_scan": 1,      "rows_produced_per_join": 1,      "filtered": "100.00",      "cost_info": {        "read_cost": "5.13",        "eval_cost": "0.10",        "prefix_cost": "5.23",        "data_read_per_join": "440"      },      "used_columns": [        "id",        "rank1",        "rank2",        "log_time",        "prefix_uid",        "desc1",        "rank3"      ],      "attached_condition": "((`ytt`.`t1`.`rank3` = 100) and (`ytt`.`t1`.`rank2` = 100) and (`ytt`.`t1`.`rank1` = 100))"    }  }}1 row in set, 1 warning (0.00 sec)

对比下以上两个,加了HINT的比不加HINT的cost小了100倍。

总结下,就是说表的cardinality值影响这张的查询计划,如果这个值没有正常更新的话,就需要手工加HINT了。相信MySQL未来的版本会带来更多的HINT。

摘至网页链接

常见Mysql配置文件:linux系统下是my.conf,windows环境下是my.ini;

数据库整体安全需求:机密性、完整性、可用性;

下面以mysql 5.7版本为例,介绍mysql常见的安全策略、配置、加固方式等等,有些策略可能只针对Linux *** 作系统,更多策略可以参考CIS Mysql Benchmark相关文档:

1、 *** 作系统级别安全配置

1.1不要将数据库放在系统分区

Windows系统:直接检查是否将数据库放置在C盘。

Linux系统:

在终端连接上mysql数据库,执行如下命令

show variables where variable_name = 'datadir'

然后返回shell命令行:

df -h <datadir>

其中datadir是上一条命令的返回值。

上述命令的返回值不应是/、/var、/usr

1.2使用专用的最小权限账号运行mysql数据库进程

Windows系统:直接打开任务管理器,查看运行mysql进程的 *** 作系统账号,不能为administrator账号。

Linux系统:

Shell命令行运行如下命令:

ps -ef | grep mysql

查看mysql服务的运行账号是否为root或其他高权限账号,如果是的,则需要创建一个非管理员专用账号来运行mysql服务。

1.3禁止使用mysql命令行历史记录

Linux系统:

执行如下命令:

find / -name ".mysql_history"

查看是否存在mysql的历史命令记录文件,如果存在,则需要进行如下加固:

(1)删除.mysql_history文件;

(2)设置环境变量MYSQL_HISTFILE为/dev/null,并添加到shell的初始化脚本中,创建mysql_history到/dev/null的链接:

ln -s /dev/null $HOME/.mysql_history

1.4 确保MYSQL_PWD环境变量未设置敏感信息

Windows系统下进入cmd命令行,使用如下命令:

Set

查看是否设置了环境变量MYSQL_PWD。

Linux系统下使用如下命令:

grep MYSQL_PWD /proc/*/environ

查看MYSQL_PWD环境变量是否设置了敏感信息。

确认那个配置文件或脚本设置了MYSQL_PWD环境变量。

2、安装

2.1使用数据库专用服务器

使用专用的服务器安装mysql服务可以减少mysql服务的攻击面,尽量卸载或删除 *** 作系统上的不必要的应用或服务,减少其他应用的安装可能给mysql的运行带来的安全风险。

2.2 不要复用数据库账号

运行mysql服务的 *** 作系统账号不要用来运行其他应用或服务,这样可以避免其他应用或服务器被攻击给mysql服务带来影响。

2.3 历史命令行密码设置为不可见

使用如下命令:

mysql -u admin -p password

连接mysql数据库服务,退出后查看历史命令,确认password是否为明文。

建议使用如下命令方式登录:

(1)先输入mysql -u admin -p

(2)根据命令行提示输入密码;

而不要在一整条命令中输入密码。

另外要控制mysql配置文件访问权限。

3、文件权限控制

3.1 控制数据目录的访问权限

数据目录是mysql数据库存放的位置,在mysql命令行界面下执行如下命令:

show variables where variable_name = 'datadir'

在终端命令行下执行如下命令:

ls -l <datadir>/.. | egrep "^d[r|w|x]{3}------\s*.\s*mysql\s*mysql\s*\d*.*mysql"

其中<datadir>是第一条命令的执行结果

如果存在问题,linux环境下在终端执行如下命令进行加固:

chmod 700 <datadir>

chown mysql:mysql <datadir>

3.2 控制二进制日志文件的权限

mysql的运行会产生很多日志,例如二进制日志、错误日志、慢查询日志等等,Mysql命令行下执行如下命令:

show variables like 'log_bin_basename'

在终端命令行执行如下命令:

ls <log_bin_basename>.*

对于发现的每一个文件,执行如下命令:

ls -l <log_bin_basename.nnnnn>| egrep "^-[r|w]{2}-[r|w]{2}----\s*.*$"

根据输出确认日志文件的权限设置是否存在问题。

对于每个日志文件,修改其权限和属组如下:

chmod 660 <log file>

chown mysql:mysql <log file>

3.3 控制错误日志文件的权限

Mysql命令行下执行如下命令:

show variables like 'log_error'

在终端命令行执行如下命令:

ls <log_error>.*

对于发现的每一个文件,执行如下命令:

ls -l <log_error>| egrep "^-[r|w]{2}-[r|w]{2}----\s*.*$"

根据输出确认日志文件的权限设置是否存在问题。

对于每个日志文件,修改其权限和属组如下:

chmod 660 <log file>

chown mysql:mysql <log file>

3.4控制慢查询日志文件的权限

Mysql命令行下执行如下命令:

show variables like 'slow_query_log_file'

在终端命令行执行如下命令:

ls <slow_query_log_file>.*

对于发现的每一个文件,执行如下命令:

ls -l <slow_query_log_file>| egrep "^-[r|w]{2}-[r|w]{2}----\s*.*$"

根据输出确认日志文件的权限设置是否存在问题。

对于每个日志文件,修改其权限和属组如下:

chmod 660 <log file>

chown mysql:mysql <log file>

3.5控制通用日志文件的权限

Mysql命令行下执行如下命令:

show variables like 'general_log_file'

在终端命令行执行如下命令:

ls <general_log_file>.*

对于发现的每一个文件,执行如下命令:

ls -l <general_log_file>| egrep "^-[r|w]{2}-[r|w]{2}----\s*.*$"

根据输出确认日志文件的权限设置是否存在问题。

对于每个日志文件,修改其权限和属组如下:

chmod 660 <log file>

chown mysql:mysql <log file>

3.6控制审计日志文件的权限

Mysql命令行下执行如下命令:

show global variables where variable_name =  'audit_log_file'

在终端执行如下命令:

ls -l <audit_log_file>| egrep "^-rw[-x]rw[-x][-r][-w][-x][ \t]*[0-9][ \t]*mysql[

\t]*mysql.*$"

根据输出确认日志文件的权限设置是否存在问题。

对于每个日志文件,修改其权限和属组如下:

chmod 660 <audit_log_file>

chown mysql:mysql <audit_log_file>

4、通用安全

4.1安装最新的补丁

在mysql命令行下查询MySQL的版本:

SHOW VARIABLES WHERE Variable_name LIKE "version"

确认是否由需要安装的补丁包,如果有请安装。

4.2 删除test数据库

Mysql数据库默认安装好后,存在一个名为test的数据库,如果存在,请执行如下命令删除:

Drop database “test”

4.3 确保读取本地文件的参数设置为失效

Mysql命令行下,使用如下命令:

SHOW VARIABLES WHERE Variable_name = 'local_infile'

查看结果是否为OFF。

如果该命令为ON,则数据库用户可以通过LOAD DATA INFILE 或者 SELECT local_file 读取到数据库所在 *** 作系统本地的文件,在这种情况下,需要在mysql配置文件中新增一行:

Local-infile=0;

然后重启数据库服务。

5、权限配置

5.1控制可以访问所有数据库的账号

Mysql数据库下的user表和db表中存放着可以授予数据库用户的权限,确保只有管理员账号才能访问所有数据库。可以访问mysql数据库的用户或许可以查看密码哈希值、修改用户权限等等。

使用如下sql语句:

SELECT user, host FROM mysql.user

WHERE (Select_priv = 'Y') OR (Insert_priv = 'Y') OR (Update_priv = 'Y')

OR (Delete_priv = 'Y')  OR (Create_priv = 'Y')  OR (Drop_priv = 'Y')

SELECT user, host FROM mysql.db WHERE db = 'mysql'

AND ((Select_priv = 'Y') OR (Insert_priv = 'Y') OR (Update_priv = 'Y')

OR (Delete_priv = 'Y') OR (Create_priv = 'Y') OR (Drop_priv = 'Y'))

确保返回结果只能是数据库管理员账号。

5.2限制非管理员用户的权限

Mysql.user表中的权限列有:

file_priv:表示是否允许用户读取数据库所在主机的本地文件;

Process:表示是否允许用户查询所有用户的命令执行信息;

Super_priv:表示用户是否有设置全局变量、管理员调试等高级别权限;

Shutdown_priv:表示用户是否可以关闭数据库;

Create_user_priv:表示用户是否可以创建或删除其他用户;

Grant_priv:表示用户是否可以修改其他用户的权限;

应确保只有数据库管理员才有上述权限,使用如下sql语句查看拥有各个权限的数据库账号:

select user, host from mysql.user where File_priv = 'Y'

select user, host from mysql.user where Process_priv = 'Y'

select user, host from mysql.user where Process_priv = 'Y'

SELECT user, host FROM mysql.user WHERE Shutdown_priv = 'Y'

SELECT user, host FROM mysql.user WHERE Create_user_priv = 'Y'

SELECT user, host FROM mysql.user WHERE Grant_priv = 'Y'

SELECT user, host FROM mysql.db WHERE Grant_priv = 'Y'

确保查询结果中不存在非管理员用户。

如果存在非管理员用户,使用如下命令进行权限回收:

REVOKE FILE ON *.* FROM '<user>'

REVOKE PROCESS ON *.* FROM '<user>'

REVOKE SUPER ON *.* FROM '<user>'

REVOKE SHUTDOWN ON *.* FROM '<user>'

REVOKE CREATE USER ON *.* FROM '<user>'

REVOKE GRANT OPTION ON *.* FROM <user>

其中user为上述查询到的非管理员用户。

5.3合理控制DML/DDL *** 作授权

DML/DDL语句包括创建或修改数据库结构的权限,例如insert、update、delete、create、drop和alter语句,在任何数据库中都要控制用户的此类权限,确保只授权给有业务需求的非管理员用户。Mysql命令行下执行如下命令:

SELECT User,Host,Db FROM mysql.db WHERE Select_priv='Y'

OR Insert_priv='Y' OR Update_priv='Y' OR Delete_priv='Y' OR Create_priv='Y'

OR Drop_priv='Y' OR Alter_priv='Y'

上述查询到的用户只能对特地的数据库才有相关的权限,使用如下命令进行相关权限的回收:

REVOKE SELECT ON <host>.<database>FROM <user>

REVOKE INSERT ON <host>.<database>FROM <user>

REVOKE UPDATE ON <host>.<database>FROM <user>

REVOKE DELETE ON <host>.<database>FROM <user>

REVOKE CREATE ON <host>.<database>FROM <user>

REVOKE DROP ON <host>.<database>FROM <user>

REVOKE ALTER ON <host>.<database>FROM <user>

其中<user>为查询到的未授权的用户,host为相关主机,database为相关数据库。

6、审计和日志

6.1开启错误日志审计功能

错误日志包括数据库运行和停止过程中的一系列活动信息,有助于分析数据库运行过程中的一些异常活动,一般情况下需要开启错误日志记录功能,使用如下命令查询:

SHOW variables LIKE 'log_error'

确保返回结果为非空,如果为空,需要在mysql数据库配置文件中增加相关配置。

6.2确保日志存放在非系统区域

日志文件随着数据库的运行会不断增加,如果存放在系统区域,则会影响系统的正常运行,使用如下命令进行查询:

SELECT @@global.log_bin_basename

确保返回结果不是如下路径:/、/var、/usr

6.3关闭原始日志功能

原始日志选项会决定一些敏感信息是否会被明文写进日志中,例如查询日志、慢查询日志、二进制日志,确保数据库配置文件中存在如下配置项:

Log-raw = OFF

7、认证

7.1 Old_passwords环境变量设置

Old_passwords决定了使用PASSWORD()函数和IDENTIFIED BY 、CREATE USER 、GRANT 等语句是时的hash算法:

0 - authenticate with the mysql_native_password plugin

1 - authenticate with the mysql_old_password plugin

2 - authenticate with the sha256_password plugin

设置为mysql_old_password代表弱hash算法,可以快速通过密码字典进行暴力破解。使用如下命令查询相关值:

SHOW VARIABLES WHERE Variable_name = 'old_passwords'

确保返回值不为1。

7.2 secure_auth 选项设置

如果客户端采用Old_passwords发起连接请求,如果服务器端设置了secure_auth,则客户端会拒绝连接请求,可以根据安全需求在配置文件中做相应配置。

7.3 密码保存

确保密码没有明文保存在全局配置文件中。

7.4 确保所有用户都要求使用非空密码登录

执行如下语句查询是否有用户不需要密码即可登录:

SELECT User,host

FROM mysql.user

WHERE (plugin IN('mysql_native_password', 'mysql_old_password')

AND (LENGTH(Password) = 0

OR Password IS NULL))

OR (plugin='sha256_password' AND LENGTH(authentication_string) = 0)

7.5不存在空账号

使用如下命令查询是否存在空账号:

SELECT user,host FROM mysql.user WHERE user = ''

8、网络设置

如果mysql数据库服务器与应用是跨信任域部署的,则需要考虑在数据库服务器与应用服务器之间建立ssl通道进行数据传输,不过这种场景一般很少见,在此不详细描述。

9、数据库备份

问题

我们有一个 SQL,用于找到没有主键 / 唯一键的表,但是在 MySQL 5.7 上运行特别慢,怎么办?

实验

我们搭建一个 MySQL 5.7 的环境,此处省略搭建步骤。

写个简单的脚本,制造一批带主键和不带主键的表:

执行一下脚本:

现在执行以下 SQL 看看效果:

...

执行了 16.80s,感觉是非常慢了。

现在用一下 DBA 三板斧,看看执行计划:

感觉有点惨,由于 information_schema.columns 是元数据表,没有必要的统计信息。

那我们来 show warnings 看看 MySQL 改写后的 SQL:

我们格式化一下 SQL:

可以看到 MySQL 将

select from A where A.x not in (select x from B) //非关联子查询

转换成了

select from A where not exists (select 1 from B where B.x = a.x) //关联子查询

如果我们自己是 MySQL,在执行非关联子查询时,可以使用很简单的策略:

select from A where A.x not in (select x from B where ...) //非关联子查询:1. 扫描 B 表中的所有记录,找到满足条件的记录,存放在临时表 C 中,建好索引2. 扫描 A 表中的记录,与临时表 C 中的记录进行比对,直接在索引里比对,

而关联子查询就需要循环迭代:

select from A where not exists (select 1 from B where B.x = a.x and ...) //关联子查询扫描 A 表的每一条记录 rA:     扫描 B 表,找到其中的第一条满足 rA 条件的记录。

显然,关联子查询的扫描成本会高于非关联子查询。

我们希望 MySQL 能先"缓存"子查询的结果(缓存这一步叫物化,MATERIALIZATION),但MySQL 认为不缓存更快,我们就需要给予 MySQL 一定指导。

...

可以看到执行时间变成了 0.67s。

整理

我们诊断的关键点如下:

\1. 对于 information_schema 中的元数据表,执行计划不能提供有效信息。

\2. 通过查看 MySQL 改写后的 SQL,我们猜测了优化器发生了误判。

\3. 我们增加了 hint,指导 MySQL 正确进行优化判断。

但目前我们的实验仅限于猜测,猜中了万事大吉,猜不中就无法做出好的诊断。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/8646334.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-19
下一篇 2023-04-19

发表评论

登录后才能评论

评论列表(0条)

保存