SLy

永远年轻,永远憧憬,永远热爱。


  • 首页

  • 归档

  • 影集

  • 关于

  • 搜索
Thread Spring JAVA JVM 游记 计算机基础 MySQL 排序算法 PMP 情感 摄影 博客

MySQL调优

发表于 2020-12-07 | 分类于 MySQL | 0 | 阅读次数 532

性能监控

SHOW PROFILE语句

SHOW PROFILE [type [, type] ... ]
    [FOR QUERY n]
    [LIMIT row_count [OFFSET offset]]

type: {
    ALL
  | BLOCK IO
  | CONTEXT SWITCHES
  | CPU
  | IPC
  | MEMORY
  | PAGE FAULTS
  | SOURCE
  | SWAPS
}

在SHOW PROFILE和 SHOW PROFILES报表显示剖析,表示当前会话过程中执行的语句资源使用信息

注意:该SHOW PROFILE和 SHOW PROFILES语句已被弃用;希望在将来的MySQL版本中将其删除。
改用 性能模式 ;请参见第27.19.1节“使用性能模式进行查询分析”。

要控制性能分析,请使用 profiling会话变量,该变量的默认值为0(OFF)。通过设置profiling 为1或来启用性能分析ON:

mysql> SET profiling = 1;

SHOW PROFILES显示发送到服务器的最新语句的列表。列表的大小由profiling_history_size会话变量控制,该 变量的默认值为15。最大值为100。将值设置为0具有禁用分析的实际效果。

除了SHOW PROFILE和以外,所有语句均已SHOW PROFILES概要分析,因此这些语句均不会出现在概要列表中。分析格式错误的语句。例如,SHOW PROFILING是一条非法语句,如果您尝试执行它,则会发生语法错误,但它会显示在性能分析列表中。

SHOW PROFILE显示有关单个语句的详细信息。如果没有该子句,则输出与最近执行的语句有关。如果 包含,则显示语句的信息。的值对应于所显示的 值 。 FOR QUERY nFOR QUERY nSHOW PROFILEnnQuery_IDSHOW PROFILES

可以提供 该子句以将输出限制为 行。如果 给出,则可以添加以将输出行开始到完整的行集中。 LIMIT row_countrow_countLIMITOFFSET offsetoffset

默认情况下,SHOW PROFILE显示 Status和Duration 列。这些Status值类似于所State显示的 值 SHOW PROCESSLIST,尽管对于某些状态值,这两个语句的解释可能略有不同(请参见 第8.14节“检查服务器线程(进程)信息”)。

type可以指定 可选值以显示特定的其他信息类型:

  • ALL 显示所有信息

  • BLOCK IO 显示块输入和输出操作的计数

  • CONTEXT SWITCHES 显示自愿和非自愿上下文切换的计数

  • CPU 显示用户和系统的CPU使用时间

  • IPC 显示已发送和已接收邮件的计数

  • MEMORY 目前尚未实施

  • PAGE FAULTS 显示主要和次要页面错误的计数

  • SOURCE 显示源代码中的函数名称以及该函数所在文件的名称和行号

  • SWAPS 显示掉期计数

每个会话均启用分析。会话结束时,其分析信息将丢失。

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)

注意:在某些体系结构上,性能分析仅部分起作用。对于取决于getrusage() 系统调用的值,在NULL不支持该调用的系统(例如Windows)上返回。此外,性能分析是按进程而不是每个线程进行的。这意味着服务器中除您自己以外的线程上的活动可能会影响您看到的计时信息。

PERFORMANCE SCHEMA语句

以下示例演示了如何使用Performance Schema语句事件和阶段事件来检索与SHOW PROFILES和SHOW PROFILE语句提供的概要分析信息相当的数据。

该setup_actors表可用于限制主机,用户或帐户对历史事件的收集,以减少运行时开销和历史表中收集的数据量。该示例的第一步显示了如何将历史事件的收集限制为特定用户。

性能架构以毫秒为单位显示事件计时器信息(万亿分之一秒),以将计时数据标准化为标准单位。在下面的示例中,将 TIMER_WAIT值除以1000000000000,以秒为单位显示数据。值也将被截断为小数点后6位,以与SHOW PROFILESand SHOW PROFILE语句相同的格式显示数据。

  1. 将历史事件的收集限制为运行查询的用户。默认情况下, setup_actors配置为允许监视和历史事件收集所有前台线程:
mysql> SELECT * FROM performance_schema.setup_actors;
+------+------+------+---------+---------+
| HOST | USER | ROLE | ENABLED | HISTORY |
+------+------+------+---------+---------+
| %    | %    | %    | YES     | YES     |
+------+------+------+---------+---------+

更新setup_actors表中的默认行 以禁用所有前台线程的历史事件收集和监视,并插入一个新行,为运行查询的用户启用监视和历史事件收集:

mysql> UPDATE performance_schema.setup_actors
       SET ENABLED = 'NO', HISTORY = 'NO'
       WHERE HOST = '%' AND USER = '%';

