1、查数据太多不会把内存用光

  InnoDB 的数据是保存在主键索引上,然后索引树分割保存在数据页上,数据页存在内存中/磁盘。change buffer 就是先把修改操作记录,然后读数据的时候,内存没有就从磁盘拿,把 change buffer 的改动应用的从磁盘读取的数据页上,返回给请求。并且此时数据页变成脏页,又会刷脏页。

  全盘扫描实际上就是扫描表的主键索引。查到符合条件的就放在结果集,然后返回给客户端。

  结果集:

  一块内存 net_buffer 写入符合条件的结果,重复获取写入到 net_buffer(默认大小16K) 直到写满,调用网络接口发出。发送成功就清空 net_buffer,然后继续获取下一行继续写到 net_buffer 中。

  然后发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,表示本地网络栈(socket send buffer) 写满了,进入等待,直到本地栈重新可写,再继续发送。客户端去读 socket receive buffer 的内容,不堵塞的话,socket send buffer 就会继续把数据发送给 socket receive buffer (服务端的网络栈) 。

  这个流程就是 "边读边发",所以并不会200G的表就会用到200G的内存。 如果客户端接收的很慢,会导致 MySQL 服务端由于结果发不出去,这个事务的执行时间变长。

  show processlist 如果看到 State 的值一直处于 " Sending to client" ,就表示服务端的 socket receive buffer 写满了(网络端查询拿数据多/拿数据慢,所以写满),net_buffer 就会放满并且还有查询结果没写入到 net_buffer 中。如果想减少处于这个状态的线程,将 net_buffer_length 参数设置为一个更大的值也是可以的。

  如果客户端使用 -quick 参数,会使用 mysql_use_result 方法。 这个方法是读一行处理一行。如果有一个业务逻辑比较复杂,每读一行数据都要处理的逻辑很慢,就会导致客户端要过很久才会去取下一行数据。

  对于正常的线上业务,一个查询返回的结果不会太多(常常如此),建议使用 mysql_store_result

  这个接口,直接把查询结果保存到本地内存。

  Sending data 的意思只是 正在执行 sql 语句,并不是发送数据。

  全表扫描对 InnoDB 的影响

  WAL 机制,InnoDB 的 redo log pool buffer 内存,是保存更新的动作,配合 redo log,避免了随机写盘。

  内存的数据页是在 Buffer Pool(BP) 中管理的,在 WAL 机制里,Buffer Pool 还起到加速更新的作用。还有更重要的作用,加速查询。一般修改都保存在内存数据页,还没有落盘,此时查询,就会先查询内存(不用查磁盘),得到最新数据就返回了。

  Buffer Pool 对查询的加速效果,依赖一个重要的指标:内存命中率。

  执行命令: show engine innodb status 可以看到 Buffer pool hit rate 就是内存命中率,稳定服务的线上系统,内存命中率在99%以上。

  InnoDB Buffer Pool 的大小由参数 innodb_buffer_pool_size 决定。如果 Buffer Pool 满了,而又要从磁盘读入一个数据页,那肯定是要淘汰一个旧数据页的。

InnoDB 引擎之前用的 LRU(最少使用淘汰)算法,然后进行了改进。把 LRU 链表分为了 young 和 old 区域。5/8 是 young 区域,靠近链表尾部的 3/8 就是 old 区域。

  全盘扫描过程中,新插入的数据页是放在 old 区域开头,该数据页第一次和最后一次访问的时间间隔不超过1秒,就继续留在 old 区域, 超过1秒,就移动到 young 区域开头。 再扫描之后的数据,该数据页不会被访问到,所以很快被淘汰。

  这个策略最大收益是全盘扫描的时候,虽然用到了 Buffer Pool,但是 young 区域完全没影响,从而保证了 Buffer Pool 响应正常业务的查询命中率。

