MySQL

前言

1、MySQL

  • MySQL 最流行的关系型数据库管理系统,在 WEB 应用方面 MySQL 是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。

  • MySQL 是一种关联数据库管理系统,关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。

  • MySQL 所使用的 SQL 语言是用于访问数据库的最常用标准化语言。

2、配置

3、存储引擎

  • 和大多数数据库不同,MySQL 中有一个存储引擎的概念,针对不同的存储需求可以选择最优的存储引擎。

  • MySQL 默认支持多种存储引擎,以适用于不同领域的数据库应用需要,用户可以通过选择使用不同的存储引擎提高应用的效率,提供灵活的存储,用户甚至可以按照自己的需要定制和使用自己的存储引擎,以实现最大程度的可定制性。

  • MySQL 5.0 支持的存储引擎包括 MyISAM、InnoDB、BDB、MEMORY、MERGE、EXAMPLE、NDB Cluster、ARCHIVE、CSV、BLACKHOLE、FEDERATED 等,其中 InnoDB 和 BDB 提供事务安全表,其他存储引擎都是非事务安全表。

  • 创建新表时如果不指定存储引擎,那么系统就会使用默认存储引擎,MySQL 5.5 之前的默认存储引擎是 MyISAM,5.5 之后改为了 InnoDB。

  • MyISAM 不支持事务、也不支持外键,其优势是访问的速度快,对事务完整性没有要求或者以 SELECT、INSERT 为主的应用基本上都可以使用这个引擎来创建表。

  • InnoDB 存储引擎提供了具有提交、回滚和崩溃恢复能力的事务安全。但是对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引。

  • MyISAM 存储引擎,最大表尺寸增加到了 65536TB(2567 – 1 字节)。由于允许的表尺寸更大,MySQL 数据库的最大有效表尺寸通常是由操作系统对文件大小的限制决定的,而不是由 MySQL 内部限制决定的。

  • InnoDB 存储引擎将 InnoDB 表保存在一个表空间内,该表空间可由数个文件创建。这样,表的大小就能超过单独文件的最大容量。表空间可包括原始磁盘分区,从而使得很大的表成为可能。表空间的最大容量为 64TB。

  • MySQL 单表大约在 2 千万条记录(4G)下能够良好运行,经过数据库的优化后 5 千万条记录(10G)下运行良好。

4、索引

  • 索引是数据库中用来提高性能的最常用工具。所有 MySQL 列类型都可以被索引,对相关列使用索引是提高 select 操作性能的最佳途径。

  • 索引用于快速找出在某个列中有一特定值的行。如果不使用索引,MySQL 必须从第 1 条记录开始然后读完整个表直到找出相关的行,表越大,花费的时间越多。

  • 根据存储引擎可以定义每个表的最大索引数和最大索引长度,每种存储引擎对每个表至少支持 16 个索引,总索引长度至少为 256 字节,大多数存储引擎有更高的限制。

  • 大多数 MySQL 索引(如 primary key、unique、index 和 fulltext 等)在 btree 中存储。只是空间列类型的索引使用 rtree,并且 MEMORY 表还支持 hash 索引。

  • MyISAM 和 InnoDB 存储引擎的表默认创建的都是 btree 索引。MEMORY 存储引擎默认创建 hash 索引,但也支持 btree 索引。只有 MyISAM 存储引擎支持空间类型索引,且索引的字段必须是非空的。

  • 对于 btree 索引当使用 >、<、>=、<=、between、!= 或者 <>,或者 LIKE ‘pattern’ (其中 ‘pattern’ 不以通配符开始)操作符时,都可以使用相关列上的索引。

  • 对于 hash 索引使用以上操作符时具有一定的限制。

    • 只用于使用 = 或 <=> 操作符的等式比较。
    • 优化器不能使用 hash 索引来加速 order by 操作。
    • MySQL 不能确定在两个值之间大约有多少行。如果将一个 MyISAM 表改为 hash 索引的 MEMORY 表,会影响一些查询的执行效率。
    • 只能使用整个关键字来搜索一行。
  • 当对索引字段进行范围查询的时候,只有 btree 索引可以通过索引访问,而 hash 索引实际上是全表扫描的。