mysql> INSERT INTO performance_schema.setup_actors
       (HOST,USER,ROLE,ENABLED,HISTORY)
       VALUES('localhost','test_user','%','YES','YES');

setup_actors现在 ,表中的数据应类似于以下内容:

mysql> SELECT * FROM performance_schema.setup_actors;
+-----------+-----------+------+---------+---------+
| HOST      | USER      | ROLE | ENABLED | HISTORY |
+-----------+-----------+------+---------+---------+
| %         | %         | %    | NO      | NO      |
| localhost | test_user | %    | YES     | YES     |
+-----------+-----------+------+---------+---------+
  1. 通过更新setup_instruments表确保已启用语句和阶段检测 。默认情况下,某些仪器可能已启用。
mysql> UPDATE performance_schema.setup_instruments
       SET ENABLED = 'YES', TIMED = 'YES'
       WHERE NAME LIKE '%statement/%';

mysql> UPDATE performance_schema.setup_instruments
       SET ENABLED = 'YES', TIMED = 'YES'
       WHERE NAME LIKE '%stage/%';
  1. 确保已启用events_statements_*和 events_stages_*使用者。默认情况下,某些使用者可能已启用。
mysql> UPDATE performance_schema.setup_consumers
       SET ENABLED = 'YES'
       WHERE NAME LIKE '%events_statements_%';

mysql> UPDATE performance_schema.setup_consumers
       SET ENABLED = 'YES'
       WHERE NAME LIKE '%events_stages_%';
  1. 在您要监视的用户帐户下,运行要分析的语句。例如:
mysql> SELECT * FROM employees.employees WHERE emp_no = 10001;
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date |
+--------+------------+------------+-----------+--------+------------+
|  10001 | 1953-09-02 | Georgi     | Facello   | M      | 1986-06-26 |
+--------+------------+------------+-----------+--------+------------+
  1. EVENT_ID通过查询events_statements_history_long 表来 标识语句 。此步骤类似于运行 SHOW PROFILES以标识 Query_ID。以下查询产生类似于以下内容的输出SHOW PROFILES:
mysql> SELECT EVENT_ID, TRUNCATE(TIMER_WAIT/1000000000000,6) as Duration, SQL_TEXT
       FROM performance_schema.events_statements_history_long WHERE SQL_TEXT like '%10001%';
+----------+----------+--------------------------------------------------------+
| event_id | duration | sql_text                                               |
+----------+----------+--------------------------------------------------------+
|       31 | 0.028310 | SELECT * FROM employees.employees WHERE emp_no = 10001 |
+----------+----------+--------------------------------------------------------+
  1. 查询 events_stages_history_long 表以检索语句的阶段事件。阶段使用事件嵌套链接到语句。每个阶段事件记录都有一个NESTING_EVENT_ID包含EVENT_ID父语句的的列。
mysql> SELECT event_name AS Stage, TRUNCATE(TIMER_WAIT/1000000000000,6) AS Duration
       FROM performance_schema.events_stages_history_long WHERE NESTING_EVENT_ID=31;
+--------------------------------+----------+
| Stage                          | Duration |
+--------------------------------+----------+
| stage/sql/starting             | 0.000080 |
| stage/sql/checking permissions | 0.000005 |
| stage/sql/Opening tables       | 0.027759 |
| stage/sql/init                 | 0.000052 |
| stage/sql/System lock          | 0.000009 |
| stage/sql/optimizing           | 0.000006 |
| stage/sql/statistics           | 0.000082 |
| stage/sql/preparing            | 0.000008 |
| stage/sql/executing            | 0.000000 |
| stage/sql/Sending data         | 0.000017 |
| stage/sql/end                  | 0.000001 |
| stage/sql/query end            | 0.000004 |
| stage/sql/closing tables       | 0.000006 |
| stage/sql/freeing items        | 0.000272 |
| stage/sql/cleaning up          | 0.000001 |
+--------------------------------+----------+

SHOW PROCESSLIST语句

SHOW [FULL] PROCESSLIST

MySQL进程列表指示服务器内部正在执行的一组线程当前正在执行的操作。该 SHOW PROCESSLIST声明是过程信息的一种来源。有关此声明与其他来源的比较,请参见 过程信息来源。

注意:从MySQL 8.0.22开始,SHOW PROCESSLIST基于Performance Schemaprocesslist表的替代实现 可用 ,与默认SHOW PROCESSLIST 实现不同,它不需要互斥体并且具有更好的性能特征。有关详细信息,请参见 第27.12.19.6节“过程列表”。

如果您具有PROCESS 特权,则可以查看所有线程,甚至包括那些属于其他用户的线程。否则(没有 PROCESS特权),非匿名用户有权访问有关其自己线程的信息,而其他用户则不能访问线程,并且匿名用户无权访问线程信息。

如果没有FULL关键字,则 SHOW PROCESSLIST仅显示该Info字段中每个语句的前100个字符 。

