本文摘自:CodeSheep

今天和大家聊一个常见的问题:慢SQL。

包括以下内容:

  • 慢SQL的危害
  • SQL语句的执行过程
  • 存储引擎和索引的那些事儿
  • 慢SQL解决之道

后续均以MySQL默认存储引擎InnoDB为例进行展开,话不多说,开始!

1.慢SQL的危害

慢SQL,就是跑得很慢的SQL语句,你可能会问慢SQL会有啥问题吗?

试想一个场景:

大白和小黑端午出去玩,机票太贵于是买了高铁,火车站的人真是乌央乌央的。

马上检票了,大白和小黑准备去厕所清理下库存,坑位不多,排队的人还真不少。

小黑发现其中有3个坑的乘客贼慢,其他2个坑位换了好几波人,这3位坑主就是不出来。

等在外面的大伙,心里很是不爽,长期占用公共资源,后面的人没法用。

小黑苦笑道:这不就是厕所版的慢SQL嘛!

这是实际生活中的例子,换到MySQL服务器也是一样的,毕竟科技源自生活嘛。

MySQL服务器的资源(CPU、IO、内存等)是有限的,尤其在高并发场景下需要快速处理掉请求,否则一旦出现慢SQL就会阻塞掉很多正常的请求,造成大面积的失败/超时等。

2.SQL语句执行过程

客户端和MySQL服务端的交互过程简介:

  1. 客户端发送一条SQL语句给服务端,服务端的连接器先进行账号/密码、权限等环节验证,有异常直接拒绝请求。
  2. 服务端查询缓存,如果SQL语句命中了缓存,则返回缓存中的结果,否则继续处理。
  3. 服务端对SQL语句进行词法解析、语法解析、预处理来检查SQL语句的合法性。
  4. 服务端通过优化器对之前生成的解析树进行优化处理,生成最优的物理执行计划。
  5. 将生成的物理执行计划调用存储引擎的相关接口,进行数据查询和处理。
  6. 处理完成后将结果返回客户端。

客户端和MySQL服务端的交互过程简图:

俗话说"条条大路通罗马",优化器的作用就是找到这么多路中最优的那一条。

存储引擎更是决定SQL执行的核心组件,适当了解其中原理十分有益。

3. 存储引擎和索引的那些事儿

3.1 存储引擎

InnoDB存储引擎(Storage Engine)是MySQL默认之选,所以非常典型。

存储引擎的主要作用是进行数据的存取和检索,也是真正执行SQL语句的组件。

InnoDB的整体架构分为两个部分:内存架构和磁盘架构,如图:

存储引擎的内容非常多,并不是一篇文章能说清楚的,本文不过多展开,我们在此只需要了解内存架构和磁盘架构的大致组成即可。

InnoDB 引擎是面向行存储的,数据都是存储在磁盘的数据页中,数据页里面按照固定的行格式存储着每一行数据。

行格式主要分为四种类型Compact、Redundant、Dynamic和Compressed,默认为Compact格式。

磁盘预读机制和局部性原理

当计算机访问一个数据时,不仅会加载当前数据所在的数据页,还会将当前数据页相邻的数据页一同加载到内存,磁盘预读的长度一般为页的整倍数,从而有效降低磁盘IO的次数。

磁盘和内存的交互

MySQL中磁盘的数据需要被交换到内存,才能完成一次SQL交互,大致如图:

  • 扇区是硬盘的读写的基本单位,通常情况下每个扇区的大小是 512B
  • 磁盘块文件系统读写数据的最小单位,相邻的扇区组合在一起形成一个块,一般是4KB
  • 页是内存的最小存储单位,页的大小通常为磁盘块大小的 2^n 倍
  • InnoDB页面的默认大小是16KB,是数倍个操作系统的页

随机磁盘IO

MySQL的数据是一行行存储在磁盘上的,并且这些数据并非物理连续地存储,这样的话要查找数据就无法避免随机在磁盘上读取和写入数据。

对于MySQL来说,当出现大量磁盘随机IO时,大部分时间都被浪费到寻道上,磁盘呼噜呼噜转,就是传输不了多少数据。

一次磁盘访问由三个动作组成:

  • 寻道:磁头移动定位到指定磁道
  • 旋转:等待指定扇区从磁头下旋转经过
  • 数据传输:数据在磁盘与内存之间的实际传输

对于存储引擎来说,如何有效降低随机IO是个非常重要的问题。

3.2 索引

可以实现增删改查的数据结构非常多,包括:哈希表、二叉搜索树、AVL、红黑树、B树、B+树等,这些都是可以作为索引的候选数据结构。