2、怎么使用 join

  select * from t1 straight_join t2 on (t1.a=t2.a);

  当使用 join 的时候,一个 SQL 语句的流程如下:

  1)、从表 t1 中读入一行数据 R;

  2)、从数据行 R 中,取出 on 条件里的 a 字段到表 t2 里去查找;

  3)、取出表 t2 中满足条件的行,跟 R 组成一行,做为结果集的一部分;

  4)、重复执行步骤1、3,知道表 t1 的末尾循环结束。

  Join 中,对 t1 表(驱动表)做了全盘扫描,去 t2 (被驱动表)查找走的是树搜索过程,所以只需要找到值符合的行就可以。

  算法:假如被搜索的行数是M,每次在被驱动表查一行数据,要先搜索索引A,再查主键索引。在被驱动表查一行的时间复杂度是 2*log2M。驱动表是N行,整个过程的复杂度是:N + N*2*log2M。

  所以 N 越小越好。

  注意事项:以小表驱动大表(小表:两个表按照各自的条件过滤,过滤完后,计算参与join的各个字段的总数据量,数据量小的那个表,就是"小表")。

  注意事项: t2 的 a 字段是 t2 表中的索引(用上被驱动表的索引,on 后的条件字段是 索引),并且称之为 "Index Nested-Loop Join"。

  如果 t2 表中的 a 字段不是索引,那么每次去 t2 表匹配就要做一次全盘扫描,复杂度就是 M*N。

  当然MySQL也没有用这个方法,而是用的 "Block Nested-Loop Join"方法,简称BNL 。

  BNL 流程如下:

  1)、把 t1 表的数据读入线程内存 join_buffer 中,扫描整个表的N行;

  2)、扫描表 t2 ,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足条件就作为结果集返回。

  这个算法里,两个表都做了一次全盘扫描,扫描总行数:M+N,内存中判断次数 M*N。

  join_buffer 大小由参数 join_buffer_size 设定,默认值 256K,如果放不下驱动表的整个表的数据,那就分段放。就是一部分满足条件返回后,清空 join_buffer 后继续放。但是 join_buffer_size 的越大,可以放入的行越多,分段就越少,对驱动表的全盘扫描次数就越少。

  总结:可以使用 Index Nested-Loop Join 算法(用上被驱动表的索引)是可以用 join的,使用Block Nested-Loop Join 算法,的话,join 就尽量不要用。

就是看 explain 结果的 Extra 字段,出现"Block Nested Loop"或"Index Nested Loop"。

  

3、join 语句的优化

  Multi-Range Read(MRR),这个优化主要的目的是尽量使用 顺序读 盘。

  如果只是按照索引 a 的值的递增顺序查,那么按主键id的回表就不一定是顺序查,可能是随机访问,性能就会相对较差。

  大多数的数据都是按照主键递增顺序插入到 主键索引的 B+ 树上的,如果是按照主键的递增顺序查询,对磁盘的读就比较接近顺序读,可以提升性能。

  所以根据条件是普通索引 a 去查询数据,语句的执行流程优化:

  1)、根据索引 a ,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中;

  2)、将 read_rnd_buffer 中的 id 进行递增排序;

  3)、排序后的 id 数据,依次到主键 id 索引中查询记录,并作为结果返回。

  read_rnd_buffer 的大小是由 read_rnd_buffer_size 控制,执行步骤一,当放满了索引 a 的值,就执行第二三步骤,然后清空 read_rnd_buffer,重新执行步骤一。

  如果想使用 MRR 优化,就得设置 set optimizer_switch="mrr_cost_based=off"。

  使用了 MRR 优化,当 explain SQL 语句时,看 Extra 关键字段,会有一个Using MRR。

  主要是在索引 a 做条件的时候,是一个范围查询。所以才会有排序,提现顺序性的优势。

  引入Batched Key Access 算法

  MySQL 5.6版本后开始引入的该 BKA 算法,是对 NLJ 的优化。

  NLJ 的执行逻辑是从驱动表 t1 表一行行取出 a 的值,然后去被驱动表 t2 去做 join。

  原理类似 MRR 的原理,也是从驱动表 t1 里面取多行数据,去被驱动表 t2 去匹配。并且驱动表 t1 的数据是存在临时内存 join_buffer 里的。

  如果要执行 BKA 算法,先设置:set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

  BNL 算法的性能问题

  大表 join 操作虽然对 IO 有影响,但是在语句执行结束后,对 IO 的影响也就结束了。但是对 Buffer Pool 的影响就是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率。(会导致 Buffer Pool 的热数据被淘汰,影响内存命中率)

  BNL 转 BKA

  如果一个 SQL 语句是一个低频的 SQL 语句,为被驱动表创建一个索引就浪费。 但是不创建索引,走 BNL 算法,如果被驱动表数据量很大,也会很慢。