SHOW PROCESSLIST如果收到“太多的连接” 错误消息并想了解发生了什么, 该语句将非常有用。MySQL保留了一个额外的连接,供具有CONNECTION_ADMIN特权(或不建议使用的SUPER 特权)的帐户使用,以确保管理员始终能够连接并检查系统(假设您未将此特权授予所有用户)。

可以使用该KILL语句杀死线程 。请参见 第13.7.8.4节“ KILL语句”。

SHOW PROCESSLIST 输出 示例:

mysql> SHOW FULL PROCESSLIST\G
*************************** 1. row ***************************
     Id: 1
   User: system user
   Host:
     db: NULL
Command: Connect
   Time: 1030455
  State: Waiting for master to send event
   Info: NULL
*************************** 2. row ***************************
     Id: 2
   User: system user
   Host:
     db: NULL
Command: Connect
   Time: 1004
  State: Has read all relay log; waiting for the slave
         I/O thread to update it
   Info: NULL
*************************** 3. row ***************************
     Id: 3112
   User: replikator
   Host: artemis:2204
     db: NULL
Command: Binlog Dump
   Time: 2144
  State: Has sent all binlog to slave; waiting for binlog to be updated
   Info: NULL
*************************** 4. row ***************************
     Id: 3113
   User: replikator
   Host: iconnect2:45781
     db: NULL
Command: Binlog Dump
   Time: 2086
  State: Has sent all binlog to slave; waiting for binlog to be updated
   Info: NULL
*************************** 5. row ***************************
     Id: 3123
   User: stefan
   Host: localhost
     db: apollon
Command: Query
   Time: 0
  State: NULL
   Info: SHOW FULL PROCESSLIST

SHOW PROCESSLIST 输出包含以下列:

  • Id

连接标识符。该值ID与INFORMATION_SCHEMA PROCESSLIST表的列中 显示的PROCESSLIST_ID值,性能模式threads 表的列中显示的值相同,并由CONNECTION_ID()线程中的函数返回 。

  • User

发出该语句的MySQL用户。值表示 system user服务器派生的非客户端线程以内部处理任务,例如,延迟行处理程序线程或副本主机上使用的I / O或SQL线程。对于system user,在该Host列中未指定主机 。unauthenticated user“线程”是指已与客户端连接关联但尚未对其进行客户端用户身份验证的线程。 event_scheduler是指监视计划的事件的线程(请参见 第25.4节“使用事件计划程序”)。

注意:甲User的值system user是从不同的 SYSTEM_USER特权。前者指定内部线程。后者区分系统用户帐户和常规用户帐户类别(请参见第6.2.11节“帐户类别”)。

  • Host

发出该语句的客户端的主机名(除了system user,没有主机)。TCP / IP连接的主机名以 格式报告, 以便更轻松地确定哪个客户端在做什么。 host_name:client_port

  • db

线程的默认数据库,或者NULL如果未选择任何数据库 。

  • Command

线程代表客户端执行的命令类型,或者Sleep会话处于空闲状态。有关线程命令的描述,请参见 第8.14节“检查服务器线程(进程)信息”。该列的值对应于 客户端/服务器协议的 命令和 状态变量。请参见第5.1.10节“服务器状态变量”。 COM_xxxCom_xxx

  • Time

线程处于其当前状态的时间(以秒为单位)。对于副本SQL线程,该值是最后一个复制事件的时间戳与副本主机的实时之间的秒数。请参见 第17.2.3节“复制线程”。

  • State

指示线程正在执行的操作,事件或状态。有关State值的描述,请参见第8.14节“检查服务器线程(进程)信息”。

大多数状态对应于非常快速的操作。如果线程在给定状态下停留数秒钟,则可能存在需要调查的问题。

  • Info

线程正在执行的语句,或者NULL如果线程 不执行任何语句。该语句可能是发送到服务器的那条语句,或者是最内部的语句(如果该语句执行其他语句)。例如,如果一条CALL 语句执行了正在执行一条SELECT语句的存储过程,则 该 Info值将显示该 SELECT语句。


SCHEMA与数据类型优化

