MySQL优化详解

MySQL优化

mysql架构

client -> mysql server -> storage engine

mysql server:
	1)链接器 --- 控制用户的链接
	2)分析器 --- 词法分析,语法分析
	3)优化器 --- 优化sql语句,规定执行流程(可以查看sql语句的执行计划,可以采用对应的优化点,来加快查询)
			- RBO:基于规则的优化
			- CBO:基于成本的优化(最终目的是要满足查询快的需求)
	4)执行器 --- sql语句的实际执行组件(跟存储引擎进行数据交互)
			- select * 该语句会造成大量不需要数据的读取操作,IO次数频繁(IO速度的提升是硬件层面的问题)
	
storage engine:
	1)InnoDB:磁盘
	2)MyISAM:磁盘
	3)memory:内存
	- 不同的存放位置,不同的文件格式

性能监控

show profile

查看sql所执行的具体时间

mysql> set profiling=1;	#开启profiles
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> select * from emp;
+-------+--------+-----------+------+------------+---------+---------+--------+
| EMPNO | ENAME  | JOB       | MGR  | HIREDATE   | SAL     | COMM    | DEPTNO |
+-------+--------+-----------+------+------------+---------+---------+--------+
|  7369 | SMITH  | CLERK     | 7902 | 1980-12-17 |  800.00 |    NULL |     20 |
|  7499 | ALLEN  | SALESMAN  | 7698 | 1981-02-20 | 1600.00 |  300.00 |     30 |
|  7521 | WARD   | SALESMAN  | 7698 | 1981-02-22 | 1250.00 |  500.00 |     30 |
|  7566 | JONES  | MANAGER   | 7839 | 1981-02-02 | 2975.00 |    NULL |     20 |
|  7654 | MARTIN | SALESMAN  | 7698 | 1981-09-28 | 1250.00 | 1400.00 |     30 |
|  7698 | BLAKE  | MANAGER   | 7839 | 1981-01-05 | 2850.00 |    NULL |     30 |
|  7782 | CLARK  | MANAGER   | 7839 | 1981-09-06 | 2450.00 |    NULL |     10 |
|  7839 | KING   | PRESIDENT | NULL | 1981-11-17 | 5000.00 |    NULL |     10 |
|  7844 | TURNER | SALESMAN  | 7698 | 1981-09-08 | 1500.00 |    0.00 |     30 |
|  7900 | JAMES  | CLERK     | 7698 | 1981-12-03 |  950.00 |    NULL |     30 |
|  7902 | FORD   | ANALYST   | 7566 | 1981-12-03 | 3000.00 |    NULL |     20 |
|  7934 | MILLER | CLERK     | 7782 | 1982-01-23 | 1300.00 |    NULL |     10 |
+-------+--------+-----------+------+------------+---------+---------+--------+
12 rows in set (0.00 sec)

mysql> show profiles; #查看所有sql执行的耗时
+----------+------------+-------------------+
| Query_ID | Duration   | Query             |
+----------+------------+-------------------+
|        1 | 0.00028175 | select * from emp |
+----------+------------+-------------------+
1 row in set, 1 warning (0.00 sec)

mysql> show profile for query 1; #查看Query_ID所对应的sql详细执行时间
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000058 |
| checking permissions | 0.000009 |
| Opening tables       | 0.000025 |
| init                 | 0.000019 |
| System lock          | 0.000009 |
| optimizing           | 0.000004 |
| statistics           | 0.000013 |
| preparing            | 0.000027 |
| executing            | 0.000003 |
| Sending data         | 0.000065 |
| end                  | 0.000004 |
| query end            | 0.000006 |
| closing tables       | 0.000007 |
| freeing items        | 0.000022 |
| cleaning up          | 0.000013 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)