5、常见问题

5.1 空密码登录

  • 网络上,广为流传这样的结论,MySQL 的默认账号是 root,默认的密码是空。当我点击 OK 的时候,提示 “登录访问被拒绝”。Google 一下,找到一个解决方案:通过 mysqld_safe 指令使得 MySQL 不需要验证就可以登录,登录成功之后,使用 MySQL workbench 修改用户密码。

  • Mac 上 MySQL root 密码忘记或权限错误的解决办法见 MySQL 数据库配置章节。

5.2 MySQL 无法启动

  • macOS 的系统升级或其他原因可能会导致 MySQL 启动或开机自动运行时在 MySQL 操作面板上会有如下提示:

    Warning:The /usr/local/mysql/data directory is not owned by the 'mysql' or '_mysql'

  • 这应该是某种情况下导致 /usr/local/mysql/data 的宿主发生了改变,只需要在终端运行下列命令即可:

    1
    $ sudo chown -R  _mysql:wheel  /usr/local/mysql/data

6、MySQL 调优

  • 数据库性能的影响因素很多,包括:
影响因素 说明 解决方案
SQL 查询速度 存在慢查询 SQL。 通过慢查日志获取存在性能问题的 SQL。
实时获取存在性能问题的 SQL(information_schema.PROCESSLIST 或 explain 语句)。
修改 SQL 或者尽量让 SQL 走索引。
大表和大事务 MySQL 单表行数超过千万行或单表数据超过 10GB 就会影响性能。 对于大表的数据修改最好要分批处理。
修改大表的表结构。
对于存在 not in 和 <> 这样的子查询语句改写成关联查询。
对于像商品的评论数这样的数据使用汇总表的方式进行优化。
数据库存储引擎
数据库参数配置 配置不当会直接影响性能。
服务器硬件
网卡流量
磁盘 IO 使用更快的磁盘设备,比如 RAID 卡、SSD、Fusion-IO 等;
检查是否存在其他大量消耗磁盘性能的计划任务,如果存在,则调整计划任务,做好磁盘维护。
  • 注意:MySQL 5.7 是单线程的服务,并不支持多 cpu 并发运算,意味着每条 sql 只能用到 1 个 cpu。

  • 如何定位并优化慢查询 SQL?具体场景具体分析,大致思路如下:

    • 根据慢查询日志定位慢查询 SQL;
    • 使用 explain 等工具分析 SQL;
    • 修改 SQL 或者尽量让 SQL 走索引。
  • SQL 查询优化,索引优化,库表结构优化需要齐头并进。

    • 在进行库表结构设计时,我们要考虑到以后的查询要如何的使用这些表。
    • 同样,编写 SQL 语句的时候也要考虑到如何使用到目前已经存在的索引。
    • 或是如何增加新的索引才能提高查询的性能。
  • 想要对存在性能问题的查询进行优化,需要能够找到这些查询,下面先看下如何获取有性能问题的 SQL。

6.1 SQL 调优

  • 获取有性能问题的 SQL 的两种方法:

    • 通过慢查日志获取存在性能问题的 SQL。
    • 实时获取存在性能问题的 SQL。

6.1.1 根据慢查询日志定位慢查询 SQL

  • MySQL 慢查询日志是一种性能开销比较低的解决方案,主要性能开销在磁盘 IO 和存储日志所需要的磁盘空间。对于磁盘 IO 来说,由于写日志是顺序存储,开销基本上忽略不计,所以主要需要关注的还是磁盘空间。

  • MySQL 提供了以下参数用于控制慢查询日志:

参数 说明
slow_query_log 是否启动慢查询日志,默认不启动,on 启动;
slow_query_log_file 指定慢查询日志的存储路径及文件,默认保存在 MySQL 的数据目录中;
long_query_time 指定记录慢查询日志 SQL 执行时间的阈值,单位秒,默认 10,对于一个繁忙的系统,改为 0.001 比较合适;
log_queries_not_using_indexes 是否记录未使用索引的 SQL;
  • 开启慢查询日志有两种方式,第一种是通过配置 /etc/my.cnf 文件开启,是永久性的,第二种是通过设置全局变量开启,MySQL 重启后会失效。

  • 设置全局变量的 SQL 如下:

    1
    2
    set global slow_query_log = on;
    set global long_query_time = 1;
  • 和二进制日志不同,慢查询日志会记录所有符合条件的 SQL,包括查询语句、数据修改语句、已经回滚的 SQL。

  • 慢查询日志中记录的内容:

    1
    2
    3
    4
    5
    6
    # Query_time: 0.000220                // 执行时间,可以精确到毫秒,220 毫秒
    # Lock_time: 0.000120 // 所使用锁的时间,可以精确到毫秒
    # Rows_sent: 1 // 返回的数据行数
    # Rows_examined: 1 // 扫描的数据行数
    SET timestamp=1538323200; // 执行 sql 的时间戳
    SELECT c FROM test1 WHERE id =100; // sql
  • 通常情况下,在一个繁忙的系统中,短时间内就可以产生几个 G 的慢查询日志,人工检查几乎是不可能的,为了快速分析慢查询日志,必须借助相关的工具。

  • 常用的慢查询日志工具:

    • 1、mysqldumpslow:一个常用的,MySQL 官方提供的慢查询日志分析工具,随着 MySQL 服务器的安装而被安装。可以汇总除查询条件外其他完全相同的 SQL,并将分析结果按照参数中所指定的顺序输出。

    • 2、pt-query-digest:用于分析 MySQL 慢查询的一个工具。

6.1.2 实时获取性能问题 SQL

  • 为了更加及时的发现当前的性能问题,我们还可以通过实时的方法来获取有性能问题的 SQL。

  • 1)最方便的一种方法就是利用 MySQL information_schema 数据库下的 PROCESSLIST 表来实现实时的发现性能问题 SQL。例如下面这条 SQL 表示查询出当前服务器中执行时间超过 1 秒的 SQL:

    1
    SELECT id, user, host, db, command, time, state, info FROM information_schema.PROCESSLIST WHERE TIME>=1
    • 然后我们可以通过脚本周期性的来执行这条 SQL,实时的发现哪些 SQL 执行的是比较慢的。
  • 2)另一种方法是使用 explain 分析 SQL。explain 可以帮助我们分析 select 语句,让我们知道查询效率低下的原因。这个关键字一般放在 select 语句的前面,用于描述 MySQL 如何执行查询操作以及 MySQL 成功返回结果集需要执行的行数,执行会输出一些 explain 的字段。例如:

    1
    EXPLAIN SELECT name FROM person_info_large order by name desc;
    • 需要注意的是,执行 explain 并不会真正的执行 SQL,而是对 SQL 做了一些分析,速度非常快。

    • explain 关键字段:

      1
      2
      3
      4
      id:表示了 MySQL 的执行顺序,id 越大越先执行;
      type:表示 MySQL 找到数据行的方式;
      key:实际使用的索引;
      Extra:额外信息。
    • type 字段的返回值,性能从最优到最差:

      • system -> const -> eq_ref -> ref -> fulltext -> ref_or_null -> index_merge -> unique_subquery -> index_subquery -> range -> index -> all

      • index 和 all 表示本次查询走的是全表扫描。如果 type 值是这两个,表明 SQL 是需要优化的。

    • Extra 中出现了以下两种意味着 MySQL 根本不能使用索引,效率会受到重大影响,应尽可能对此进行优化:

      • Using filesort:表示 MySQL 会对结果使用一个外部索引排序,而不是从表里按索引次序读到相关内容。可能在内存或者磁盘上进行排序。MySQL 中无法利用索引完成的排序操作称为 “文件排序”。

      • Using temporary:表示 MySQL 在对查询结果排序时使用的临时表,常见于排序 order by 和分组查询 group by。

6.1.3 修改 SQL 或者尽量让 SQL 走索引

  • 例如为上面 SQL 中的 name 加索引:

    1
    ALTER TABLE person_info_large add index idx_name(name);
  • MySQL 的查询优化器。