数据类型的优化

  1. 更小的通常更好

    应该尽量使用可以正确存储数据的最小数据类型,更小的数据类型通常更快,因为它们占用更少的磁盘、内存和CPU缓存,并且处理时需要的CPU周期更少,但是要确保没有低估需要存储的值的范围,如果无法确认哪个数据类型,就选择你认为不会超过范围的最小类型

  2. 简单就好

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

    1. 整型比字符操作代价更低,因为字符集和校对规则是字符比较比整型比较更复杂,
    2. 使用mysql自建类型而不是字符串来存储日期和时间
    3. 用整型存储IP地址
  3. 尽量避免NULL

    如果查询中包含可为NULL的列,对MySQL来说很难优化,因为可为NULL的列使得索引、索引统计和值比较都更加复杂,坦白来说,通常情况下NULL的列改为NOT NULL带来的性能提升比较小,所有没有必要将所有的表的SCHEMA进行修改,但是应该尽量避免设计成可为NULL的列

  4. 实际细则

  • 整数类型

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

  • 字符和字符串类型

    1. char长度固定,即每条数据占用等长字节空间;最大长度是255个字符,适合用在身份证号、手机号等定长字符串
    2. varchar可变程度,可以设置最大长度;最大空间是65535个字节,适合用在长度可变的属性
    3. text不设置长度,当不知道属性的最大长度时,适合用text
      按照查询速度:char>varchar>text
  • BLOB和TEXT类型

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

  • datetime和timestamp

    1. 不要使用字符串类型来存储日期时间数据
    2. 日期时间类型通常比字符串占用的存储空间小
    3. 日期时间类型在进行查找过滤时可以利用日期来进行比对
    4. 日期时间类型还有着丰富的处理函数,可以方便的对时间类型进行日期计算
    5. 使用int存储日期时间不如使用timestamp类型
  • 使用枚举代替字符串类型

    有时可以使用枚举类代替常用的字符串类型,mysql存储枚举类型会非常紧凑,会根据列表值的数据压缩到一个或两个字节中,mysql在内部会将每个值在列表中的位置保存为整数,并且在表的.frm文件中保存“数字-字符串”映射关系的查找表
    create table enum_test(e enum('fish','apple','dog') not null);
    insert into enum_test(e) values('fish'),('dog'),('apple');
    select e+0 from enum_test;

  • 特殊类型数据

    人们经常使用varchar(15)来存储ip地址,然而,它的本质是32位无符号整数不是字符串,可以使用INET_ATON()和INET_NTOA函数在这两种表示方法之间转换
    案例:
    select inet_aton('1.1.1.1')
    select inet_ntoa(16843009)

合理使用范式和反范式

  1. 范式
  • 优点

    范式化的更新通常比反范式要快
    当数据较好的范式化后,很少或者没有重复的数据
    范式化的数据比较小,可以放在内存中,操作比较快

  • 缺点

    通常需要进行关联

  1. 反范式
  • 优点

    所有的数据都在同一张表中,可以避免关联
    可以设计有效的索引

  • 缺点

    表格内的冗余较多,删除数据时候会造成表有些有用的信息丢失

  1. 注意

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

主键的选择

  1. 代理主键

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

  2. 自然主键

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

  3. 推荐使用代理主键

    它们不与业务耦合,因此更容易维护
    一个大多数表,最好是全部表,通用的键策略能够减少需要编写的源码数量,减少系统的总体拥有成本

字符集的选择

字符集直接决定了数据在MySQL中的存储编码方式,由于同样的内容使用不同字符集表示所占用的空间大小会有较大的差异,所以通过使用合适的字符集,可以帮助我们尽可能减少数据量,进而减少IO操作次数。

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

存储引擎的选择

存储引擎的对比.png

适当的数据冗余

  1. 被频繁引用且只能通过 Join 2张(或者更多)大表的方式才能得到的独立小字段。
  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

执行计划中包含的信息

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;
  1. 如果id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
explain select * from emp e where e.deptno in (select d.deptno from dept d where d.dname = 'SALES');
  1. 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;

通过索引进行优化

索引基本知识

一个索引会创建一个B+树,叶子节点存储数据。INNODB 默认会给表的主键创建主键索引,此时叶子节点存储的是表的整行数据,而当我们自行为普通列创建索引时,叶子节点存储的是主键ID值。

  1. 索引的优点

    1. 大大减少了服务器需要扫描的数据量
    2. 帮助服务器避免排序和临时表
    3. 将随机io变成顺序io
  2. 索引的用处

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

    1. 主键索引
    2. 唯一索引
    3. 普通索引
    4. 全文索引
    5. 组合索引
  4. 面试技术名词

    1. 回表
      假设给name创建一个索引,则name所属叶子节点存储的是当前name列的id值
      select * from emp where name = 1;
      因为叶子节点存储的是列的id值,则需要去索引为id的B+树里面重新查找当前id的记录数,称之为回表。

    2. 覆盖索引
      假设给name创建一个索引,则name所属叶子节点存储的是当前name列的id值
      select id from emp where name = 1;
      因为叶子节点存储的是列的id值,查询本身也只是查询id值,则不需要回表,称之为覆盖索引。

    3. 最左匹配
      假设给name+age创建组合索引,
      select * from emp where name=? and age=?;
      则左边?匹配name,右边?匹配age,如果此时
      select * from emp where age=?;
      没有name,则不会使用该组合索引去查询数据,
      select * from emp where name=?;
      但是此时name+age索引可以匹配该name语句。

    4. 索引下推
      假设给name+age创建组合索引,
      select * from emp where name=? and age=?;
      老版本取数据会从存储引擎取name=?的数据出来到server层,再在server层过滤age=?的数据,新版本则是直接从存储引擎过滤name=? and age=?的值,称之为索引下推。

  5. 索引采用的数据结构

    1. 哈希表
    2. B+树
  6. 索引匹配方式

    全值匹配
    全值匹配指的是和索引中的所有列进行匹配
    explain select * from staffs where name = 'July' and age = '23' and pos = 'dev';

    匹配最左前缀
    只匹配前面的几列
    explain select * from staffs where name = 'July' and age = '23';
    explain select * from staffs where name = 'July';

    匹配列前缀
    可以匹配某一列的值的开头部分
    explain select * from staffs where name like 'J%';
    explain select * from staffs where name like '%y';

    匹配范围值
    可以查找某一个范围的数据
    explain select * from staffs where name > 'Mary';

    精确匹配某一列并范围匹配另外一列
    可以查询第一列的全部和第二列的部分
    explain select * from staffs where name = 'July' and age > 25;

    只访问索引的查询
    查询的时候只需要访问索引,不需要访问数据行,本质上就是覆盖索引
    explain select name,age,pos from staffs where name = 'July' and age = 25 and pos = 'dev';

    示例SQL:

    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);
    

