高性能mysql学习总结笔记

MySQL 可以分为 Server 层和存储引擎层两部分。

    1.Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),
    所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
    
    2.而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。
    现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。

优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。比如你执行下面这样的语句,这个语句是执行两个表的 join:


查询过程
    select * from T where ID=10;
        比如我们这个例子中的表 T 中,ID 字段没有索引,那么执行器的执行流程是这样的:
        1.调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
        2.调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
        3.执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
    对于有索引的表,执行的逻辑也差不多。第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。
    
    你会在数据库的慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。


    
redo log(重做日志)和 binlog(归档日志)
    与查询流程不一样的是,更新流程还涉及两个重要的日志模块,它们正是我们今天要讨论的主角:redo log(重做日志)和 binlog(归档日志)。
    如果接触 MySQL,那这两个词肯定是绕不过的,我后面的内容里也会不断地和你强调。不过话说回来,redo log 和 binlog 在设计上有很多有意思的地方,这些设计思路也可以用到你自己的程序里。
    
    
重要的日志模块:redo log  
    是 InnoDB 引擎特有的日志
   《孔乙己》老板繁忙的时候用粉笔版记账,空闲的时候把粉笔版的账写到记账本,不然忙的时候要翻开记账本要很慢,尤其是赊账的那些客户

    同样,在 MySQL 里也有这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。
    为了解决这个问题,MySQL 的设计者就用了类似酒店掌柜粉板的思路来提升更新效率。
    
    具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(粉板)里面,并更新内存,这个时候更新就算完成了。
    同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。
    
    如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,粉板写满了,又怎么办呢?
    这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。
    
    
    与此类似,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。(有点像环形队列)

    有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe
    
    要理解 crash-safe 这个概念,可以想想我们前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。

重要的日志模块:binlog
    上面我们聊到的粉板 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。
    
    因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。
    而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
    
两种日志的对比:    
    1.redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
    2.redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
    3.redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

两阶段提交:
        将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"
        更新某行的数据,先更新到内存,再写redog日志(service层)(prepare状态),再写binlog(存储层),再提价事务,将relog改为commit状态
        
        如果在写入 redo log 处于 prepare 阶段之后、写 binlog 之前,发生了崩溃(crash),由于此时 binlog 还没写,redo log 也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog 还没写,所以也不会传到备库。到这里,大家都可以理解。
        大家出现问题的地方,也就是 binlog 写完,redo log 还没 commit 前发生 crash,那崩溃恢复的时候 MySQL 会怎么处理?
        我们先来看一下崩溃恢复时的判断规则:
            1.如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交;
            2.如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整:    
                a.  如果是,则提交事务;        
                b.  否则,回滚事务。
                
误删操作如何恢复数据:    
    前面我们说过了,binlog 会记录所有的逻辑操作,并且是采用“追加写”的形式。如果你的 DBA 承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有 binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
    
    当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:
    
    1.首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
    
    2.然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。
    
    3.这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。
    
    
redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,
表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。

sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。


事务:原子性、一致性、隔离性、持久性(一元吃个)
    
当数据库上有多个事务同时执行的时候,
就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。

隔离性与隔离级别:
    在谈隔离级别之前,你首先要知道,你隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。
    SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。下面我逐一为你解释:
    
    读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。(未提交事务,其他事务能读到变更)
    读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。    (提交事务后,其他事务才能读到变更)    
    可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。    
    串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
    
我们可以看到在不同的隔离级别下,数据库行为是有所不同的。
Oracle 数据库的默认隔离级别其实就是“读提交”,因此对于一些从 Oracle 迁移到 MySQL 的应用,为保证数据库隔离级别的一致,你一定要记得将 MySQL 的隔离级别设置为“读提交”。
配置的方式是,将启动参数 transaction-isolation 的值设置成 READ-COMMITTED。你可以用 show variables 来查看当前的值。


总结来说,存在即合理,哪个隔离级别都有它自己的使用场景,你要根据自己的业务情况来定。我想

你可能会问那什么时候需要“可重复读”的场景呢
假设你在管理一个个人银行账户表。一个表存了每个月月底的余额,一个表存了账单明细。这时候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。

这时候使用“可重复读”隔离级别就很方便。事务启动时的视图可以认为是静态的,不受其他事务更新的影响。

索引的类型:哈希表、有序数组和搜索树。
    
    索引的出现是为了提高查询效率,但是实现索引的方式却有很多种,所以这里也就引入了索引模型的概念。
    可以用于提高读写效率的数据结构很多,这里我先给你介绍三种常见、也比较简单的数据结构,它们分别是哈希表、有序数组和搜索树。
    
    
哈希表:其实就是无需数组
    哈希表是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的值即 key,就可以找到其对应的值即 Value。
    哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。
    
    哈希冲突:不可避免地,多个 key 值经过哈希函数的换算,会出现同一个值的情况。处理这种情况的一种方法是,拉出一个链表。
    

    需要注意的是,key值并不是递增的,这样做的好处是增加新的数据时速度会很快,只需要往后追加。但缺点是,因为不是有序的,所以哈希索引做区间查询的速度是很慢的。
    
    你可以设想下,如果你现在要找身份证号在 [ID_card_X, ID_card_Y] 这个区间的所有用户,就必须全部扫描一遍了。
    
    哈希表这种结构适用于只有等值查询的场景,比如 Memcached 及其他一些 NoSQL 引擎。
    
    
有序数组:
        有序数组在等值查询和范围查询场景中的性能就都非常优秀
        key值是递增的,用二分法就可以快速得到,这个时间复杂度是 O(log(N))。
        同时很显然,这个索引结构支持范围查询。你要查身份证号在 [ID_card_X, ID_card_Y] 区间的 User,可以先用二分法找到 ID_card_X(如果不存在 ID_card_X,就找到大于 ID_card_X 的第一个 User),然后向右遍历,直到查到第一个大于 ID_card_Y 的身份证号,退出循环。
        
        如果仅仅看查询效率,有序数组就是最好的数据结构了。但是,在需要更新数据的时候就麻烦了,你往中间插入一个记录就必须得挪动后面所有的记录,成本太高。
        
        有序数组索引只适用于静态存储引擎,比如你要保存的是 2017 年某个城市的所有人口信息,这类不会再修改的数据。
        
二叉搜索树:
        二叉树是搜索效率最高的,但是实际上大多数的数据库存储却并不使用二叉树。其原因是,索引不止存在内存中,还要写到磁盘上。
        为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。那么,我们就不应该使用二叉树,而是要使用“N 叉”树。这里,“N 叉”树中的“N”取决于数据块的大小。
        
        
        以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。
        考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。
        其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。
        
InnoDB 索引模型:
    每一个索引在 InnoDB 里面对应一棵 B+ 树。
    
    根据叶子节点的内容,索引类型分为主键索引和非主键索引。
    
    主键索引:
        主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
    非主键索引:
        非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
        
        
    区别:

        假设,我们有一个主键列为 ID 的表,表中有字段 name,k,并且在 k 上有索引。
        
        如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
        
        如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。
        
        也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
        
    索引的维护:
        B+ 树为了维护索引有序性,如果给表插入的新的数据的主键在出在已有数据的主键范围的中间,需要逻辑上挪动后面的数据,空出位置。
        而更糟的情况是,如果新数据所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能自然会受影响。
        除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。
        当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。
        
        
    主键自增:
        自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
        
        如果是高并发的插入操作,主键索引是按顺序插入,会造成明显的锁竞争
        
        如果是UUID作为主键,因为是随机的,因为主键索引是自增的,为了有序,插入的过程,就需要挪动其他的操作
        
    主键长度越小,普通索引(二级索引)的叶子节点就越小,普通索引占用的空间也就越小。
    
    
    这时候我们就要优先考虑上一段提到的“尽量使用主键查询”原则,直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。
    
    
    覆盖索引:
        如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。
        也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。
        
        由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
        
    联合索引:
        我们知道,身份证号是市民的唯一标识。也就是说,如果有根据身份证号查询市民信息的需求,我们只要在身份证号字段上建立索引就够了。
        而再建立一个(身份证号、姓名)的联合索引,是不是浪费空间?
        
        如果现在有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间。
        
        当然,索引字段的维护总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这正是业务 DBA,或者称为业务数据架构师的工作。
        
        
    最左前缀原则:
        看到这里你一定有一个疑问,如果为每一种查询都设计一个索引,索引是不是太多了。
        如果我现在要按照市民的身份证号去查他的家庭地址呢?虽然这个查询需求在业务中出现的概率不高,但总不能让它走全表扫描吧?
        反过来说,单独为一个不频繁的请求创建一个(身份证号,地址)的索引又感觉有点浪费。应该怎么做呢?
        
        为了直观地说明这个概念,我们用(name,age)这个联合索引(其实也是二级索引,只是索引里面包含了key是(name,age),value存的是主键)来分析。
        
        ("李四",20) ("王五",10) ("张六",30) ("张三",10) ("张三",10) ("张三",20)
           ID1            ID2            ID3            ID4            ID5            ID6
           
        可以看到,索引项是按照索引定义里面出现的字段顺序排序的。
        
        当你的逻辑需求是查到所有名字是“张三”的人时,可以快速定位到 ID4,然后向后遍历得到所有需要的结果。
        
        如果你要查的是所有名字第一个字是“张”的人,你的 SQL 语句的条件是"where name like ‘张 %’"。
        这时,你也能够用上这个索引,查找到第一个符合条件的记录是 ID3,然后向后遍历,直到不满足条件为止。(这里侧面说明模糊查询也是走索引的)
        
        可以看到,不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。
        这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
        
        
        这里我们的评估标准是,索引的复用能力。因为可以支持最左前缀,所以当已经有了 (a,b) 这个联合索引后,一般就不需要单独在 a 上建立索引了。因此,
        第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
        
        那么,如果既有联合查询,又有基于 a、b 各自的查询呢?
        查询条件里面只有 b 的语句,是无法使用 (a,b) 这个联合索引的,这时候你不得不维护另外一个索引,也就是说你需要同时维护 (a,b)、(b) 这两个索引。
        
        这时候,我们要考虑的原则就是空间了。比如上面这个市民表的情况,name 字段是比 age 字段大的 ,那我就建议你创建一个(name,age) 的联合索引和一个 (age) 的单字段索引。
    

    给字符串加索引:
    前缀索引:
        定义字符串的一部分作为索引。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
        例如:
            mysql> alter table SUser add index index1(email);
            或
            mysql> alter table SUser add index index2(email(6));
            
        第一个语句创建的 index1 索引里面,包含了每个记录的整个字符串;而第二个语句创建的 index2 索引里面,对于每个记录都是只取前 6 个字节。
        
        email(6) 这个索引结构中每个邮箱字段都只取前 6 个字节(即:zhangs),所以占用的空间会更小,这就是使用前缀索引的优势。
        但,这同时带来的损失是,可能会增加额外的记录扫描次数。
        
    
        使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。
        
        创建前缀索引时,确定应该使用多长的前缀方法:
            实际上,我们在建立索引时关注的是区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。因此,我们可以通过统计索引上有多少个不同的值来判断要使用多长的前缀。
            1.算出这个列上有多少个不同的值:
                mysql> select count(distinct email) as L from SUser;
            2.然后,依次选取不同长度的前缀来看这个值,比如我们要看一下 4~7 个字节的前缀索引,可以用这个语句:
                mysql> select 
                        count(distinct left(email,4))as L4,
                        count(distinct left(email,5))as L5,
                        count(distinct left(email,6))as L6,
                        count(distinct left(email,7))as L7,
                    from SUser;
            3.预先设定一个可以接受的损失比例,比如 5%。然后,在返回的 L4~L7 中,找出不小于 L * 95% 的值,假设这里 L6、L7 都满足,你就可以选择前缀长度为 6。

        前缀索引对覆盖索引的影响:
            1.select id,email from SUser where email='[email protected]';
            2.select id,name,email from SUser where email='[email protected]';
        两个sql相比,这个语句只要求返回 id 和 email 字段。
            所以,如果使用 index1(即 email 整个字符串的索引结构)的话,可以利用覆盖索引,从 index1 查到结果后直接就返回了,不需要回到 ID 索引再去查一次。
            而如果使用 index2(即 email(6) 索引结构)的话,就不得不回到 ID 索引再去判断 email 字段的值。
            
            即使你将 index2 的定义修改为 email(18) 的前缀索引,这时候虽然 index2 已经包含了所有的信息,但 InnoDB 还是要回到 id 索引再查一下,因为系统并不确定前缀索引的定义是否截断了完整信息。
            
            所以:前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。
            
            
    其他方式:        
        对于类似于邮箱这样的字段来说,使用前缀索引的效果可能还不错。但是,遇到前缀的区分度不够好的情况时,我们要怎么办呢?
        比如,我们国家的身份证号,一共 18 位,其中前 6 位是地址码,所以同一个县的人的身份证号前 6 位一般会是相同的。
        假设你维护的数据库是一个市的公民信息系统,这时候如果对身份证号做长度为 6 的前缀索引的话,这个索引的区分度就非常低了。
        
        按照我们前面说的方法,可能你需要创建长度为 12 以上的前缀索引,才能够满足区分度要求。
        但是,索引选取的越长,占用的磁盘空间就越大,相同的数据页能放下的索引值就越少,搜索的效率也就会越低。
        那么,如果我们能够确定业务需求里面只有按照身份证进行等值查询的需求,还有没有别的处理方法呢?这种方法,既可以占用更小的空间,也能达到相同的查询效率。
        
        第一种方式是使用倒序存储:
            如果你存储身份证号的时候把它倒过来存,每次查询的时候,你可以这么写:
            mysql> select field_list from t where id_card = reverse('input_id_card_string');
            由于身份证号的最后 6 位没有地址码这样的重复逻辑,所以最后这 6 位很可能就提供了足够的区分度。当然了,实践中你不要忘记使用 count(distinct) 方法去做个验证。
        第二种方式是使用 hash 字段:
            你可以在表上再创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引。
            mysql> alter table t add id_card_crc int unsigned, add index(id_card_crc);
            
            然后每次插入新记录的时候,都同时用 crc32() 这个函数得到校验码填到这个新字段。
            由于校验码可能存在冲突,也就是说两个不同的身份证号通过 crc32() 函数得到的结果可能是相同的,所以你的查询语句 where 部分要判断 id_card 的值是否精确相同。
            
            mysql> select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
            这样,索引的长度变成了 4 个字节,比原来小了很多。
            
        使用倒序存储和使用 hash 字段这两种方法的异同点。
            首先,它们的相同点是,都不支持范围查询。倒序存储的字段上创建的索引是按照倒序字符串的方式排序的,
            已经没有办法利用索引方式查出身份证号码在 [ID_X, ID_Y] 的所有市民了。同样地,hash 字段的方式也只能支持等值查询。
            
            从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而 hash 字段方法需要增加一个字段。当然,倒序存储方式使用 4 个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个 hash 字段也差不多抵消了。
            
            在 CPU 消耗方面,倒序方式每次写和读的时候,都需要额外调用一次 reverse 函数,而 hash 字段的方式需要额外调用一次 crc32() 函数。如果只从这两个函数的计算复杂度来看的话,reverse 函数额外消耗的 CPU 资源会更小些。
            
        总结:
            1.直接创建完整索引,这样可能比较占用空间;
            2.创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;
            3.倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题;
            4.创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描。
            
        


        
        