6.2 SQL 的解析预处理及生成执行计划

  • 找到了那些查询存在性能问题的 SQL,那么下面我们就看下,为什么这些 SQL 会存在性能问题?

  • 为了搞清楚这个问题,我们先来看下 MySQL 服务器处理一条 SQL 请求所需要经历的步骤都有哪些:

    • 客户端通过 MySQL 的接口发送 SQL 请求给服务器,这一步通常不会影响查询性能;
    • MySQL 服务器检查是否可以在查询缓存中命中该 SQL,如果命中,则立即返回存储在缓存中的结果,否则进入下一阶段;
    • MySQL 服务器进行 SQL 解析,预处理,再由 SQL 优化器生成对应的执行计划;
    • 根据执行计划,调用存储引擎 API 来查询数据;
    • 将结果返回给客户端。
  • 这就是 MySQL 服务器处理查询请求的整个过程。在第二到第五步,都有可能对查询的响应速度造成影响,下面来分别看下这些过程可能对查询的响应速度有影响的因素都有些什么:

  • 在解析查询语句前,如果查询缓存是打开的,那么 MySQL 优先检查这个查询是否命中查询缓存中的数据,这个检查是通过一个对大小写敏感的 Hash 查找实现的。由于 Hash 查找只能进行全值匹配,所以请求的查询和缓存中的查询就算只有一个字节的不同,那么也不会匹配到缓存中的结果,这种情况下,查询就会进入到下一阶段处理。如果正好命中查询缓存,在返回查询结果之前,MySQL 就会检查用户权限,也是无需解析 SQL 语句的,因为在查询缓存中,已经存放了当前查询所需要访问的表的信息,如果权限没有问题,MySQL 会跳过所有的其他阶段,直接从缓存中拿到结果,并返回给客户端,这种情况下查询是不会被解析的,也不会生成查询计划,不会被执行。

  • 可以发现,从查询缓存中直接返回结果并不容易。

  • 查询缓存对 SQL 性能的影响:

    • 如果查询缓存,一旦数据更新,都要对缓存中数据进行刷新,影响性能;
    • 每次在查询缓存中检查 SQL 是否被命中,都要对缓存加锁,影响性能;
  • 对于一个读写频繁的系统来说,查询缓存很可能会降低查询处理的效率。所以在这种情况下建议大家不要使用查询缓存。

  • 对查询缓存影响的一些系统参数:

参数 说明
query_cache_type 设置查询缓存是否可用,可以设置为ON、OFF、DEMAND,DEMAND表示只有在查询语句中使用 SQL_CACHE 和 SQL_NO_CACHE 来控制是否需要缓存。
query_cache_size 设置查询缓存的内存大小,必须是1024字节的整数倍。
query_cache_limit 设置查询缓存可用存储的最大值,如果知道很大不会被缓存,可以在查询上加上 SQL_NO_CACHE 提高效率。
query_cache_wlock_invalidate 设置数据表被锁后是否返回缓存中的数据,默认关闭。
query_cache_min_res_unit 设置查询缓存分配的内存块最小单位。
  • 对于一个读写频繁的系统来说,可以把 query_cache_type 设置为 OFF,并且把 query_cache_size 设置为 0。

  • 当查询缓存未启用或者未命中则会进入下一阶段,也就是需要将一个 SQL 转换成一个执行计划,MySQL 再依据这个执行计划和存储引擎进行交互,这个阶段包括了多个子过程:解析 SQL,预处理,优化 SQL 执行计划。在这个过程中,出现任何错误,比如语法错误等,都有可能中止查询的过程。

  • 在语法解析阶段,主要是通过关键字对 MySQL 语句进行解析,并生成一棵对应的 “解析树”。这一阶段,MySQL 解析器将使用 MySQL 语法规则验证和解析查询,包括检查语法是否使用了正确的关键字、关键字的顺序是否正确等。

  • 预处理阶段则是根据 MySQL 规则进一步检查解析树是否合法,比如检查查询中所涉及的表和数据列是否存在、检查名字或别名是否存在歧义等。

  • 如果语法检查全部都通过了,查询优化器就可以生成查询计划了。

  • 会造成 MySQL 生成错误的执行计划的原因:

    • 统计信息不准确;
    • 执行计划中的成本估算不等同于实际的执行计划的成本;
    • MySQL 查询优化器所认为的最优可能与你所认为的最优不一样;
    • MySQL 从不考虑其他并发的查询,这可能会影响当前查询的速度;
    • MySQL 有时候也会基于一些固定的规则来生成执行计划;
    • MySQL 不会考虑不受其控制的成本,例如存储过程、用户自定义的函数等。
    • MySQL 的查询优化器可以优化的 SQL 类型:
  • 重新定义表的关联顺序,优化器会根据统计信息来决定表的关联顺序;

    • 将外连接转化为内连接,比如 where 条件和库表结构都可能让一个外连接等价于内连接;
    • 使用等价变换规则,比如 (5=5 and a>5) 将被改写为 a>5;
    • 利用索引和列是否为空来优化 count()、min() 和 max() 等聚合函数;
    • 将一个表达式转换为常数表达式;
    • 使用等价变换规则,比如覆盖索引,当 MySQL 查询优化器发现索引中的列包含所有查询中所需要的信息的时候,MySQL 就能使用索引返回需要的数据;
    • 子查询优化,比如把子查询转换为关联查询,减少表的查询次数;
    • 提前终止查询;
    • 对 in() 条件进行优化。
  • 以上这些就是 MySQL 查询优化器可以自动对查询所做的一些优化。经过查询优化器改写后的 SQL,查询优化器会对其生成一个 SQL 执行计划,然后 MySQL 服务器就可以根据执行计划调用存储引擎的 API,通过存储引擎获取数据了。

