MySQL调优

1. MySQL分为三层

MySQL调优_第1张图片

  • 查看语句执行时间
mysql> select * from test_20221120;
Empty set (0.00 sec)
  • 从上面的执行结果看不出来语句执行所需要的时间,使用以下命令
mysql> set profiling=1;
  • 再次执行查看语句,然后使用以下命令查看执行时间
mysql> show profiles;
+----------+------------+-----------------------------+
| Query_ID | Duration   | Query                       |
+----------+------------+-----------------------------+
|        1 | 0.00043925 | select * from test_20221120 |
+----------+------------+-----------------------------+
1 row in set, 1 warning (0.00 sec)

  • Navicat 中
    MySQL调优_第2张图片
  • 查询语句各阶段的执行时间
show PROFILE;
Status Duration
starting 0.000087
checking permissions 0.000008
Opening tables 0.000023
init 0.000021
System lock 0.000010
optimizing 0.000005
statistics 0.000016
preparing 0.000013
executing 0.000004
Sending data 0.000038
end 0.000004
query end 0.000008
closing tables 0.000008
freeing items 0.000029
cleaning up 0.000015
  • SHOW PROFILE 语法
SHOW PROFILE [type [, type] ... ]
    [FOR QUERY n]
    [LIMIT row_count [OFFSET offset]]

type: {
    ALL:显示所有信息
  | BLOCK IO: 显示块IO操作的次数
  | CONTEXT SWITCHES : 上下文切换次数,被动和主动;
  | CPU:显示用户CPU时间、系统CPU时间
  | IPC:发送和接受的消息数量
  | MEMORY:暂未实现
  | PAGE FAULTS:显示页错误数量
  | SOURCE:显示源码中的函数名与名称
  | SWAPS:显示swap的次数
}
mysql> SELECT @@profiling;
+-------------+
| @@profiling |
+-------------+
|           0 |
+-------------+
1 row in set (0.00 sec)

mysql> SET profiling = 1;
Query OK, 0 rows affected (0.00 sec)

mysql> DROP TABLE IF EXISTS t1;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> CREATE TABLE T1 (id INT);
Query OK, 0 rows affected (0.01 sec)

mysql> SHOW PROFILES;
+----------+----------+--------------------------+
| Query_ID | Duration | Query                    |
+----------+----------+--------------------------+
|        0 | 0.000088 | SET PROFILING = 1        |
|        1 | 0.000136 | DROP TABLE IF EXISTS t1  |
|        2 | 0.011947 | CREATE TABLE t1 (id INT) |
+----------+----------+--------------------------+
3 rows in set (0.00 sec)

mysql> SHOW PROFILE;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| checking permissions | 0.000040 |
| creating table       | 0.000056 |
| After create         | 0.011363 |
| query end            | 0.000375 |
| freeing items        | 0.000089 |
| logging slow query   | 0.000019 |
| cleaning up          | 0.000005 |
+----------------------+----------+
7 rows in set (0.00 sec)

mysql> SHOW PROFILE FOR QUERY 1;
+--------------------+----------+
| Status             | Duration |
+--------------------+----------+
| query end          | 0.000107 |
| freeing items      | 0.000008 |
| logging slow query | 0.000015 |
| cleaning up        | 0.000006 |
+--------------------+----------+
4 rows in set (0.00 sec)

mysql> SHOW PROFILE CPU FOR QUERY 2;
+----------------------+----------+----------+------------+
| Status               | Duration | CPU_user | CPU_system |
+----------------------+----------+----------+------------+
| checking permissions | 0.000040 | 0.000038 |   0.000002 |
| creating table       | 0.000056 | 0.000028 |   0.000028 |
| After create         | 0.011363 | 0.000217 |   0.001571 |
| query end            | 0.000375 | 0.000013 |   0.000028 |
| freeing items        | 0.000089 | 0.000010 |   0.000014 |
| logging slow query   | 0.000019 | 0.000009 |   0.000010 |
| cleaning up          | 0.000005 | 0.000003 |   0.000002 |
+----------------------+----------+----------+------------+
7 rows in set (0.00 sec)
SHOW PROFILE FOR QUERY 2;

SELECT STATE, FORMAT(DURATION, 6) AS DURATION
FROM INFORMATION_SCHEMA.PROFILING
WHERE QUERY_ID = 2 ORDER BY SEQ;

[注意]Profile 命令只推荐在MySQL5使用,推荐使用performance_schema

关于performance_schema有两个基本概念

  • instruments:生产者,用于采集mysql中各种各样的操作产生的事件信息,对应配置表中配置项我们可以称为监控采集配置项。
  • consumers:消费者,对应的消费者表用于存储来自instruments采集的数据,对应配置表中的配置我们称为消费存储配置项。

performance_schema表的分类

  • 等待时间记录表,与语句事件类型的相关记录表,类似于:
    show tables like '%wait%';

  • 阶段事件记录表,记录语句执行的阶段事件表
    show tables like '%stage%';

  • 事务事件记录表,记录事务相关的事件表
    show tables like '%transaction%';

  • 监控文件系统层调用的表
    show tables like '%file%';

  • 监视内存使用的表
    show tables like '%memory%';

  • 动态对performance_schema进行配置的配置表
    show tables like ''%setup%;

performance_schema的简单配置与使用

数据库刚刚初始化并启动的时候,并不是所有的instruments信息都会被采集。

select * from setup_instruments;
  • 打开等待事件的采集器配置项开关,需要修改setup_instruments配置表中对应的采集配置项
