你需要掌握的 Mysql 优化的一些要点

248 阅读18分钟

本文是学习《高性能 Mysql》中关于 Mysql 中查询优化需要注意的一些要点的总结:

Schema 和数据类型优化

  • 尽量避免使用 NULL 值,尤其存在索引时,因为如果 NULL 列是索引,索引统计以及值的比较更加复杂
  • 尽量选择小的简单的数据类型,因为它们占用更少的磁盘,内存和 CPU 缓存
  • 尽量使用 TIMESTAMP 代替 DATETIME,因为 TIMESTAMP 只是 DATETIME 一半大小存储空间,还会跟时区变化,但是 TIMESTAMP 允许的时间范围比较小(1970年~2038年)
  • 对于字符串列的最大长度比平均长度大很多的情况建议使用 VARCHAR 类型
  • 对于非常短且比较均衡的列建议使用 CHAR 类型,不容易产生太多的碎片
  • Mysql 对于 BLOB 和 TEXT 类型的排序和其它类型规则不同,只会对每个列的前 max_sort_length 字节的字符串进行排序,这样必然会使用临时表,所以尽量确保 max_sort_length 的值下不要超过 max_heap_table_size 或者 max_table_size,以保证排序时使用内存临时表
  • 不同类型字段进行关联查询时往往成本比较高,建议如果需要关联查询尽量改为相同类型
  • 在查询时尽量不要使用太多的关联,虽然 Mysql 限制了每个关联操作最多只能有 61 张表,但是为了让查询执行的速度快且并发性好,单个查询不要超过 12 张表关联
  • 除非枚举值是一些固定不变的值,例如“性别”,建议不要过度使用枚举,因为在修改枚举值时需要 ALTER TABLE 成本非常高,而且枚举值的排序是按照枚举顺序来排序,并不是字面值

索引优化

  • 如果查询中某个列是范围查询,那么其右边的所有列将无法使用索引优化,所以尽量将范围条件放在右边或者使用多个等值条件来代替范围查询
  • ORDER BY 中的排序的列如果建了索引,则有可能使用索引进行排序,进行优化性能
  • 只有当索引的列和 ORDER BY 子句的顺序完全一致且所有列的排序方向一致时才能使用索引做排序
  • 哈希索引对于等值查询的性能提升非常高,但是哈希索引无法用来排序,也不支持部分索引列匹配查找
  • 在使用索引时对应的索引列必须独立,不能是表达式的一部分也不能是函数的参数,否则不能使用索引:
-- 虽然 id 上建立了索引,但是无法使用索引优化
select id from user where id + 1 =5;
  • 当服务器出现多个列做 AND 操作查询时,通常需要建了一个多列索引,而不是多个独立的单列索引
  • 当不需要考虑排序和分组时,将选择项最高的列放在前面通常是最好的,因为可以很快的过滤出需要的行
  • 如果索引包含了需要查询的所有字段值,那么就是可以使用覆盖索引查询,只需要读取索引,极大地减少了数据访问量,在 EXPLAIN 分析的 Extra 字段中可以看到 “Using index” 信息
  • 查询时尽量不要返回多余的列,第一可以减少网络流量,第二增加使用覆盖索引的可能性
  • 如果关联多张表时,只有当 ORDER BY 子句引用的字段全部是第一张表时才能使用索引排序
  • 默认类型转换不仅增加开销,还会使索引失效,比如 col 是 VCHAR 类型,那么 where col = '10' 会使用索引,而 where col = 10 不会使用索引
  • 不要创建冗余的索引,Mysql 不仅需要单独维护索引列,并且在优化器查询时也需要逐个索引进行过滤,会影响性能

下面是创建冗余索引的几个例子:

- 创建了索引(A,B)再创建索引(A),那后者便是冗余索引
- 将一根索引扩展为(A,ID),其中 ID 是主键,对于 InnoDB 来说主键已经包含在二级索引中了,所以这也是冗余的
  • 有一些索引可能服务器永远都不会用到,建议考虑删除,在 percona 版本或 marida 中可以通过 information_schea.index_statistics 查看得到索引的使用情况,在官方版本中可以使用 performance_schema.table_io_waits_summary_by_index_usage 查看索引使用情况:
- 可以查到使用最多或者使用最少的表和索引
- 可以查到从未使用过的索引,考虑删除之
- 可以查到线程的使用情况等等

事务优化

  • 尽量不要在事务中混合使用存储引擎,如果有些表支持事务,有些表不支持事务,回滚时会导致数据不一致问题
  • 在应用层应该检查在事务中是否存在 RPC 调用、HTTP 调用、消息队列、缓存、循环查询等耗时的操作,这个操作应该尽量移到事务之外,因为这些操作会增加事务的处理时间,使 sql 查询不稳定,理想的情况是事务内只处理数据库操作;

其它查询优化

  • 一个大的 DELETE 或者 UPDATE 查询很可能会一次性锁住很多数据,占满整个事务日志,阻塞其它小的重要的查询,如果有可能可以把大的查询拆分成多个小的查询。
  • 关联查询分解:
- 让单表查询的缓存效率更高
- 拆分后用 IN() 代替关联查询,可以让 Mysql 按照 ID 顺序去查找
- 可以将数据分布到不同的 Mysql 服务器上
  • 使用 IN 加子查询性能通常都会很低,所以建议使用 EXISTS 等效的查询来获取更好的效率
  • UNION 操作会比 UNION ALL 操作耗时,因为 UNION 操作在合并以后,还要作去重排序操作,除非必须使用 UNION 查询,否则就使用 UNION ALL 查询
  • 能写在 WHERE 条件中判断不要写在 HAVING 子句中,因为 GROUP BY 会对数据进行排序,如果事先排除掉一些数据,会减少排序量,还有就是聚合后的视图可能索引条件已经丢失
  • IS NULL 或者 IS NOT NULL 查询会使索引失效
  • 当以为当前查询只有一行数据时使用可以使用 LIMIT 1,这样检索到一条数据后,就停止搜索了
  • HAVING 子句和 GROUP BY 子句一起使用时比先 GROUP BY 成中间表再执行 WHERE 要快
  • GROUP BY 子句会自动对分组的列进行排序,如果不希望进行排序可以使用 ORDER BY NULL
  • 尽可能将 GROUP BY WITH ROLLUP 放到应用程序去完成,因为 Mysql 做超级聚合往往性能不佳
  • 优化策略在 UNION 查询中没法很好的使用,一般需要将 WHERE,ORDER BY,LIMIT 子句下推到各个子查询中
  • 优化 COUNT() 查询:
- 如果是统计结果集的大小,请使用 COUNT(*),使用 COUNT(cloumn) 有可能某个列存在 NULL 导致统计不准确,排除 NULL 计算也是要成本的
- 对于 MyIsam 存储引擎,如果不带任何 WHERE 条件的情况下, COUNT(*) 不需要计算,直接通过存储引擎特性获得
  • LIMIT 分页优化
-- 分页时对于偏移量特别大的情况下,查询所有列分页将非常耗时,可以使用“延迟关联”的方式,其中一个查询中尽可能的使用索引覆盖扫描方式 LIMIT 查询出主键 ID,然后再和原表做一次关联返回需要的列:
-- 优化前
select * from user order by id limit 1000, 5;
-- 优化后
select user.* from user join (select id from user order by id limit 1000, 5) new_user on new_user.id = user.id;

-- 如果在一个位置上预先计算出了边界,可以将 limit 查询转换为已知位置的查询进行优化
select * from user where id between 1000 and 1005 order by id 

使用查询提示进行优化

如果对优化器的执行计划不满意可以使用优化器的几个提示来控制最终的执行计划:

HIGH_PRIORITY 和 LOW_PRIORITY

HIGH_PRIORITY 和 LOW_PRIORITY 对于使用表锁的存储引擎有效,HIGH_PRIORITY 会将当前查询插入到所有处于表锁等待的 SQL 队列前面,而 LOW_PRIORITY 会将当前查询放在所有等待表锁的 SQL 队列队尾,只要队列中还有需要访问同一张表的 SQL, 它就被处于等待状态。

DELAYED