普通索引和唯一索引如何选择:
    查询过程:
        假设,执行查询的语句是 select id from T where k=5。这个查询语句在索引树上查找的过程,
        先是通过 B+ 树从树根开始,按层搜索到叶子节点,也就是图中右下角的这个数据页,然后可以认为数据页内部通过二分法来定位记录。
        
        普通索引:
            1.对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。
        唯一索引:
            2.对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
            
        那么,这个不同带来的性能差距会有多少呢?答案是,微乎其微。
        
        你知道的,InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。
    
        因为引擎是按页读写的,所以说,当找到 k=5 的记录的时候,它所在的数据页就都在内存里了。那么,对于普通索引来说,要多做的那一次“查找和判断下一条记录”的操作,就只需要一次指针寻找和一次计算。
        
    更新过程:
        change buffer:
        
        1.数据页在内存中:
            就直接更新
        2.数据页还没有在内存:
            更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。同时会持久化日志到磁盘中
            
            在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
    
            merge:
                将原数据页和change buffer数据合并的过程
                
                除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。
            
            显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。
            而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。
            
        change buffer使用条件:
            唯一索引:(没必要使用)
                所有的更新操作都要先判断这个操作是否违反唯一性约束。
                比如,要插入 (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。
            普通索引:
                change buffer 用的是 buffer pool 里的内存,因此不能无限增大。
                change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。
            
            如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的:
                这个记录要更新的目标页在内存中:
                    唯一索引:
                        对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束
                    普通所以:
                        对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。
                    这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU 时间。
                这个记录要更新的目标页不在内存中:
                    唯一索引:
                        对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束
                    普通索引:
                        对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。
            将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。
            
        change buffer使用场景:写多读少的业务
        
            因为 merge 的时候是真正进行数据更新的时刻,而 change buffer 的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。
                    
            写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。    
                
            反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代价。所以,对于这种业务模式来说,change buffer 反而起到了副作用。
            
    总结:
        普通索引和唯一索引应该怎么选择。其实,这两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以,我建议你尽量选择普通索引。
        如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭 change buffer。而在其他情况下,change buffer 都能提升更新性能。
        在实际使用中,你会发现,普通索引和 change buffer 的配合使用,对于数据量大的表的更新优化还是很明显的。
        
        
    mysql> insert into t(id,k) values(id1,k1),(id2,k2);
    分析这条更新语句,你会发现它涉及了四个部分:内存、redo log(ib_log_fileX)、 数据表空间(t.ibd)、系统表空间(ibdata1)。
        1.Page 1 在内存中,直接更新内存;
        2.Page 2 没有在内存中,就在内存的 change buffer 区域,记录下“我要往 Page 2 插入一行”这个信息
        3.将上述两个动作记入 redo log 中
        
    做完上面这些,事务就可以完成了。所以,你会看到,执行这条更新语句的成本很低,就是写了两处内存,然后写了一处磁盘(两次操作合在一起写了一次磁盘),而且还是顺序写的。
    
    比如,我们现在要执行 select * from t where k in (k1, k2)。
    如果读语句发生在更新语句后不久,内存中的数据都还在,那么此时的这两个读操作就与系统表空间(ibdata1)和 redo log(ib_log_fileX)无关了。
        1.读 Page 1 的时候,直接从内存返回
        2.要读 Page 2 的时候,需要把 Page 2 从磁盘读入内存中,然后应用 change buffer 里面的操作日志,生成一个正确的版本并返回结果。
    redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。
            
锁:
    今天我要跟你聊聊 MySQL 的锁。数据库锁设计的初衷是处理并发问题。作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。而锁就是用来实现这些访问规则的重要数据结构。
    
    锁的类型:
        根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类
        
    全局锁:
        顾名思义,全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。
        当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
        应用:
            全局锁的典型使用场景是,做全库逻辑备份。
        弊端:
            如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆;
            如果你在从库上备份,那么备份期间从库不能执行主库同步过来的 binlog,会导致主从延迟。
            
        例子:
                现在发起一个逻辑备份。假设备份期间,有一个用户,他购买了一门课程,业务逻辑里就要扣掉他的余额,然后往已购课程里面加上一门课。
                如果时间顺序上是先备份账户余额表 (u_account),然后用户购买,然后备份用户课程表 (u_course),
                
                可以看到,这个备份结果里,用户 A 的数据状态是“账户余额没扣,但是用户课程表里面已经多了一门课”。(因为备份的过程如果不加锁,表的数据是可以更新的)
                如果后面用这个备份来恢复数据的话,用户 A 就发现,自己赚了。
                
                也就是说,不加锁的话,备份系统备份的得到的库不是一个逻辑时间点,这个视图是逻辑不一致的。
                说到视图你肯定想起来了,我们在前面讲事务隔离的时候,其实是有一个方法能够拿到一致性视图的,对吧?
                是的,就是在可重复读隔离级别下开启一个事务。
                
                官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。
                而由于 MVCC 的支持,这个过程中数据是可以正常更新的。
                
    表级锁:
        MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
        表锁:
            语法是 lock tables … read/write。
            与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
            
            举个例子, 如果在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句,则其他线程写 t1、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能访问其他表。
            
            在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。而对于 InnoDB 这种支持行锁的引擎,一般不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面还是太大。
        
        元数据锁:
            是 MDL(metadata lock)。
            MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性。
            你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
            因此,在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
            
            虽然 MDL 锁是系统默认会加的,但却是你不能忽略的一个机制。比如下面这个例子,我经常看到有人掉到这个坑里:给一个小表加个字段,导致整个库挂了。
            
            例子:
                session A 是查询,session B也是查询,session C也给表加一个字段(这里的3个session时间是顺序的)
                我们可以看到 session A 先启动,这时候会对表 t 加一个 MDL 读锁。由于 session B 需要的也是 MDL 读锁,因此可以正常执行。
                
                之后 session C 会被 blocked,是因为 session A 的 MDL 读锁还没有释放,而 session C 需要 MDL 写锁,因此只能被阻塞。
                
                如果只有 session C 自己被阻塞还没什么关系,但是之后所有要在表 t 上新申请 MDL 读锁的请求也会被 session C 阻塞。前面我们说了,所有对表的增删改查操作都需要先申请 MDL 读锁,就都被锁住,等于这个表现在完全不可读写了。
                
                
                但考虑一下这个场景。如果你要变更的表是一个热点表,虽然数据量不大,但是上面的请求很频繁,而你不得不加个字段,你该怎么做呢?
                这时候 kill 可能未必管用,因为新的请求马上就来了。比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。
                
                MariaDB 已经合并了 AliSQL 的这个功能,所以这两个开源分支目前都支持 DDL NOWAIT/WAIT n 这个语法。
                
                ALTER TABLE tbl_name NOWAIT add column ...
                ALTER TABLE tbl_name WAIT N add column ... 
                
    全局锁主要用在逻辑备份过程中。对于全部是 InnoDB 引擎的库,我建议你选择使用–single-transaction 参数,对应用会更友好。
    表锁一般是在数据库引擎不支持行锁的时候才会被用到的。如果你发现你的应用程序里有 lock tables 这样的语句,你需要追查一下,比较可能的情况是:
        要么是你的系统现在还在用 MyISAM 这类不支持事务的引擎,那要安排升级换引擎;
        要么是你的引擎升级了,但是代码还没升级。我见过这样的情况,最后业务开发就是把 lock tables 和 unlock tables 改成 begin 和 commit,问题就解决了。
        
    MDL 会直到事务提交才释放,在做表结构变更的时候,你一定要小心不要导致锁住线上查询和更新。
    
    行级锁:
        MySQL 的行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如 MyISAM 引擎就不支持行锁。
        不支持行锁意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任何时刻只能有一个更新在执行,这就会影响到业务并发度。
        InnoDB 是支持行锁的,这也是 MyISAM 被 InnoDB 替代的重要原因之一。
        
        顾名思义,行锁就是针对数据表中行记录的锁。这很好理解,比如事务 A 更新了一行,而这时候事务 B 也要更新同一行,则必须等事务 A 的操作完成后才能进行更新。
        
        两阶段锁:
            在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
            
            知道了这个设定,对我们使用事务有什么帮助呢?那就是,
            如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
            
            例子:
                假设你负责实现一个电影票在线交易业务,顾客 A 要在影院 B 购买电影票。我们简化一点,这个业务需要涉及到以下操作:
                    1.从顾客 A 账户余额中扣除电影票价;
                    2.给影院 B 的账户余额增加这张电影票价;
                    3.记录一条交易日志。
                    
                也就是说,要完成这个交易,我们需要 update 两条记录,并 insert 一条记录。当然,为了保证交易的原子性,
                我们要把这三个操作放在一个事务中。那么,你会怎样安排这三个语句在事务中的顺序呢?
                试想如果同时有另外一个顾客 C 要在影院 B 买票,那么这两个事务冲突的部分就是语句 2 了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。
                
                根据两阶段锁协议,不论你怎样安排语句顺序,所有的操作需要的行锁都是在事务提交的时候才释放的。
                所以,如果你把语句 2 安排在最后,比如按照 3、1、2 这样的顺序,那么影院账户余额这一行的锁时间就最少。这就最大程度地减少了事务之间的锁等待,提升了并发度。
                
        好了,现在由于你的正确设计,影院余额这一行的行锁在一个事务中不会停留很长时间。但是,这并没有完全解决你的困扰。
        如果这个影院做活动,可以低价预售一年内所有的电影票,而且这个活动只做一天。于是在活动时间开始的时候,你的 MySQL 就挂了。
        你登上服务器一看,CPU 消耗接近 100%,但整个数据库每秒就执行不到 100 个事务。这是什么原因呢?
    死锁:
        例:
                事务A:                    事务B
                begin                    
                更新第一行数据            begin
                                        更新第二行数据
                更新第二行数据
                                        更新第一行数据
                                    
                                    
        这时候,事务 A 在等待事务 B 释放 id=2 的行锁,而事务 B 在等待事务 A 释放 id=1 的行锁。 
        事务 A 和事务 B 在互相等待对方的资源释放,就是进入了死锁状态。
        当出现死锁以后,有两种策略:
            一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
            另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
            
            
        在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,
        然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。
        
        但是,我们又不可能直接把这个时间设置成一个很小的值,比如 1s。这样当出现死锁的时候,确实很快就可以解开,
        但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。
        
        
        所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 innodb_deadlock_detect 的默认值本身就是 on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。
        
        你可以想象一下这个过程:每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。
        
        每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是 O(n) 的操作。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。
        虽然最终检测的结果是没有死锁,但是这期间要消耗大量的 CPU 资源。因此,你就会看到 CPU 利用率很高,但是每秒却执行不了几个事务。
        
    减少锁冲突:
        1.一种头痛医头的方法,就是如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。
        
        2.另一个思路是控制并发度。
            根据上面的分析,你会发现如果并发能够控制住,比如同一行同时最多只有 10 个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。
            一个直接的想法就是,在客户端做并发控制。但是,你会很快发现这个方法不太可行,因为客户端很多。我见过一个应用,有 600 个客户端,这样即使每个客户端控制到只有 5 个并发线程,汇总到数据库服务端以后,峰值并发数也可能要达到 3000。
            因此,这个并发控制要做在数据库服务端。如果你有中间件,可以考虑在中间件实现;如果你的团队有能修改 MySQL 源码的人,也可以做在 MySQL 里面。基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。
            
        3.将一行数据分成多行数据来处理
            你可以考虑通过将一行改成逻辑上的多行来减少锁冲突。还是以影院账户为例,可以考虑放在多条记录上,
            比如 10 个记录,影院的账户总额等于这 10 个记录的值的总和。这样每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成原来的 1/10,可以减少锁等待个数,也就减少了死锁检测的 CPU 消耗。

            这个方案看上去是无损的,但其实这类方案需要根据业务逻辑做详细设计。如果账户余额可能会减少,比如退票逻辑,那么这时候就需要考虑当一部分行记录变成 0 的时候,代码要有特殊处理。
            
        
    问题:
    最后,我给你留下一个问题吧。如果你要删除一个表里面的前 10000 行数据,有以下三种方法可以做到:
        第一种,直接执行 delete from T limit 10000;
        第二种,在一个连接中循环执行 20 次 delete from T limit 500;
        第三种,在 20 个连接中同时执行 delete from T limit 500。
    
    第一种方式(即:直接执行 delete from T limit 10000)里面,单个语句占用时间长,锁的时间也比较长;而且大事务还会导致主从延迟。
    确实是这样的,第二种方式是相对较好的。
    
    第三种方式(即:在 20 个连接中同时执行 delete from T limit 500),会人为造成锁冲突。
    
    @Knight²º¹⁸ 提到了如果可以加上特定条件,将这 10000 行天然分开,可以考虑第三种。是的,实际上在操作的时候我也建议你尽量拿到 ID 再删除。
    
    
count()操作:
    对于 count(主键 id) 来说:
            InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。
    对于 count(1) 来说:
            InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。
            
    单看这两个用法的差别的话,你能对比出来,count(1) 执行得要比 count(主键 id) 快。因为从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作。
    
    对于 count(字段) 来说
        如果这个“字段”是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;
        如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。
        
        
    但是 count(*) 是例外
        并不会把全部字段取出来,而是专门做了优化,不取值。count(*) 肯定不是 null,按行累加。
         MySQL 已经优化过 count(*) 了,你直接使用这种用法就可以了。
    所以结论是:按照效率排序的话,count(字段)<count(主键 id)<count(1)≈count(*),所以我建议你,尽量使用 count(*)。
    
    用缓存系统保存计数:(精度可能会不准确)
        你可以用一个 Redis 服务来保存这个表的总行数。这个表每被插入一行 Redis 计数就加 1,每被删除一行 Redis 计数就减 1。
        Redis 异常重启以后,到数据库里面单独执行一次 count(*) 获取真实的行数,再把这个值写回到 Redis 里就可以了。
        异常重启毕竟不是经常出现的情况,这一次全表扫描的成本,还是可以接受的。
        
        将计数保存在缓存系统中的方式,还不只是丢失更新的问题。即使 Redis 正常工作,这个值还是逻辑上不精确的。
        因为你redis异常重启后,数据丢了,数据库里面单独执行一次 count(*) 获取真实的行数后,在把这个值写回到 Redis 过程中,数据库可能又有新的数据插入了
        
    如果我们把这个计数直接放到数据库里单独的一张计数表 C 中,就可以保证数据一致性了
        
        
order by工作机制:
    在 city 字段上创建索引之后,我们用 explain 命令来看看这个语句的执行情况。
    Extra 这个字段中的“Using filesort”表示的就是需要排序,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。
    
    
    以我们前面举例用过的市民表为例,假设你要查询城市是“杭州”的所有人名字,并且按照姓名排序返回前 1000 个人的姓名、年龄。
        CREATE TABLE `t` (
        `id` int(11) NOT NULL,
        `city` varchar(16) NOT NULL,
        `name` varchar(16) NOT NULL,
        `age` int(11) NOT NULL,
        `addr` varchar(128) DEFAULT NULL,
        PRIMARY KEY (`id`),
        KEY `city` (`city`)
        ENGINE=InnoDB;
    select city,name,age from t where city='杭州' order by name limit 1000  ;
    从图中可以看到,满足 city='杭州’条件的行,是从 ID_X 到 ID_(X+N) 的这些记录。
    全字段排序:
        1.初始化 sort_buffer,确定放入 name、city、age 这三个字段(因为查询返回的字段中有这三个字段)
        2.从索引 city 找到第一个满足 city='杭州’条件的主键 id,也就是图中的 ID_X;
        3.到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer 中;
        4.从索引 city 取下一个记录的主键 id;
        5.重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的 ID_Y;
        6.对 sort_buffer 中的数据按照字段 name 做快速排序;
        7.按照排序结果取前 1000 行返回给客户端。
    
        
    图中“按 name 排序”这个动作,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数 sort_buffer_size。
    sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。
    但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
    
    你可以用下面介绍的方法,来确定一个排序语句是否使用了临时文件。
    
        /* 打开 optimizer_trace,只对本线程有效 */
        SET optimizer_trace='enabled=on'; 
        
        /* @a 保存 Innodb_rows_read 的初始值 */
        select VARIABLE_VALUE into @a from  performance_schema.session_status where variable_name = 'Innodb_rows_read';
        
        /* 执行语句 */
        select city, name,age from t where city='杭州' order by name limit 1000; 
        
        /* 查看 OPTIMIZER_TRACE 输出 */
        SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
        
        /* @b 保存 Innodb_rows_read 的当前值 */
        select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';
        
        /* 计算 Innodb_rows_read 差值 */
        select @b-@a;
        
        
        这个方法是通过查看 OPTIMIZER_TRACE 的结果来确认的,你可以从 number_of_tmp_files 中看到是否使用了临时文件。
        
        number_of_tmp_files 表示的是,排序过程中使用的临时文件数。你一定奇怪,为什么需要 12 个文件?内存放不下时,就需要使用外部排序,外部排序一般使用归并排序算法。可以这么简单理解,
        MySQL 将需要排序的数据分成 12 份,每一份单独排序后存在这些临时文件中。然后把这 12 个有序文件再合并成一个有序的大文件。
        如果 sort_buffer_size 超过了需要排序的数据量的大小,number_of_tmp_files 就是 0,表示排序可以直接在内存中完成。
        否则就需要放在临时文件中排序。sort_buffer_size 越小,需要分成的份数越多,number_of_tmp_files 的值就越大。
        
        我们的示例表中有 4000 条满足 city='杭州’的记录,所以你可以看到 examined_rows=4000,表示参与排序的行数是 4000 行。
        sort_mode 里面的 packed_additional_fields 的意思是,排序过程对字符串做了“紧凑”处理。即使 name 字段的定义是 varchar(16),在排序过程中还是要按照实际长度来分配空间的。
        
        这里需要注意的是,为了避免对结论造成干扰,我把 internal_tmp_disk_storage_engine 设置成 MyISAM。否则,select @b-@a 的结果会显示为 4001。
        
        这是因为查询 OPTIMIZER_TRACE 这个表时,需要用到临时表,而 internal_tmp_disk_storage_engine 的默认值是 InnoDB。如果使用的是 InnoDB 引擎的话,把数据从临时表取出来的时候,会让 Innodb_rows_read 的值加 1。
        
        
    简单总结流程就是:
        1.需要返回什么字段的数据,sort_buffer 中都要定义有,通过索引满足条件的数据读取放到sort_buffer再进行排序,
        2.如果sort_buffer放不下所有数据,则需要用到磁盘排序,会用到多个文件排序,因为是用的归并排序算法,
        然后取前1000条数据返回
        
        
    rowid排序:
        在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操作都是在 sort_buffer 和临时文件中执行的。
        但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。
        
        所以如果单行很大,这个方法效率不够好。
        
        SET max_length_for_sort_data = 16;
        max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。
        
        city、name、age 这三个字段的定义总长度是 36,我把 max_length_for_sort_data 设置为 16,我们再来看看计算过程有什么改变。
        
        新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id。
        
        但这时,排序的结果就因为少了 city 和 age 字段的值,不能直接返回了,整个执行流程就变成如下所示的样子:
            1.初始化 sort_buffer,确定放入两个字段,即 name 和 id;
            2.从索引 city 找到第一个满足 city='杭州’条件的主键 id,也就是图中的 ID_X;
            3.到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中;
            4.从索引 city 取下一个记录的主键 id;
            5.重复步骤 3、4 直到不满足 city='杭州’条件为止,也就是图中的 ID_Y;
            6.对 sort_buffer 中的数据按照字段 name 进行排序;
            7.遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。
            
        简单总结流程:
            rowid算法,因为字段不够放sort_buffer进内存了,所以只放了需要排序的字段和id,通过所以把满足条件的数据放进sort_buffer后,排序完后遍历排序结果,需要回表通过主键id取回每前1000行数据
            
        首先,图中的 examined_rows 的值还是 4000,表示用于排序的数据是 4000 行。但是 select @b-@a 这个语句的值变成 5000 了。
        因为这时候除了排序过程外,在排序完成后,还要根据 id 去原表取值。由于语句是 limit 1000,因此会多读 1000 行。
        
        从 OPTIMIZER_TRACE 的结果中,你还能看到另外两个信息也变了。
        sort_mode 变成了 <sort_key, rowid>,表示参与排序的只有 name 和 id 这两个字段。
        number_of_tmp_files 变成 10 了,是因为这时候参与排序的行数虽然仍然是 4000 行,但是每一行都变小了,因此需要排序的总数据量就变小了,需要的临时文件也相应地变少了。
        
        
    全字段排序 VS rowid 排序:
        如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
        如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
        
        这也就体现了 MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。
        对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。
        
        
    order by优化:
        优化思考:
            看到这里,你就了解了,MySQL 做排序是一个成本比较高的操作。那么你会问,是不是所有的 order by 都需要排序操作呢?
            如果不排序就能得到正确的结果,那对系统的消耗会小很多,语句的执行时间也会变得更短。
            
            其实,并不是所有的 order by 语句,都需要排序操作的。从上面分析的执行过程,我们可以看到,MySQL 之所以需要生成临时表,并且在临时表上做排序操作,
            其原因是原来的数据都是无序的。
            你可以设想下,如果能够保证从 city 这个索引上取出来的行,天然就是按照 name 递增排序的话,是不是就可以不用再排序了呢?
        
        创建联合索引:
            所以,我们可以在这个市民表上创建一个 city 和 name 的联合索引,(原先的索引是city,但是因为需要用name排序,所以创建的联合索引是city 和 name)
            对应的 SQL 语句是:
            
            alter table t add index city_user(city, name);
            这样整个查询过程的流程就变成了:
                1.从索引 (city,name) 找到第一个满足 city='杭州’条件的主键 id;
                2.到主键 id 索引取出整行,取 name、city、age 三个字段的值,作为结果集的一部分直接返回;
                3.从索引 (city,name) 取下一个记录主键 id;
                4.重复步骤 2、3,直到查到第 1000 条记录,或者是不满足 city='杭州’条件时循环结束。
                
            可以看到,这个查询过程不需要临时表,也不需要排序。接下来,我们用 explain 的结果来印证一下。
            
            从图中可以看到,Extra 字段中没有 Using filesort 了,也就是不需要排序了。而且由于 (city,name) 这个联合索引本身有序,
            所以这个查询也不用把 4000 行全都读一遍,只要找到满足条件的前 1000 条记录就可以退出了(然然执行计划中rows=4000,但这个是预估)。也就是说,在我们这个例子里,只需要扫描 1000 次。
        
        覆盖索引:
            是指,索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。
            进一步优化:
                针对这个查询,我们可以创建一个 city、name 和 age 的联合索引,(因为查询语句返回的数据要这个三个字段)
                对应的 SQL 语句就是:
                alter table t add index city_user_age(city, name, age);
            这时,对于 city 字段的值相同的行来说,还是按照 name 字段的值递增排序的,此时的查询语句也就不再需要排序了。这样整个查询语句的执行流程就变成了:
                1.从索引 (city,name,age) 找到第一个满足 city='杭州’条件的记录,取出其中的 city、name 和 age 这三个字段的值,作为结果集的一部分直接返回;
                2.从索引 (city,name,age) 取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;
                3.从索引 (city,name,age) 取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;
                
            然后,我们再来看看 explain 的结果。
            可以看到,Extra 字段里面多了“Using index”,表示的就是使用了覆盖索引,性能上会快很多。
            
            当然,这里并不是说要为了每个查询能用上覆盖索引,就要把语句中涉及的字段都建上联合索引,毕竟索引还是有维护代价的。这是一个需要权衡的决定。
            
        思考:
            假设你的表里面已经有了 city_name(city, name) 这个联合索引,然后你要查杭州和苏州两个城市中所有的市民的姓名,并且按名字排序,显示前 100 条记录。如果 SQL 查询语句是这么写的 :
                mysql> select * from t where city in ('杭州'," 苏州 ") order by name limit 100;
            那么,这个语句执行的时候会有排序过程吗,为什么?
            如果业务端代码由你来开发,需要实现一个在数据库端不需要排序的方案,你会怎么实现呢?
            进一步地,如果有分页需求,要显示第 101 页,也就是说语句最后要改成 “limit 10000,100”, 你的实现方法又会是什么呢?
            
            

            
内存临时表:
    随机查出三个单词
    首先,你会想到用 order by rand() 来实现这个逻辑。
    mysql> select word from words order by rand() limit 3;
    执行explain命令结果:
        Extra 字段显示 Using temporary,表示的是需要使用临时表;Using filesort,表示的是需要执行排序操作。
        因此这个 Extra 的意思就是,需要临时表,并且需要在临时表上排序。
        
    然后,我再问你一个问题,你觉得对于临时内存表的排序来说,它会选择哪一种算法呢?回顾一下上一篇文章的一个结论:
    
    对于 InnoDB 表(非内存表)来说,执行全字段排序会减少磁盘访问,因此会被优先选择。
    
    对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘。
    优化器没有了这一层顾虑,那么它会优先考虑的,就是用于排序的行越小越好了,所以,MySQL 这时就会选择 rowid 排序。
    
    
    语句执行流程:
        1.创建一个临时表。这个临时表使用的是 memory 引擎,表里有两个字段,第一个字段是 double 类型,为了后面描述方便,记为字段 R,第二个字段是 varchar(64) 类型,记为字段 W。并且,这个表没有建索引。
        2.从 words 表中,按主键顺序取出所有的 word 值。对于每一个 word 值,调用 rand() 函数生成一个大于 0 小于 1 的随机小数,并把这个随机小数和 word 分别存入临时表的 R 和 W 字段中,到此,扫描行数是 10000。
        3.现在临时表有 10000 行数据了,接下来你要在这个没有索引的内存临时表上,按照字段 R 排序。
        4.初始化 sort_buffer。sort_buffer 中有两个字段,一个是 double 类型,另一个是整型。
        5.从内存临时表中一行一行地取出 R 值和位置信息(我后面会和你解释这里为什么是“位置信息”),分别存入 sort_buffer 中的两个字段里。这个过程要对内存临时表做全表扫描,此时扫描行数增加 10000,变成了 20000。
        6.在 sort_buffer 中根据 R 的值进行排序。注意,这个过程没有涉及到表操作,所以不会增加扫描行数。
        7.排序完成后,取出前三个结果的位置信息,依次到内存临时表中取出 word 值,返回给客户端。这个过程中,访问了表的三行数据,总扫描行数变成了 20003。
        
        简单总结流程:
            1.创建包含随机数,word这连个字字段的临时表(memory 引擎)
            2.从words表按主键顺序取出所有的word值,每个word值用随机函数生成0-1d的随机小时,将word和随机数存入临时表
            3.对临时表1000行数据进行按照R字段排序
            4.初始化 sort_buffer,里面有两个字段,对应R和位置信息
            5.从临时表一行一行取出R和位置信息存入sort_buffer
            6.sort_buffer 中根据 R 的值进行排序
            7.排序完,取出前三个结果未知信息,一次到内存临时表中取出word值
            
        接下来,我们通过慢查询日志(slow log)来验证一下我们分析得到的扫描行数是否正确。
            # Query_time: 0.900376  Lock_time: 0.000347 Rows_sent: 3 Rows_examined: 20003
            SET timestamp=1541402277;
            select word from words order by rand() limit 3;
            
        这里插一句题外话,在平时学习概念的过程中,你可以经常这样做,先通过原理分析算出扫描行数,然后再通过查看慢查询日志,来验证自己的结论。我自己就是经常这么做,这个过程很有趣,分析对了开心,分析错了但是弄清楚了也很开心。
        
        这也就是排序模式里面,rowid 名字的来历。实际上它表示的是:每个引擎用来唯一标识数据行的信息。
            1.对于有主键的 InnoDB 表来说,这个 rowid 就是主键 ID;
            2.对于没有主键的 InnoDB 表来说,这个 rowid 就是由系统生成的;
            3.MEMORY 引擎不是索引组织表。在这个例子里面,你可以认为它就是一个数组。因此,这个 rowid 其实就是数组的下标。
            
            
        总结:
            order by rand() 使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。
            
磁盘临时表:
    那么,是不是所有的临时表都是内存表呢?
    其实不是的。tmp_table_size 这个配置限制了内存临时表的大小,默认值是 16M。如果临时表大小超过了 tmp_table_size,那么内存临时表就会转成磁盘临时表。
    
    磁盘临时表使用的引擎默认是 InnoDB,是由参数 internal_tmp_disk_storage_engine 控制的。
    当使用磁盘临时表的时候,对应的就是一个没有显式索引的 InnoDB 表的排序过程。
    
    为了复现这个过程,我把 tmp_table_size 设置成 1024,把 sort_buffer_size 设置成 32768, 把 max_length_for_sort_data 设置成 16。
    
    set tmp_table_size=1024;
    set sort_buffer_size=32768;
    set max_length_for_sort_data=16;
    /* 打开 optimizer_trace,只对本线程有效 */
    SET optimizer_trace='enabled=on'; 
    
    /* 执行语句 */
    select word from words order by rand() limit 3;
    
    /* 查看 OPTIMIZER_TRACE 输出 */
    SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
    
    然后,我们来看一下这次 OPTIMIZER_TRACE 的结果:
    
        因为将 max_length_for_sort_data 设置成 16,小于 word 字段的长度定义,所以我们看到 sort_mode 里面显示的是 rowid 排序,这个是符合预期的,参与排序的是随机值 R 字段和 rowid 字段组成的行。
        
        OPTIMIZER_TRACE 结果中,filesort_priority_queue_optimization 这个部分的 chosen=true,就表示使用了优先队列排序算法,这个过程不需要临时文件,因此对应的 number_of_tmp_files 是 0。
        
        但是,number_of_tmp_files 的值居然是 0,难道不需要用临时文件吗
        
        这个 SQL 语句的排序确实没有用到临时文件,采用是 MySQL 5.6 版本引入的一个新的排序算法,即:优先队列排序算法。接下来,我们就看看为什么没有使用临时文件的算法,也就是归并排序算法,而是采用了优先队列排序算法。
    
    优先队列排序算法:
        其实,我们现在的 SQL 语句,只需要取 R 值最小的 3 个 rowid。但是,如果使用归并排序算法的话,虽然最终也能得到前 3 个值,但是这个算法结束后,已经将 10000 行数据都排好序了。
        
        而优先队列算法,就可以精确地只得到三个最小值,执行流程如下:
            1.对于这 10000 个准备排序的 (R,rowid),先取前三行,构造成一个堆;
            2.取下一个行 (R’,rowid’),跟当前堆里面最大的 R 比较,如果 R’小于 R,把这个 (R,rowid) 从堆中去掉,换成 (R’,rowid’);
            3.重复第 2 步,直到第 10000 个 (R’,rowid’) 完成比较。
    
        这个流程结束后,我们构造的堆里面,就是这个 10000 行里面 R 值最小的三行。
        然后,依次把它们的 rowid 取出来,去临时表里面拿到 word 字段,这个过程就跟上一篇文章的 rowid 排序的过程一样了。
        
        我们再看一下上面一篇文章的 SQL 查询语句:
            select city,name,age from t where city='杭州' order by name limit 1000  ;
        你可能会问,这里也用到了 limit,为什么没用优先队列排序算法呢?
        原因是,这条 SQL 语句是 limit 1000,如果使用优先队列算法的话,需要维护的堆的大小就是 1000 行的 (name,rowid),超过了我设置的 sort_buffer_size 大小,所以只能使用归并排序算法。
        
总之,不论是使用哪种类型的临时表,order by rand() 这种写法都会让计算过程非常复杂,需要大量的扫描行数,因此排序过程的资源消耗也会很大。
    随机排序方法:
        mysql> select count(*) into @C from t;
        set @Y1 = floor(@C * rand());
        set @Y2 = floor(@C * rand());
        set @Y3 = floor(@C * rand());
        select * from t limit @Y1,1; // 在应用代码里面取 Y1、Y2、Y3 值,拼出 SQL 后执行
        select * from t limit @Y2,1;
        select * from t limit @Y3,1;
        
        
        当然也可以先取回 id 值(自己先从mysql查出所有id),在应用中确定了三个 id 值以后,再执行三次 where id=X 的语句也是可以的
        
        提到了用 rowid 的方法,是类似的思路,就是让表里面保存一个无空洞的自增值,这样就可以用我们的随机算法 1 来实现;
        
        
使用了函数导致不走索引:
    对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
    例子:
        假设你现在维护了一个交易系统,其中交易记录表 tradelog 包含交易流水号(tradeid)、交易员 id(operator)、交易时间(t_modified)等字段。
        为了便于描述,我们先忽略其他字段。这个表的建表语句如下:
            mysql> CREATE TABLE `tradelog` (
            `id` int(11) NOT NULL,
            `tradeid` varchar(32) DEFAULT NULL,
            `operator` int(11) DEFAULT NULL,
            `t_modified` datetime DEFAULT NULL,
            PRIMARY KEY (`id`),
            KEY `tradeid` (`tradeid`),
            KEY `t_modified` (`t_modified`)
            ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
            
        假设,现在已经记录了从 2016 年初到 2018 年底的所有数据,运营部门有一个需求是,要统计发生在所有年份中 7 月份的交易记录总数。这个逻辑看上去并不复杂,你的 SQL 语句可能会这么写:
            mysql> select count(*) from tradelog where month(t_modified)=7;

        执行explain命令结果:
            key="t_modified"表示的是,使用了 t_modified 这个索引;
            我在测试表数据中插入了 10 万行数据,rows=100335,说明这条语句扫描了整个索引的所有值;
            Extra 字段的 Using index,表示的是使用了覆盖索引。
            
        也就是说,由于在 t_modified 字段加了 month() 函数操作,导致了全索引扫描。为了能够用上索引的快速定位能力,
        我们就要把 SQL 语句改成基于字段本身的范围查询。按照下面这个写法,优化器就能按照我们预期的,用上 t_modified 索引的快速定位能力了。
        
        mysql> select count(*) from tradelog where
        -> (t_modified >= '2016-7-1' and t_modified<'2016-8-1') or
        -> (t_modified >= '2017-7-1' and t_modified<'2017-8-1') or 
        -> (t_modified >= '2018-7-1' and t_modified<'2018-8-1');
        当然,如果你的系统上线时间更早,或者后面又插入了之后年份的数据的话,你就需要再把其他年份补齐。
        到这里我给你说明了,由于加了 month() 函数操作,MySQL 无法再使用索引快速定位功能,而只能使用全索引扫描。
        
隐式类型转换不走索引:
    mysql> select * from tradelog where tradeid=110717;
    交易编号 tradeid 这个字段上,本来就有索引,但是 explain 的结果却显示,这条语句需要走全表扫描。
    你可能也发现了,tradeid 的字段类型是 varchar(32),而输入的参数却是整型,所以需要做类型转换。
    
    先来看第一个问题,你可能会说,数据库里面类型这么多,这种数据类型转换规则更多,我记不住,应该怎么办呢?
    这里有一个简单的方法,看 select “10” > 9 的结果:
        1.如果规则是“将字符串转成数字”,那么就是做数字比较,结果应该是 1;
        2如果规则是“将数字转成字符串”,那么就是做字符串比较,结果应该是 0。
        
    从图中可知,select “10” > 9 返回的是 1,所以你就能确认 MySQL 里的转换规则了:在 MySQL 中,字符串和数字做比较的话,是将字符串转换成数字。
    
    就知道对于优化器来说, select * from tradelog where tradeid=110717这个语句相当于:
        mysql> select * from tradelog where  CAST(tradid AS signed int) = 110717;
    也就是说,这条语句触发了我们上面说到的规则:对索引字段做函数操作,优化器会放弃走树搜索功能。

隐式字符编码转换不走索引:
    假设系统里还有另外一个表 trade_detail,用于记录交易的操作细节。为了便于量化分析和复现,我往交易日志表 tradelog 和交易详情表 trade_detail 这两个表里插入一些数据。
    mysql> CREATE TABLE `trade_detail` (
    `id` int(11) NOT NULL,
    `tradeid` varchar(32) DEFAULT NULL,
    `trade_step` int(11) DEFAULT NULL, /* 操作步骤 */
    `step_info` varchar(32) DEFAULT NULL, /* 步骤信息 */
    PRIMARY KEY (`id`),
    KEY `tradeid` (`tradeid`)
    ENGINE=InnoDB DEFAULT CHARSET=utf8;

    insert into tradelog values(1, 'aaaaaaaa', 1000, now());
    insert into tradelog values(2, 'aaaaaaab', 1000, now());
    insert into tradelog values(3, 'aaaaaaac', 1000, now());
    
    insert into trade_detail values(1, 'aaaaaaaa', 1, 'add');
    insert into trade_detail values(2, 'aaaaaaaa', 2, 'update');
    insert into trade_detail values(3, 'aaaaaaaa', 3, 'commit');
    insert into trade_detail values(4, 'aaaaaaab', 1, 'add');
    insert into trade_detail values(5, 'aaaaaaab', 2, 'update');
    insert into trade_detail values(6, 'aaaaaaab', 3, 'update again');
    insert into trade_detail values(7, 'aaaaaaab', 4, 'commit');
    insert into trade_detail values(8, 'aaaaaaac', 1, 'add');
    insert into trade_detail values(9, 'aaaaaaac', 2, 'update');
    insert into trade_detail values(10, 'aaaaaaac', 3, 'update again');
    insert into trade_detail values(11, 'aaaaaaac', 4, 'commit');
    
    
    这时候,如果要查询 id=2 的交易的所有操作步骤信息,SQL 语句可以这么写:
    mysql> select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2; /* 语句 Q1*/
    
    执行计划结果:
        第一行显示优化器会先在交易记录表 tradelog 上查到 id=2 的行,这个步骤用上了主键索引,rows=1 表示只扫描一行;
        第二行 key=NULL,表示没有用上交易详情表 trade_detail 上的 tradeid 索引,进行了全表扫描。
        
    在这个执行计划里,是从 tradelog 表中取 tradeid 字段,再去 trade_detail 表里查询匹配字段。
    因此,我们把 tradelog 称为驱动表,把 trade_detail 称为被驱动表,把 tradeid 称为关联字段。
    
    执行流程:
        1.是根据 id 在 tradelog 表里找到 L2 这一行;
        2.是从 L2 中取出 tradeid 字段的值;
        3.是根据 tradeid 值到 trade_detail 表中查找条件匹配的行。
            explain 的结果里面第二行的 key=NULL 表示的就是,这个过程是通过遍历主键索引(主键索引存了每行的所有的数据)的方式,一个一个地判断 tradeid 的值是否匹配。
            
    进行到这里,你会发现第 3 步不符合我们的预期。因为表 trade_detail 里 tradeid 字段上是有索引的,我们本来是希望通过使用 tradeid 索引能够快速定位到等值的行。但,这里并没有。
    
    如果你去问 DBA 同学,他们可能会告诉你,因为这两个表的字符集不同,一个是 utf8,一个是 utf8mb4,所以做表连接查询的时候用不上关联字段的索引。这个回答,也是通常你搜索这个问题时会得到的答案。
    
    我们说问题是出在执行步骤的第 3 步,如果单独把这一步改成 SQL 语句的话,那就是:
        mysql> select * from trade_detail where tradeid=$L2.tradeid.value; 
    其中,$L2.tradeid.value 的字符集是 utf8mb4。
    
    参照前面的两个例子,你肯定就想到了,字符集 utf8mb4 是 utf8 的超集,所以当这两个类型的字符串在做比较的时候,
    MySQL 内部的操作是,先把 utf8 字符串转成 utf8mb4 字符集(小转大做比较,不会丢失精度),再做比较。
    
    也就是说,实际上这个语句等同于下面这个写法:
        select * from trade_detail  where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value; 
    CONVERT() 函数,在这里的意思是把输入的字符串转成 utf8mb4 字符集。
    
    这就再次触发了我们上面说到的原则:对索引字段做函数操作,优化器会放弃走树搜索功能。
    
    作为对比验证,我给你提另外一个需求,“查找 trade_detail 表里 id=4 的操作,对应的操作者是谁”,再来看下这个语句和它的执行计划。
    
    mysql>select l.operator from tradelog l , trade_detail d where d.tradeid=l.tradeid and d.id=4;

    
    这个语句里 trade_detail 表成了驱动表,但是 explain 结果的第二行显示,这次的查询操作用上了被驱动表 tradelog 里的索引 (tradeid),扫描行数是 1。
    
    这也是两个 tradeid 字段的 join 操作,为什么这次能用上被驱动表的 tradeid 索引呢?我们来分析一下:
        假设驱动表 trade_detail 里 id=4 的行记为 R4,那么在连接的时候(图 5 的第 3 步),被驱动表 tradelog 上执行的就是类似这样的 SQL 语句:
        
        select operator from tradelog  where traideid =$R4.tradeid.value; 
        这时候 $R4.tradeid.value 的字符集是 utf8, 按照字符集转换规则,要转成 utf8mb4,所以这个过程就被改写成:
        select operator from tradelog  where traideid =CONVERT($R4.tradeid.value USING utf8mb4); 
        
    你看,这里的 CONVERT 函数是加在输入参数上的,这样就可以用上被驱动表的 traideid 索引。
    
    
    理解了原理以后,就可以用来指导操作了。如果要优化语句
    
        select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2;
    两种做法:
        修改字符集:
            比较常见的优化方法是,把 trade_detail 表上的 tradeid 字段的字符集也改成 utf8mb4,这样就没有字符集转换的问题了。
            alter table trade_detail modify tradeid varchar(32) CHARACTER SET utf8mb4 default null;
        修改语句:
            如果能够修改字段的字符集的话,是最好不过了。但如果数据量比较大, 或者业务上暂时不能做这个 DDL 的话,那就只能采用修改 SQL 语句的方法了。
            mysql> select d.* from tradelog l , trade_detail d where d.tradeid=CONVERT(l.tradeid USING utf8) and l.id=2; 
            这里,我主动把 l.tradeid 转成 utf8,就避免了被驱动表上的字符编码转换,从 explain 结果可以看到,这次索引走对了。
            
            
            
join操作:
    我们 DBA 不让使用 join,使用 join 有什么问题呢?
    如果有两个大小不同的表做 join,应该用哪个表做驱动表呢?
    
    先创建两个表:
        CREATE TABLE `t2` (
        `id` int(11) NOT NULL,
        `a` int(11) DEFAULT NULL,
        `b` int(11) DEFAULT NULL,
        PRIMARY KEY (`id`),
        KEY `a` (`a`)
        ) ENGINE=InnoDB;
        
        drop procedure idata;
        delimiter ;;
        create procedure idata()
        begin
        declare i int;
        set i=1;
        while(i<=1000)do
            insert into t2 values(i, i, i);
            set i=i+1;
        end while;
        end;;
        delimiter ;
        call idata();
        
        create table t1 like t2;
        insert into t1 (select * from t2 where id<=100)

    可以看到,这两个表都有一个主键索引 id 和一个索引 a,字段 b 上无索引。存储过程 idata() 往表 t2 里插入了 1000 行数据,在表 t1 里插入的是 100 行数据。
    
    Index Nested-Loop Join:
        先看一个sql语句:
            select * from t1 straight_join t2 on (t1.a=t2.a);
            
        如果直接使用 join 语句,MySQL 优化器可能会选择表 t1 或 t2 作为驱动表,这样会影响我们分析 SQL 语句的执行过程。
        所以,为了便于分析执行过程中的性能问题,我改用 straight_join 让 MySQL 使用固定的连接方式执行查询,这样优化器只会按照我们指定的方式去 join。在这个语句里,t1 是驱动表,t2 是被驱动表。    
        执行计划结果:    
        id    select_type    table    partitions    type    possible_keys    key        key_len        ref                rows    filtered    Extra
        1    SIMPLE        t1        (null)        ALL            a            (null)    (null)        (null)            100        100.00        Using where
        1    SIMPLE        t2        (null)        ref            a             a         5            test666.t1.a    1        100.00        (null)
        
        可以看到,在这条语句里,被驱动表 t2 的字段 a 上有索引,join 过程用上了这个索引,因此这个语句的执行流程是这样的:
            1.从表 t1 中读入一行数据 R;
            2.从数据行 R 中,取出 a 字段到表 t2 里去查找;
            3.取出表 t2 中满足条件的行,跟 R 组成一行,作为结果集的一部分;
            4.重复执行步骤 1 到 3,直到表 t1 的末尾循环结束。
        形式上和嵌套一样
        
        在这个流程里:
            1.对驱动表 t1 做了全表扫描,这个过程需要扫描 100 行;
            2.而对于每一行 R,根据 a 字段去表 t2 查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描 100 行;
            3.所以,整个执行流程,总扫描行数是 200。
        在这个 join 语句执行过程中,驱动表是走全表扫描,而被驱动表是走树搜索。
        假设被驱动表的行数是 M。每次在被驱动表查一行数据,要先搜索索引 a,再搜索主键索引。每次搜索一棵树近似复杂度是以 2 为底的 M 的对数,记为 log
        假设驱动表的行数是 N,执行过程就要扫描驱动表 N 行,然后对于每一行,到被驱动表上匹配一次。
        
        因此整个执行过程,近似复杂度是 N + N*2*log2M  ,这里的是M是次幂
        
        显然,N 对扫描行数的影响更大,因此应该让小表来做驱动表。
        
        如果你没觉得这个影响有那么“显然”, 可以这么理解:N 扩大 1000 倍的话,扫描行数就会扩大 1000 倍;而 M 扩大 1000 倍,扫描行数扩大不到 10 倍。
        
        总结:
            1.使用 join 语句,性能比强行拆成多个单表执行 SQL 语句的性能要好;
            2.如果使用 join 语句的话,需要让小表做驱动表。
            
    Simple Nested-Loop Join:
    
        现在sql改成这样:
            select * from t1 straight_join t2 on (t1.a=t2.b);
        由于表 t2 的字段 b 上没有索引,因此再用图 2 的执行流程时,每次到 t2 去匹配的时候,就要做一次全表扫描。
        但是,这样算来,这个 SQL 请求就要扫描表 t2 多达 100 次,总共扫描 100*1000=10 万行。
        
        这还只是两个小表,如果 t1 和 t2 都是 10 万行的表(当然了,这也还是属于小表的范围),就要扫描 100 亿行,这个算法看上去太“笨重”了。
        
        当然,MySQL 也没有使用这个 Simple Nested-Loop Join 算法,而是使用了另一个叫作“Block Nested-Loop Join”的算法,简称 BNL。
        
    Block Nested-Loop Join:
        这时候,被驱动表上没有可用的索引,算法的流程是这样的:
            1.把表 t1 的数据读入线程内存 join_buffer 中,由于我们这个语句中写的是 select *,因此是把整个表 t1 放入了内存;
            2.扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回。
        执行计划:
        id    select_type    table    partitions    type    possible_keys    key            key_len        ref        rows    filtered    Extra
        1    SIMPLE        t1        (null)        ALL            a            (null)        (null)        (null)    100        100.00        (null)
        1    SIMPLE        t2        (null)        ALL            (null)        (null)        (null)        (null)    1000    10.00        Using where; Using join buffer (Block Nested Loop)
        
        可以看到,在这个过程中,对表 t1 和 t2 都做了一次全表扫描,因此总的扫描行数是 1100。
        由于 join_buffer 是以无序数组的方式组织的,因此对表 t2 中的每一行,都要做 100 次判断,总共需要在内存中做的判断次数是:100*1000=10 万次。
        
        前面我们说过,如果使用 Simple Nested-Loop Join 算法进行查询,扫描行数也是 10 万行。因此,从时间复杂度上来说,这两个算法是一样的。
        但是,Block Nested-Loop Join 算法的这 10 万次判断是内存操作,速度上会快很多,性能也更好。
        
        接下来,我们来看一下,在这种情况下,应该选择哪个表做驱动表。
        假设小表的行数是 N,大表的行数是 M,那么在这个算法里:
            1.两个表都做一次全表扫描,所以总的扫描行数是 M+N;
            2.内存中的判断次数是 M*N。
        可以看到,调换这两个算式中的 M 和 N 没差别,因此这时候选择大表还是小表做驱动表,执行耗时是一样的。
        
        join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k。如果放不下表 t1 的所有数据话,策略很简单,就是分段放。我把 join_buffer_size 改成 1200,再执行:
            select * from t1 straight_join t2 on (t1.a=t2.b);
        执行过程就变成了:
            1.扫描表 t1,顺序读取数据行放入 join_buffer 中,放完第 88 行 join_buffer 满了,继续第 2 步;
            2.扫描表 t2,把 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回;
            3.清空 join_buffer;
            4.继续扫描表 t1,顺序读取最后的 12 行数据放入 join_buffer 中,继续执行第 2 步。
        这个流程才体现出了这个算法名字中“Block”的由来,表示“分块去 join”。
        可以看到,这时候由于表 t1 被分成了两次放入 join_buffer 中,导致表 t2 会被扫描两次。虽然分成两次放入 join_buffer,但是判断等值条件的次数还是不变的,依然是 (88+12)*1000=10 万次。
        
        我们再来看下,在这种情况下驱动表的选择问题。
        假设,驱动表的数据行数是 N,需要分 K 段才能完成算法流程,被驱动表的数据行数是 M。
        所以,在这个算法的执行过程中:
            1.扫描行数是 N+k*M;
            2.内存判断 N*M 次。
            
            显然,内存判断次数是不受选择哪个表作为驱动表影响的。而考虑到扫描行数,在 M 和 N 大小确定的情况下,N 小一些,k也会小写,整个算式的结果会更小。
        所以结论是,应该让小表当驱动表。    
        N 固定的时候,什么参数会影响 K 的大小呢?答案是 join_buffer_size。join_buffer_size 越大,一次可以放入的行越多,分成的段数也就越少,对被驱动表的全表扫描次数就越少。
            
        这就是为什么,你可能会看到一些建议告诉你,如果你的 join 语句很慢,就把 join_buffer_size 改大。
        
    第一个问题:能不能使用 join 语句?
        1.如果可以使用 Index Nested-Loop Join 算法(使用索引),也就是说可以用上被驱动表上的索引,其实是没问题的;
        2.如果使用 Block Nested-Loop Join 算法(未使用索引),扫描行数就会过多。尤其是在大表上的 join 操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种 join 尽量不要用。
        
        所以你在判断要不要使用 join 语句时,就是看 explain 结果里面,Extra 字段里面有没有出现“Block Nested Loop”字样。
        
    第二个问题是:如果要使用 join,应该选择大表做驱动表还是选择小表做驱动表?
        1.如果是 Index Nested-Loop Join 算法(使用索引),应该选择小表做驱动表;
        2.如果是 Block Nested-Loop Join 算法(未使用索引):
            在 join_buffer_size 足够大的时候,是一样的;
            在 join_buffer_size 不够大的时候(这种情况更常见),应该选择小表做驱动表。
    所以,这个问题的结论就是,总是应该使用小表做驱动表。
    
    
    什么叫作“小表”:
        我们前面的例子是没有加条件的。如果我在语句的 where 条件加上 t2.id<=50 这个限定条件,再来看下这两条语句:
            select * from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=50;
            select * from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=50;
        注意,为了让两条语句的被驱动表都用不上索引,所以 join 字段都使用了没有索引的字段 b。
        但如果是用第二个语句的话,join_buffer 只需要放入 t2 的前 50 行,显然是更好的。所以这里,“t2 的前 50 行”是那个相对小的表,也就是“小表”。
        
        我们再来看另外一组例子:
            select t1.b,t2.* from  t1  straight_join t2 on (t1.b=t2.b) where t2.id<=100;
            select t1.b,t2.* from  t2  straight_join t1 on (t1.b=t2.b) where t2.id<=100;
        这个例子里,表 t1 和 t2 都是只有 100 行参加 join。但是,这两条语句每次查询放入 join_buffer 中的数据是不一样的:
            表 t1 只查字段 b,因此如果把 t1 放到 join_buffer 中,则 join_buffer 中只需要放入 b 的值;
            表 t2 需要查所有的字段,因此如果把表 t2 放到 join_buffer 中的话,就需要放入三个字段 id、a 和 b。
        这里,我们应该选择表 t1 作为驱动表。也就是说在这个例子里,“只需要一列参与 join 的表 t1”是那个相对小的表。
        
        在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。
        
        
join语句优化:

    先创建两个表,t1插入1000行数据,t2插入100万行数据
        create table t1(id int primary key, a int, b int, index(a));
        create table t2 like t1;
        drop procedure idata;
        delimiter ;;
        create procedure idata()
        begin
        declare i int;
        set i=1;
        while(i<=1000)do
            insert into t1 values(i, 1001-i, i);
            set i=i+1;
        end while;
        
        set i=1;
        while(i<=1000000)do
            insert into t2 values(i, i, i);
            set i=i+1;
        end while;
        
        end;;
        delimiter ;
        call idata();
    为了便于后面量化说明,我在表 t1 里,插入了 1000 行数据,每一行的 a=1001-id 的值。也就是说,表 t1 中字段 a 是逆序的,索引树a是递增的。同时,我在表 t2 中插入了 100 万行数据。
    
    Multi-Range Read 优化:
        在介绍 join 语句的优化方案之前,我需要先和你介绍一个知识点,即:Multi-Range Read 优化 (MRR)。这个优化的主要目的是尽量使用顺序读盘。
        我们先来回顾一下这个概念。回表是指,InnoDB 在普通索引 a 上查到主键 id 的值后,再根据一个个主键 id 的值到主键索引上去查整行数据的过程。
        
        我们先来看看这个问题。假设,我执行这个语句:
            select * from t1 where a>=1 and a<=100;
        主键索引是一棵 B+ 树,在这棵树上,每次只能根据一个主键 id 查到一行数据。因此,回表肯定是一行行搜索主键索引的
        
        如果随着 a 的值递增顺序查询的话,id 的值就变成随机的,那么就会出现随机访问,性能相对较差。虽然“按行查”这个机制不能改,但是调整查询的顺序,还是能够加速的。
        因为大多数的数据都是按照主键递增顺序插入得到的,所以我们可以认为,如果按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。
        
        这,就是 MRR 优化的设计思路。此时,语句的执行流程变成了这样:
            1.根据索引 a,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中 ;
            2.将 read_rnd_buffer 中的 id 进行递增排序;
            3.排序后的 id 数组,依次到主键 id 索引中查记录,并作为结果返回。
    
        这里,read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制的。如果步骤 1 中,read_rnd_buffer 放满了,就会先执行完步骤 2 和 3,然后清空 read_rnd_buffer。之后继续找索引 a 的下个记录,并继续循环。
        
        从图 3 的 explain 结果中,我们可以看到 Extra 字段多了 Using MRR,表示的是用上了 MRR 优化。
        而且,由于我们在 read_rnd_buffer 中按照 id 做了排序,所以最后得到的结果集也是按照主键 id 递增顺序的,也就是与图 1 结果集中行的顺序相反。
        
        另外需要说明的是,如果你想要稳定地使用 MRR 优化的话,需要设置set optimizer_switch="mrr_cost_based=off"。
        (官方文档的说法,是现在的优化器策略,判断消耗的时候,会更倾向于不使用 MRR,把 mrr_cost_based 设置为 off,就是固定使用 MRR 了。)
    
        MRR 能够提升性能的核心:
            在于,这条查询语句在索引 a 上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键 id。
            这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势。
            
    NLJ算法优化:
        Batched Key Access:(BKA算法)
        理解了 MRR 性能提升的原理,我们就能理解 MySQL 在 5.6 版本后开始引入的 Batched Key Acess(BKA) 算法了。这个 BKA 算法,其实就是对 NLJ 算法的优化。
        NLJ 算法执行的逻辑是:从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去做 join。也就是说,对于表 t2 来说,每次都是匹配一个值。这时,MRR 的优势就用不上了。
        
        那怎么才能一次性地多传些值给表 t2 呢?方法就是,从表 t1 里一次性地多拿些行出来,一起传给表 t2。
        既然如此,我们就把表 t1 的数据取出来一部分,先放到一个临时内存。这个临时内存不是别人,就是 join_buffer。
        
        通过上一篇文章,我们知道 join_buffer 在 BNL 算法里的作用,是暂存驱动表的数据。但是在 NLJ 算法里并没有用。那么,我们刚好就可以复用 join_buffer 到 BKA 算法中。
        如果要使用 BKA 优化算法的话,你需要在执行 SQL 语句之前,先设置
        set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
        
    BNL 算法的优化:
        我们执行语句之前,需要通过理论分析和查看 explain 结果的方式,确认是否要使用 BNL 算法。如果确认优化器会使用 BNL 算法,就需要做优化。
        
        
        BNL 转 BKA:
            1.给被驱动表的 join 字段加上索引:
                优化的常见做法是,给被驱动表的 join 字段加上索引,把 BNL 算法转成 BKA 算法。
                
            2.一些情况下,我们可以直接在被驱动表上建索引,这时就可以直接转成 BKA 算法了。
            
            但是,有时候你确实会碰到一些不适合在被驱动表上建索引的情况。比如下面这个语句:
            select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;
                
            我们在文章开始的时候,在表 t2 中插入了 100 万行数据,但是经过 where 条件过滤后,需要参与 join 的只有 2000 行数据。
            如果这条语句同时是一个低频的 SQL 语句,那么再为这个语句在表 t2 的字段 b 上创建一个索引就很浪费了。
            
            但是,如果使用 BNL 算法来 join 的话,这个语句的执行流程是这样的:
                1.把表 t1 的所有字段取出来,存入 join_buffer 中。这个表只有 1000 行,join_buffer_size 默认值是 256k,可以完全存入。
                2.扫描表 t2,取出每一行数据跟 join_buffer 中的数据进行对比,
                    如果不满足 t1.b=t2.b,则跳过;
                    如果满足 t1.b=t2.b, 再判断其他条件,也就是是否满足 t2.b 处于 [1,2000] 的条件,如果是,就作为结果集的一部分返回,否则跳过。
                我在上一篇文章中说过,对于表 t2 的每一行,判断 join 是否满足的时候,都需要遍历 join_buffer 中的所有行。因此判断等值条件的次数是 1000*100 万 =10 亿次,这个判断的工作量很大。
                
                可以看到,explain 结果里 Extra 字段显示使用了 BNL 算法。在我的测试环境里,这条语句需要执行 1 分 11 秒。
                
                在表 t2 的字段 b 上创建索引会浪费资源,但是不创建索引的话这个语句的等值条件要判断 10 亿次,想想也是浪费。那么,有没有两全其美的办法呢?
        使用临时表:
            这时候,我们可以考虑使用临时表。使用临时表的大致思路是:
                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);
                    
            可以看到,整个过程 3 个语句执行时间的总和还不到 1 秒,相比于前面的 1 分 11 秒,性能得到了大幅提升。接下来,我们一起看一下这个过程的消耗:
                1.执行 insert 语句构造 temp_t 表并插入数据的过程中,对表 t2 做了全表扫描,这里扫描行数是 100 万。
                2.之后的 join 语句,扫描表 t1,这里的扫描行数是 1000;join 比较过程中,做了 1000 次带索引的查询。相比于优化前的 join 语句需要做 10 亿次条件判断来说,这个优化效果还是很明显的。
        总体来看,不论是在原表上加索引,还是用有索引的临时表,我们的思路都是让 join 语句能够用上被驱动表上的索引,来触发 BKA 算法,提升查询性能。
        
    扩展 -hash join:
        看到这里你可能发现了,其实上面计算 10 亿次那个操作,看上去有点儿傻。
        如果 join_buffer 里面维护的不是一个无序数组,而是一个哈希表的话,那么就不是 10 亿次判断,而是 100 万次 hash 查找。这样的话,整条语句的执行速度就快多了吧?
        
        确实如此。
        这,也正是 MySQL 的优化器和执行器一直被诟病的一个原因:不支持哈希 join。并且,MySQL 官方的 roadmap,也是迟迟没有把这个优化排上议程。
        
        实际上,这个优化思路,我们可以自己实现在业务端。实现流程大致如下:
            1.取得表 t1 的全部 1000 行数据,在业务端存入一个 hash 结构,比如 C++ 里的 set、PHP 的数组这样的数据结构。
            2. 获取表 t2 中满足条件的 2000 行数据。
            3.把这 2000 行数据,一行一行地取到业务端,到 hash 结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行。
            
        理论上,这个过程会比临时表方案的执行速度还要快一些。如果你感兴趣的话,可以自己验证一下。
        
    总结:
        1.BKA 优化是 MySQL 已经内置支持的,建议你默认使用;
        2.BNL 算法效率低,建议你都尽量转成 BKA 算法。优化的方向就是给被驱动表的关联字段加上索引;
        3.基于临时表的改进方案,对于能够提前过滤出小数据的 join 语句来说,效果还是很好的;
        4.MySQL 目前的版本还不支持 hash join,但你可以配合应用端自己模拟出来,理论上效果要好于临时表的方案。
        
        
临时表:

    在上一篇文章中,我们在优化 join 查询的时候使用到了临时表。当时,我们是这么用的:
        create temporary table temp_t like t1;
        alter table temp_t add index(b);
        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);

    这里,我需要先帮你厘清一个容易误解的问题:有的人可能会认为,临时表就是内存表。但是,这两个概念可是完全不同的。
    
    内存表:
        指的是使用 Memory 引擎的表,建表语法是 create table … engine=memory。这种表的数据都保存在内存里,系统重启的时候会被清空,
        但是表结构还在。除了这两个特性看上去比较“奇怪”外,从其他的特征上看,它就是一个正常的表。
        
    临时表:
        可以使用各种引擎类型 。如果是使用 InnoDB 引擎或者 MyISAM 引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用 Memory 引擎。
        
    临时表特点:
        1.一个临时表只能被创建它的 session 访问,对其他线程不可见。
        2.临时表可以与普通表同名。
        3.session A 内有同名的临时表和普通表的时候,show create 语句,以及增删改查语句访问的是临时表。
        4.show tables 命令不显示临时表。
        
    由于临时表只能被创建它的 session 访问,所以在这个 session 结束的时候,会自动删除临时表。也正是由于这个特性,
    临时表就特别适合我们文章开头的 join 优化这种场景
    
    原因主要包括以下两个方面:
        1.不同 session 的临时表是可以重名的,如果有多个 session 同时执行 join 优化,不需要担心表名重复导致建表失败的问题。
        2.不需要担心数据删除问题。如果使用普通表,在流程执行过程中客户端发生了异常断开,或者数据库发生异常重启,
        还需要专门来清理中间过程中生成的数据表。而临时表由于会自动回收,所以不需要这个额外的操作。
        
    临时表的应用:
        由于不用担心线程之间的重名冲突,临时表经常会被用在复杂查询的优化过程中。其中,分库分表系统的跨库查询就是一个典型的使用场景。
        一般分库分表的场景,就是要把一个逻辑上的大表分散到不同的数据库实例上。比如。将一个大表 ht,按照字段 f,拆分成 1024 个分表,然后分布到 32 个数据库实例上。如下图所示:
        
            client
              ↓
            proxy
              ↓
            ht_1,ht_2,ht_3 ...ht_1024 个表
            
        一般情况下,这种分库分表系统都有一个中间层 proxy。不过,也有一些方案会让客户端直接连接数据库,也就是没有 proxy 这一层。
        在这个架构中,分区 key 的选择是以“减少跨库和跨表查询”为依据的。如果大部分的语句都会包含 f 的等值条件,那么就要用 f 做分区键。
        这样,在 proxy 这一层解析完 SQL 语句以后,就能确定将这条语句路由到哪个分表做查询。
        比如下面这条语句:
            select v from ht where f=N;
        这时,我们就可以通过分表规则(比如,N%1024) 来确认需要的数据被放在了哪个分表上。这种语句只需要访问一个分表,是分库分表方案最欢迎的语句形式了。
        
        但是,如果这个表上还有另外一个索引 k,并且查询语句是这样的:
            select v from ht where k >= M order by t_modified desc limit 100;
        这时候,由于查询条件里面没有用到分区字段 f,只能到所有的分区中去查找满足条件的所有行,然后统一做 order by 的操作。这种情况下,有两种比较常用的思路。
        
        
        第一种思路是,在 proxy 层的进程代码中实现排序。
        
            这种方式的优势是处理速度快,拿到分库的数据以后,直接在内存中参与计算。不过,这个方案的缺点也比较明显:
            1.需要的开发工作量比较大。我们举例的这条语句还算是比较简单的,如果涉及到复杂的操作,比如 group by,甚至 join 这样的操作,对中间层的开发能力要求比较高;
            2.对 proxy 端的压力比较大,尤其是很容易出现内存不够用和 CPU 瓶颈的问题。
        
        另一种思路就是,把各个分库拿到的数据,汇总到一个 MySQL 实例的一个表中,然后在这个汇总实例上做逻辑操作。
        
        比如上面这条语句,执行流程可以类似这样:
            1.在汇总库上创建一个临时表 temp_ht,表里包含三个字段 v、k、t_modified;
            2.在各个分库上执行
                select v,k,t_modified from ht_x where k >= M order by t_modified desc limit 100;
            3.把分库执行的结果插入到 temp_ht 表中;
                
            4.执行最终查询
                select v from temp_ht order by t_modified desc limit 100; 
        在实践中,我们往往会发现每个分库的计算量都不饱和,所以会直接把临时表 temp_ht 放到 32 个分库中的某一个上。
        
        
        什么时候会用到临时表:
            group by:
                另外一个常见的使用临时表的例子是 group by,我们来看一下这个语句:
                select id%10 as m, count(*) as c from t1 group by m;
            这个语句的逻辑是把表 t1 里的数据,按照 id%10 进行分组统计,并按照 m 的结果排序后输出。它的 explain 结果如下:
            在 Extra 字段里面,我们可以看到三个信息:
                1.Using index,表示这个语句使用了覆盖索引,选择了索引 a,不需要回表;
                2.Using temporary,表示使用了临时表;
                3.Using filesort,表示需要排序。
            这个语句的执行流程是这样的:
                1.创建内存临时表,表里有两个字段 m 和 c,主键是 m;
                2.扫描表 t1 的索引 a,依次取出叶子节点上的 id 值,计算 id%10 的结果,记为 x;
                    如果临时表中没有主键为 x 的行,就插入一个记录 (x,1);
                    如果表中有主键为 x 的行,就将 x 这一行的 c 值加 1;
                3.遍历完成后,再根据字段 m 做排序,得到结果集返回给客户端。
                
            
            如果你的需求并不需要对结果进行排序,那你可以在 SQL 语句末尾增加 order by null,也就是改成:
                select id%10 as m, count(*) as c from t1 group by m order by null;
                
            这样就跳过了最后排序的阶段,直接从临时表中取数据返回。
            
            这个例子里由于临时表只有 10 行,内存可以放得下,因此全程只使用了内存临时表。
            但是,内存临时表的大小是有限制的,参数 tmp_table_size 就是控制这个内存大小的,默认是 16M。
            
            如果我执行下面这个语句序列:
                set tmp_table_size=1024;
                select id%100 as m, count(*) as c from t1 group by m order by null limit 10;
            把内存临时表的大小限制为最大 1024 字节,并把语句改成 id % 100,这样返回结果里有 100 行数据。
            但是,这时的内存临时表大小不够存下这 100 行数据,也就是说,执行过程中会发现内存临时表大小到达了上限(1024 字节)。
            
            那么,这时候就会把内存临时表转成磁盘临时表,磁盘临时表默认使用的引擎是 InnoDB。
            
            如果这个表 t1 的数据量很大,很可能这个查询需要的磁盘临时表就会占用大量的磁盘空间。
            
        group by 优化方法 -- 索引(分组字段上有索引):
        
            可以看到,不论是使用内存临时表还是磁盘临时表,group by 逻辑都需要构造一个带唯一索引的表,执行代价都是比较高的。
            如果表的数据量比较大,上面这个 group by 语句执行起来就会很慢,我们有什么优化的方法呢?
            
            group by 的语义逻辑,是统计不同的值出现的个数。但是,由于每一行的 id%100 的结果是无序的,所以我们就需要有一个临时表,来记录并统计结果。
            
            那么,如果扫描过程中可以保证出现的数据是有序的,是不是就简单了呢?
            
            图10:
            
            0 0 0 ... 0 1 1 ... 1 2 2 ...2
            |           |         |        |
                x个o      y个1      z个2
                
            可以看到,如果可以确保输入的数据是有序的,那么计算 group by 的时候,就只需要从左到右,顺序扫描,依次累加。也就是下面这个过程:
            
                当碰到第一个 1 的时候,已经知道累积了 X 个 0,结果集里的第一行就是 (0,X);
                
                当碰到第一个 2 的时候,已经知道累积了 Y 个 1,结果集里的第二行就是 (1,Y);
                
            按照这个逻辑执行的话,扫描到整个输入的数据结束,就可以拿到 group by 的结果,不需要临时表,也不需要再额外排序。
            
            你一定想到了,InnoDB 的索引,就可以满足这个输入有序的条件。
            
            在 MySQL 5.7 版本支持了 generated column 机制,用来实现列数据的关联更新。
            你可以用下面的方法创建一个列 z,然后在 z 列上创建一个索引(如果是 MySQL 5.6 及之前的版本,你也可以创建普通列和索引,来解决这个问题)。
            
            alter table t1 add column z int generated always as(id % 100), add index(z);
            
            这样,索引 z 上的数据就是类似图 10 这样有序的了。上面的 group by 语句就可以改成:
            
                select z, count(*) as c from t1 group by z;
            
            优化后的 group by 语句的 explain 结果,如下图所示:
            
            从 Extra 字段可以看到,只有Using index,
            这个语句的执行不再需要临时表,也不需要排序了。
        
        group by 优化方法 -- 直接排序(在要分组的字段前加SQL_BIG_RESULT)
            所以,如果可以通过加索引来完成 group by 逻辑就再好不过了。但是,如果碰上不适合创建索引的场景,我们还是要老老实实做排序的。那么,这时候的 group by 要怎么优化呢?
            
            如果我们明明知道,一个 group by 语句中需要放到临时表上的数据量特别大,却还是要按照“先放到内存临时表,插入一部分数据后,发现内存临时表不够用了再转成磁盘临时表”,看上去就有点儿傻。
            
            那么,我们就会想了,MySQL 有没有让我们直接走磁盘临时表的方法呢?
            
            在 group by 语句中加入 SQL_BIG_RESULT 这个提示(hint),就可以告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表。
            
            MySQL 的优化器一看,磁盘临时表是 B+ 树存储,存储效率不如数组来得高。所以,既然你告诉我数据量很大,那从磁盘空间考虑,还是直接用数组来存吧。
            
                select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;
                
            的执行流程就是这样的:
                1.初始化 sort_buffer,确定放入一个整型字段,记为 m;
                2.扫描表 t1 的索引 a,依次取出里面的 id 值, 将 id%100 的值存入 sort_buffer 中;
                3.扫描完成后,对 sort_buffer 的字段 m 做排序(如果 sort_buffer 内存不够用,就会利用磁盘临时文件辅助排序);
                4.排序完成后,就得到了一个有序数组。
            
            根据有序数组,得到数组里面的不同值,以及每个值的出现次数。这一步的逻辑,你已经从前面的图 10 中了解过了。
            
            从 Extra 字段可以看到,Using index ; Using filesort
            这个语句的执行没有再使用临时表,而是直接用了排序算法。
            
        总结;
            1.如果对 group by 语句的结果没有排序要求,要在语句后面加 order by null;
            2.尽量让 group by 过程用上表的索引(分组字段),确认方法是 explain 结果里没有 Using temporary 和 Using filesort;
            3.如果 group by 需要统计的数据量不大,尽量只使用内存临时表;也可以通过适当调大 tmp_table_size 参数,来避免用到磁盘临时表;
            4.如果数据量实在太大,使用 SQL_BIG_RESULT 这个提示,来告诉优化器直接使用排序算法得到 group by 的结果。

            
            
            

            
优化数据类型:
    选择更小的数据类型
    简单就好:
        存储时间和日期用自带的date,time,datetime
        用整型存储IP,用无符号整数存储,INET_ATON()和INET_NTOA()函数可以在两种表示法之间转换
    尽量避免Null:
    
    DATETIME 和 TIMESAMP列可以存储相同类型的数据,时间和日期,精确到秒
        TIMESAMP只使用DATETIME一半空间,TIMESAMP允许的时间范围要小
        
实数类型:
    INT(11)对大多数应用没有意义,不会限制值的范围,只是一些mysql客户端交互工具用来显示字符的个数
    对应存储和计算来说,INT(1)和INT(20)是一样的
    
    尽量使用整型类型作为标识列
    
字符串类型:
    varchar类型:(不定长)
        1.用于存储可变长字符串,比定长省空间,根据列的长度使用不同字节
        2.适合场景:字符串的最大长度比平均长度大很多,列的更新很好
        
    char类型:(定长)
        1.适合场景:存储MD%密码
        2.使用char(1)记录Y或N,只需要一个字节,但是varchar(1)需要两个字节,因为还有一个记录长度的额外字节
        
    varchar(5)和varchar(200) 存储"hello" 的空间开销是一样的,当使用内存临时表或磁盘临时表排序时,短的会更快
    
    日期类型:
        DATETIME 和 TIMESAMP列可以存储相同类型的数据,时间和日期,精确到秒
        TIMESAMP(4字节)只使用DATETIME(8字节)一半空间,TIMESAMP允许的时间范围要小
        
        尽量使用TIMESAMP
                    
    bit类型:
        尽量避免使用
        
        
范式和反范式:
    适当反范式可以提高查询速度,不需要关联表了,但同时提高了更新效率,因为可能要同时更新两张表的同一个字段,如果某个字段后期更改的概率很小,或者基本不修改,则可以反范式,例如性别,省份,地市这些基本上不会变的
    
    
缓存表和汇总表:
    缓存表:(也可以用redis做缓存,或者将整个表的结果导出到ES)
        保存每次查询速度都非常慢的结果的语句或者逻辑上冗余的数据,或者一些比较少改变的数据
        例如:
            1.地市表,地区表等等这些不怎么改变的数据其实可以用redis缓存起来
            2.想全国IP地区数据,既可以用redis,mysql存一份,将所有的结果查出来缓存到本地,或者redis,用二分查找法非常快
            2.一些关联查询,查询比较慢的也可以缓存起来
    汇总表:(累计表)
        保存的是使用group by语句聚合数据的表
        
        例子:计算某网站过去24小时的发送的消息数
            1.每小时汇总一次(汇总当前这个小时内产生的数据)
            2.把钱23小时的汇总技术全部加起来
            3.加上最后一个小时实时的数据
            
    计数器表:
        假设有一个计数器表 ,只有一行数据,记录了网站的点击次数,网站每次点击都会导致对计数器进行更新
        问题:
            对于任何想要更新这一行数据的事务来说,这条记录都会有一个全局的互斥锁,相当于变成串行了
        方案:
            为了提高并发更新性能,预先添加100行数据,每次更新随机更新一行数据
            要获得统计结果,只需要select sum(count) from t
            
        其实可以用redis一直实时计数,然后开一个定时线程,每小时计算一次一个小时的数据(参考大数据方案)

索引:
    如果有个表存储大量url,并需要根据url的值进行查找,如果直接在url列上建索引,索引树会很大,因为url本身就很大,新增一个带索引的列url_crc,这列存储了url的哈希值
    虽然有哈希冲突,但是可以再加多url列的等值过滤条件,这样非常快
    
    触发器语句在高性能mysql的第5章149页有,可以用触发器再使用函数crc(url)函数,维护url_cc列的值

    索引对于非常小的表没什么用,还不如不建索引,对于中表或者大表,索引非常有效 ,但是对于超级大表使用索引的代码就非常大,可以使用分库分表分区
    
    如果表的数量特别多,可以建立一个元数据表,
    例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录"哪个用户的信息储存在哪个表中"的元数据,这样查询的时候可以直接忽略那些不包含指定用户信息的表

    使用索引扫描来做排序:
        EXPLAIN 的type列的值为index,则说明使用了索引扫描来做排序(不要和Extra列的Using index 搞混了)
        扫描索引很快,只需要从一条索引记录移动到紧接着的下下一条记录,但如果索引不能覆盖查询需要的全部列(所以尽量让索引覆盖),那就不得每扫描一次就要回表查询一次对应的行,这基本上是随机io.
        
        1.当索引的列和order by的子句的顺序完全一致,并且所有列的排序方向(正序或倒序)都一样
        2.如果查询需要关联多张表,order by引用的字段全部为第一个表时,才能使用索引做排序
        3.order by的子句满足索引的最左前缀
        4.order by的应用的列必选都在索引中,例如order by a,b   a在索引,b不在
        5.where 和 order by的字段都必须满足最左前缀
        
    如果使用了某个索引进行范围查询,就无法再使用另外一个索引进行排序
    
    支持多种过滤条件:
        有个表,有国家,地区,城市,性别,年龄,眼睛颜色等
        联合索引(sex,country),几何每个网站都会有安装sex查询,即使查询条件没sex,也可以用技巧绕过,
        技巧:
            查询中不限制性别,可以在查询条件新增and sex in('m','f')来让mysqsl选择索引,但必须加上这个条件,才能满足索引的最左前缀,但如果列有太多不同的值就不合适,会让in()列表太长
            
            可能需要这样的查询不同的(sex,country,age),(sex,country,region,age),(sex,country,region,city,age)
            如果为每种查询都建立所以,就需要大量的索引
            
            如果想避免建立大量索引,想尽可能的重用索引,则可以利用上面的in技巧来避免同时需要(sex,country,age),(sex,country,region,age)索引
            如果没有指定国家或者地区字段搜索,in()条件会很长,则需要一个全部国家列表,或者国家的全部地区表,先用子查询所有国家或者地区作为in里面的条件值
            
            上面的联合索引中都把年龄放在最后,因为查询只能使用最左前缀索引,知道遇到第一个范围条件,而大部分查询条件都是按年龄范围查询
            当然也可以用in(18,19,20,21)来替代范围查询,但是不是所有的范围查询都可以转换的
            
    范围查询:
        where id>10 是范围查询  where id in(10,20,30)是多个等值查询
        对于范围查询,无法使用后面的索引了,而对于多个等值查询则没有这个限制
        
        如果要查询过去几周上线的过的用户
        where last_onlie > DATE_SUB(NOW(),INTERVAL 7 DAY) and between 18 and 25
        这是一个范围查询 ,但无法使用last_onlie后者age列的索引,如果无法使用age转化为一个INT()列表,当仍然要按照这个两个范围快速查询,没有办法直接解决.
        方案:
            增加多一个active列,由一个定时任务来维护,用户每次登录是设为1,过去7天未登录的设为0,可以创建索引(active,sex,country,age)来满足上面的业务查询
            
    排序:
        表t有联合索引(sex,rating)
        对于查询 select <cols> from t where sex = 'M' order by rating limit 100000,10;
        
        无论如何创建索引,翻页比较靠后,会很慢,需要花费大量时间扫描需要丢弃的数据;反范式,预先计算和缓存可能是解决之类查询仅有的策略,还有一个办法限制用户能翻页的数量,很少用户会翻页到第100000页
        
        还有一个方法,使用关联延迟:
            通过使用覆盖索引查询需要返回的主键,再根据主键关联原表获得需要的行
            select <cols> from t inner join (select <primary key cols> from t where sex = 'M' order by rating limit 100000,10) AS X USING(primary key cols);
            
    一个复杂查询分解为多个简单查询:
        以前硬件没现在好,认为网络通信,查询解析是一件事代码很高事,对mysql并不适用,mysql断开和连接都是很轻量级的
        有时候分解成多个简单查询是很有用的,尤其是多表关联
        好处:
            1.命中缓存
            2.执行单个查询可以减少锁的竞争
            3.减少冗余记录的查询
            4.在应用程序中实现哈希关联,而不是使用mysql的嵌套循环关联,某些场景下效率高非常多
            
        场景:
            1.应用能够方便的缓存单个查询结果
            2.可以将数据分布到不同的Mysql服务器
            3.能够使用in()的方式代替关联查询
            4.查询中使用同一个数据表
            
            
关联子查询:
    t1和t2都走索引
    EXPLAIN SELECT * FROM t2 WHERE a IN
    (SELECT a FROM t2 WHERE a = 10)
            
            
    
show full procedureList  有详情
        
        
6.5章
            

猜你喜欢

转载自blog.csdn.net/qq_28394359/article/details/89639888
今日推荐