6.3 确定查询处理各个阶段的耗时

  • SQL 查询优化的主要目的就是减少查询所消耗的时间,加快查询的响应速度。下面来介绍如何度量查询处理各个阶段所消耗的时间。

  • 对于一个存在性能问题的 SQL 来说,必须知道在查询的哪一阶段消耗的时间最多,然后才能有针对性的进行优化。度量查询处理各个阶段所消耗的时间,常用的方法有两种:

    • 使用 profile;
    • 使用 performance_schema;

6.4 特定 SQL 的查询优化

  • 前面介绍的方法,已经可以获取一个存在性能问题的 SQL 和获取一个 SQL 在执行的各个阶段所消耗的时间了。得到这些信息后,我们就可以针对性的对 SQL 进行优化了,下面举几个对特定 SQL 优化的案例:

6.4.1 大表的更新和删除

  • 对于大表的数据修改最好要分批处理,比如我们要在一个 1000 万行记录的表中删除/更新 100 万行记录,那么我们最好分多个批次进行删除/更新,一次只删除/更新 5000 行记录,避免长时间的阻塞,并且为了减少对主从复制带来的压力,每次删除/修改数据后需要暂停几秒。这里提供一个可以完成这样工作的 MySQL 存储过程的实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    DELIMITER $$
    USE 'db_name'$$
    DROP PROCEDURE IF EXISTS 'p_delete_rows'$$
    CREATE DEFINER='mysql'@'127.0.0.1' PROCEDURE 'p_delete_rows'()
    BEGIN
    DECLARE v_rows INT;
    SET v_rows = 1;
    WHERE v_rows > 0
    DO
    DELETE FROM table_name WHERE id >= 9000 AND id <= 290000 LIMIT 5000;
    SELECT ROW_COUNT() INTO v_rows;
    SELECT SLEEP(5);
    END WHERE;
    END$$
    DELIMITER;
  • 大家可以根据自己的情况来修改这个存储过程,或者使用自己熟悉的开发语言实现这个处理过程,使用这个存储过程只需要修改 DELETE FROM table_name WHERE id >= 9000 AND id <= 290000 LIMIT 5000; 部分的内容即可。