可以考虑使用临时表,流程:

  1)、把被驱动表 t2 中满足条件的数据放在临时表 tmp_t 中;

  2)、为了让 join 使用 BKA 算法,给临时表 tmp_t 的字段 b 上加上索引;

  3)、让驱动表 t1 和 tmp_t 做 join 操作。

  SQL 写法:

  create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;

  insert into temp_t select * from t2 where b>=1 and b<=2000;

  select * from t1 join temp_t on (t1.b=temp_t.b);

4、临时表

  临时表只存在于创建该临时表的线程里,对其他线程不可见。所以临时表之间可以重名。

  分库分表的跨库查询一般就是使用临时表。

  分库分表:把一个逻辑上的大表,分散到不同的数据库实例上。比如将一个大表 ht ,按字段 a,拆分成1024个分表,然后分布到 32 个数据库实例上。一般情况在分库分表系统都有一个中间层 proxy。

  当 SQL 语句包含了 a 字段的等值条件,那么就用 f 做分区 key。 这样能迅速定位数据在哪个分表里,发送该 SQL 去该表做查询。

  但是如果 SQL 语句有另一个条件索引 k ,并且有 order by 和 limit 条件。 SQL 语句只能到所有的分区去查找满足条件的所有行,然后做统一 order by 的操作。

  有两种思路,思路一

  在 proxy 层的进程代码中实现排序。从分库中拿到数据后,直接在内存中参与计算。但是缺点也比较明显:需要的开发工作量发,如果涉及到复杂的操作,比如 group by 和 join 这样的操作,对中间层的开发能力要求较高。并且对 proxy 层的压力较大,尤其很容易出现内存不够用和 CPU 瓶颈问题。

  另一种思路

  把分库拿到的数据汇总到一个 MySQL 实例的一个表中,然后再该表做逻辑操作。

  在实践中我们会直接把临时表放在32个分库的某一个上。

  因为创建临时表在 MySQL 5.7之后是在内存的临时表空间,命名规则是进程号+线程号+表名,所以不存在不同的线程之间的表重名的问题。

  在 session 结束的时候,对链表的每个临时表执行 drop temporary+表名 ,并且记录到 binlog 中。(记录到 binlog 是涉及到了主备复制)

5、使用内部临时表

  union 语句是把两个查询的子集的并集作为结果集返回,一般是要在内存中划出临时内存表存该2个子查询的结果集。(去重)

  union all 就不需要去重,所以也不需要临时表,只需要将2个子查询的结果集合并返回。

  group by 执行流程

  group by 也是使用了临时表,以什么分组。

  临时表的大小由 tmp_table_size 参数控制,默认16M。如果数据大于了该参数的值,那就会把内存临时表转成磁盘临时表。。磁盘临时表默认使用的也是 InnoDB 引擎。

  group by 逻辑都需要构造一个带唯一索引的表,执行代价比较高。

  group by 优化方法 -- 索引

  当数据是根据 group by 的字段为索引,排好序。 那么 SQL 语句就不需要临时表,也不需要排序了。

  group by 优化方法 -- 直接索引

  如果遇上不好做索引的场景,那就让 group by 直接用磁盘临时表。临时表是 B+ 数存储,存储效率不如数组高,那就使用数据来存。

  如果对 group by 语句的结果没有排序要求,要在语句后面加上 order by null

  尽量让 group bu 过程用上表的索引。

  尽量使用临时表,或者调大 tmp_table_size 参数。

  如果数据量实在太大,那就使用 SQL_BIG_RESULT 这个提示,告诉优化器直接选择排序算法得到 group by 的结果。

  InnoDB 表的数据都是放在主键索引上的,主键索引就是B+树。 在 select * 的时候,就会从叶子节点从左往右扫描。

  不建议在生成环境中使用内存表:锁粒度问题、数据持久化问题。

  内存表不支持行锁,只支持表锁。一张表只要有更新,就会堵住其他所有这个表的读写操作。

  数据放内存中,当数据库重启的时候,所有的内存表会被清空。