该提示对 INSERT 和 REPLACE 有效,使用该提示后会立即返回给客户端,然后将插入的行放入缓存区,等待表空闲时批量写入数据。

该操作导致 1495048 函数无法正常工作。

对于一些数据记录,即使插入失败也不影响服务正常运行,可以使用该操作,及时响应客户端,加快响应速度。

STRAIGHT_JOIN

让所有查询中的的表按照语句中出现的顺序进行关联,不需要 Mysql 优化器去重新选择关联顺序,如果能确保自己写的关联顺序性能比较好的情况下可以选择该提示,减少 Mysql 优化器本身选择分析的时间。

SQL_SMALL_RESULT 和 SQL_BIG_RESULT

这两个提示针对 select 操作,告诉优化器对 group by 或 distinct 如何使用临时表及排序,如果 SQL_SMALL_RESULT 表示结果集很小,使用内存排序,如果是 SQL_BIG_RESULT 表示结果集很大,使用磁盘临时表排序。

SQL_CACHE 和 SQL_NO_CACHE

这个提示告诉 Mysql 结果集是否要缓存在查询缓存中

SQL_CALC_FOUND_ROWS

FOUND_ROWS 这个函数一般情况下只会返回上一次查询的数据集大小,但是如果加了 SQL_CALC_FOUND_ROWS 提示,那么将返回不带 limit 情况下整个数据集大小,这个参数对于分页有一定的用处,不需要多次查询。

FOR_UPDATE 和 LOCK IN SHARE MODE

该提示只对支持行级锁的存储引擎生效,该提示会对查询中符合条件的数据加锁

这两个提示会让 InnoDB 覆盖索引优化失效,因为 InnoDB 需要访问主键中的版本信息。

USE INDEX 和 IGNORE INDEX 及 FORCE INDEX

告诉优化器是否使用某个索引

合理使用分区表

  • 分区表数据更容易维护,想删除大量数据可以直接使用清除某个分区的方式,并且可以独立备份和恢复某个分区
  • 分区表的数据可以分布到多个物理设备上,有效的利用硬件设备
  • 如果分区列有 NULL 值,可能使分区过滤无效,因为 NULL 值会被存储在第一个分区中
  • 避免建立与分区列不匹配的索引,因为这样根据索引查询会使分区无法区分
  • 在查找访问分区时,Mysql 需要打开并锁住所有的底层表,对于简单的查询来说这个消耗还是有点高,可以使用批量操作减少开销次数
  • 所有分区都必须使用相同的存储引擎,分区中可以使用的函数和表达式也有一定的限制
  • Mysql 只能使用分区函数列本身查询时才可以使用分区过滤,不能将分区列放入表达式,此时无法找到对应分区进行过滤

合理使用视图/外键/触发器

  • 创建视图有两种算法:临时表算法和合并算法,如果可能尽量使用合并算法,使用合并算法时 Mysql 会将视图与基于视图的查询语句进行合并然后优化器基于此进行优化
  • 通过 explain 解析字段 select_type 判断视图使用临时表算法还是合并算法,在创建查询时可以指定具体使用什么算法,
  • 如果只是使用外键做约束,那么通常在应用程序里实现会更好,外键会带来很大的额外开销
  • 触发器容易掩盖背后的工作,而且问题比较难以排查,可能导致死锁,尽量不要使用触发器

合理使用绑定变量

  • 使用绑定变量,Mysql 服务器只需要解析一次 SQL 语句,并且会缓存一部分执行计划
  • 使用绑定变量每次仅仅发送的参数,而不是整个查询语句,减少网络开销
  • 绑定变量也相对安全,不需要处理转义,大大减少 SQL 注入和攻击的风险
  • 绑定变量是会话级别的,不同连接之间不能共用