结合MySQL的实际情况:磁盘和内存交互、随机磁盘IO、排序和范围查找、增删改的复杂度等等,综合考量之下B+树脱颖而出。

B+树作为多叉平衡树,对于范围查找和排序都可以很好地支持,并且更加矮胖,访问数据时的平均磁盘IO次数取决于树的高度,因此B+树可以让磁盘的查找次数更少。

在InnoDB中B+树的高度一般都在2~4层,并且根节点常驻内存中,也就是说查找某值的行记录时最多只需要1~3次磁盘I/O操作。

MyISAM是将数据和索引分开存储的,InnoDB存储引擎的数据和索引没有分开存储,这也就是为什么有人说Innodb索引即数据,数据即索引,如图:

说到InnoDB的数据和索引的存储,就提到一个名词:聚集索引

聚集索引

聚集索引将索引和数据完美地融合在一起,是每个Innodb表都会有的一个特殊索引,一般来说是借助于表的主键来构建的B+树。

假设我们有student表,将id作为主键索引,那么聚集索引的B+树结构,如图:

  • 非叶子节点不存数据,只有主键和相关指针
  • 叶子节点包含主键、行数据、指针
  • 叶子节点之间由双向指针串联形成有序双向链表,叶子节点内部也是有序的

聚集索引按照如下规则创建:

  • 有主键时InnoDB利用主键来生成
  • 没有主键,InnoDB会选择一个非空的唯一索引来创建
  • 无主键且非NULL唯一索引时,InnoDB会隐式创建一个自增的列来创建

假如我们要查找id=10的数据,大致过程如下:

  • 索引的根结点在内存中,10>9 因此找到P3指针
  • P3指向的数据并没有在内存中,因此产生1次磁盘IO读取磁盘块3到内存
  • 在内存中对磁盘块3进行二分查找,找到ID=9的全部值

非聚集索引

非聚集索引的叶子节点中存放的是二级索引值和主键键值,非叶子节点和叶子节点都没有存储整行数据值。

假设我们有student表,将name作为二级索引,那么非聚集索引的B+树结构,如图:

由于非聚集索引的叶子节点没有存储行数据,如果通过非聚集索引来查找非二级索引值,需要分为两步:

  • 第一:通过非聚集索引的叶子节点来确定数据行对应的主键
  • 第二:通过相应的主键值在聚集索引中查询到对应的行记录

我们把通过非聚集索引找到主键值,再根据主键值从聚集索引找对于行数据的过程称为:回表查询

换句话说:select * from student where name = 'Bob' 将产生回表查询,因为在name索引的叶子节点没有其他值,只能从聚集索引获得。

所以如果查找的字段在非聚集索引就可以完成,就可以避免一次回表过程,这种称为:覆盖索引,所以select * 并不是好习惯,需要什么拿什么就好。

假如我们要查找name=Tom的记录的所有值,大致过程如下:

  • 从非聚集索引开始,根节点在内存中,按照name的字典序找到P3指针
  • P3指针所指向的磁盘块不在内存中,产生1次磁盘IO加载到内存
  • 在内存中对磁盘块3的数据进行搜索,获得name=tom的记录的主键值为4
  • 根据主键值4从聚集索引的根节点中获得P2指针
  • P2指针所指向的磁盘块不在内存中,产生第2次磁盘IO加载到内存
  • 将上一步获得的数据,在内存中进行二分查找获得全部行数据

上述查询就包含了一次回表过程,因此性能比主键查询慢了一倍,因此尽量使用主键查询,一次完事。

4. 慢SQL解决思路

出现慢SQL的原因很多,我们抛开单表数亿记录和无索引的特殊情况,来讨论一些更有普遍意义的慢SQL原因和解决之道。

我们从两个方面来进行阐述:

  • 数据库表索引设置不合理
  • SQL语句有问题,需要优化

4.1 索引设置原则

程序员的角度和存储引擎的角度是不一样的,索引写的好,SQL跑得快。

  • 索引区分度低

假如表中有1000w记录,其中有status字段表示状态,可能90%的数据status=1,可以不将status作为索引,因为其对数据记录区分度很低。

  • 切忌过多创建索引

每个索引都需要占用磁盘空间,修改表数据时会对索引进行更新,索引越多,更新越复杂。

因为每添加一个索引,.ibd文件中就需要多维护一个B+Tree索引树,如果某一个table中存在10个索引,那么就需要维护10棵B+Tree,写入效率会降低,并且会浪费磁盘空间。

  • 常用查询字段建索引

如果某个字段经常用来做查询条件,那么该字段的查询速度会影响整个表的查询速度,属于热门字段,为其建立索引非常必要。

  • 常排序/分组/去重字段建索引