6、自增主键为什么不是连续的

  自增主键可以让主键索引尽量的保持递增顺序插入,避免页分裂,因此索引更紧凑。

  当前插入的主键为 null 的时候,下一行插入的数据的主键是上一行的主键值+2,所以主键可能不是连续的。

  表的结构定义放在后缀名为 .frm 的文件中,但是并不会保存自增值。

  MySQL 8.0 版本,自增值的变更记录再 redo log 中,重启的时候恢复重启之前的值。MySQL 5.7 及之前的版本,自增值保存在内存,重启后第一次打开表,回去找自增值的最大值 max(id),然后将 max(id) +1 作为这个表的当前自增值。

  如果字段 id 定义为 AUTO_INCREMENT,插入一行数据,如果主键 id 为0或者null 的时候,就把这个表当前的 AUTO_INCREMENT 值填到自增字段。如果插入数据时 id 制定了具体的值,就直接使用该值。

  在插入过程中,就算出现错误(唯一索引冲突等),自增值增加了,也不会改回去。所以就会产生自增值的 空洞 。

  所以 唯一索引冲突是导致自增主键 id 不连续的第一种原因

  事务回滚也会产生类似的现象,这就是第二种原因。

  不允许自增值回退,保证了递增,但是不能保证连续。

  自增锁并不是一个事务锁,而是每次申请完就马上释放,以便允许其他事务再申请。

7、复制表

  mysqldump 方法

  使用 mysqldump 命令将数据导出成一组 INSERT 语句,把结果输出到临时文件。此时一条 INSERT 语句里面会包含多个 value 对,执行速度会更快。

  导出 CSV 文件

  直接将结果导出成 .csv 文件。然后使用 load data 命令将数据导入到目标表中。

  物理拷贝方法

8、grant 之后要跟着 flush privileges

  grant 语句会同时修改数据表和内存,判断权限的时候使用的是内存数据。所以规范地使用 grant 和 revoke 语句,是不需要加上 flush privileges 语句的。

flush privileges 语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能不一致的情况下再使用。

  自增ID达到上限后的逻辑是:再申请下一个ID时,得到的值保持不变。如果键是唯一,就可能报主键冲突错误。

全局扩展

  如果用 left join 的话,左边的表不一定就是驱动表,如果需要 left join 的语义,就不能把被驱动表的字段放在 where 条件里做等值判断或不等值判断,必须都写在 on 里面。

该文章只是笔记,记录的是我自己大概的总结。

如果想看完整的知识点,可以去 极客时间app 上找 MySQL实战45讲 --- 林晓斌 老师的课程。

链接:https://time.geekbang.org/column/139