合理使用查询缓存

  • 如果表发生变化,对应的查询缓存则会失效
  • 查询缓存是否命中与本身查询 SQL,查询的数据库,客户端协议的版本有关系
  • 查询中包含自定义函数,存储函数,用户变量,临时表,Mysql 库中的系统表都不会设置缓存,也不会命中缓存
  • 只有整个事务提交后,相关的查询结果才会被缓存
  • 查询缓存对于复杂计算,耗时比较长的查询有很大优化效果,
  • 对于简单的查询,因为查询缓存的预判检查也本身比较耗时,再加上数据变化比较快时,相反会降低性能
  • 建议查询时使用 SQL_CACHE 和 SQL_NO_CACHE 来进行选择性的使用查询缓存
  • 对于 InnoDB 如果表上有任何锁,那么任何查询都无法从缓存中读取与这个表相关的缓存结果
  • 如何优化查询缓存:
- 用多个小表代替一个大表,可以让缓存失效在一个更细的粒度上进行
- 批量写入时只做一次缓存失效,所以比单条写入更好
- 如果无法在数据库或者表级别控制查询缓存,则可以使用 SQL_CACHE 和 SQL_NO_CACHE 来控制单个 select 语句是否进行缓存,并且可以修改会话级别的 query_cache_type 来控制查询缓存
- 对于写密集型的应用来说,关闭查询缓存对性能会更好

合理使用 Mysql 服务器配置

  • mysql 的配置文件一般在 /etc/my.cnf 或者 /etc/mysql/my.cnf
  • 任何打算长期保存的配置都应该通过配置文件保存,不应该在命令行里生效,以防下次启动失效
  • DEFAULT 是一个特殊值可以通过 SET 设置给变量:这个值会把会话级变量设置为全局变量,会把全局变量设置为编译器内置的默认值
  • mysql 主要的几个环境变量配置说明:
datadir=    /var/lib/mysql                  # 数据的存储位置

user=       mysql                           # 执行 mysql 用户运行 mysql 实例,要保证该用户存在

port=       3306                            # mysql 实例的端口号

socket:    =/var/lib/mysql/mysql.sock      # socket 文件存储位置,用于 TCP/IP 套接字连接数据库

pid_file    = /var/lib/mysql/mysql.pid      # mysql 进程 id

default_storage_engine        = InnoDB      # 默认的存储引擎

innodb                        = FORCE       # 只有在 Innodb 存储引擎正常启动时,服务器才能正常启动,一般建议设置为 FORCE,保证可以正确使用 InnoDB 存储引擎

innodb_buffer_pool_size       = <value>     # InnoDB 存储引擎可以使用的缓存大小

innodb_log_file_size          = <value>     # 设置重做日志大小,太小写入日志需要频繁的刷新磁盘,使写入变慢,太大奔溃恢复时间变慢,要合理设置

innodb_thread_concurrency     = 0           # 它可以限制一次性有多少线程进入内核,0 表示不限制。一般建议设置为:CPU 数量 * 磁盘数量 * 2

innodb_thread_sleep_delay     = 10000       # 为了减少因为操作系统调度引起的上下文切换,线程第一次无法进入内核会休眠 innodb_thread_sleep_delay 秒以后再尝试
                                            # 如果再次无法进入内核,则放入线程等待队列,让操作系统来处理
                                            
innodb_file_per_table         = 1           # 是否让每一张表使用一个独立文件存储,使得删除表变的简单,并且容易分散到不同的磁盘上,但是会导致空间的浪费

innodb_flush_method           = 0_DIRECT    # 控制 InnoDB 如何和文件系统相互作用,控制将数据刷新到磁盘的方式,要不要使用磁盘缓存等

key_buffer_size               = <value>     # MyISAM 存储引擎分配的键缓存大小,该值对使用 MyISAM 存储引擎的数据库非常重要
                                            # 即使是使用 InnoDB 存储引擎也应该分配一定空间(32M),因为 Mysql 中一些系统表会使用 MyISAM 存储引擎
                                            # Group by 创建临时表时也可能使用 MyISAM 存储引擎
                                            
sort_buffer_size              = <value>     # 该参数会在查询使用内存排序时分配内存,一旦需要排序就会指定这么大的内存,不管是否需要这么大的内存  
                                            # 一般建议把 sort_buffer_size 修改的小一点,如果某个查询确实需要很大内存排序,可以在会话级临时调大该值
                                            
log_error=/var/lib/mysql/mysql-error.log    # 错误日志存放位置

slow_query_log=/var/lib/mysql/mysql-show.log# 慢查询日志存放位置