对于需要经常使用ORDER BY、GROUP BY、DISTINCT和UNION等操作的字段建立索引,可以有效借助B+树的特性来加速执行。

  • 主键和外键建索引

主键可以用来创建聚集索引,外键也是唯一的且常用于表关联的字段,也需要建索引来提高性能。

4.2 SQL的优化

如果数据库表的索引设置比较合理,SQL语句书写不当会造成索引失效,甚至造成全表扫描,迅速拉低性能。

索引失效

我们在写SQL的时候在某些情况下会出现索引失效的情况:

  • 对索引使用函数

select id from std upper(name) = 'JIM';

  • 对索引进行运算

select id from std where id+1=10;

  • 对索引使用<> 、not in 、not exist、!=

select id from std where name != 'jim';

  • 对索引进行前导模糊查询

select id from std name like '%jim';

  • 隐式转换会导致不走索引

比如:字符串类型索引字段不加引号,select id from std name = 100;保持变量类型与字段类型一致

  • 非索引字段的or连接

并不是所有的or都会使索引失效,如果or连接的所有字段都设置了索引,是会走索引的,一旦有一个字段没有索引,就会走全表扫描。

  • 联合索引仅包含复合索引非前置列

联合索引包含key1,key2,key3三列,但SQL语句没有key1,根据联合索引的最左匹配原则,不会走联合索引。
select name from table where key2=1 and key3=2;

好的建议

  • 使用连接代替子查询

对于数据库来说,在绝大部分情况下,连接会比子查询更快,使用连接的方式,MySQL优化器一般可以生成更佳的执行计划,更高效地处理查询
而子查询往往需要运行重复的查询,子查询生成的临时表上也没有索引, 因此效率会更低。

  • LIMIT偏移量过大的优化

禁止分页查询偏移量过大,如limit 100000,10

  • 使用覆盖索引
    减少select * 借助覆盖索引,减少回表查询次数。

  • 多表关联查询时,小表在前,大表在后

在MySQL中,执行from后的表关联查询是从左往右执行的,第一张表会涉及到全表扫描,所以将小表放在前面,先扫小表,扫描快效率较高,在扫描后面的大表,或许只扫描大表的前100行就符合返回条件并return了。

  • 调整Where字句中的连接顺序

MySQL采用从左往右的顺序解析where子句,可以将过滤数据多的条件放在前面,最快速度缩小结果集。

  • 使用小范围事务,而非大范围事务

  • 遵循最左匹配原则

  • 使用联合索引,而非建立多个单独索引

4.3 慢SQL的分析

在分析慢SQL之前需要通过MySQL进行相关设置:

  • 开启慢SQL日志
  • 设置慢SQL的执行时间阈值
开启:SET GLOBAL slow_query_log = 1;
开启状态:SHOW VARIABLES LIKE '%slow_query_log%';
设置阈值:SET GLOBAL long_query_time=3;
查看阈值:SHOW GLOBAL VARIABLES LIKE 'long_query_time%'; 

explain分析SQL

explain命令只需要加在select之前即可,例如:

explain select * from std where id < 100;

该命令会展示sql语句的详细执行过程,帮助我们定位问题,网上关于explain的用法和讲解很多,本文不再展开。

5. 小结

本文从慢SQL的危害、Innodb存储引擎、聚集索引、非聚集索引、索引失效、SQL优化、慢SQL分析等角度进行了阐述。如果本文能在某些方面对读者有所启发,足矣。