哈希索引

1. 基于哈希表的实现,只有精确匹配索引所有列的查询才有效
2. 在mysql中,只有memory的存储引擎显式支持哈希索引
3. 哈希索引自身只需存储对应的hash值,所以索引的结构十分紧凑,这让哈希索引查找的速度非常快
4. 哈希索引的限制
    1) 哈希索引只包含哈希值和行指针,而不存储字段值,索引不能使用索引中的值来避免读取行
    2) 哈希索引数据并不是按照索引值顺序存储的,所以无法进行排序
    3) 哈希索引不支持部分列匹配查找,哈希索引是使用索引列的全部内容来计算哈希值
    4) 哈希索引支持等值比较查询,也不支持任何范围查询
    5) 访问哈希索引的数据非常快,除非有很多哈希冲突,当出现哈希冲突的时候,存储引擎必须遍历链表中的所有行指针,逐行进行比较,直到找到所有符合条件的行
    6) 哈希冲突比较多的话,维护的代价也会很高

组合索引

当包含多个列作为索引,需要注意的是正确的顺序依赖于该索引的查询,同时需要考虑如何更好的满足排序和分组的需要,如下创建了组合索引(a,b,c),倒数第二条情况:当b使用范围查找的时候,会忽略后面的索引,除非b也是精确匹配的情况,才会使用c的索引
不同SQL语句使用索引情况.png

聚簇索引与非聚簇索引

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

非聚簇索引
数据文件跟索引文件分开存放

覆盖索引

基本介绍
1. 如果一个索引包含所有需要查询的字段的值,我们称之为覆盖索引
2. 不是所有类型的索引都可以称为覆盖索引,覆盖索引必须要存储索引列的值
3. 不同的存储实现覆盖索引的方式不同,不是所有的引擎都支持覆盖索引,memory不支持覆盖索引

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

优化小细节

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

  2. 尽量使用主键查询,而不是其他索引,因此主键查询不会触发回表查询

  3. 使用前缀索引

    有时候需要索引很长的字符串,这会让索引变的大且慢,通常情况下可以使用某个列开始的部分字符串,这样大大的节约索引空间,从而提高索引效率,但这会降低索引的选择性,索引的选择性是指不重复的索引值和数据表记录总数的比值,范围从1/#T到1之间。索引的选择性越高则查询效率越高,因为选择性更高的索引可以让mysql在查找的时候过滤掉更多的行。

    一般情况下某个列前缀的选择性也是足够高的,足以满足查询的性能,但是对应BLOB,TEXT,VARCHAR类型的列,必须要使用前缀索引,因为mysql不允许索引这些列的完整长度,使用该方法的诀窍在于要选择足够长的前缀以保证较高的选择性,通过又不能太长。

    基数:基数(cardinality)统计,即求一个集合中,不重复的元素个数。例如集合{1, 1, 2, 3, 4}的基数是4。

    案例演示:

    --创建数据表
    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。 
    
  4. 使用索引扫描来排序

    mysql有两种方式可以生成有序的结果:通过排序操作或者按索引顺序扫描,如果explain出来的type列的值为index,则说明mysql使用了索引扫描来做排序

    扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那么就不得不每扫描一条索引记录就得回表查询一次对应的行,这基本都是随机IO,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢

    mysql可以使用同一个索引即满足排序,又用于查找行,如果可能的话,设计索引时应该尽可能地同时满足这两种任务。

    只有当索引的列顺序和order by子句的顺序完全一致,并且所有列的排序方式都一样时,mysql才能够使用索引来对结果进行排序,如果查询需要关联多张表,则只有当order by子句引用的字段全部为第一张表时,才能使用索引做排序。order by子句和查找型查询的限制是一样的,需要满足索引的最左前缀的要求,否则,mysql都需要执行顺序操作,而无法利用索引排序。

    注意:有一种情况可以不满足索引的最左前缀的要求,就是前导列为常量的时候。如果where字句或者join字句中对这些列指定了常量。

    --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
    *************************** 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)
    
    
  5. union all,in,or都能够使用索引,但是推荐使用in

    explain select * from actor where actor_id = 1 union all select * from actor where actor_id = 2;
    explain select * from actor where actor_id in (1,2);
    explain select * from actor where actor_id = 1 or actor_id =2;
    
  6. 范围列可以用到索引

    范围条件是:<、>
    范围列可以用到索引,但是范围列后面的列无法用到索引,索引最多用于一个范围列

  7. 强制类型转换会全表扫描

    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';

  8. 更新十分频繁,数据区分度不高的字段上不宜建立索引

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

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

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

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

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

  13. 单索引字段数不允许超过5个(组合索引)

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

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