6.4.2 修改大表的表结构

  • 对于 InnoDB 存储引擎来说,对表中的列的字段类型进行修改或者改变字段的宽度时还是会锁表,同时也无法解决主从数据库延迟的问题。

  • 解决方案:

  • 在主服务器上建立新表,新表的结构就是修改之后的结构,再把老表的数据导入到新表中,并且在老表上建立一系列的触发器,把老表数据的修改同步更新到新表中,当老表和新表的数据同步后,再对老表加一个排它锁,然后重新命名新表为老表的名字,最好删除重命名的老表,这样就完成了大表表结构修改的工作。这样处理的好处是可以尽量减少主从延迟,以及在重命名之前不需要加任何的锁,只需要在重命名的时候加一个短暂的锁,这对应用通常是无影响的,缺点就是操作比较复杂。好在有工具可以帮我们实行这个过程,这个工具同样是 percona 公司 MySQL 工具集中的一个,叫做 pt-online-schema-change:

    1
    2
    3
    4
    pt-online-schema-change \
    --alter="MODIFY c VARCHAR(150) NOT NULL DEFAULT ''" \
    --user=root --password=password D=db_name,t=table_name \
    --charset=utf8 --execute
  • 这个命令就是把 db_name 数据库下的 table_name 表中 c 列的宽度改为 VARCHAR(150)。

6.4.3 优化 not in 和 <> 查询

  • 对于存在 not in 和 <> 这样的子查询语句改写成关联查询。

  • MySQL 查询优化器可以自动的把一些子查询优化为关联查询,但是对于存在 not in 和 <> 这样的子查询语句来说,就无法进行自动优化了,这就造成了会循环多次来查找子表来确认是否满足过滤条件,如果子查询恰好是一个很大的表的话,这样做的效率会非常低,所以我们在进行 SQL 开发时,最好把这类查询自行改写成关联查询。

  • 改写前:

    1
    2
    3
    4
    SELECT id, name, email 
    FROM customer
    WHERE id
    NOT IN(SELECT id FROM payment)
  • 优化改写后:

    1
    2
    3
    4
    SELECT a.id, a.name, a.email 
    FROM customer a
    LEFT JOIN payment b ON a.id=b.id
    WHERE b.id IS NULL
  • 使用 LEFT JOIN 关联替代了 NOT IN 过滤,这样避免了对 payment 表进行多次查询,这是一种非常常用的对 NOT IN 的优化方式。

6.4.4 使用汇总表优化查询

  • 最常见的就是商品的评论数,如果我们在用户访问页面时,实时的访问商品的评论数,通常来说,查询的 SQL 会类似于下面这个样子:

    1
    SELECT COUNT(*) FROM product_comment WHERE product_id = 10001;
  • 这个 SQL 就是统计出所有 product_id = 10001 的评论,假设评论表中有上亿条记录,那么这个 SQL 执行起来是非常的慢的,如果有大量的并发访问,则会对数据库带来很大的压力。对于这么情况,我们通常使用汇总表的方式进行优化。所谓的汇总表就是提前把要统计的数据进行汇总并记录到表中已备后续的查询使用。针对这个查询,我们可以使用下面的方式进行优化:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 建立汇总表
    CREATE TABLE product_comment_cnt(product_id INT, cnt INT);

    // 查询评论数
    SELECT SUM(cnt) FROM (
    SELECT cnt FROM product_comment_cnt WHERE product_id = 10001
    UNION ALL
    SELECT COUNT(*) FROM product_comment WHERE product_id = 10001
    AND timestr > DATE(NOW())
    );
文章目录
  1. 1. 前言
  2. 2. 1、MySQL
  3. 3. 2、配置
  4. 4. 3、存储引擎
  5. 5. 4、索引
  6. 6. 5、常见问题
    1. 6.1. 5.1 空密码登录
    2. 6.2. 5.2 MySQL 无法启动
  7. 7. 6、MySQL 调优
    1. 7.1. 6.1 SQL 调优
      1. 7.1.1. 6.1.1 根据慢查询日志定位慢查询 SQL
      2. 7.1.2. 6.1.2 实时获取性能问题 SQL
      3. 7.1.3. 6.1.3 修改 SQL 或者尽量让 SQL 走索引
    2. 7.2. 6.2 SQL 的解析预处理及生成执行计划
    3. 7.3. 6.3 确定查询处理各个阶段的耗时
    4. 7.4. 6.4 特定 SQL 的查询优化
      1. 7.4.1. 6.4.1 大表的更新和删除
      2. 7.4.2. 6.4.2 修改大表的表结构
      3. 7.4.3. 6.4.3 优化 not in 和 <> 查询
      4. 7.4.4. 6.4.4 使用汇总表优化查询
隐藏目录