如何根治慢SQL?的更多相关文章

  1. Python连接不上SQL Server的两种根治思路

    连接不上数据库,首先可以排除是代码的问题,连接方式都是千篇一律的. 大多数问题都是本机的两个原因造成的,1.服务没有开启,2.没有启动SQL配置的TCP/IP 下面给出统一解决方案: 首先从开始菜单找 ...

  2. SQL SERVER全面优化-------Expert for SQL Server 诊断系列

    现在很多用户被数据库的慢的问题所困扰,又苦于花钱请一个专业的DBA成本太高.软件维护人员对数据库的了解又不是那么深入,所以导致问题迟迟不能解决,或只能暂时解决不能得到根治.开发人员解决数据问题基本又是 ...

  3. 浅析history hack、心血漏洞、CSS欺骗、SQL注入与CSRF攻击

    漏洞产生的原因主要有系统机制和编码规范两方面,由于网络协议的开放性,目前以 Web 漏洞居多 关于系统机制漏洞的典型有JavaScript/CSS history hack,而编码规范方面的漏洞典型有 ...

  4. 如何剔掉 sql 语句中的尾巴,我用 C# 苦思了五种办法

    一:背景 1. 讲故事 这几天都在修复bug真的太忙了,期间也遇到了一个挺有趣bug,和大家分享一下,这是一块sql挺复杂的报表相关业务,不知道哪一位大佬在错综复杂的 嵌套 + 平行 if判断中sql ...

  5. 最近帮客户实施的基于SQL Server AlwaysOn跨机房切换项目

    最近帮客户实施的基于SQL Server AlwaysOn跨机房切换项目 最近一个来自重庆的客户找到走起君,客户的业务是做移动互联网支付,是微信支付收单渠道合作伙伴,数据库里存储的是支付流水和交易流水 ...

  6. SQL Server 大数据搬迁之文件组备份还原实战

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 解决方案(Solution) 搬迁步骤(Procedure) 搬迁脚本(SQL Codes) ...

  7. Sql Server系列:分区表操作

    1. 分区表简介 分区表在逻辑上是一个表,而物理上是多个表.从用户角度来看,分区表和普通表是一样的.使用分区表的主要目的是为改善大型表以及具有多个访问模式的表的可伸缩性和可管理性. 分区表是把数据按设 ...

  8. SQL Server中的高可用性(2)----文件与文件组

        在谈到SQL Server的高可用性之前,我们首先要谈一谈单实例的高可用性.在单实例的高可用性中,不可忽略的就是文件和文件组的高可用性.SQL Server允许在某些文件损坏或离线的情况下,允 ...

  9. EntityFramework Core Raw SQL

    前言 本节我们来讲讲EF Core中的原始查询,目前在项目中对于简单的查询直接通过EF就可以解决,但是涉及到多表查询时为了一步到位就采用了原始查询的方式进行.下面我们一起来看看. EntityFram ...

随机推荐

  1. MySQL到ClickHouse实时同步-CloudCanal实战

    简述 CloudCanal 近期实现了 MySQL(RDS) 到 ClickHouse 实时同步的能力,功能包含全量数据迁移.增量数据迁移.结构迁移能力,以及附带的监控.告警.HA等能力(平台自带). ...

  2. docker4-docker网络,容器编排,集群部署

    1,docker网络 1.1,docker0 有三个网络环境,那么docker是如何处理容器网络访问的? 1.2,测试 docker run -d -p 80:8080 --name tomcat01 ...

  3. git schnnel failed to receive handshake, SSLTLS connection failed

    git schnnel failed to receive handshake, SSLTLS connection failed 报错,查看原因为git安装时ssl选择的不是openssl.重新安装 ...

  4. Redis之阻塞分析

    Redis是典型的单线程架构,所有的读写操作都是在一条主线程中完成的.当Redis用于高并发场景时,这条线程就变成了它的生命线.如果出现阻塞,哪怕是很短时间,对于我们的应用来说都是噩梦.导致阻塞问题的 ...

  5. 7、linux快捷键

    ctrl +a:切换到命令行开始 ctrl+e:切换到命令行结尾 ctrl+c:终止当前命令或脚本 ctrl+d:退出当前shell,相当于exit ctrl+l:清除当前屏幕的内容,相当于clear ...

  6. 10、pfile和spfile文件详解

    10.1.介绍: 1.Oracle中的参数文件是一个包含一系列参数以及参数对应值的操作系统文件.它们是在数据库实例启动时候加载的, 决定了数据库的物理结构.内存.数据库的限制及系统大量的默认值.数据库 ...

  7. 通过MMIO的方式实现VIRTIO-BLK设备(一)

    背景知识 什么是VIRTIO 使用完全虚拟化,Guest不加任何修改就可以运行在任何VMM上,VMM对于Guest是完全透明的.但每次I/O都将导致CPU在Guest模式与Host模式间切换,在I/O ...

  8. 『心善渊』Selenium3.0基础 — 25、unittest单元测试框架

    目录 1.unittest基本简介 2.unittest基本概念 (1)unittest核心的四个概念 (2)如何创建一个测试类 (3)test fixture常用的四个方法 (4)unittest编 ...

  9. 深入理解Java多线程——ThreadLocal

    目录 定义 API 场景分析 场景实验,观察Spring框架在多线程场景的执行情况 10000此请求,单线程 10000次请求,线程数加到100 对c的访问加锁 把c设为ThreadLocal 收集多 ...

  10. .h .cpp区别

    首先,我们可以将所有东西都放在一个.cpp文件内. 然后编译器就将这个.cpp编译成.obj,obj是什么东西? 就是编译单元了.一个程序,可以由一个编译单元组成, 也可以有多个编译单元组成. 如果你 ...