索引监控

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:从数据节点读取下一条数据的次数

查询优化

在编写快速的查询之前,需要清楚一点,真正重要的是响应时间,而且要知道在整个SQL语句的执行过程中每个步骤都花费了多长时间,要知道哪些步骤是拖垮执行效率的关键步骤,想要做到这点,必须要知道查询的生命周期,然后进行优化,不同的应用场景有不同的优化方式,不要一概而论,具体情况具体分析。

查询慢的原因

  1. 网络
  2. CPU
  3. IO
  4. 上下文切换
  5. 系统调用
  6. 生成统计信息
  7. 锁等待时间

优化数据访问

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

    确认应用程序是否在检索大量超过需要的数据
    确认mysql服务器层是否在分析大量超过需要的数据行

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

    查询不需要的记录
    我们常常会误以为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 *,同时建议给连接表增加别名

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

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

执行过程的优化

  1. 查询缓存

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

  2. 查询优化处理

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

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

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

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

      1). 可以看到这条查询语句大概需要做1104个数据页才能找到对应的数据,这是经过一系列的统计信息计算来的
      select count(*) from film_actor;
      show status like 'last_query_cost';

      每个表或者索引的页面个数
      索引的基数
      索引和数据行的长度
      索引的分布情况

      2). 在很多情况下mysql会选择错误的执行计划,原因如下:
      统计信息不准确
      执行计划的成本估算不等同于实际执行的成本
      mysql的最优可能跟你想的不一样
      mysql不考虑其他并发执行的查询
      mysql不会考虑不受其控制的操作成本

      3). 优化器的优化策略
      静态优化 - 直接对解析树进行分析,并完成优化
      动态优化 - 动态优化与查询的上下文有关,也可能跟取值、索引对应的行数有关
      mysql对查询的静态优化只需要一次,但对动态优化在每次执行时都需要重新评估

      4). 优化器的优化类型
      重新定义关联表的顺序
      将外连接转化成内连接,内连接的效率要高于外连接
      使用等价变换规则,mysql可以使用一些等价变化来简化并规划表达式
      优化count(),min(),max()
      预估并转化为常数表达式,当mysql检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行处理
      索引覆盖扫描,当索引中的列包含所有查询中需要使用的列的时候,可以使用覆盖索引
      子查询优化
      等值传播

      5). 关联查询
      mysql的关联查询很重要,但其实关联查询执行的策略比较简单:mysql对任何关联都执行嵌套循环关联操作,即mysql先在一张表中循环取出单条数据,然后再嵌套到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列。mysql会尝试再最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行之后,mysql返回到上一层次关联表,看是否能够找到更多的匹配记录,以此类推迭代执行。整体的思路如此,但是要注意实际的执行过程中有多个变种形式:

      6). 排序优化
      排序的算法

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

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

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

Simple NestedLoop Join.png
Index NestedLoop Join.png
Block NestedLoop Join.png

优化特定类型的查询

  1. 优化count()查询

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

  2. 优化关联查询

    确保on或者using子句中的列上有索引,在创建索引的时候就要考虑到关联的顺序
    确保任何的group by和order by中的表达式只涉及到一个表中的列,这样mysql才有可能使用索引来优化这个过程

  3. 优化子查询

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

  4. 优化limit分页

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

    limit 优化:
    select * from student limit 1000000,5;
    select * from student a join(select id from student b limit 1000000,5) on a.id = b.id;
    
  5. 优化union查询

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

  6. 推荐使用用户自定义变量

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


分区表

分库分表:垂直切分和水平切分

  • 垂直切分:将数据库表按模块或业务划分,分别部署在不同的服务器,并将请求分发到对应的服务器上即可。适用于分布式场景。
  • 水平切分:将数据库数据按照量级划分,切分成相同结构的几张表。适用于单表数据量很大的场景。例如:1 ~ 1000000放在表user1, 1000000 ~ 2000000放在表user2。

分区表的含义更类似与水平切分这一概念。

参考:https://www.cnblogs.com/leeego-123/p/12094728.html

