MySQL之优化

8.性能优化

索引优化

  1. **前缀索引优化:**使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。

  2. 覆盖索引优化:覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。

  3. 主键索引锁好是自增:

    • InnoDB 创建主键索引默认为聚簇索引 ,数据被存放在了 B+Tree 的叶子节点上。也就是说,同一个叶子节点内的各个数据是按主键顺序存放的 ,因此,每当有一条新的数据插入时,数据库会根据主键将其插入到对应的叶子节点中

    • 使用自增主键 ,那么每次插入的新数据就会按顺序添加到当前索引节点的位置不需要移动已有的数据当页面写满,就会自动开辟一个新页面因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。

    • 使用非自增主键 ,由于每次插入主键的索引值都是随机的 ,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置 ,这将不得不移动其它数据来满足新数据的插入甚至需要从一个页面复制数据到另外一个页面 ,我们通常将这种情况称为页分裂 。页分裂还有可能会造成大量的内存碎片导致索引结构不紧凑,从而影响查询效率。

    • 主键字段的长度不要太大,因为主键字段长度越小,意味着二级索引的叶子节点越小 (二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小

  4. 索引最好设置为 NOT NULL:

    • 索引列存在 NULL会导致优化器在做索引选择的时候更加复杂,更加难以优化 ,因为可为 NULL 的列会使索引、索引统计和值比较都更复杂,比如进行索引统计时,count 会省略值为 NULL 的行。

    • NULL 值是一个没意义的值,但是它会占用物理空间 ,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段 ,那么行格式中至少会用 1 字节空间存储 NULL 值列表。

  5. 防止索引失效:

    • 当我们使用左或者左右模糊匹配 的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;

    • 当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;

    • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。

    • 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

count(*)优化

性能:count(*) = count(1) > count(主键字段) > count(字段)

在通过 count 函数统计有多少个记录时,MySQL 的 server 层会维护一个名叫 count 的变量。

在执行时,优先选择二级索引 ,因为相同数量的二级索引记录可以比聚簇索引记录占用更少的存储空间,所以二级索引树比聚簇索引树小,这样遍历二级索引的 I/O 成本比遍历聚簇索引的 I/O 成本小,因此「优化器」优先选择的是二级索引。

  1. count(*): count(*) 其实等于 count(0),也就是说,当你使用 count(*) 时,MySQL 会将 * 参数转化为参数 0 来处理。

  2. count(1): 将读取到的记录返回给 server 层,但是不会读取记录中的任何字段的值,因为 count 函数的参数是 1,不是字段,所以不需要读取记录中的字段值。参数 1 很明显并不是 NULL,因此 server 层每从 InnoDB 读取到一条记录,就将 count 变量加 1。

  3. **count(主键字段):**server 层会循环向 InnoDB 读取一条记录,如果 count 函数指定的参数不为 NULL,那么就会将变量 count 加 1,直到符合查询的全部记录被读完,就退出循环。最后将 count 变量的值发送给客户端。

  4. count(字段):字段不是索引字段的话就会全表扫描。

  • count(1) 相比 count(主键字段) 少一个步骤,就是**不需要读取记录中的字段值判断是否为 NULL,**所以通常会说 count(1) 执行效率会比 count(主键字段) 高一点。

  • count(*)和 count(1)底层一样,但 MySQL 会对 count(*) 和 count(1) 有个优化,**如果有多个二级索引的时候,优化器会使用 key_len 最小的二级索引进行扫描(代表着扫描节点数据量更少)。**只有当没有二级索引的时候,才会采用主键索引来进行统计。

为什么要通过遍历方式计数?

使用 MyISAM 引擎时,执行 count 函数只需要 O(1 )复杂度,这是因为每张 MyISAM 的数据表都有一个 meta 信息有存储了 row_count 值,由表级锁保证一致性,所以直接读取 row_count 值就是 count 函数的执行结果。

而 InnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表"应该返回多少行"也是不确定的,所以无法像 MyISAM 一样,只维护一个 row_count 变量。

优化 count(*)

如果对一张大表经常用 count(*) 来做统计,其实是很不好的。

  1. 近似值: 执行 explain 命令效率是很高的,因为它并不会真正的去查询,explain 中的 rows 字段值就是 explain 命令对表记录的估算值

  2. **额外表保存数值:**想精确的获取表的记录总数,可以将这个计数值保存到单独的一张计数表中。

深度分页

基于主键索引的 limit 过程

同样都是拿 10 条数据,查第一页和第一百页的查询速度是一样的吗?

  1. server 层会调用 innodb 的接口,在 innodb 里的主键索引中获取到第 0 到 10 条完整行数据,依次返回给 server 层,并放到 server 层的结果集中,返回给客户端。

  2. server 层会调用 innodb 的接口,由于这次的 offset=990,会在 innodb 里的主键索引中获取到第 0 到(990 + 10)条完整行数据,返回给 server 层之后根据 offset 的值挨个抛弃,最后只留下最后面的 size 条,也就是 10 条数据,放到 server 层的结果集中,返回给客户端。

当 offset 非 0 时,server 层会从引擎层获取到很多无用的数据,而当 select 后面是*号时,就需要拷贝完整的行信息,拷贝完整数据跟只拷贝行数据里的其中一两个列字段耗时是不同的,这就让原本就耗时的操作变得更加离谱。

因为前面的 offset 条数据最后都是不要的,所以我们可以将 sql 语句修改成下面这样:

上面这条 sql 语句,里面先执行子查询 select id from page order by id limit 6000000, 1, 这个操作,其实也是将在 innodb 中的主键索引中获取到6000000+1条数据,然后 server 层会抛弃前 6000000 条,只保留最后一条数据的 id

但不同的地方在于,在返回 server 层的过程中,只会拷贝数据行内的 id 这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。

这样 innodb 再走一次主键索引,通过 B+树快速定位到 id=6000000 的行数据,时间复杂度是 lg(n),然后向后取 10 条数据。

基于非主键索引的 limit 过程

server 层会调用 innodb 的接口,在 innodb 里的非主键索引中获取到第 0 条数据对应的主键 id 后,回表到主键索引中找到对应的完整行数据,然后返回给 server 层,server 层将其放到结果集中,返回给客户端。

而当 offset>0 时,且 offset 的值较小时,逻辑也类似,区别在于,offset>0 时会丢弃前面的 offset 条数据。

也就是说非主键索引的 limit 过程,比主键索引的 limit 过程,多了个回表的消耗。

当 limit offset 过大时,非主键索引查询非常容易变成全表扫描。是真·性能杀手。

这种情况也能通过一些方式去优化。比如

通过select id from page order by user_name limit 6000000, 100。先走 innodb 层的 user_name 非主键索引取出 id,因为只拿主键 id,不需要回表 ,所以这块性能会稍微快点,在返回 server 层之后,同样抛弃前 600w 条数据,保留最后的 100 个 id 。然后再用这 100 个 id 去跟 t1 表做 id 匹配,此时走的是主键索引,将匹配到的 100 条行数据返回。这样就绕开了之前的 600w 条数据的回表。

像这种,当 offset 变得超大时,比如到了百万千万的量级,问题就突然变得严肃了。这就是深度分页。

深度分页问题 --- 无解,只能缓解,问题原始需求:

  • 如果是要拿一个表的所有数据:select * from page,直接操作肯定是不行的,可以用主键 id 分批获取,数据在主键索引中是有序的,一个 for 循环每次获取 100 条,将当前批次的最大 id 作为下次筛选的条件进行查询,查询性能很稳定。

  • 如果是用户分页:深度分页问题必然存在, 用户随机选取第几页,就要 offset n 10 进行查询。可以更换业务:**只支持上一页或下一页,**可以使用上面提到的 start_id 方式,采用分批获取,每批数据以 start_id 为起始位置。这个解法最大的好处是不管翻到多少页,查询速度永远稳定。

优化 IO

事务在提交的时候**,需要将 binlog 和 redo log 持久化到磁盘**,那么如果出现 MySQL 磁盘 I/O 很高的现象,可以通过控制以下参数,来 "延迟" binlog 和 redo log 刷盘的时机,从而降低磁盘 I/O 的频率:

  • 设置组提交的两个参数: binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,延迟 binlog 刷盘的时机,从而减少 binlog 的刷盘次数。这个方法是基于"额外的故意等待"来实现的,因此可能会增加语句的响应时间,但即使 MySQL 进程中途挂了,也没有丢失数据的风险,因为 binlog 早被写入到 page cache 了,只要系统没有宕机,缓存在 page cache 里的 binlog 就会被持久化到磁盘。

  • 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000),表示每次提交事务都 write,但累积 N 个事务后才 fsync,相当于延迟了 binlog 刷盘的时机。但是这样做的风险是,主机掉电时会丢 N 个事务的 binlog 日志。

  • 将 innodb_flush_log_at_trx_commit 设置为 2。表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,专门用来缓存文件数据的,所以写入「 redo log 文件」意味着写入到了操作系统的文件缓存,然后交由操作系统控制持久化到磁盘的时机。但是这样做的风险是,主机掉电的时候会丢数据。

相关推荐
zhangchaoxies2 小时前
PHP源码能否在NAS设备上运行_NAS部署PHP源码可行性【教程】
jvm·数据库·python
2301_764150562 小时前
如何在 Laravel 中正确保存嵌套动态表单数据(主服务 + 子服务)
jvm·数据库·python
2401_832635582 小时前
如何进行SQL安全基线评估_定期核对数据库安全配置
jvm·数据库·python
zhangchaoxies2 小时前
HTML怎么实现键盘操作全站导航_HTML全局快捷键说明面板【方法】
jvm·数据库·python
vegetablec2 小时前
如何用 location.reload(true) 强制浏览器从服务器刷新页面
jvm·数据库·python
2301_814809862 小时前
如何让导航栏的下落动画效果更缓慢?
jvm·数据库·python
Elastic 中国社区官方博客2 小时前
多大才算太大?Elasticsearch 容量规划最佳实践
大数据·运维·数据库·elasticsearch·搜索引擎·全文检索
InfinteJustice2 小时前
如何加固SQL通信安全_启用SSL加密确保数据传输安全
jvm·数据库·python
切糕师学AI2 小时前
深入解析SqlSugar:.NET领域的高性能多数据库ORM框架
数据库·.net·orm