tmp_table_size/max_heap_table_size = 32M    # 这两个变量用于控制使用内存临时表(Memory存储引擎)的大小,如果超过这个值,将使用磁盘临时表(MyISAM存储引擎)
                                            # 通过 show status 观察 Created_tmp_disk_tables 和 create_tmp_tables 的变化来调整这两个参数
                                            
query_cache_type              = 0           # 控制查询缓存功能的开启和关闭,0 表示关闭,1 表示开启,2 表示只有 select 中明确指定 SQL_CACHE 才缓存

query_cache_size              = 0           # 设置查询缓存的大小

max_connections               = <value>     # 最大连接数,默认是 100,往往太小,如果太小会报太多连接被拒绝的错误,观察 Max_used_connections 状态变量来设置该参数

thread_cache_size             = <value>     # 指定 Mysql 可以保存在缓存中的线程数,一般通过观察 Thread_connected 变量的大小来调整该值的大小

table_cache_size              = 1000        # 设置表缓存大小,设置足够的大小以避免总是需要重新打开并重新解析表定义

open_files_limit              = 65535       # 这个参数可以尽量设置大,因为打开句柄的开销很小,否则会出现“too many open files”

expire_logs_days              = 10          # 服务器在指定的天数后清理二进制日志

max_connect_errors            = 100         # 容许某个应用最大错误次数,如果超过该值,将被加入黑名单,除非刷新主机缓存
  • 几个 timeout 相关参数说明:
- connect_timeout

在获取连接阶段(authenticate)起作用,获取 MySQL 连接是多次握手的结果,除了用户名和密码的匹配校验外,还有 IP->HOST->DNS->IP 验证,任何一步都可能因为网络问题导致线程阻塞。
为了防止线程浪费在不必要的校验等待上,超过 connect_timeout 的连接请求将会被拒绝,默认值 10 秒。

- interactive_timeout 和 wait_timeout

在连接空闲阶段(sleep)起作用,即使没有网络问题,也不能允许客户端一直占用连接。
对于保持 sleep 状态超过了 wait_timeout(或 interactive_timeout,取决于 client_interactive 标志)的客户端,MySQL 会主动断开连接,默认值是 8 小时。

- net_read_timeout 和 net_write_timeout

则是在连接繁忙阶段(query)起作用,即使连接没有处于 sleep 状态,即客户端忙于计算或者存储数据,MySQL 也选择了有条件的等待。
在数据包的分发过程中,客户端可能来不及响应(发送、接收、或者处理数据包太慢)。
为了保证连接不被浪费在无尽的等待中,MySQL 也会选择有条件(net_read_timeout和net_write_timeout)地主动断开连接。默认是 30 秒。

- innodb_lock_wait_timeout

innodb 使用这个参数能够有效避免在资源有限的情况下产生太多的锁等待,指的是事务等待获取资源时等待的最长时间,超过这个时间还未分配到资源则会返回应用失败。
参数的时间单位是秒,最小可设置为1s(一般不会设置得这么小),最大可设置1073741824秒(34年),默认安装时这个值是 50 s。
超过这个时间会报 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction。

- innodb_rollback_on_timeout

默认情况下 innodb_lock_wait_timeout 超时后只是超时的 sql 执行失败,整个事务并不回滚,也不做提交。
如需要事务在超时的时候回滚,则需要设置 innodb_rollback_on_timeout=ON,该参数默认为 OFF。

- lock_wait_timeout

和 innodb_lock_wait_timeout 的区别是前者是 Innodb 的 DML 操作的行级锁的等待时间,后面是数据结构 DDL 操作的锁的等待时间。

- innodb_flush_log_at_timeout

参数 innodb_flush_log_at_trx_commit = 1 时,此超时参数不起作用。当 innodb_flush_log_at_trx_commit=0/2 时才起作用。
表示每 innodb_flush_log_at_timeout 秒进行一次的频率刷新 redo log(在 5.6.6 版本之前是固定每秒一次刷新 redo log,5.6.6 版本之后刷新频率可以通过这个参数设置,当然,这个参数本身默认值也是 1S)