UPDATE setup_instruments SET ENABLED ='YES' ,TIMED ='YES' WHERE NAME LIKE 'wait%';
  • 打开等待事件的表配置开关,修改setup_consumers配置表中对应的配置项
UPDATE setup_consumers SET ENABLED ='YES' WHERE NAME LIKE 'wait'
  • 当配置完成后可以查看当前server正在做什么,可以通过查询events_waits_current表来得知,该表中每个线程只包含一行数据,适用于显示每个线程的最新监视事件。
    select * from events_waits_current\G

performance_schema实践操作

  • 哪类SQL执行最多
SELECT DIGEST_TEXT,COUNT_STAR,FIRST_SEEN,LAST_SEEN FROM 
events_statements_summary_by_digest ORDER BY COUNT_STAR DESC
  • 哪类SQL的平均响应时间最久
SELECT DIGEST_TEXT,AVG_TIMER_WAIT FROM 
events_statements_summary_by_digest ORDER  BY COUNT_STAR DESC
  • 哪类SQL排序记录数最多
SELECT DIGEST_TEXT,SUM_SORT_ROWS FROM 
events_statements_summary_by_digest ORDER BY COUNT_STAR DESC 
  • 哪类SQL返回结果集最多?
SELECT DIGEST_TEXT,SUM_ROWS_SENT FROM
events_statements_summary_by_digest ORDER BY COUNT_STAR DESC

查看数据库当前线程池

show processlist
  • 数据库连接池
    druid

2. schema与数据类型优化

数据类型的优化

  • 更小的通常更好

应该尽量使用可以正确存储数据的最小数据类型,更小的数据类型通常更快,因为它们占用磁盘、内存和CPU更少缓存,并且处理时需要的CPU周期更少,但是要确保没有低估需要存储的值的范围,如果无法确认哪个数据类型,就选择你认为不会超过范围的最小类型。
案例:
      在工作中会涉及到存储状态1、0,在选择类型的时候我们一般默认选择为INT,这是不对的。不同的类型的数据空间是不一样的。Mysql中int类型有TINYINTSMALLINTMEDIUMINTINTBIGINTmysql的数据类型有哪些?

  • 简单就好

简单的数据类型的操作通常需要更少的CPU周期,比如:

  • 整型比字符操作代价更低,因为字符集和校对规则是字符比较比整型比较更复杂。
  • 使用mysql自建类型而不是字符串来存储日期和时间。
  • 使用整型存储IP地址。
 SELECT INET_ATON('192.189.190.101');
 SELECT INET_NTOA(3233660517);
 输出结果:
 INET_ATON('192.189.190.101')
 3233660517
 SELECT INET_NTOA(3233660517);
 192.189.190.101
  • 尽量避免null