proformance schema(性能模块

相较于show profile统计的信息更为详细;mysql5.7默认开启,不建议关闭(虽然有资源的消耗但能监控数据库的性能问题),关闭需在my.cnf配置文件中进行关闭

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema | #主要关注数据库运行过程中的元数据信息
| demo               |
| mysql              |
| performance_schema | #默认采用并不多见的performance_schema存储引擎(不会对数据进行持久化,也不会被写入binlog),通过监视mysqlServer的事件来实现监视内部server的运行情况(主要关注数据库运行中性能相关数据)
| sys                |
+--------------------+
5 rows in set (0.01 sec)

mysql> show tables;
+------------------------------------------------------+
| Tables_in_performance_schema                         |
+------------------------------------------------------+
| accounts                                             |
| cond_instances                                       |
| events_stages_current                                |
| events_stages_history                                |
| events_stages_history_long                           |
| events_stages_summary_by_account_by_event_name       |
| events_stages_summary_by_host_by_event_name          |
| events_stages_summary_by_thread_by_event_name        |
| events_stages_summary_by_user_by_event_name          |
| events_stages_summary_global_by_event_name           |
| events_statements_current                            |
| events_statements_history                            |
| events_statements_history_long                       |
| events_statements_summary_by_account_by_event_name   |
| events_statements_summary_by_digest                  |
| events_statements_summary_by_host_by_event_name      |
| events_statements_summary_by_program                 |
| events_statements_summary_by_thread_by_event_name    |
| events_statements_summary_by_user_by_event_name      |
| events_statements_summary_global_by_event_name       |
| events_transactions_current                          |
| events_transactions_history                          |
| events_transactions_history_long                     |
| events_transactions_summary_by_account_by_event_name |
| events_transactions_summary_by_host_by_event_name    |
| events_transactions_summary_by_thread_by_event_name  |
| events_transactions_summary_by_user_by_event_name    |
| events_transactions_summary_global_by_event_name     |
| events_waits_current                                 |
| events_waits_history                                 |
| events_waits_history_long                            |
| events_waits_summary_by_account_by_event_name        |
| events_waits_summary_by_host_by_event_name           |
| events_waits_summary_by_instance                     |
| events_waits_summary_by_thread_by_event_name         |
| events_waits_summary_by_user_by_event_name           |
| events_waits_summary_global_by_event_name            |
| file_instances                                       |
| file_summary_by_event_name                           |
| file_summary_by_instance                             |
| global_status                                        |
| global_variables                                     |
| host_cache                                           |
| hosts                                                |
| memory_summary_by_account_by_event_name              |
| memory_summary_by_host_by_event_name                 |
| memory_summary_by_thread_by_event_name               |
| memory_summary_by_user_by_event_name                 |
| memory_summary_global_by_event_name                  |
| metadata_locks                                       |
| mutex_instances                                      |
| objects_summary_global_by_type                       |
| performance_timers                                   |
| prepared_statements_instances                        |
| replication_applier_configuration                    |
| replication_applier_status                           |
| replication_applier_status_by_coordinator            |
| replication_applier_status_by_worker                 |
| replication_connection_configuration                 |
| replication_connection_status                        |
| replication_group_member_stats                       |
| replication_group_members                            |
| rwlock_instances                                     |
| session_account_connect_attrs                        |
| session_connect_attrs                                |
| session_status                                       |
| session_variables                                    |
| setup_actors                                         |
| setup_consumers                                      |
| setup_instruments                                    |
| setup_objects                                        |
| setup_timers                                         |
| socket_instances                                     |
| socket_summary_by_event_name                         |
| socket_summary_by_instance                           |
| status_by_account                                    |
| status_by_host                                       |
| status_by_thread                                     |
| status_by_user                                       |
| table_handles                                        |
| table_io_waits_summary_by_index_usage                |
| table_io_waits_summary_by_table                      |
| table_lock_waits_summary_by_table                    |
| threads                                              |
| user_variables_by_thread                             |
| users                                                |
| variables_by_thread                                  |
+------------------------------------------------------+
87 rows in set (0.00 sec) #performance_schema的87张表

  • instruments:生产者,用于采集mysql中各种各样的操作产生的事件信息(采集)

  • consumers:消费者,存储来自instruments采集的数据(存储)

  • mysql> select * from setup_instruments; #通过查询发现,有些配置并不是默认开启的
    +--------------------------------------------------------------------------------+---------+-------+
    | NAME                                                                           | ENABLED | TIMED |
    +--------------------------------------------------------------------------------+---------+-------+
    | wait/synch/mutex/sql/TC_LOG_MMAP::LOCK_tc                                      | NO      | NO    |
    | wait/synch/mutex/sql/LOCK_des_key_file                                         | NO      | NO    |
    ...
    
    # ENABLED:表示改属性是否开启
    # TIMED:表示计时器是否开启
    
    #为了保证能监控到完整的信息,最好将全部属性开启
    mysql> update setup_instruments set ENABLED = 'YES',TIMED = 'YES' where name like '%wait%'; #开启生产者所有属性与计时器
    Query OK, 314 rows affected (0.00 sec)
    Rows matched: 368  Changed: 314  Warnings: 0
    
    mysql> update setup_consumers set ENABLED = 'YES' where name like '%wait%'; #开启消费者所有属性与计时器
    Query OK, 3 rows affected (0.00 sec)
    Rows matched: 3  Changed: 3  Warnings: 0
    
    

查看相关信息的统计

mysql> select * from events_waits_current\G; #\G代表按列表格式输出
*************************** 1. row ***************************
            THREAD_ID: 11 #线程ID
             EVENT_ID: 9350
         END_EVENT_ID: 9350
           EVENT_NAME: wait/synch/mutex/innodb/buf_dblwr_mutex
               SOURCE: 
          TIMER_START: 2992355867557490
            TIMER_END: 2992355867651674
           TIMER_WAIT: 94184
                SPINS: NULL
        OBJECT_SCHEMA: NULL
          OBJECT_NAME: NULL
           INDEX_NAME: NULL
          OBJECT_TYPE: NULL
OBJECT_INSTANCE_BEGIN: 74332472
     NESTING_EVENT_ID: NULL
   NESTING_EVENT_TYPE: NULL
            OPERATION: lock
      NUMBER_OF_BYTES: NULL
                FLAGS: NULL
*************************** 2. row ***************************
...

数据类型

通常更小的数据类型,数据大小不变但占用的磁盘空间更小(存在data📁->具体数据库📁中)

  • *.frm:表结构文件
  • *.ibd:存放数据的文件
  • *.MYD:MyISAM存储引擎中的数据文件
  • *.MYI:MyISAM存储引擎中的索引文件

数据类型合理选择,可使查询速度更快

  • 比如一个日期,一个使用varchar存储,另一个使用date存储,在查询速度上看date类型快于varchar
  • IP使用函数转换为整型进行存储,即节约了存储空间又提高了查询效率(不存在的IP将返回null)
    • INET_ATON('[IP]'):将IP转行为整型
    • INET_NTOA('[转换后的整型IP]'):将整型转换为IP

尽量避免null

  • 如果查询中包含可为null的列,对mysql来说很难优化,因为可为null的列使得索引、索引统计和值比较都更加复杂
  • 字符串类型可使用’'空字符串进行null的替代,状态类型也可使用规定默认值填充null

实际细则

  • 整数类型:TINYINTSMALLINTMEDIUMINTINTBIGINT分别使用8,16,24,32,64位存储空间(定义的数据长度可被数据长度超过,但不能超过类型所占用的最大空间);尽量使用满足需求的最小数据类型

  • 字符和字符串类型:按照查询速度:char>varchar>text

    • char长度固定,即每条数据占用等长字节空间;最大长度是255个字符,适合用在身份证号、手机号等定长字符串

    • varchar可变程度,可以设置最大长度;最大空间是65535个字节,适合用在长度可变的属性

    • text不设置长度,当不知道属性的最大长度时,适合用text

    • - varchar根据实际内容长度保存数据
      			1、使用最小的符合需求的长度。
      			2、varchar(n) n小于等于255使用额外一个字节保存长度,n>255使用额外两个字节保存长度。
      			3、varchar(5)与varchar(255)保存同样的内容,硬盘存储空间相同,但内存空间占用不同,是指定的大小。
      			4、varchar在mysql5.6之前变更长度,或者从255一下变更到255以上时时,都会导致锁表。
      			应用场景
      				1、存储长度波动较大的数据,如:文章,有的会很短有的会很长
      				2、字符串很少更新的场景,每次更新后都会重算并使用额外存储空间保存长度
      				3、适合保存多字节字符,如:汉字,特殊字符等
      - char固定长度的字符串
      			1、最大长度:255
      			2、会自动删除末尾的空格
      			3、检索效率、写效率 会比varchar高,以空间换时间
      			应用场景
      				1、存储长度波动不大的数据,如:md5摘要
      				2、存储短字符串、经常更新的字符串
      
  • BLOBTEXT类型:MySQL 把每个 BLOB 和 TEXT 值当作一个独立的对象处理;两者都是为了存储很大数据而设计的字符串类型,分别采用二进制和字符方式存储(不推荐使用,在不确定需要存储较大文本内容时,建议存储到文件中,然后在数据库中存储该文件地址,这样在性能上有质的提升【I/O、缓存问题】

  • 关于时间的类型:

    • 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> create table enum_test(e enum('fish','apple','dog') not null); #插入枚举类型fish,apple,dog -> 1,2,3
      Query OK, 0 rows affected (0.03 sec)
      
      mysql> insert into enum_test(e) values('fish'),('dog'),('apple'); #1,3,2
      Query OK, 3 rows affected (0.01 sec)
      Records: 3  Duplicates: 0  Warnings: 0
      
      mysql> select * from enum_test;
      +-------+
      | e     |
      +-------+
      | fish  |
      | dog   |
      | apple |
      +-------+
      3 rows in set (0.00 sec)
      
      mysql> select e+1 from enum_test; #e+1 -> 1+1,3+1,2+1
      +-----+
      | e+1 |
      +-----+
      |   2 |
      |   4 |
      |   3 |
      +-----+
      3 rows in set (0.00 sec)
      
      mysql> select * from enum_test where e=1; #根据枚举key找到对应的值
      +------+
      | e    |
      +------+
      | fish |
      +------+
      1 row in set (0.00 sec)
      
      mysql> select * from enum_test where e='fish'; #根据枚举value找到值
      +------+
      | e    |
      +------+
      | fish |
      +------+
      1 row in set (0.00 sec)
      
  • 特殊类型数据:

    • mysql> select inet_aton('1.1.1.1');
      +----------------------+
      | inet_aton('1.1.1.1') |
      +----------------------+
      |             16843009 |
      +----------------------+
      1 row in set (0.00 sec)
      
      mysql> select inet_ntoa(16843009);
      +---------------------+
      | inet_ntoa(16843009) |
      +---------------------+
      | 1.1.1.1             |
      +---------------------+
      1 row in set (0.00 sec)
      

合理使用范式和反范式

三范式详情:https://blog.csdn.net/CharlesYooSky/article/details/89204229

  • 1NF:保证每列的原子性

  • 2NF:保证一张表只描述一件事情

  • 3NF:保证每列都和主键直接相关

  • 范式:

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

    • 优点
      • 所有的数据都在同一张表中,可以避免关联
      • 可以设计有效的索引(mysql索引底层采用B+Tree【有序的,天然的排序方式,使用该方式排序效率更高】)
    • 缺点
      • 表格内的冗余较多,删除数据时候会造成表有些有用的信息丢失
    • 在这里插入图片描述

主键的选择

  • 代理主键:与业务无关的,无意义的数字序列(推荐:不与业务耦合,更易于维护)
  • 自然主键:事物属性中的自然唯一标识

字符集选择

  • 纯拉丁字符能表示的内容,没必要选择 latin1 之外的其他字符编码,因为这会节省大量的存储空间
  • 如果我们可以确定不需要存放多种语言,就没必要非得使用UTF8或者其他UNICODE字符类型,这回造成大量的存储空间浪费
  • MySQL的数据类型可以精确到字段,所以当我们需要大型数据库中存放多字节数据的时候,可以通过对不同表不同字段使用不同的数据类型来较大程度减小数据存储量,进而降低 IO 操作次数并提高缓存命中率

存储引擎的选择

在这里插入图片描述

  • 非聚簇索引:索引与数据文件分开存储
  • 聚簇索引:索引与数据文件存储在同一文件
  • 如果查询的列是索引的列采用行锁否则采用表锁
  • 全文索引:如同一个字段存储了一篇文章,在文章中找到某关键字(不推荐:效率低,建议使用ES

适当数据冗余

  • 被频繁引用且只能通过 Join 2张(或者更多)大表的方式才能得到的独立小字段
  • 这样的场景由于每次Join仅仅只是为了取得某个小字段的值,Join到的记录又大,会造成大量不必要的 IO,完全可以通过空间换取时间的方式来优化。不过,冗余的同时需要确保数据的一致性不会遭到破坏,确保更新的同时冗余字段也被更新

适当拆分

  • 垂直切分
  • 水平切分

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

mysql执行计划

​ 在企业的应用场景中,为了知道优化SQL语句的执行,需要查看SQL语句的具体执行过程,以加快SQL语句的执行效率。

​ 可以使用explain+SQL语句来模拟优化器执行SQL查询语句,从而知道mysql是如何处理sql语句的。

​ 官网地址: https://dev.mysql.com/doc/refman/5.5/en/explain-output.html

1、执行计划中包含的信息

ColumnMeaning
idThe SELECT identifier
select_typeThe SELECT type
tableThe table for the output row
partitionsThe matching partitions
typeThe join type
possible_keysThe possible indexes to choose
keyThe index actually chosen
key_lenThe length of the chosen key
refThe columns compared to the index
rowsEstimate of rows to be examined
filteredPercentage of rows filtered by table condition
extraAdditional information

id

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

id号分为三种情况:

​ 1、如果id相同,那么执行顺序从上到下

explain select * from emp e join dept d on e.deptno = d.deptno join salgrade sg on e.sal between sg.losal and sg.hisal;

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

explain select * from emp e where e.deptno in (select d.deptno from dept d where d.dname = 'SALES');

​ 3、id相同和不同的,同时存在:相同的可以认为是一组,从上往下顺序执行,在所有组中,id值越大,优先级越高,越先执行

explain select * from emp e join dept d on e.deptno = d.deptno join salgrade sg on e.sal between sg.losal and sg.hisal where e.deptno in (select d.deptno from dept d where d.dname = 'SALES');

select_type

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

select_type ValueMeaning
SIMPLESimple SELECT (not using UNION or subqueries)
PRIMARYOutermost SELECT
UNIONSecond or later SELECT statement in a UNION
DEPENDENT UNIONSecond or later SELECT statement in a UNION, dependent on outer query
UNION RESULTResult of a UNION.
SUBQUERYFirst SELECT in subquery
DEPENDENT SUBQUERYFirst SELECT in subquery, dependent on outer query
DERIVEDDerived table
UNCACHEABLE SUBQUERYA subquery for which the result cannot be cached and must be re-evaluated for each row of the outer query
UNCACHEABLE UNIONThe second or later select in a UNION that belongs to an uncacheable subquery (see UNCACHEABLE SUBQUERY)
--sample:简单的查询,不包含子查询和union
explain select * from emp;

--primary:查询中若包含任何复杂的子查询,最外层查询则被标记为Primary
explain select staname,ename supname from (select ename staname,mgr from emp) t join emp on t.mgr=emp.empno ;

--union:若第二个select出现在union之后,则被标记为union
explain select * from emp where deptno = 10 union select * from emp where sal >2000;

--dependent union:跟union类似,此处的depentent表示union或union all联合而成的结果会受外部表影响
explain select * from emp e where e.empno  in ( select empno from emp where deptno = 10 union select empno from emp where sal >2000)

--union result:从union表获取结果的select
explain select * from emp where deptno = 10 union select * from emp where sal >2000;

--subquery:在select或者where列表中包含子查询
explain select * from emp where sal > (select avg(sal) from emp) ;

--dependent subquery:subquery的子查询要受到外部表查询的影响
explain select * from emp e where e.deptno in (select distinct deptno from dept);

--DERIVED: from子句中出现的子查询,也叫做派生类,
explain select staname,ename supname from (select ename staname,mgr from emp) t join emp on t.mgr=emp.empno ;

--UNCACHEABLE SUBQUERY:表示使用子查询的结果不能被缓存
 explain select * from emp where empno = (select empno from emp where deptno=@@sort_buffer_size);
 
--uncacheable union:表示union的查询结果不能被缓存:sql语句未验证

table

对应行正在访问哪一个表,表名或者别名,可能是临时表或者union合并结果集
1、如果是具体的表名,则表明从实际的物理表中获取数据,当然也可以是表的别名

​ 2、表名是derivedN的形式,表示使用了id为N的查询产生的衍生表

​ 3、当有union result的时候,表名是union n1,n2等的形式,n1,n2表示参与union的id

type

type显示的是访问类型,访问类型表示我是以何种方式去访问我们的数据,最容易想的是全表扫描,直接暴力的遍历一张表去寻找需要的数据,效率非常低下,访问的类型有很多,效率从最好到最坏依次是:

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

一般情况下,得保证查询至少达到range级别,最好能达到ref

--all:全表扫描,一般情况下出现这样的sql语句而且数据量比较大的话那么就需要进行优化。
explain select * from emp;

--index:全索引扫描这个比all的效率要好,主要有两种情况,一种是当前的查询时覆盖索引,即我们需要的数据在索引中就可以索取,或者是使用了索引进行排序,这样就避免数据的重排序
explain  select empno from emp;

--range:表示利用索引查询的时候限制了范围,在指定范围内进行查询,这样避免了index的全索引扫描,适用的操作符: =, <>, >, >=, <, <=, IS NULL, BETWEEN, LIKE, or IN() 
explain select * from emp where empno between 7000 and 7500;

--index_subquery:利用索引来关联子查询,不再扫描全表
explain select * from emp where emp.job in (select job from t_job);

--unique_subquery:该连接类型类似与index_subquery,使用的是唯一索引
 explain select * from emp e where e.deptno in (select distinct deptno from dept);
 
--index_merge:在查询过程中需要多个索引组合使用,没有模拟出来

--ref_or_null:对于某个字段即需要关联条件,也需要null值的情况下,查询优化器会选择这种访问方式
explain select * from emp e where  e.mgr is null or e.mgr=7369;

--ref:使用了非唯一性索引进行数据的查找
 create index idx_3 on emp(deptno);
 explain select * from emp e,dept d where e.deptno =d.deptno;

--eq_ref :使用唯一性索引进行数据查找
explain select * from emp,emp2 where emp.empno = emp2.empno;

--const:这个表至多有一个匹配行,
explain select * from emp where empno = 7369;
 
--system:表只有一行记录(等于系统表),这是const类型的特例,平时不会出现

possible_keys

​ 显示可能应用在这张表中的索引,一个或多个,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用

explain select * from emp,dept where emp.deptno = dept.deptno and emp.deptno = 10;

key

​ 实际使用的索引,如果为null,则没有使用索引,查询中若使用了覆盖索引,则该索引和查询的select字段重叠。

explain select * from emp,dept where emp.deptno = dept.deptno and emp.deptno = 10;

key_len

表示索引中使用的字节数,可以通过key_len计算查询中使用的索引长度,在不损失精度的情况下长度越短越好。

explain select * from emp,dept where emp.deptno = dept.deptno and emp.deptno = 10;

ref

显示索引的哪一列被使用了,如果可能的话,是一个常数

explain select * from emp,dept where emp.deptno = dept.deptno and emp.deptno = 10;

rows

根据表的统计信息及索引使用情况,大致估算出找出所需记录需要读取的行数,此参数很重要,直接反应的sql找了多少数据,在完成目的的情况下越少越好

explain select * from emp;

extra

包含额外的信息。

--using filesort:说明mysql无法利用索引进行排序,只能利用排序算法进行排序,会消耗额外的位置
explain select * from emp order by sal;

--using temporary:建立临时表来保存中间结果,查询完成之后把临时表删除
explain select ename,count(*) from emp where deptno = 10 group by ename;

--using index:这个表示当前的查询时覆盖索引的,直接从索引中读取数据,而不用访问数据表。如果同时出现using where 表名索引被用来执行索引键值的查找,如果没有,表面索引被用来读取数据,而不是真的查找
explain select deptno,count(*) from emp group by deptno limit 10;

--using where:使用where进行条件过滤
explain select * from t_user where id = 1;

--using join buffer:使用连接缓存,情况没有模拟出来

--impossible where:where语句的结果总是false
explain select * from emp where empno = 7469;

通过索引进行优化

存储引擎使用索引底层的原理

  • memory:hash
  • InnoDB:B+Tree(索引与数据存储在同一文件)叶子节点键值对应的数据是一整行数据
  • MyISAM:B+Tree(索引与数据存储在不同文件)叶子节点键值对应的数据为具体数据的文件地址

红黑树 左右子树深度相差不能超过最小深度的两倍

  • 是平衡树(AVL Tree:左右子树的深度差不能大于1查询效率较高,插入性能低)的一个变种,损失一部分的查询性能来平衡插入性能,加入红黑节点减少树的旋转(很耗性能)通过旋转和变色两种行为方式提升插入效率,同时也损失了一部分查询效率
  • 特点:从根节点出发,每一条链路的黑色节点个数相等

B树

  • 所有键值分布在整颗树中
  • 搜索有可能在非叶子结点结束,在关键字全集内做一次查找,性能逼近二分查找
  • 每个节点最多拥有m个子树
  • 根节点至少有2个子树
  • 分支节点至少拥有m/2颗子树(除根节点和叶子节点外都是分支节点)
  • 所有叶子节点都在同一层、每个节点最多可以有m-1个key,并且以升序排列

在这里插入图片描述

实例图说明:
每个节点占用一个磁盘块,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为 16 和 34,P1 指针指向的子树的数据范围为小于 16,P2 指针指向的子树的数据范围为 16~34,P3 指针指向的子树的数据范围为大于 34。 
查找关键字过程:
1、根据根节点找到磁盘块 1,读入内存。【磁盘 I/O 操作第 1 次】
2、比较关键字 28 在区间(16,34),找到磁盘块 1 的指针 P2。
3、根据 P2 指针找到磁盘块 3,读入内存。【磁盘 I/O 操作第 2 次】
4、比较关键字 28 在区间(25,31),找到磁盘块 3 的指针 P2。
5、根据 P2 指针找到磁盘块 8,读入内存。【磁盘 I/O 操作第 3 次】
6、在磁盘块 8 中的关键字列表中找到关键字 28
  • 缺点
    • 每个节点都有key,同时也包含data,而每个页存储空间是有限的,如果data比较大的话会导致每个节点存储的key数量变小
    • 当存储的数据量很大的时候会导致深度较大,增大查询时磁盘io次数,进而影响查询性能

B+树

  • B+Tree每个节点可以包含更多的节点,这个做的原因有两个,第一个原因是为了降低树的高度,第二个原因是将数据范围变为多个区间,区间越多,数据检索越快
  • 非叶子节点存储key,叶子节点存储key和数据
  • 叶子节点两两指针相互连接(符合磁盘的预读特性),顺序查询性能更高

在这里插入图片描述

注意:在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对 B+Tree 进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。

  • InnoDB–B+Tree,叶子节点直接放置数据
    • 在这里插入图片描述
    • 注意
      • InnoDB是通过B+Tree结构对主键创建索引,然后叶子节点中存储记录,如果没有主键,那么会选择唯一键,如果没有唯一键,那么会生成一个6位的row_id来作为主键
      • 如果创建索引的键是其他字段,那么在叶子节点中存储的是该记录的主键,然后再通过主键索引找到对应的记录,叫做回表
  • MyISAM–B+Tree
  • 在这里插入图片描述

索引基本知识

索引的优点
  • 大大减少了服务器需要扫描的数据量(I/O量)
  • 帮助服务器避免排序和临时表(order by全排序,效率低)
  • 将随机io变成顺序io(随机io慢,顺序io快)
索引的用处
  • 快速查找匹配WHERE子句的行
  • 从consideration中消除行,如果可以在多个索引之间进行选择,mysql通常会使用找到最少行的索引
  • 如果表具有多列索引,则优化器可以使用索引的任何最左前缀来查找行
  • 当有表连接的时候,从其他表检索行数据
  • 查找特定索引列的min或max值
  • 如果排序或分组时在可用索引的最左前缀上完成的,则对表进行排序和分组
  • 在某些情况下,可以优化查询以检索值而无需查询数据行
索引的分类
  • 主键索引(唯一且非空)
  • 唯一索引(唯一键,数据库默认索引)
  • 普通索引(普通列建立的索引)
  • 全文索引(一般应用在数据类型varchar,char,text
  • 组合索引(多个列组合建立的索引)
面试技术名词
  • 回表:在使用非主键列索引列查询,首先会根据索引B+Tree找到叶子节点,该叶子节点数据部分存储的为所找数据行的主键,根据该主键找到主键索引的叶子节点,然后再找到数据行,再返回,从主键索引B+Tree中获取行数据这个过程叫做回表

    • select * from emp where name = "zs"; #name为普通索引,先查询出主键,根据主键获取数据行
      
  • 覆盖索引:不回表,在只查询主键列值时,不需要回表查询行数据时(能用覆盖索引尽量使用)

    • select id from emp where name = "zs"; #id主键索引,直接获取主键索引
      
  • 最左匹配:多列建立联合索引,根据索引顺序从左往右条件排列,否则触发的是全表扫描

    • #如一个表有a,b,c 三个字段,然后建立联合索引 index(a,b,c) 注意这里索引字段的顺序
      select * from table where a = "1"; //会走索引
      select * from table where a = "1" and b = "2"; //会走索引
      select * from table where a = "1" and b = "2" and c = "2" //会走索引
      select * from table where b = "2" and a = "1"; //会走索引
       
      select * from table where c = "3"; //不会走索引
      select * from table where b = 2 and  c = "3"; //不会走索引
      
  • 索引下推(ICP):在组合索引中根据第一个索引条件在回表前进行第二个条件的过滤,只用进行一次回表(MySQL5.6版本的新特性,它能减少回表查询次数,提高查询效率

    • #存储引擎根据(name,age)联合索引,找到,由于联合索引中包含列,所以存储引擎直接再联合索引里按照age=10过滤。按照过滤后的数据再一一进行回表扫描
      select * from tuser where name like '张%' and age=10;
      

索引匹配方式

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);
  • 全值匹配:全值匹配指的是和索引中的所有列进行匹配

    • #组合索引列(name, age, pos)
      explain select * from staffs where name = 'July' and age = 23 and pos = 'dev'; #type ref
      
  • 匹配最左前缀:只匹配前面的几列

    • explain select * from staffs where name = 'July' and age = 23; #type ref
      explain select * from staffs where name = 'July'; #type ref
      
  • 匹配列前缀:可以匹配某一列的值的开头部分

    • explain select * from staffs where name like 'J%'; #type range
      explain select * from staffs where name like '%J'; #type ALL(全匹配,)
      
  • 匹配范围值:可以查找某一个范围的数据

    • explain select * from staffs where name > 'Mary'; #type range
      
  • 精确匹配某一列并范围匹配另外一列:可以查询第一列的全部和第二列的部分

    • explain select * from staffs where name = 'July' and age > 25; #type range
      
      explain select * from staffs where name = 'July' and pos > 25; #pos条件与类型不符合该索引列会被忽略 #type ref
      explain select * from staffs where name = 'July' and pos = 25 and age = 20; #匹配的索引列也仅有name、age #type ref
      explain select * from staffs where name = 'July' and pos = 'dev' and age = 20; #与全值匹配相同,此处在存储引擎层面进行索引下推(MySQL5.6及之后版本)
      
  • 只访问索引的查询:查询的时候只需要访问索引,不需要访问数据行,本质上就是覆盖索引

    • explain select name,age,pos from staffs where name = 'July' and age = 25 and pos = 'dev'; #type ref #Extra Using index(索引覆盖)
      

哈希索引

  • 基于哈希表的实现,只有精确匹配索引所有列的查询才有效

  • 在mysql中,只有memory的存储引擎显式支持哈希索引

  • 哈希索引自身只需存储对应的hash值,所以索引的结构十分紧凑,这让哈希索引查找的速度非常快

  • 哈希索引的限制

    • 哈希索引只包含哈希值和行指针,而不存储字段值,索引不能使用索引中的值来避免读取行
    • 哈希索引数据并不是按照索引值顺序存储的,所以无法进行排序
    • 哈希索引不支持部分列匹配查找,哈希索引是使用索引列的全部内容来计算哈希值
    • 哈希索引支持等值比较查询,不支持任何范围查询
    • 访问哈希索引的数据非常快,除非有很多哈希冲突,当出现哈希冲突的时候,存储引擎必须遍历链表中的所有行指针,逐行进行比较,直到找到所有符合条件的行
    • 哈希冲突比较多的话,维护的代价也会很高
      • 如何避免hash冲突:
        • 开放定址法
        • 在哈希法
        • 链地址法
        • 建立公共益处区
  • 案例

    • #当需要存储大量的URL,并且根据URL进行搜索查找,如果使用B+树,存储的内容就会很大
      select id from url where url=""
      #也可以利用将url使用CRC32做哈希,可以使用以下查询方式:
      select id fom url where url="" and url_crc=CRC32("") #CRC(循环冗余校验)
      #此查询性能较高原因是使用体积很小的索引来完成查找
      

组合索引

  • 当包含多个列作为索引,需要注意的是正确的顺序依赖于该索引的查询,同时需要考虑如何更好的满足排序和分组的需要

    • 案例

      • #建立组合索引a,b,c不同SQL语句使用索引情况
        where a = 3 #使用了索引a
        where a = 3 and b = 5 #使用了索引a、b
        where a = 3 and b = 5 and c = 4 #使用了索引a、b、c
        where b = 3 or c = 4 #未使用索引
        where a = 3 and c = 4 #使用了索引a
        where a = 3 and b > 10 and c = 7 #使用了索引a、b(在范围条件后的索引都是无效索引)
        where a = 3 and b like '%xx%' and c = 7 #使用了索引a(模糊查询匹配列前缀【不能以%开头】不满足则当前索引及其之后索引无效)
        

聚簇索引与非聚簇索引

聚簇索引

不是单独的索引类型,而是一种数据存储方式,指的是数据行跟相邻的键值紧凑的存储在一起(在批量前一大量数据时,建议先关闭创建索引属性,在数据迁移完成后再开启,避免频繁索引的更新【数据越量大性能越低】)
优点
​ 1、可以把相关数据保存在一起
​ 2、数据访问更快,因为索引和数据保存在同一个树中
​ 3、使用覆盖索引扫描的查询可以直接使用页节点中的主键值
缺点
​ 1、聚簇数据最大限度地提高了IO密集型应用的性能,如果数据全部在内存,那么聚簇索引就没有什么优势
​ 2、插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式
​ 3、更新聚簇索引列的代价很高,因为会强制将每个被更新的行移动到新的位置
​ 4、基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临页分裂的问题
​ 5、聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候

非聚簇索引

​ 数据文件跟索引文件分开存放

覆盖索引

基本介绍

  • 如果一个索引包含所有需要查询的字段的值,我们称之为覆盖索引
  • 不是所有类型的索引都可以称为覆盖索引,覆盖索引必须要存储索引列的值(MyISAM存储的为地址值)
  • 不同的存储实现覆盖索引的方式不同,不是所有的引擎都支持覆盖索引,memory不支持覆盖索引

优势

  • 索引条目通常远小于数据行大小,如果只需要读取索引,那么mysql就会极大的较少数据访问量
  • 因为索引是按照列值顺序存储的,所以对于IO密集型的范围查询会比随机从磁盘读取每一行数据的IO要少的多
    • IO密集型:需要频繁进行IO,消耗IO的资源是比较大的(数据库一般为IO密集型)
    • CPU密集型:需要很多消耗型资源,而IO的消耗是比较小的
  • 一些存储引擎如MYISAM在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用,这可能会导致严重的性能问题
  • 由于INNODB的聚簇索引,覆盖索引对INNODB表特别有用

优化小细节

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

    • explain select actor_id from actor where actor_id=4; #type const
      explain select actor_id from actor where actor_id+1=5; #type index
      
  • 尽量使用主键查询,而不是其他索引,因此主键查询不会触发回表查询(where条件尽量带上主键)

  • 使用前缀索引

    • #   有时候需要索引很长的字符串,这会让索引变的大且慢,通常情况下可以使用某个列开始的部分字符串,这样大大的节约索引空间,从而提高索引效率,但这会降低索引的选择性,索引的选择性是指不重复的索引值和数据表记录总数的比值,范围从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-65次,
      select count(*) as cnt,city from citydemo group by city order by cnt desc limit 10;
      
      --查找最频繁出现的城市前缀,先从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;
      --此时前缀的选择性接近于完整列的选择性
      
      --还可以通过另外一种方式来计算完整列的选择性,可以看到当前缀长度到达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;
      
      --计算完成之后可以创建前缀索引
      alter table citydemo add key(city(7));
      
      --注意:前缀索引是一种能使索引更小更快的有效方法,但是也包含缺点:mysql无法使用前缀索引做order by 和 group by
      
      # Cardinality(基数):city前缀索引列大概的唯一值有多少
      基数就是指一个集合中不同值的数目,比如[a,b,c,d]的基数就是4[a,b,c,d,a]的基数还是4,因为a重复了一个,不算。基数也可以称之为Distinct Value,简称DV【HyperLogLog,用来计算基数的算法:https://www.jianshu.com/p/55defda6dcd2】
      show index from citydemo
      
  • 使用索引扫描来排序

    • 	mysql有两种方式可以生成有序的结果:通过排序操作或者按索引顺序扫描,如果explain出来的type列的值为index,则说明mysql使用了索引扫描来做排序
      
      ​		扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那么就不得不每扫描一条索引记录就得回表查询一次对应的行,这基本都是随机IO,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢
      
      ​		mysql可以使用同一个索引即满足排序,又用于查找行,如果可能的话,设计索引时应该尽可能地同时满足这两种任务。
      
      ​		只有当索引的列顺序和order by子句的顺序完全一致,并且所有列的排序方式都一样时,mysql才能够使用索引来对结果进行排序,如果查询需要关联多张表,则只有当orderby子句引用的字段全部为第一张表时,才能使用索引做排序。order by子句和查找型查询的限制是一样的,需要满足索引的最左前缀的要求,否则,mysql都需要执行顺序操作,而无法利用索引排序
      
      
      --sakila数据库中rental表在rental_date,inventory_id,customer_id上有rental_date的索引
      --使用rental_date索引为下面的查询做排序
      explain select rental_id,staff_id from rental where rental_date='2005-05-25' order by inventory_id,customer_id\G
      *************************** 1. row ***************************
                 id: 1
        select_type: SIMPLE
              table: rental
         partitions: NULL
               type: ref
      possible_keys: rental_date
                key: rental_date
            key_len: 5
                ref: const
               rows: 1
           filtered: 100.00
              Extra: Using index condition
      1 row in set, 1 warning (0.00 sec)
      --order by子句不满足索引的最左前缀的要求,也可以用于查询排序,这是因为所以你的第一列被指定为一个常数
      
      --该查询为索引的第一列提供了常量条件,而使用第二列进行排序,将两个列组合在一起,就形成了索引的最左前缀
      explain select rental_id,staff_id from rental where rental_date='2005-05-25' order by inventory_id desc\G
      *************************** 1. row ***************************
                 id: 1
        select_type: SIMPLE
              table: rental
         partitions: NULL
               type: ref
      possible_keys: rental_date
                key: rental_date
            key_len: 5
                ref: const
               rows: 1
           filtered: 100.00
              Extra: Using where
      1 row in set, 1 warning (0.00 sec)
      
      --下面的查询不会利用索引
      explain select rental_id,staff_id from rental where rental_date>'2005-05-25' order by rental_date,inventory_id\G	#使用索引范围查询后,之后的索引列条件将失去索引优势(最左前缀匹配后面的索引列无效【包含索引排序】)
      *************************** 1. row ***************************
                 id: 1
        select_type: SIMPLE
              table: rental
         partitions: NULL
               type: ALL
      possible_keys: rental_date
                key: NULL
            key_len: NULL
                ref: NULL
               rows: 16005
           filtered: 50.00
              Extra: Using where; Using filesort
      
      --该查询使用了两中不同的排序方向,但是索引列都是正序排序的
      explain select rental_id,staff_id from rental where rental_date>'2005-05-25' order by inventory_id desc,customer_id asc\G #当索引列排序为多列,排序顺序不统一则索引排序无效(失去索引排序优势)
      *************************** 1. row ***************************
                 id: 1
        select_type: SIMPLE
              table: rental
         partitions: NULL
               type: ALL
      possible_keys: rental_date
                key: NULL
            key_len: NULL
                ref: NULL
               rows: 16005
           filtered: 50.00
              Extra: Using where; Using filesort
      1 row in set, 1 warning (0.00 sec)
      
      --该查询中引用了一个不再索引中的列
      explain select rental_id,staff_id from rental where rental_date>'2005-05-25' order by inventory_id,staff_id\G #staff_id非索引列导致排序效率低
      *************************** 1. row ***************************
                 id: 1
        select_type: SIMPLE
              table: rental
         partitions: NULL
               type: ALL
      possible_keys: rental_date
                key: NULL
            key_len: NULL
                ref: NULL
               rows: 16005
           filtered: 50.00
              Extra: Using where; Using filesort
      1 row in set, 1 warning (0.00 sec)
      
  • union all,in,or都能够使用索引,但是推荐使用in

    • explain select * from actor where actor_id = 1 union all select * from actor where actor_id = 2; #union单独使用有去重的特性,建议使用union all
      explain select * from actor where actor_id in (1,2); #in的查询效率高于其他两种方式
      explain select * from actor where actor_id = 1 or actor_id =2;
      
  • exists的使用:在查询需要关联多张表中的条件,但仅查询主表中的字段,推荐使用

    • -- 查询的数据为所有数据,exists中子查询为null也作为了查询的结果,则查询出的为所有数据
      select * from emp e where exists(select deptno from dept d where deptno = 20 or deptno = 30);
      -- exists 中只能是子查询,且子查询中的字段在外层查询中无法展示
      select * from emp e where exists(select deptno from dept d where (deptno = 20 or deptno = 30) and e.deptno = d.deptno);
      -- 以上语句等价于
      select * from emp where deptno = 20 or deptno = 30;
      
  • 范围列可以用到索引

    • 范围条件是:<、>、<=、>=、between
    • 范围列可以用到索引,但是范围列后面的列无法用到索引,索引最多用于一个范围列
  • 强制类型转换会全表扫描

    • create table user(id int,name varchar(10),phone varchar(11));
      alter table user add index idx_1(phone);
      
    • explain select * from user where phone=13800001234; #不会触发索引
      
    • explain select * from user where phone='13800001234'; #触发索引
      
  • 更新十分频繁,数据区分度不高的字段上不宜建立索引

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

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

    • join在比较列不是索引的情况下,采用的查找是嵌套循环,效率低

      • 在这里插入图片描述
    • 在非驱动表上有索引,通过索引比较,join采用小表join大表(循环次数为驱动表行数),效率更高

      • 在这里插入图片描述
    • 如果join的列没有索引列采用Block Nested-Loop join

      • show variables like '%join_buffer%'
      • 在这里插入图片描述

    • 大表join大表(分而治之)

      • 将结果拆分为n条语句进行执行
      • 对表进行分区运算
    • inner join, left join, right join

      • (1) select * from t1 join t2 on t1.id = t2.id and t1.name = 'zs';
        	  OR
        (2) select * from t1 join t2 on t1.id = t2.id where t1.name = 'zs';
        
        -- 结论:
        -- 当使用内链接的时候,两种方式一样
        -- (1)当使用左外链接的时候,会把左表的数据全部查出(如果左表件条不满足显示NULL)
        -- (1)当使用右外链接的时候,会把右表中的数据全部查出(如果右表件条不满足显示NULL)
        
  • 如果明确知道只有一条结果返回,limit 1能提高效率(limit:限制输出

    • 加入limit 1后数据库服务会直接返回找到的第一行,不会再判断往下数据是否还有
  • 单表索引建议控制在5个以内

    • 索引越多占用的磁盘空间越大,在列中NULL会占用存储空间(在进行比较的时候,NULL 会参与字段比较,所以对效率有一部分影响),而varchar中空串不占用空间
  • 单索引字段数不允许超过5个(组合索引)

  • 创建索引的时候应该避免以下错误概念

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

索引监控

show status like 'Handler_read%';(索引是否有效主要通过Handler_read_key与Handler_read_rnd_next判断,通常值越高代表索引利用率高)

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

索引优化案例

预先准备好数据
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `itdragon_order_list`;
CREATE TABLE `itdragon_order_list` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id,默认自增长',
  `transaction_id` varchar(150) DEFAULT NULL COMMENT '交易号',
  `gross` double DEFAULT NULL COMMENT '毛收入(RMB)',
  `net` double DEFAULT NULL COMMENT '净收入(RMB)',
  `stock_id` int(11) DEFAULT NULL COMMENT '发货仓库',
  `order_status` int(11) DEFAULT NULL COMMENT '订单状态',
  `descript` varchar(255) DEFAULT NULL COMMENT '客服备注',
  `finance_descript` varchar(255) DEFAULT NULL COMMENT '财务备注',
  `create_type` varchar(100) DEFAULT NULL COMMENT '创建类型',
  `order_level` int(11) DEFAULT NULL COMMENT '订单级别',
  `input_user` varchar(20) DEFAULT NULL COMMENT '录入人',
  `input_date` varchar(20) DEFAULT NULL COMMENT '录入时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10003 DEFAULT CHARSET=utf8;

INSERT INTO itdragon_order_list VALUES ('10000', '81X97310V32236260E', '6.6', '6.13', '1', '10', 'ok', 'ok', 'auto', '1', 'itdragon', '2017-08-28 17:01:49');
INSERT INTO itdragon_order_list VALUES ('10001', '61525478BB371361Q', '18.88', '18.79', '1', '10', 'ok', 'ok', 'auto', '1', 'itdragon', '2017-08-18 17:01:50');
INSERT INTO itdragon_order_list VALUES ('10002', '5RT64180WE555861V', '20.18', '20.17', '1', '10', 'ok', 'ok', 'auto', '1', 'itdragon', '2017-09-08 17:01:49');

逐步开始进行优化:
第一个案例
select * from itdragon_order_list where transaction_id = "81X97310V32236260E";
-- 通过查看执行计划发现type=all,需要进行全表扫描
explain select * from itdragon_order_list where transaction_id = "81X97310V32236260E";

-- 优化一、为transaction_id创建唯一索引
 create unique index idx_order_transaID on itdragon_order_list (transaction_id);
-- 当创建索引之后,唯一索引对应的type是const,通过索引一次就可以找到结果,普通索引对应的type是ref,表示非唯一性索引赛秒,找到值还要进行扫描,直到将索引文件扫描完为止,显而易见,const的性能要高于ref
 explain select * from itdragon_order_list where transaction_id = "81X97310V32236260E";
 
 -- 优化二、使用覆盖索引,查询的结果变成 transaction_id,当extra出现using index,表示使用了覆盖索引
 explain select transaction_id from itdragon_order_list where transaction_id = "81X97310V32236260E";
第二个案例
-- 创建复合索引
create index idx_order_levelDate on itdragon_order_list (order_level,input_date);

-- 创建索引之后发现跟没有创建索引一样,都是全表扫描,都是文件排序
explain select * from itdragon_order_list order by order_level,input_date;

-- 可以使用force index强制指定索引
explain select * from itdragon_order_list force index(idx_order_levelDate) order by order_level,input_date;
-- 其实给订单排序意义不大,给订单级别添加索引意义也不大,因此可以先确定order_level的值,然后再给input_date排序
explain select * from itdragon_order_list where order_level=3 order by input_date;

查询优化

查询慢的原因

  • 网络
  • CPU
  • IO
  • 上下文切换
  • 系统调用
  • 生成统计信息
  • 锁等待时间
    • MyISAM:只能锁表
      • 共享读锁
      • 独占写锁
    • InnoDB:可以锁表也可以锁行,锁的对象为索引(如果锁的列为索引列则为行锁否则表锁)
      • 共享锁
      • 排它锁
      • 自增锁:https://juejin.cn/post/6968420054287253540
      • 间隙锁

优化数据访问

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

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

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

      mysql> show index from rental;
      +--------+------------+---------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
      | Table  | Non_unique | Key_name            | Seq_in_index | Column_name  | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
      +--------+------------+---------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
      | rental |          0 | PRIMARY             |            1 | rental_id    | A         |       16005 |     NULL | NULL   |      | BTREE      |         |               |
      | rental |          0 | rental_date         |            1 | rental_date  | A         |       15815 |     NULL | NULL   |      | BTREE      |         |               |
      | rental |          0 | rental_date         |            2 | inventory_id | A         |       16005 |     NULL | NULL   |      | BTREE      |         |               |
      | rental |          0 | rental_date         |            3 | customer_id  | A         |       16005 |     NULL | NULL   |      | BTREE      |         |               |
      | rental |          1 | idx_fk_inventory_id |            1 | inventory_id | A         |        4580 |     NULL | NULL   |      | BTREE      |         |               |
      | rental |          1 | idx_fk_customer_id  |            1 | customer_id  | A         |         599 |     NULL | NULL   |      | BTREE      |         |               |
      | rental |          1 | idx_fk_staff_id     |            1 | staff_id     | A         |           2 |     NULL | NULL   |      | BTREE      |         |               |
      +--------+------------+---------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
      7 rows in set (0.00 sec)
      
      mysql> explain select rental_id,staff_id from rental where rental_date>'2005-05-25' order by rental_date,inventory_id\G;
      *************************** 1. row ***************************
                 id: 1
        select_type: SIMPLE
              table: rental
         partitions: NULL
               type: ALL
      possible_keys: rental_date
                key: NULL
            key_len: NULL
                ref: NULL
               rows: 16005
           filtered: 50.00
              Extra: Using where; Using filesort
      1 row in set, 1 warning (0.00 sec)
      
      mysql> explain select rental_id,staff_id from rental where rental_date>'2006-05-25' order by rental_date,inventory_id\G;
      *************************** 1. row ***************************
                 id: 1
        select_type: SIMPLE
              table: rental
         partitions: NULL
               type: range
      possible_keys: rental_date
                key: rental_date
            key_len: 5
                ref: NULL
               rows: 1
           filtered: 100.00
              Extra: Using index condition
      1 row in set, 1 warning (0.00 sec)
      

      查询时间为2005时,使用的是全表扫描,而2006是范围查询,前者影响行数基本为全表数据量的行数,后者仅一行,这是由于大于条件时间的数据在判断的时候没有满足条件的数据

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

    • 查询不需要的记录

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

      优化方式是在查询后面添加limit

    • 多表关联时返回全部列

      select * from actor inner join film_actor using(actor_id) inner join film using(film_id) where film.title='Academy Dinosaur';

      select actor.* from actor...;

    • 总是取出全部列

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

    • 重复查询相同的数据

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

执行过程优化

查询缓存

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

查询优化处理

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

  • 语法解析器和预处理

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

  • 查询优化器

    当语法树没有问题之后,相应的要由优化器将其转成执行计划,一条查询语句可以使用非常多的执行方式,最后都可以得到对应的结果,但是不同的执行方式带来的效率是不同的,优化器的最主要目的就是要选择最有效的执行计划

    mysql使用的是基于成本的优化器(CBO),在优化的时候会尝试预测一个查询使用某种查询计划时候的成本,并选择其中成本最小的一个

    • 可以看到这条查询语句大概需要做1104个数据页才能找到对应的数据,这是经过一系列的统计信息计算来的

      • mysql> select count(*) from film_actor;
        +----------+
        | count(*) |
        +----------+
        |     5462 |
        +----------+
        1 row in set (0.01 sec)
        # 可以看到这条查询语句大概需要做1104个数据页才能找到对应的数据,这是经过一系列的统计信息计算来的
        mysql> show status like 'last_query_cost';
        +-----------------+-------------+
        | Variable_name   | Value       |
        +-----------------+-------------+
        | Last_query_cost | 1104.399000 |
        +-----------------+-------------+
        1 row in set (0.01 sec)
        
        mysql> select count(*) from actor;
        +----------+
        | count(*) |
        +----------+
        |      200 |
        +----------+
        1 row in set (0.00 sec)
        # 数据量小查询所花费的数据
        mysql> show status like 'last_query_cost';
        +-----------------+-----------+
        | Variable_name   | Value     |
        +-----------------+-----------+
        | Last_query_cost | 40.999000 |
        +-----------------+-----------+
        1 row in set (0.00 sec)
        

        在sql执行之前可以从以下几个方面进行预估

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

      • 统计信息不准确

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

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

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

      • mysql的最优可能跟你想的不一样

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

      • mysql不考虑其他并发执行的查询

      • mysql不会考虑不受其控制的操作成本

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

    • 优化器的优化策略

      • 静态优化

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

      • 动态优化

        动态优化与查询的上下文有关(A join B,AB数据量的大小,以及存储的位置(内存/磁盘)进行分析得出是A join B还是B join A),也可能跟取值、索引对应的行数有关

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

    • 优化器的优化类型

      • 重新定义关联表的顺序

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

      • 将外(左/右)连接转化成内连接,内连接的效率要高于外连接

      • 使用等价变换规则,mysql可以使用一些等价变化来简化并规划表达式

        如:a != 4 替换为 a > 4 or a < 4

      • 优化count(),min(),max()

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

      • 预估并转化为常数表达式,当mysql检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行处理

        explain select film.film_id,film_actor.actor_id from film inner join film_actor using(film_id) where film.film_id = 1;
        -- 在两张表中都有相同列,并需要进行关联时 using([列名]) 等价于 on [表1相同列名] = [表2相同列名]
        
      • 索引覆盖扫描,当索引中的列包含所有查询中需要使用的列的时候,可以使用覆盖索引

      • 子查询优化

        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;
        
    • 关联查询:mysql的优化器大多数执行的sql都是最优的,个别情况需要自己调整执行顺序

      • -- 查看不同的顺序执行方式对查询性能的影响:
        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);
        -- 查看执行的成本:
        show status like 'last_query_cost'; 
        -- 按照自己预想的规定顺序执行:
        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);
        -- 查看执行的成本:
        show status like 'last_query_cost'; 
        
    • 排序优化

      • 排序算法

        • 两次传输排序

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

        • 单次传输排序

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

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

          mysql> show variables like '%max_length_for_sort_data%';
          +--------------------------+-------+
          | Variable_name            | Value |
          +--------------------------+-------+
          | max_length_for_sort_data | 1024  |
          +--------------------------+-------+
          1 row in set (0.00 sec)
          

优化特定类型的查询

优化count()查询

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

count([任意列/值])的查询效率都一样,无任何区别,前提:如果是列则列不允许空值,不然计算出的结果不包含NULL

  • 总有人认为myisam的count函数比较快,这是有前提条件的,只有没有任何where条件的count(*)才是比较快的

  • 使用近似值

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

  • 更复杂的优化

    一般情况下,count()需要扫描大量的行才能获取精确的数据,其实很难优化,在实际操作的时候可以考虑使用索引覆盖扫描,或者增加汇总表,或者增加外部缓存系统(每次更新的时候计算出count缓存在内存之类的方式)

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

    当表A和表B使用列C关联的时候,如果优化器的关联顺序是B、A,那么就不需要再B表的对应列上建上索引,没有用到的索引只会带来额外的负担,一般情况下来说,只需要在关联顺序中的第二个表的相应列上创建索引

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

优化子查询

子查询的优化最重要的优化建议是尽可能使用关联查询代替,子查询的结果会暂时存放在临时表中,然后再进行关联查询(相对于关联查询IO次数更多了)

优化limit分页

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

优化此类查询的最简单的办法就是尽可能地使用覆盖索引,而不是查询所有的列

(1) explain select film_id,description from film order by title limit 50,5
(2) explain select film.film_id,film.description from film inner join (select film_id from film order by title limit 50,5) as lim using(film_id);
-- 查看执行计划查看扫描的行数,(2)的查询效率优于(1)
优化union查询

mysql总是通过创建并填充临时表的方式来执行union查询,因此很多优化策略在union查询中都没法很好的使用。经常需要手工的将where、limit、order by等子句下推到各个子查询中,以便优化器可以充分利用这些条件进行优化

除非确实需要服务器消除重复的行,否则一定要使用union all,因此没有all关键字,mysql会在查询的时候给临时表加上distinct的关键字,这个操作的代价很高

推荐使用用户自定义变量

用户自定义变量是一个容易被遗忘的mysql特性,但是如果能够用好,在某些场景下可以写出非常高效的查询语句,在查询中混合使用过程化和关系话逻辑的时候,自定义变量会非常有用。
用户自定义变量是一个用来存储内容的临时容器,在连接mysql的整个过程中都存在。

  • 自定义变量的使用

    • set @one :=1
      set @min_actor :=(select min(actor_id) from actor)
      set @last_week :=current_date-interval 1 week;
      
  • 自定义变量的限制

    1. 无法使用查询缓存
    2. 不能在使用常量或者标识符的地方使用自定义变量,例如表名、列名或者limit子句
    3. 用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通信
    4. 不能显式地声明自定义变量地类型
    5. mysql优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想地方式运行
    6. 赋值符号:=的优先级非常低,所以在使用赋值表达式的时候应该明确的使用括号
    7. 使用未定义变量不会产生任何语法错误
  • 自定义变量的使用案例

    • 优化排名语句

      • 在给一个变量赋值的同时使用这个变量

        select actor_id,@rownum:=@rownum+1 as rownum from actor limit 10;
        
      • 查询获取演过最多电影的前10名演员,然后根据出演电影次数做一个排名

        select actor_id,count(*) as cnt from film_actor group by actor_id order by cnt  desc limit 10;
        
    • 避免重新查询刚刚更新的数据

      • 当需要高效的更新一条记录的时间戳,同时希望查询当前记录中存放的时间戳是什么

        • update t1 set  lastUpdated=now() where id =1;
          select lastUpdated from t1 where id =1;
          
        • update t1 set lastupdated = now() where id = 1 and @now:=now();
          select @now;
          
    • 确定取值的顺序

      • 在赋值和读取变量的时候可能是在查询的不同阶段

        • set @rownum:=0;
          select actor_id,@rownum:=@rownum+1 as cnt from actor where @rownum<=1;
          -- 因为where和select在查询的不同阶段执行,所以看到查询到两条记录,这不符合预期
          
        • set @rownum:=0;
          select actor_id,@rownum:=@rownum+1 as cnt from actor where @rownum<=1 order by first_name
          -- 当引入了order by之后,发现打印出了全部结果,这是因为order by引入了文件排序,而where条件是在文件排序操作之前取值的(通常情况下order by在最后执行,而这里引用了自定义变量需要在自定义变量前执行)
          
        • -- 解决这个问题的关键在于让变量的赋值和取值发生在执行查询的同一阶段:
          set @rownum:=0;
          select actor_id,@rownum as cnt from actor where (@rownum:=@rownum+1)<=1;
          

分区表

持续更新中…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值