分区表的应用场景

  1. 表非常大以至于无法全部都放在内存中,或者只在表的最后部分有热点数据,其他均是历史数据
  2. 分区表的数据更容易维护
  3. 分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备
  4. 可以使用分区表来避免某些特殊的瓶颈
  5. 可以备份和恢复独立的分区

分区表的限制

  1. 一个表最多只能有1024个分区,在5.7版本的时候可以支持8196个分区
  2. 在早期的mysql中,分区表达式必须是整数或者是返回整数的表达式,在mysql5.5中,某些场景可以直接使用列来进行分区
  3. 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来
  4. 分区表无法使用外键约束

分区表的原理

分区表由多个相关的底层表实现,这个底层表也是由句柄对象标识,我们可以直接访问各个分区。存储引擎管理分区的各个底层表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引知识在各个底层表上各自加上一个完全相同的索引。从存储引擎的角度来看,底层表和普通表没有任何不同,存储引擎也无须知道这是一个普通表还是一个分区表的一部分。

分区表的操作按照以下的操作逻辑进行:

select查询

当查询一个分区表的时候,分区层先打开并锁住所有的底层表,优化器先判断是否可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据

insert操作

当写入一条记录的时候,分区层先打开并锁住所有的底层表,然后确定哪个分区接受这条记录,再将记录写入对应底层表

delete操作

当删除一条记录时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区,最后对相应底层表进行删除操作

update操作

当更新一条记录时,分区层先打开并锁住所有的底层表,mysql先确定需要更新的记录再哪个分区,然后取出数据并更新,再判断更新后的数据应该再哪个分区,最后对底层表进行写入操作,并对源数据所在的底层表进行删除操作

有些操作时支持过滤的,例如,当删除一条记录时,MySQL需要先找到这条记录,如果where条件恰好和分区表达式匹配,就可以将所有不包含这条记录的分区都过滤掉,这对update同样有效。如果是insert操作,则本身就是只命中一个分区,其他分区都会被过滤掉。mysql先确定这条记录属于哪个分区,再将记录写入对应得曾分区表,无须对任何其他分区进行操作

虽然每个操作都会“先打开并锁住所有的底层表”,但这并不是说分区表在处理过程中是锁住全表的,如果存储引擎能够自己实现行级锁,例如innodb,则会在分区层释放对应表锁。

分区表的类型

  1. 范围分区 [RANGE PARTITIONING]

    根据列值在给定范围内将行分配给分区

    CREATE TABLE employees (
    id INT NOT NULL,
    fname VARCHAR(30),
    lname VARCHAR(30),
    hired DATE NOT NULL DEFAULT '1970-01-01',
    separated DATE NOT NULL DEFAULT '9999-12-31',
    job_code INT NOT NULL,
    store_id INT NOT NULL
    )
    PARTITION BY RANGE (store_id) (
        PARTITION p0 VALUES LESS THAN (6),
        PARTITION p1 VALUES LESS THAN (11),
        PARTITION p2 VALUES LESS THAN (16),
        PARTITION p3 VALUES LESS THAN MAXVALUE
    );
    

    官网资料:
    https://dev.mysql.com/doc/refman/5.7/en/partitioning-range.html

  2. 列表分区 [LIST PARTITIONING]

    类似于按range分区,区别在于list分区是基于列值匹配一个离散值集合中的某个值来进行选择

    CREATE TABLE employees (
    id INT NOT NULL,
    fname VARCHAR(30),
    lname VARCHAR(30),
    hired DATE NOT NULL DEFAULT '1970-01-01',
    separated DATE NOT NULL DEFAULT '9999-12-31',
    job_code INT,
    store_id INT
    )
    PARTITION BY LIST(store_id) (
        PARTITION pNorth VALUES IN (3,5,6,9,17),
        PARTITION pEast VALUES IN (1,2,10,11,19,20),
        PARTITION pWest VALUES IN (4,12,13,14,18),
        PARTITION pCentral VALUES IN (7,8,15,16)
    );
    

    官网资料:
    https://dev.mysql.com/doc/refman/5.7/en/partitioning-list.html

  3. 列分区 [COLUMNS PARTITIONING]

    • RANGE COLUMNS PARTITIONING
    • LIST COLUMNS PARTITIONING

    mysql从5.5开始支持column分区,可以认为i是range和list的升级版,在5.5之后,可以使用column分区替代range和list,但是column分区只接受普通列不接受表达式

    官网资料:
    https://dev.mysql.com/doc/refman/5.7/en/partitioning-columns.html

  4. hash分区 [HASH PARTITIONING]

    基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含myql中有效的、产生非负整数值的任何表达式

    CREATE TABLE employees (
    id INT NOT NULL,
    fname VARCHAR(30),
    lname VARCHAR(30),
    hired DATE NOT NULL DEFAULT '1970-01-01',
    separated DATE NOT NULL DEFAULT '9999-12-31',
    job_code INT,
    store_id INT
    )
    PARTITION BY HASH( YEAR(hired) )
    PARTITIONS 4;
    

    官网资料:
    https://dev.mysql.com/doc/refman/5.7/en/partitioning-hash.html

  5. key分区 [KEY PARTITIONING]

    类似于hash分区,区别在于key分区只支持一列或多列,且mysql服务器提供其自身的哈希函数,必须有一列或多列包含整数值

    CREATE TABLE tm1 (
    s1 CHAR(32) PRIMARY KEY
    )
    PARTITION BY KEY(s1)
    PARTITIONS 10;
    

    官网资料:
    https://dev.mysql.com/doc/refman/5.7/en/partitioning-key.html

  6. 子分区 [SUBPARTITIONING]

    在分区的基础之上,再进行分区后存储

    CREATE TABLE ts (id INT, purchased DATE)
    PARTITION BY RANGE( YEAR(purchased) )
    SUBPARTITION BY HASH( TO_DAYS(purchased) ) (
        PARTITION p0 VALUES LESS THAN (1990) (
            SUBPARTITION s0,
            SUBPARTITION s1
        ),
        PARTITION p1 VALUES LESS THAN (2000) (
            SUBPARTITION s2,
            SUBPARTITION s3
        ),
        PARTITION p2 VALUES LESS THAN MAXVALUE (
            SUBPARTITION s4,
            SUBPARTITION s5
        )
    );
    

    官网资料:
    https://dev.mysql.com/doc/refman/5.7/en/partitioning-subpartitions.html