在数据中null不等于null;
如果查询中包含可为NULL的列,对mysql来说很难优化,因为可为null的列使得索引、索引统计和值比较都更加复杂,坦白来说,通常情况下null列改为not null带来的性能提升比较小,所以没有必要将所有的表的schema进行修改,但是应该尽量避免设计成为null的列。空字符串''是可以的(使用默认值时候请谨慎)

  • 实际细则
    • 整数类型

      可以使用的几种整数类型:TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT分别使用8、16、24、32、64位存储空间。尽量使用满足需求的最小数据类型。

    • 字符和字符串类型
      • varchar根据实际内容长度保存数据
        • 使用最小的符合需求的长度
        • varchar(n)n小于等于255使用额外一个字节保存长度,n>255使用额外两个字节保存长度
        • varchar(5)与varchar(255) 保存同样的内容,硬盘存储空间相同,但内存空间占用不同,是指定的大小
        • varchar在mysql5.6之前变更长度,或者从255一下变更到255以上时,都会导致锁表。
        • 应用场景
          • 存储长度波动较大的数据,如:文章,有的会很短有的又会很长;
          • 字符串很少更新的场景,每次更新后都会重算并使用额外存储空间保存长度。
          • 适合保存多字节字符,如:汉字,特殊字符等
      • char 固定长度的字符串
        • 最大长度:255
        • 会自动删除末尾的空格
        • 检索效率、写效率会比varchar高,以空间换时间
        • 应用场景
          • 存储长度波动不大的数据,如:md5摘要
          • 存储短字符串、经常更新的字符串
      • BLOB和TEXT类型(一般不建议使用,涉及到IO问题)

        MySQL把每个BLOB和TEXT值当作一个独立的对象处理。
        两者都是为了存储很大数据而涉及的字符串类型,分别使用二进制和字符方式存储。

    • datetime和timestamp
      • 不要使用字符串来存储日期和时间数据
      • 日期时间类型通常比字符串占用的存储空间更小
      • 日期时间类型在进行查找过滤时可以利用日期来进行比较
      • 日期类型还有着丰富的处理函数,可以方便的对事件类型进行日期计算
      • 使用int 存储日期事件不如使用timestamp类型
      • datetime
        • 占用8个字节
        • 与时区无关,数据库底层时区配置,对datetime无效
        • 可保存到毫秒
        • 可保存时间范围大
        • 不要使用字符串存储日期类型,占用空间大,损失日期类型函数的便捷性
      • timestamp
        • 占用4个字节
        • 时间范围:1970-01-01到2038-01-19
        • 精确到秒
        • 采用整型存储
        • 依赖数据库设置的时区
        • 自动更新timestamp列的值
      • date
        • 占用的字节数比使用字符串、datetime、int存储更少,使用date类型存储只需要3个字节
        • 使用date类型还可以利用日期事件函数进行日期之间的计算
        • date类型用于保存1000-01-01到9999-12-31之间的日期
    • 用枚举类型替代字符串

      MySQL存储枚举类型会非常紧凑,会根据列表值的数据压缩到一个或者两个字节中,MySQL在内部会将每个值在列表中的而为之保存为整数,并且在表的.frm文件中保存"数字·字符串"映射关系的查找表

      CREATE TABLE `insert_20221123_5` (
      `id` char(3) DEFAULT NULL,
      `gender` enum('FEMAL','MALE') DEFAULT NULL,
      `name` varchar(3) DEFAULT NULL,
      `realname` char(3) DEFAULT NULL
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8
      
      
    • 特殊类型数据

      存IP推荐使用整型,IP的本质是32位无符号整型,INET_ATON()INET_NTOA()

合理使用范式和反范式

数据库设计三大范式

  • 第一范式(1NF):属性不可分割,即每个属性都是不可分割的原子项。(实体的属性即表中的列)

  • 第二范式(2NF):满足第一范式;且不存在部分依赖,即非主属性必须完全依赖于主属性。(主属性即主键;完全依赖是针对于联合主键的情况,非主键列不能只依赖于主键的一部分)

  • 第三范式(3NF):满足第二范式;且不存在传递依赖,即非主属性不能与非主属性之间有依赖关系,非主属性必须直接依赖于主属性,不能间接依赖主属性。(A -> B, B ->C, A -> C)

范式

  • 优点
    • 范式化的更新通常比反范式更块
    • 当数据较好的范式化后,很少或者没有重复的数据
    • 范式化的数据比较小,可以放在内存中,操作比较快
  • 缺点
    • 通常需要进行关联

反范式

  • 优点
    • 所有数据都在一张表,可以避免关联
    • 可以设计有效的索引
  • 缺点
    • 表格内的冗余比较多,删除数据的时候会造成有些有用的信息丢失

注意

  • 在企业中很难做好严格意义上的范式或反范式,一般需要混合使用

    在一个网站实例中,这个网站,允许用户发送消息,并且一些用户是付费用户。现在想看付费用户最近的10条信息。在user表和message表中都存储用户类型(account_type)而不用完全的反范式化。这避免了完全反范式化的删除和插入问题,即使没有message表的数据被删除也不会丢失用户信息。这样也不会把user_message表搞的太大,有利于高效地获取数据。

    另一个从父表冗余一些数据到子表的理由是排序的需要

  • 示例

    • 范式设计
      用户表 ->|用户ID|姓名|电话|地址|邮编|
      订单表->|订单ID|用户ID|下单时间|支付类型|订单状态|
      订单商品表->|订单ID|商品数量|商品价格|
      商品表->|商品ID|名称|描述|过期时间|
    • 反范式设计
      用户表 ->|用户ID|姓名|电话|地址|邮编|
      订单表->|订单ID|用户ID|下单时间|支付类型|订单状态|订单价格|用户名|电话|地址|
      订单商品表->|订单ID|商品数量|商品价格|
      商品表->|商品ID|名称|描述|过期时间|

主键的选择

  • 代理主键

    与业务无关,无意义的数字序列

  • 自然主键

    事物属性中的自然唯一标识

  • 推荐使用代理主键

    他们不与业务耦合,因此更容易维护;

    一个大多数表,最好是全部表,通用的键策略能减少需要编写的代码源数量,减少系统的总体拥有成本。

字符集的选择

纯拉丁字符能表示的内容,没必要选择latin1之外的其它字符编码,因为这会节省大量的存储空间。utf-8下,1字符=3字节。(uft-8也称之为utf-8mb3) utf-8mb4下,1字符=4字节;latin1 1个字符是1字节

如果我们可以确定不需要存放多种语言,就没必要非得使用UTF8或其他UNICODE字符类型,会造成大量的存储空间浪费。

MySQL的数据类型可以精确到字段,所以当我们需要大型数据库中粗放多字节数据的时候,可以通过对不同表不同字段使用不同的数据类型来较大程度减少数据库存储量,进而降低IO操作次数并提高缓存命中率。

中文使用utf8mb4;

存储引擎的选择

存储引擎:数据文件的组织形式

MyISAM InnoDB
索引类型 非聚簇索引 聚簇索引(数据文件和索引文件放在一起)
支持事务
支持表锁
支持行锁
支持外键
支持全文检索 是(5.6后支持)
适合操作类型 大量SELECT 大量INSERT、DELETE、UPDATE

适当的数据冗余

被频繁引用且只能通过Jion2张(或者更多)打标的方式才能得到的独立小字段

这样的场景由于每次Join仅仅只是为了取得某个小字段的值,Join到的记录又大,会造成大量不必要的IO,完全可以通过空间换取时间的方式来优化。不过,冗余的同时需要确保数据的一致性不会遭到破坏,确保更新的同时冗余字段也被更新。

适当拆分

当我们的表中存在类似于TEXT或者是很大的VARCHAR类型的大字段的时候,如果我们大部分访问这张表的时候都用不到这个字段,我们就该义无反顾的将其拆分到另外独立的表中,以减少常用数据所占用的存储空间。这样做的一个明显好处就是每个数据块中可以存储的数据条数可以大大增加,既减少物理IO次数,也能大大提高内存中的缓存命中率。

数据库水平拆分和垂直拆分

3. 执行计划

MySQL执行计划

EXPLAIN输出格式

  • 执行计划中包含的信息
Column JSON Name Meaning
id select_id The SELECT identifier
select_type None The SELECT type
table table_name The table for the output row
partitions partitions The matching partitions
type access_type The join type
possible_keys possible_keys The possible indexes to choose
key key The index actually chosen
key_len key_length The length of the chosen key
ref ref The columns compared to the index
rows rows Estimate of rows to be examined
filtered filtered Percentage of rows filtered by table condition
Extra None Additional information
  • id

    SELECT查询的序列号,包含一组数字,表示查询中执行SELECT子句或者操作表的顺序;

       CREATE TABLE person_20221125 (
        id char(3) DEFAULT NULL,
         user_name varchar(3) DEFAULT NULL
       ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
       
       CREATE TABLE order_20221125 (
        id char(3) DEFAULT NULL,
        user_id char(3) DEFAULT NULL
       ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    
    • 如果id相同,那么执行顺序从上到下

       EXPLAIN	SELECT * FROM person_20221125 p 
       left join order_20221125 o on p.id = o.user_id
      

      在这里插入图片描述

    • 如果id不同,如果是子查询,id的序号会递增,id值越大,优先级越高,越先被执行

       -- create index person_id_idx on person_20221125(id);
       -- create index order_id_idx on order_20221125(id);
       EXPLAIN	SELECT * FROM 
           person_20221125 p
       where p.id in 
       (select user_id from order_20221125 where id ='1')
      

      在这里插入图片描述

    • id相同和不同的同时存在,相同的认为是一组,从上往下执行,在所有组中,id值越大,优先级越高,越先执行

  • select_type

    主要用来分辨查询的类型,是普通查询还是联合查询。

  • 执行计划的 type

    访问类型,SQL 查询优化中一个很重要的指标,结果值从好到坏依次是:system > const > eq_ref > ref > range > index > ALL。

    • system

      系统表,少量数据,往往不需要进行磁盘IO

    • const

      常量连接

    • eq_ref

      主键索引(primary key)或者非空唯一索引(unique not null)等值扫描

    • ref

      非主键非唯一索引等值扫描

    • range

      索引上的范围扫描

    • index

      索引上的全扫描count(*)

    • ALL

      全表扫描(full table scan)

  • extra

    包含额外的信息

    • using filesort

      说明mysql无法利用索引进行排序,只能利用排序算法进行排序,会消耗额外的位置

    • using temporary

      建立临时表来保存中间结果,查询完成之后把临时表删除

    • using index

      这个表示当前的查询是覆盖索引的,直接从索引中读取数据,而不用访问数据表。如果同时出现useing where表明索引被用来执行索引键值的查找,如果没有,表面索引被用来读取数据,而不是真的查找

    • using where

      使用where进行条件过滤

    • using join buffer

      使用连接缓存

    • impossible where

      where语句的结果总是false;

4. 通过索引进行优化

[注意]:

  • InnoDB是通过B+Tree结构对主键创建索引,然后叶子节点中存储记录,如果没有主键,那么会选择唯一键,如果没有唯一键,那么会生成一个6位的row_id来作为主键
  • 如果创建索引的键是其他字段,那么在叶子节点中存储的是该记录的主键,然后再根据主键索引找到对应的记录。

B树MySQL调优_第3张图片

实例图说明:
        每个节点占用一个磁盘块,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据域。以根节点为例,关键字为16和34,P1指针指向的子树的数据范围为小于16,P2指针指向的子树的数据范围为16~34,P3指针指向的子树的数据范围大于34。

B+树

3层查询支持千万级数据
MySQL调优_第4张图片

索引基本知识

  • 索引的优点

    • 大大减少了服务器需要的扫描数量
    • 帮助服务器避免排序和临时表

      order by是全排序,效率很低

    • 将随机IO变成顺序IO
  • 索引的用处

    • 快速匹配WHERE子句的行
    • 从consideration中消除行,如果可以在多个索引间进行选择,mysql通常会使用找到最少行的索引
    • 如果表具有多列索引,则优化器可以使用索引的任何最左前缀来查找行
    • 当有表连接的时候,从其它表检索行数据
    • 查找特定索引列的min或max值
    • 如果排序或分组时在可用索引的最左前缀上完成的,则对表进行排序和分组
    • 在某些情况下,可以优化查询以检索值而无需查询数据行
  • 索引的分类

    • 主键索引
    • 唯一索引
    • 普通索引
    • 全文索引
    • 组合索引
      • 当包含多个列作为索引,需要注意的是正确的顺序依赖于该索引的查询,同时需要考虑如何更好的满足排序和分组的需要。
      • 如果组合索引
  • 面试技术名词

    • 回表

      在InnoDB中默认会为主键创建索引,更多的情况是为普通列创建索引。

      • InnoDB是通过B+Tree结构对主键创建索引,然后叶子节点中存储记录,如果没有主键,那么会选择唯一键,如果没有唯一键,那么会生成一个6位的row_id来作为主键。
      • 如果创建索引的键是其他字段,那么在叶子节点中存储的是该记录的主键,然后通过主键索引找到对应的记录叫做回表。
    • 覆盖索引

      select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖。

    • 最左匹配

      最左匹配原则

    • 索引合并

      索引合并

    • 索引页分裂
    • 索引页合并
    • 索引下推
  • 索引采用的数据结构

    • 哈希表
    • B+树
  • 索引匹配方式

create table staffs(
	id int primary key auto_increment,
	name varchar(24) not null default '' comment '姓名',
	age int not null default 0 comment '年龄',
	pos varchar(20) not null default '' comment '职位',
	add_time timestamp not null default current_timestamp comment '入职时间',
) charset utf8 comment '员工记录表';
alter table staffs add index idx_nap(name,age,pos)
  • 全值匹配

    全值匹配值得是索引中的所有列进行匹配
    explain select * from staffs where name ='Mike' and age =30 and pos='dev'

在这里插入图片描述

  • 匹配最左前缀

    只匹配前面的几列
    explain select * from staffs where name ='Tom' and age =30
    explain select * from staffs where name ='Tom'

  • 匹配列前缀

    可以匹配某一列的值的开头部分
    explain select * from staff where name like 'T%'
    explain select * from staff where name like '%om'

  • 匹配范围值

    可以查找某一范围的数据
    explain select * from staffs where age > 30

  • 精确匹配某一列范围匹配另一列

    查询第一列的全部和第二列的部分
    explain select * from staffs where name ='Tom' and age >=30

  • 字段类型不一致的时候会导致不使用索引
    explain select * from staffs where name ='del' and age = 20 and pos=20; 在这里插入图片描述

  • 只访问索引的查询

    查询的时候只需要访问索引,不需要访问数据行,本质上就是覆盖索引
    explain select name,age,pos from staffs where name ='Mike' and age =30 and pos='dev' MySQL调优_第5张图片
    Extra不为空则说明使用了索引覆盖

5. 索引进行优化

聚簇索引与非聚簇索引

  • 聚簇索引

    不是单独的索引类型,而是一种数据存储方式,指的是数据行跟相邻的键值紧凑的存储在一起

    • 优点
    • 可以把相关数据保存到一起。
    • 数据访问更快,因为索引和数据保存在同一个树中。
    • 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。
    • 缺点
    • 聚簇索引最大限度地提高了IO密集型应用的性能,如果数据全部在内存,那么聚簇索引就没什么优势 。
    • 插入速度严重依赖插入顺序,按照主键的顺序插入是最快的方式。
    • 更新聚簇索引列的代价很高,因为会强制将每个被更新的行移动到新的位置。
    • 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临页分裂的问题。
    • 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者是由于页分裂导致数据存储不连续的时候。
  • 非聚簇索引

    数据文件根索引文件分开存放

覆盖索引

  • 基本介绍
    • 如果一个索引包含所需要查询的字段的值,我们称之为覆盖索引。
    • 不是所有类型的索引都可以称为覆盖索引,覆盖索引必须要存储索引列的值。
    • 不同的存储实现覆盖索引的方式不同,不是所有的引擎都支持覆盖索引,memory不支持覆盖索引
  • 优势
    • 索引条目通常远小于数据行大小,如果只需要读取索引,那么mysql就会极大的减少数据访问量
    • 因为索引是按照列值顺序存储的,对于IO密集型的范围查询会比随机从磁盘读取一行数数据的IO要少的多
    • 一些存储引擎如MYISAM在内存中只缓存索引,数据则依赖操作系统来缓存,因此要访问数据需要一次系统调用,这可能会导致严重的性能问题
    • 由于INNODB的聚簇索引,覆盖索引对INNODB表特别有用
  • 案例演示

6. 优化的小细节

当使用索引列进行查询的时候尽量不要使用表达式,把计算放到业务层而不是数据层。

  • select actor_id from actor where act_id+1 = 5

尽量使用主键查询而不是其他所有,因为主键查询不会触发回表查询。

使用前缀索引(不是最左匹配)

      有时候需要索引很长的字符串,这会让索引变的大且慢,通常情况下可以使用某个列开始的部分字符串,这样大大节约索引空间,从而提高索引效率,但这会降低索引的选择性,索引的选择性是指不重复的索引值和数据表记录总数的比值,范围从1/#T到1之间。索引的选择性越高则查询 效率越高,因为更高的索引可以让MySQL在查找的时候过滤掉更多的行。
      一般情况下某个列前缀的选择性也是足够高的,足以满足查询的性能,但是对应BLOB,TEXT,VARCHAR类型的列,必须要使用前缀索引,因为mysql不允许索引这些列的完整长度,使用该方法的诀窍在于要选择足够长的前缀以保证较高的选择性,通过又不能太长。

  • 案例演示
    create table citydemo(city varchar(50) not null);
    insert into citydemo(city) select city from city;
    

    重复执行5次下面的sql语句

    insert into citydemo(city) select city from citydemo;
    

    更新城市表的名称

    update citydemo set city=(select city from city order by rand() limit 1);
    

    查找最常见的城市列表,发现每个值都出现45-56次

    select count(*) as cnt,city from citydemo group by city order by cnt desc limit 10;
    
    MySQL调优_第6张图片

    查找最频繁出现的城市前缀,先从3个前缀字母开始,发现比原来出现的次数更多,可以分别截取多个字符串查看城市出现的次数

     select count(*) as cnt,left(city,3) as pref from citydemo group by pref order by cnt desc limit 10;
     select count(*) as cnt,left(city,7) as pref from citydemo group by pref order by cnt desc limit 10;
    
    MySQL调优_第7张图片
    MySQL调优_第8张图片

    还可以通过另外一种计算完整列的选择性,可以看到当前缀长度达到7之后,再增加前缀长度,选择性提升的幅度已经很小了

    select count(distinct left(city,3))/count(*) as sel3,
    count(distinct left(city,4))/count(*) as sel4,
    count(distinct left(city,5))/count(*) as sel5,
    count(distinct left(city,6))/count(*) as sel6,
    count(distinct left(city,7))/count(*) as sel7,
    count(distinct left(city,8))/count(*) as sel8
    from citydemo;
    
    MySQL调优_第9张图片

    计算完成之后可以创建前缀索引

     alter table citydemo add key(city(7));
    

    注意:

    前缀索引是一种能使索引更小更快的有效方法,但是也包含缺点:mysql无法使用前缀索引做order by和group by;

    查看索引

    在这里插入图片描述
    • 关于Cardinality

      基数:该列的唯一值(近似值)。
      探索HyperLogLog算法

使用索引扫描来排序

      mysql有两种方式可以生成有序的结果:通过排序操作或者按索引顺序扫描,如果explain出来的type列的值为index,则说明mysql使用了索引扫描来排序;
MySQL调优_第10张图片
      扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那么就不得不每扫描一条记录就得回表查询一次对应的行,这基本都是随机IO,因此按索引顺序读取的速度通常要比顺序地全表扫描慢。
      mysql可以使用同一个索引既满足排序,又用于查找行,如果可能的话,设计索引时是应该尽可能地满足这两种任务。
      只有当索引的列顺序和order by子句的顺序完全一致,并且所有列的排序方式都一样时,MySQL才能够使用索引来对结果进行排序,如果插叙需要关联多张表,则只有当order by子句引用的字段全部为第一张表时,才能使用索引做排序。order by子句和查找型查询的限制是一样的,需要满足索引的最左前缀的要求,否则,MySQL都需要执行顺序操作,而无法利用索引排序。

使用rental_date索引为下面的查询做排序

explain select rental_id,staff_id from rental where rental_date='2005-05-25' order by inventory_id,customer_id;

MySQL调优_第11张图片

order by子句不满足索引最左前缀的要求,也可以用于查询排序,这是因为你的第一列(rental_date='2005-05-25')被指定为一个常数。
该查询为索引的第一列提供了常量条件,而使用第二列进行排序,将两个组合排序在一起,就形成了最左前缀。

explain select rental_id,staff_id from rental where rental_date='2005-05-25' order by inventory_id desc\G

MySQL调优_第12张图片

union all,in,or都能够使用索引,但是推荐使用in

explain select * from actor where actor_id =1
union all
select * from actor where actor_id =2;

MySQL调优_第13张图片

explain select * from actor where actor_id in (1,2);

MySQL调优_第14张图片

explain select * from actor where actor_id ='1' or actor_id = '2';

MySQL调优_第15张图片

后两条SQL比较;

MySQL调优_第16张图片
注意

优先使用in,如果条件允许的话首先使用exists

范围列可以用到索引

范围条件是:<<=>>=boolean

范围列可以用到索引,但是范维列后面的列无法用到索引,索引最多用于一个范围列;

强制类型转换会全表扫描

create table user(
    id int,
    name varchar(10),
    phone varchar(11)
);
alter table user add index user_idx_phone(phone);
insert into user values(1,'Tom','17787652467');

MySQL调优_第17张图片

更新频繁,数据区分度不高字段不宜建立索引

  • 更新会变更B+树,更新频繁的字段建立索引会大大降低数据库性能;
  • 类似于性别这类区分不大的属性,建立索引是没有意义的,不能有效的过滤数据;
  • 一般区分度在80%以上的时候就可以建立索引,区分度可以使用count(distinct(列名))/count(*)来计算

创建索引的列,不允许为null,可能会得到不符合预期的结果

当需要进行表连接的时候,最好不要超过三张表因为需要join的字段数据类型必须一致

join的多种方式

Simple Nested-Loop Join
MySQL调优_第18张图片

r为驱动表,s为匹配表,可以看到从r中分别取出每一个记录去匹配s表的列,然后再合并数据,对s表进行r表的行数次访问,对数据的开销比较大。

Index Nested-Loop Join
MySQL调优_第19张图片

      这个要求非驱动表(匹配表s)上有索引,可以通过索引来减少比较,加速查询。
      在查询时,驱动表®会根据关联字段的索引进行查找,当在索引上找到符合的值,再回表进行查询,也就是只有当匹配到索引以后才会进行回表查询。
      如果非驱动表(s)的关联键是主键的话,性能就会非常高,如果不是主键,要进行多次回表查询,先关联索引,然后再根据二级索引的主键ID进行回表操作,性能上索引比主键要慢。

Block Nested-Loop Join
MySQL调优_第20张图片

如果有索引,会选取第二种方式进行join,但如果join列没有索引,就会采用Block Nested-Loop join。可以看到中间有个join buffer缓冲区,是将驱动表的所有join相关的列都先缓存到join buffer中,然后批量与匹配表进行匹配,将第一种多次比较合并为一次,降低了非驱动表(s)的访问频率。默认情况下join_buffer_size=256K,在查找的时候Mysql会将所有的需要的列缓存到join buffer当中,包括select的列,而不仅仅只缓存关联列。在一个有N个JOIN关联的SQL当中会在执行时候分配N-1个join buffer。

能使用limit的时候尽量使用limit

limit的作用应该是限制输出,而不是分页,分页只是应用场景。

单表索引建议控制在5个以内

字段值为null,也会占用存储空间。

单索引字段数不允许超过5个

组合索引

创建索引应该避免以下错误概念

  • 索引越多越好
  • 过早优化,在不了解系统的情况下进行优化

索引监控

show status like 'Handler_read%'

参数解释

Handler_read_first:读取索引第一个条目(根节点)的次数
Handler_read_key:通过index获取数据的次数
Handler_read_last:读取索引最后一个条目的次数
Handler_read_next:通过索引读取下一条数据的次数
Handler_read_prev:通过索引读取上一条数据的次数
Handler_read_rnd:从固定位置读取数据的次数
Handler_read_rnd_next:从数据节点读取下一条数据的次数

Handler_read_next
注意

重点查看Handler_read_keyHandler_read_rnd_next,这两个值越大越好

7. 查询优化

7.1 查询慢的原因

7.1.1 网络

7.1.2 CPU

时间片

7.1.3 IO

7.1.4 上下文切换

7.1.5 系统调用

7.1.6 生成统计信息

7.1.7 锁等待时间

7.2 优化数据访问

7.2.1 查询性能低下的主要原因是访问的数据太多,某些查询不可避免的需要筛选大量数据。我们可以通过减少访问数据量的方式进行优化.

  • 确认应用程序是否再检索大量超过需要的数据。

查看执行计划,大概扫描了多少数据。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SSfSSPcG-1677401425934)(C:\Users\Whaleson\AppData\Roaming\Typora\typora-user-images\image-20230226162717277.png)]

我们需要查询rental表的第10000行~第10004行

explain select * from rental limit 10000,5;

在这里插入图片描述

从截图中可以看出为了获取5条数据而扫描了16008条数据(总共16044行,基本上是进行了全表扫描)。

  • 确认mysql服务器层是否在分析大量超过需要的数据行

7.2.2 是否向数据请求了不需要的数据

  • 查询不需要的记录

            我们常常会误以为mysql会只返回需要的数据,实际上mysql确实先返回全部结果再进行计算,再日常开发习惯中,进场是先用select语句查询大量的结果,然后获取前面的N行后关闭结果集。
            优化方式是在查询后面添加limit

  • 多表关联时返回全部列
    select * from actor inner join file_actor using(actor_id) 
    inner join file using(film_id) where 
    film.title='Academy';
    
    select actor.* from actor....
    
  • 总是取出全部列

            在实际需求中,禁止使用select *,虽然这种方式能简化开发,但是会影响查询的性能,所以尽量不要使用。

  • 重复查询相同的数据

            如果需要不断的重复执行相同的查询,且每次返回完全相同的数据。因此,基于这样的应用场景,我们可以将这部分数据缓存(Redis)起来,这样的话能够提高查询效率。LRU

7.3 执行过程的优化

7.3.1 查询缓存

8.0已经弃用在解析一个查询语句之前,如果查询缓存是打开的,那么mysql会优先检查这个查询是否命中查询缓存中的数据,如果查询恰好命中了查询缓存,那么会在返回结果之前检查用户权限,如果权限没问题,那么MySQL会跳过所有的阶段,就直接从内存中拿到结果并返回给客户端。

7.3.2 查询过程的优化

        MySQL查询完缓存之后会经过以下几个步骤:解析SQL、预处理、优化SQL执行计划,这几个步骤出现的任何错误,都可能会终止查询。

7.3.2.1 语法解析器和预处理

        MySQL通过关键字将SQL语句进行解析,并生成一棵解析树(AST-Tree),MySQL解析器将使用MySQL语法检查规则验证和解析查询,例如验证使用了错误的关键字或者顺序是否正确等等,预处理会进一步检查解析树是否合法,例如表名和列名是否存在,是否有歧义,还会验证权限等等。

7.3.2.2 查询优化器

        当语法树没有问题之后,相应的要由优化器将其转换成执行计划,一条查询语句可以使用非常多的执行方式,最后都可以得到对应的结果,但是不同的执行方式带来的效率是不同的,优化器的最主要目的就是要选择最有效的而执行计划。
        mysql使用的是基于成本的优化器(RBO和CBO),在优化的时候会尝试预测一个查询使用某种查询计划时候的成本,并选择其中成本最小的一个。

  • select count(*) from file_actor;
    show status like 'last_query_cost';
    

    可以看到这条查询语句大概需要1104个数据页才能查找到对应的数据,这是经过一系列的统计信息计算来的。
    MySQL调优_第21张图片

    • 每个表或索引的页面个数
    • 索引的基数
    • 索引和数据行的长度
    • 索引分布情况
  • 在很多情况下MySQL会选择错误的执行计划,原因如下:

    • 统计信息不准确

              MySQL因为其mvcc的架构,并不能维护一个数据表的行数的精确统计信息

    • 执行计划的成本估算不等于实际执行的成本

              有时候某个执行计划虽然读取更多的页,但是它的成本却更小,因为如果这些页面都是顺序或者这些页面都已经在内存中的话,那么它的访问成本将很小,MySQL层面并不知道哪些页面在内存中,哪些在磁盘,所以查询实际过程中到底需要多少次IO是无法得知的。

    • MySQL最优可能和实际不一样

              MySQL的优化是基于成本模型的优化,但是有可能不是最快的优化。

    • MySQL不考虑其它并发执行的查询
    • MySQL不会考虑不受其控制的操作成本

      执行存储过程或者用户自定义函数的成本。

  • 优化器的优化策略

    • 静态优化

      直接对解析树进行分析,并完成优化

    • 动态优化

      动态优化与查询的上下文有关,也可能跟取值、索引对应的行数有关。

    MySQL对查询的静态化优化只需要一次,但是对动态优化在每次执行的时候都需要重新评估

  • 优化器的优化类型

    • 重新定义关联表的顺序

      数据表的关联并不总是按照在查询中指定的顺序进行,决定关联顺序是优化器很重要的功能

    • 将外连接转换成内连接,内连接的效率要高于外连接
    • 使用等价变换规则,MySQL可以使用一些等价变化来简化并规划表达式
    • 优化count(),min(),max()

      索引和列是否可以为空通常可以帮助MySQL优化这类表达式:列如,要找到某一列的最小值,只需要查询所有的最左端的记录即可,不需要全文扫描比较。

    • 预估并转化为常数表达式,当MySQL检测到一个表达式可以转换为常数的时候,就会一直把该表达式作为常数进行处理
    • 索引覆盖扫描,当索引中的列包含所有查询中需要使用的列的时候,使用覆盖索引
    • 子查询优化

      MySQL在某些情况下可以将子查询转换成一种效率更高的形式,从而减少多个查询多次对数据进行访问,列如将经常查询的数据放到缓存中

    • 等值传播

      如果两个列的值通过等式关联,那么MySQL能够把其中一个列的where条件传递到另一个上:

      explain select film.film_id from film 
      inner join film_actor
      using(film_id) where film.film_id > 500;
      

      这里使用film_id字段进行等值关联,film_id这个列不仅适用于film表而且适用于film_actor表

      explain select film.film_id from
      film inner join film_actor using(film_id) 
      where film.film_id > 500 and film_actor.film_id > 500;
      
  • 关联查询

    (1) Join Buffer会缓存所有参与查询的列而不是只有Join的列。(比如select后面的列)

    (2)可以通过join_buffer_size缓存大小

    (3)join_buffer_size的默认值是256K,join_buffer_size的最大值在MySQL 5.1.222版本之前是4G,而之后的版本才能在64位操作系统虾申请大于4G-1的Join Buffer空间。

    (4) 使用Block Nested-Loop Join算法需要开启优化器管理配置的optimizer_switch的设置block_nested_loopon,默认为开启。

    show variables like '%optimizer_switch%';
    
    
  • 案例演示

    查看不同的顺序执行方式对查询性能的影响:

    explain select film.film_id,film.title,film.release_year
    ,actor.actor_id,actor.first_name,actor.last_name from film inner join
    film_actor using(film_id) inner join
    actor using(actor_id)
    

    在这里插入图片描述
    查看执行成本
    在这里插入图片描述
    按照自己预想的规定顺序执行

    explain select straight_join film.film_id,film.title,film.release_year  
    ,actor.actor_id,actor.first_name,actor.last_name from film inner join film_actor 
    using(film_id) inner join actor using(actor_id);
    

    在这里插入图片描述

    查看两个结果集的row列,可以看出扫描的行数不同。

  • 排序优化
    排序的算法

    • 两次传输排序

              第一次数据读取是将需要排序的字段读取出来,然后进行排序,第二次是将排好序的结果按照需要去读取数据行。
              这种方式效率比较低,原因是第二次读取数据的时候因为已经排好序,需要去读取所有记录而此时更多的是随机IO,读取数据成本会比较高。
              两次传输的优势,在排序的时候存储尽可能少的数据,让排序缓冲区可以尽可能多的容纳行数来进行排序操作

    • 单次传输排序

              先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果,此方式只需要一次顺序IO读取所有的数据,而无须任何的随机IO,问题在于查询的列特别多的时候,会占用大量的存储空间,无法存储大量的数据

    当需要排序的列总大小加上order by的列的大小超过max_lenght_for_sort_data定义的字节,MySQL会选择双次排序,反之使用单次排序,当然,用户可以设置此参数的值来选择排序的方式。

    show variables like '%max_length_for_sort_data%';
    

    MySQL调优_第22张图片
    1024字节

7.4 优化特定类型的查询

7.4.1 优化COUNT查询

count()是特殊的函数,有两种不同的作用,一种是某个列值 的数量,也可以铜价行数。

  • 总有人认为MYISAM的count函数比较快,这是有前提条件的,只有没有任何where条件的count(*)才是比较快的。(MYISAM游曳哥变量用来保存整体的行数)MySQL调优_第23张图片

在这里插入图片描述

  • 使用近似值

          在某些应用场景中,不需要完全精确的值,可以参考使用近似值来代替,比如可以使用explain来获取近似的值。
          其实在很多OLAP的应用中,需要计算某一个列的基数,有一个计算近似值的算法叫做hyperloglog。

  • 更复杂的优化

           一般情况下,count()需要扫描大量的行数据才能获取精确的数据,其实很难优化,在实际操作的过程的时候可以考虑使用索引覆盖扫描,或者添加汇总表,或者增加外部缓存系统。

7.4.2 优化关联查询

  • 确保on或者using子句的列上有索引,在创建索引的时候就要考虑到关联的顺序

  • 确保任何的group byorder by中的表达式只涉及到一个表中 的列,这样Mysql才能使用索引来优化这个过程。

7.4.3 优化子查询

      子查询的优化最重要的优化建议是尽可能使用关联查询代替。

7.4.4 优化group by和distinct(忽略)

很多场景下,mysql使用相同的方法来优化group by和distinct的查询,使用索引是最有效的方式,但是有很多的情况下无法使用索引,可以使用临时表或者文件排序来分组

如果对关联查询做分组,并且是按照查找表中的某个列进行分组,那么可以采用表的标识列分组的效率比其他列更高

  • select actor.first_name,actor.last_name,count(*) fromfile_actor inner join actor using(actor_id) group by actor.first_name,actor.last_name

  • select actor.first_name,actor.last_name,count(*) fromfile_actor inner join actor using(actor_id) group by actor.actor_id(未必准确)

7.4.5 优化limit分页

        在很多应用场景中,我们需要将数据进行分页,一般会使用limit加上偏移量的方法实现,同时加上合适的order by子句,如果这种方式有索引的帮助,效率通常不错,否则的话需要进行大量的文件排序操作,还有一种情况,当偏移量非常大的时候,前面的大部分数据会被抛弃,这样的代价太高。
        要优化这种查询的话,要么是在页面中限制分页的数量,要么优化大偏移量的性能。

  • select film_id,description from film order by title lmit 50,5
  • select film.film_id,film,description from film inner join (select film_id from film oder by title limit 50,5) as lim using(film_id);
  • MySQL的limit优化

8.分区表

你可能感兴趣的:(MySQL)