MySQL学习(四)Join 等开发常用的操作 --- 2019年2月的更多相关文章

  1. MySQL 学习四 SQL优化

    MySQL逻辑架构: 第一层:客户端层,连接处理,授权认证,安全等功能.   第二层:核心层,查询解析,分析,优化,缓存,内置函数(时间,数学,加密),存储过程,触发器,视图   第三层:存储引擎.负 ...

  2. Mysql学习总结(1)——常用sql语句汇总

    一.基础 1.说明:创建数据库 CREATE DATABASE database-name  2.说明:删除数据库 drop database dbname 3.说明:备份sql server --- ...

  3. MySql学习(四) —— 函数、视图

    注:该MySql系列博客仅为个人学习笔记. 本篇博客主要涉及MySql 函数(数学函数.字符串函数.日期时间函数.流程控制函数等),视图. 一.函数 1. 数学函数 对于数学函数,若发生错误,所有数学 ...

  4. MySQL(四)字段及常用函数

    一.字段 数据库表中的每一行叫做一个“记录”,每一个记录包含这行中的所有信息,但记录在数据库中并没有专门的记录名,常常用它所在的行数表示这是第几个记录. 在数据库中存放在表行列交叉处的数据叫做“值”, ...

  5. MySQL学习(四)

    1 MySQL日期和时间类型 创建一个包含DATE类型的表 mysql> create table test3( -> star varchar(20) not null default ...

  6. mysql学习之join用法

    转载  一张图看懂 SQL 的各种 join 用法 一.JOIN 使用介绍 下面例子使用的数据表如下: -- ---------------------------- -- Table structu ...

  7. mysql学习之join从句

    一.join从句共有5种类型 内连接(inner join) 全外连接(full outer join) 左外连接(left outer join) 右外连接(right outer join) 交叉 ...

  8. MySQL学习之路3-MySQL中常用数据类型

    MySQL中常用数据类型 字符型 存储字符型数据.例如姓名,地址,电话号码等.使用引号括起来,一般使用单引号. 常用类型: char(255) 定长字符串,最大长度255个字符. varchar(25 ...

  9. MYSQL学习笔记 (五)常用的聚合函数

    1.COUNT(e1) 语法:COUNT(e1) 参数:e1为一个表达式,可以是任意的数据类型 返回:返回数值型数据 作用:返回e1指定列不为空的记录总数 例子: 1)单独使用

随机推荐

  1. is not eligible for getting processed by all BeanPostProcessors

    BeanPostProcessor是控制Bean初始化开始和初始化结束的接口.换句话说实现BeanPostProcessor的bean会在其他bean初始化之前完成,BeanPostProcessor ...

  2. 使用JWT的OAuth2的SSO分析

    参考:https://github.com/spring-guides/tut-spring-security-and-angular-js/blob/master/oauth2/README.ado ...

  3. LeetCode Javascript实现 283. Move Zeroes 349. Intersection of Two Arrays 237. Delete Node in a Linked List

    283. Move Zeroes var moveZeroes = function(nums) { var num1=0,num2=1; while(num1!=num2){ nums.forEac ...

  4. 如何打开JSP文件/JS和JSP的区别/Servlet的本质是什么,是如何工作的?

    一:如何打开JSP文件 1.安装JAVA 2.安装TOMCAT——免费开源的JAVAWEB服务器 3.安装ECLIPSE 二:JS和JSP区别 名字: JS:JavaScript JSP:Java S ...

  5. Python cmp() 函数

    描述 cmp(x,y) 函数用于比较2个对象,如果 x < y 返回 -1, 如果 x == y 返回 0, 如果 x > y 返回 1. 语法 以下是 cmp() 方法的语法:cmp( ...

  6. Go语言Context(设计及分析)

    context简单概述: Go服务器的每个请求都有自己的goroutine,而有的请求为了提高性能,会经常启动额外的goroutine处理请求,当该请求被取消或超时,该请求上的所有goroutines ...

  7. C++中的内联函数和C中的宏定义的区别

    在C++中内联函数: 内联函数即是在函数的声明和和定义前面加上“inline”关键字,内联函数和常规函数一样,都是按照值来传递参数的,如果参数为表达式,如4.5+7.5,则函数将传递表达式的值(这里为 ...

  8. jenkins+ant+jmeter测试环境部署

    1.安装java 2.安装jenkins 3.下载apache-jmeter-4.0,解压后放在 /home/用户名/ 下 4.下载apache-ant-1.10.3,解压后放在 /home/用户名/ ...

  9. mysql5.7连接不上可能的问题(针对新安装的mysql5.7可能出现的问题)

    "ERROR 2003 (HY000): Can't connect to MySQL server on 'localhost' (10061)" 今天刚刚安装好的mysql5. ...

  10. SAP S4系统创建Customer和Vendor的BAPI

    对应的BAPI是:RFC_CVI_EI_INBOUND_MAIN SAP 又调皮了,又不安常理出牌!