如何使用分区表

如果需要从非常大的表中查询出某一段时间的记录,而这张表中包含很多年的历史数据,数据是按照时间排序的,此时应该如何查询数据呢?
因为数据量巨大,肯定不能在每次查询的时候都扫描全表。考虑到索引在空间和维护上的消耗,也不希望使用索引,即使使用索引,会发现会产生大量的碎片,还会产生大量的随机IO,但是当数据量超大的时候,索引也就无法起作用了,此时可以考虑使用分区来进行解决

在使用分区表的时候需要注意的问题

  1. null值会使分区过滤无效
  2. 分区列和索引列不匹配,会导致查询无法进行分区过滤
  3. 选择分区的成本可能很高
  4. 打开并锁住所有底层表的成本可能很高
  5. 维护分区的成本可能很高

在使用分区表的时候需要注意的问题

  1. null值会使分区过滤无效
  2. 分区列和索引列不匹配,会导致查询无法进行分区过滤
  3. 选择分区的成本可能很高
  4. 打开并锁住所有底层表的成本可能很高
  5. 维护分区的成本可能很高

分库、分表、分区、读写分离、主从复制

分库

使用多台数据库,eg:订单表使用A库,库存表使用B库

分表

拆分表结构,eg:将订单表结构拆分成两个不同的表(太复杂)

分区

拆分表数据,eg:将订单表数据拆分成两张表,A表(1 ~ 10000)B表(10001 ~ 20000)

读写分离

  • 一主一从

  • 一主多从

    提高读性能

  • 多主一从

    将多个mysql数据库备份到一台存储性能比较好的服务器上

  • 双主复制

    互做主从复制,每个服务器既是master,又是另外一台服务器的slave。任何一方所做的变更,都会通过复制应用到另外一方的数据库中。

  • 级联复制

    部分slave的数据同步不连接主节点,而是连接从节点。不仅可以缓解主节点的压力,并且对数据一致性没有负面影响。

主从复制

主从复制涉及到三个线程,一个运行在主节点(log dump thread),其余两个(I/O thread, SQL thread)运行在从节点。

当从节点连接主节点时,主节点会创建一个log dump线程,用于发送bin-log的内容。在读取bin-log中的操作时,此线程会对主节点上的bin-log加锁。一旦该事件被线程读取完之后,该锁会被释放,即使在该事件完全发送到从库之前,该锁也会被释放。

当从节点上执行start slave命令之后,从节点会创建一个I/O线程用来连接主节点,请求主库中更新的bin-log。I/O线程接收到主节点log dump进程发来的更新之后,保存在本地relay-log中。
SQL线程负责读取relay log中的内容,解析成具体的操作并执行,最终保证主从数据的一致性。

对于每一个主从连接,都需要三个进程来完成。当主节点有多个从节点时,主节点会为每一个当前连接的从节点建一个binary log dump 进程,而每个从节点都有自己的I/O进程,SQL进程。

主从复制


详细文档参见:
MySQL - XMind

  • 本文作者: SLy
  • 本文链接: https://sly.plus/archives/mysql
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 4.0 许可协议。转载请注明出处!
# Thread # Spring # JAVA # JVM # 游记 # 计算机基础 # MySQL # 排序算法 # PMP # 情感 # 摄影 # 博客
希尔排序
计算机基础
  • 文章目录
  • 站点概览
SLy

SLy

永远年轻,永远憧憬,永远热爱。

37 日志
12 分类
12 标签
RSS
Github E-mail
Creative Commons
© 2020 — 2025 SLy