一、mysql架构
Server层涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB。
1. 连接器
连接器负责跟客户端建立连接、获取权限、维持和管理连接。
mysql -h$ip -P$port -u$user -p
如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。
mysql连接默认为长连接,时长为8小时。
MySQL在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM)。
解决方案:
- 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
- 如果你用的是MySQL 5.7或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
2. 查询缓存
mysql将之间查询过的语句以key-value形式保存在内存中,其中key表示查询语句,value表示查询结果。在之后的查找中如果发现相同的select会直接将value返回,以加快查询效率。
缺点:只要表上有任何一条数据更新,整张表上的缓存都会被清空,这意味着查询缓存的命中率非常低,因此整体上查询缓存弊大于利,一般只会在静态表中才会使用。
select SQL_CACHE * from T where ID=10;
MySQL 8.0版本直接将查询缓存的整块功能删掉了。
3.分析器
做什么
mysql对输入进来的sql语句进行词法分析与语法分析。
词法分析判断select,表名,字段等信息。
语法分析判断输入语法是否正确,如果不正确会报错。
4. 优化器
怎么做
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。优化器的作用就是决定选择使用哪一个方案。
5.执行器
着手做
判断是否有对对应表的执行权限,若有,进行下一步;若没有,返回错误信息。如果命中查询缓存,会在查询缓存放回结果的时候,做权限验证。
根据引擎定义,使用引擎接口进行操作
- 调用InnoDB引擎接口取这个表的第一行,判断ID值是不是10,如果不是则跳过,如果是则将这行存在结果集中;
- 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
- 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
二、日志
WAL技术,WAL的全称是Write-Ahead Logging,它的关键点就是先写日志(redo log),再写磁盘(binlog),也就是先写粉板,等不忙的时候再写账本。
write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos和checkpoint之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果write pos追上checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint推进一下。
- redo log使得InnoDB可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。 - redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。
- redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
Binlog有两种模式,statement 格式的话是记sql语句, row格式会记录行的内容,记两条,更新前和更新后都有。
两阶段提交:利用事务机制保证数据的一致性
- 先写redo log后写binlog。假设在redo log写完,binlog还没有写完的时候,MySQL进程异常重启。由于我们前面说过的,redo log写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行c的值是1。
但是由于binlog没写完就crash了,这时候binlog里面就没有记录这个语句。因此,之后备份日志的时候,存起来的binlog里面就没有这条语句。
然后你会发现,如果需要用这个binlog来恢复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了这一次更新,恢复出来的这一行c的值就是0,与原库的值不同。 - 先写binlog后写redo log。如果在binlog写完之后crash,由于redo log还没写,崩溃恢复以后这个事务无效,所以这一行c的值是0。但是binlog里面已经记录了“把c从0改成1”这个日志。所以,在之后用binlog来恢复的时候就多了一个事务出来,恢复出来的这一行c的值就是1,与原库的值不同。
三、事务隔离
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
脏读是指一个事务还未提交,就被另一个事务读出,如果提交前回滚事务,将导致严重后果。
不可重复读是指一个事务还未提交,涉及到的行数据被另一个事务修改,导致不能重复读出来。
幻读是指在一个事务的修改过程中,另一个事务对表有插入,导致事务认为有行没有做修改。
隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。SQL标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。
- 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);
- 若隔离级别是“读未提交”, 则V1的值就是2。这时候事务B虽然还没有提交,但是结果已经被A看到了。因此,V2、V3也都是2。
- 若隔离级别是“读提交”,则V1是1,V2的值是2。事务B的更新在提交后才能被A看到。所以, V3的值也是2。
- 若隔离级别是“可重复读”,则V1、V2是1,V3是2。之所以V2还是1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
- 若隔离级别是“串行化”,则在事务B执行“将1改成2”的时候,会被锁住。直到事务A提交后,事务B才可以继续执行。所以从A的角度看, V1、V2值是1,V3的值是2。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
可以用show variables来查看当前的状态。
可重复读的实现:事务回滚
在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如图中看到的,在视图A、B、C里面,这一个记录的值分别是1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。
回滚日志在不需要的时候会被删除,当系统里没有比这个回滚日志更早的read-view的时候,回滚就会消失。
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,因此要尽量避免使用长事务。
事务的启动方式
- 显式启动事务语句, begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback。
- set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个select语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行commit 或 rollback 语句,或者断开连接。
- commit work and chain,提交事务并自动启动下一个事务,这样也省去了再次执行begin语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。
四、索引
索引的出现是为了提升数据查询的效率,类似于书的目录。
索引模型:哈希表HashMap、有序数组、二叉树
1.哈希表
哈希表的特点是指定值查询写入(key-value)速度非常快,但范围读写时,如果需要遍历链表速度非常慢。
2. 有序数组
查询时采用二分法查询,时间复杂度O(LOG(N));数据插入时需要移动后面所有记录,时间复杂度O(N)。因此,有序数组只适合作为静态存储引擎。
3. 二叉树
查询时间复杂度O(LOG(N)),写入时间复杂度O(LOG(N))。
这种索引模型看起来是最优办法,但实际中却不使用。原因是,索引不仅存在于内存中,还可能保存在磁盘上,从磁盘上读数据相当慢(10ms),二叉树过高意味着需要大量从磁盘上读取数据,花费时间非常长。因此实际中的数据库使用N叉树代替。
3.1 B+树
innodb引擎采用B+树作为索引,这里只做简单介绍,详细见数据结构。根据叶子节点的内容,索引类型分为主键索引和非主键索引。
主键索引的叶子节点存的是整行数据。在InnoDB里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在InnoDB里,非主键索引也被称为二级索引(secondary index)。
二者的区别举例:
- 如果语句是select * from T where ID=500,即主键查询方式,则只需要搜索ID这棵B+树;
- 如果语句是select * from T where k=5,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次。这个过程称为回表。
3.2 索引维护
B+树为了维护索引有序性,在插入新值的时候需要做必要的维护。在序列后面添加数据可以做到直接添加;在序列中间添加数据需要挪动后面所有的数据,如果当前数据页已满,还需要新建页,并把多出来的数据进行迁移,这会导致索引性能下降,空间利用率降低。
基于以上情况,一般数据库在建表时最好设置自增主键,防止中间插入和页面分裂。
特殊情况,如K-V场景:
- 只有一个索引
- 该索引为唯一索引
可不设置自增主键,避免每次查询都需要搜索两棵树。
3.3 覆盖索引
回表会导致多次索引查询,减少回表可以加快查询。如:
select ID from T where k between 3 and 5
不需要再查一次主键ID就可以完成
覆盖索引是减少回表的常见方法
通过建立联合索引
CREATE TABLE `tuser` (
`id` int(11) NOT NULL,
`id_card` varchar(32) DEFAULT NULL,
`name` varchar(32) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`ismale` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `id_card` (`id_card`),
#联合索引
KEY `name_age` (`name`,`age`)
) ENGINE=InnoDB
可以做到根据name直接查询age而不使用回表,且不用查询整行记录。
3.4 最左前缀
联合索引的最左N个字段或字符串索引的最左M个字符,可以作为最左索引,用于其他查询。所以当已经有了(a,b)这个联合索引后,一般就不需要单独在a上建立索引了。因此,第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
当既需要(a,b)联合索引,又需要a与b单独索引时,优先考虑空间因素,单独将站空间较小的字段设为独立索引。
3.5 索引下推
select * from tuser where name like '张%' and age=10 and ismale=1
name时联合索引的最左,可以索引查询,其余查询条件
- 在mysql 5.6之前需要查询name一个个回表,在主键上找出数据行作比较。
- 在mysql 5.6之后引入索引下推优化,可以在索引遍历中,对联合索引内部包含字段(name,age)做判断,直接过滤掉不满足条件的记录,减少回表次数。
总之,在满足语句要求,索引的目的就是尽可能少的查询。在设计表结构时,也要以减少资源消耗作为目标。
注:重建主键方法:
alter table T engine=InnoDB
这样可以清楚索引缓存记录,不能直接删除再添加,否则会引起全表重建。
如果查询条件使用的是普通索引(或是联合索引的最左原则字段),查询结果是联合索引的字段或是主键,不用回表操作,直接返回结果,减少IO磁盘读写读取正行数据
五、锁
乐观锁:默认相信没有冲突,不加锁,每次读会记录数据版本(版本号或时间戳),更新时做数据验证,相同执行并更新数据版本,不同认为是过期数据。
悲观锁:默认相信有冲突,加锁。
innodb采用表锁和行锁
myisam和memory使用表锁
5.1 库锁
Flush tables with read lock
风险非常大,如:
- 如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆;
- 如果你在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,会导致主从延迟。
5.2 表锁
命中非索引字段,锁住整张表,锁冲突几率比较高,不会出现死锁。
5.3 行锁
行锁符合两阶段锁协议,行锁在需要时添加,在事务提交时消失,因此事务如果需要锁多行,要把冲突率较高的行尽可能放在后面。
基于索引存在,只有命中索引才会出现行锁,锁中where指向行。
特征:锁冲突概率低,并发性高,会出现死锁。
死锁处理方法:
- 添加超时,等待直到超时,通过参数innodb_lock_wait_timeout(默认50s)来设置。
- 发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on。
- 控制并发度或关闭索引。
5.3.1 记录锁
命中索引且索引是唯一索引,如主键ID,只会锁住命中行。
5.3.2 间隙锁
范围查询索引但未命中结果,锁住查询区间,左开右闭。
间隙锁可防止幻读。
#TABLE
#ID 1 3 5 7 PRIMARY KEY
SELECT *FROM TABLE WHERE ID>1 AND ID<3
锁住区间( 1,3 ]
5.3.3 临键锁
范围查询索引有查询到结果,但查询边界没有对应索引,锁住到下一个索引范围。
#TABLE
#ID 1 3 5 7 PRIMARY KEY
SELECT *FROM TABLE WHERE ID>1 AND ID<8
#锁住区间( 1 , +∞)
#TABLE
#ID 1 3 5 7 PRIMARY KEY
SELECT *FROM TABLE WHERE ID>1 AND ID<6
#锁住区间( 1 , 7]
5.4 共享锁 排它锁
共享锁又称读锁,读锁可以叠加,但不能再加排它锁,即不能修改。
select * from table lock in share mode
排它锁又称写锁,写锁不能叠加任何其他锁。
select * from table for update
注意:MDL锁只有在事务结束时才会释放,所以在大批量读表中添加字段要格外小心,防止读写锁累加。最好在添加前检测是否有读的长事务或添加等待时间,超时则放弃更改。
5.5 意向共享锁 意向排它锁
事务如果要给整表加共享锁需要先取得表的意向共享锁;
事务如果要给整表加排它锁需要先取得表的意向排它锁。
原因行级排它锁与表级排它锁表级共享锁不兼容;
存在行级共享锁也与表级排它锁不兼容。
所以加表级锁时需要先进行所有锁确认,过程十分繁琐。
innodb为了简化过程,加入了意向的概念,加了行锁之后添加意向
不能再添加表锁,避免索引扫描。
六、视图
6.1 事务开启
set autocommit = 0 不自动提交事务
begin/start transaction-commit
start transaction with consistent snapshot-commit
6.2 视图类型
- 一个是view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view ... ,而它的查询方法与表一样。
- 另一个是InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。
6.3 读隔离
InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。
而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id。
事务在启动时以自己的ID为基准,只有小于等于ID号且事务ID不在数组内的数据才可以被读取。
InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。
对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能:
- 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
- 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
- 如果落在黄色部分,那就包括两种情况
a. 若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。
简单总结,对于一个事务,除了自己的更新总是可见以外,有三种情况:
- 版本未提交,不可见;
- 版本已提交,但是是在视图创建后提交的,不可见;
- 版本已提交,而且是在视图创建前提交的,可见。
6.4 写隔离
update
select...lock in share mode
select...for update
更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。
之后事务内如果再进行读,因为是自己写的数据,因此是可见的。
加锁读也只能是当前读。
如果有其他事务的写入还未提交,会等待锁释放再写或当前读。
总结:可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
七、普通与唯一索引
7.1 查询
- 普通索引查找到满足条件的第一个记录(5,500)后,需要查找下一个记录,直到碰到第一个不满足k=5条件的记录。
- 由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
这个操作成本对于现在的CPU来说可以忽略不计。
7.2 更新
- 唯一索引需要将数据读入内存做重复判断,不能应用change buffer。
- 普通索引可以使用change buffger,数据更新时,如果已经在内存中,可以直接更新内存;如果数据不在内存中,在不影响数据一致性的前提下,InooDB会将这些更新操作缓存在change buffer中,在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作。change buffer会持久化数据,同时保存在内存和磁盘中。
将change buffer中的操作应用到原数据页,得到最新结果的过程称为merge。除了访问这个数据页会触发merge外,系统有后台线程会定期merge。在数据库正常关闭(shutdown)的过程中,也会执行merge操作。
change buffer用的是buffer pool里的内存,因此不能无限增大。change buffer的大小,可以通过参数innodb_change_buffer_max_size来动态设置。这个参数设置为50的时候,表示change buffer的大小最多只能占用buffer pool的50%。
综上,唯一与普通索引当数据在内存中时处理过程是相似的,只在于去不去重。当数据在磁盘时区别较大,唯一索引需要直接存入磁盘,普通索引会先将操作写在change buffer,合适时再merge。
将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一。change buffer因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。
特殊情况,如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭change buffer。而在其他情况下,change buffer都能提升更新性能。
7.3 redo log与change buffer
redo log 主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。
八、索引选择
server层中,优化器负责选择最佳索引执行语句,以达到扫描行数最少,访问磁盘次数最少的效果。
show index from table
扫描行数通过采样统计估算索引的不同值个数,即基数,基数越大,索引的区分度越好。
INNODB默认选择N个数据页统计页面的不同值,取每页平均值,再乘页面数,得到总的基数。当变更数据行数超过数据库的1/M时,会自动重新做一次索引统计。
在MySQL中,有两种存储索引统计的方式,可以通过设置参数innodb_stats_persistent的值来选择:
- 设置为on的时候,表示统计信息会持久化存储。这时,默认的N是20,M是10。
- 设置为off的时候,表示统计信息只存储在内存中。这时,默认的N是8,M是16。
除基数以外,优化器还要判断语句本身要扫描多少行
explain select *from table where a between 10 and 20
扫描行数会把回表等操作计算在内,因此不会十分准确。
解决办法:
1.修正索引基数统计
analyze table t
2.强制索引
select *from table force index(a) where a between 10 and 20
3.删除无用索引
九、字符串索引
9.1 前缀索引
字符串支持前缀索引,如果不指定长度会包含全字符串。
alter table SUser add index index1(email);
alter table SUser add index index2(email(6));
全字符串索引的好处是减少磁盘扫描次数,前缀索引的好处是占用空间更小。
在建立索引时关注的是区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。因此,我们可以通过统计索引上有多少个不同的值来判断要使用多长的前缀。
select count(distinct email) as L from SUser;
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;
此外,前缀索引与覆盖索引冲突,使用前缀索引截取了字符串,系统回表后一定还要再查一次主键外的其他字段。如:
index(id,email)
select id,email from SUser where email='zhangssxyz@xxx.com';
select id,name,email from SUser where email='zhangssxyz@xxx.com';
前者只需要查询一次email就结束了;后者查询email,回表ID,还要再查一次email。
9.2 优化方法
倒序存储可以有效避免字符串前一段都相同的场景;
将倒序存储的数据正序查询
select field_list from t where id_card = reverse('input_id_card_string');
创建Hash字段也可以避免重复情况,同时还可以减小索引长度
alter table t add id_card_crc int unsigned, add index(id_card_crc);
select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
它们的区别,主要体现在以下三个方面:
- 从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而hash字段方法需要增加一个字段。当然,倒序存储方式使用4个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个hash字段也差不多抵消了。
- 在CPU消耗方面,倒序方式每次写和读的时候,都需要额外调用一次reverse函数,而hash字段的方式需要额外调用一次crc32()函数。如果只从这两个函数的计算复杂度来看的话,reverse函数额外消耗的CPU资源会更小些。
- 从查询效率上看,使用hash字段方式的查询性能相对更稳定一些。因为crc32算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近1。而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。
十、order by
10.1 全字段排序
select city,name,age from t where city='杭州' order by name limit 1000 ;
执行过程:
- 初始化sort_buffer,确定放入name、city、age这三个字段;
- 从索引city找到第一个满足city='杭州’条件的主键id,也就是图中的ID_X;
- 到主键id索引取出整行,取name、city、age三个字段的值,存入sort_buffer中;
- 从索引city取下一个记录的主键id;
- 重复步骤3、4直到city的值不满足查询条件为止,对应的主键id也就是图中的ID_Y;
- 对sort_buffer中的数据按照字段name做快速排序;
-
按照排序结果取前1000行返回给客户端。
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;
内存放不下时,就需要使用外部排序,外部排序一般使用归并排序算法。可以这么简单理解,MySQL将需要排序的数据分成12份,每一份单独排序后存在这些临时文件中。然后把这12个有序文件再合并成一个有序的大文件。
10.2 rowid排序
SET max_length_for_sort_data = 16;
max_length_for_sort_data,是MySQL中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL就认为单行太大,要换一个算法。
执行过程:
- 初始化sort_buffer,确定放入两个字段,即name和id;
- 从索引city找到第一个满足city='杭州’条件的主键id,也就是图中的ID_X;
- 到主键id索引取出整行,取name、id这两个字段,存入sort_buffer中;
- 从索引city取下一个记录的主键id;
- 重复步骤3、4直到不满足city='杭州’条件为止,也就是图中的ID_Y;
- 对sort_buffer中的数据按照字段name进行排序;
-
遍历排序结果,取前1000行,并按照id的值回到原表中取出city、name和age三个字段返回给客户端。
rowid排序相比全字段多了一次查询,对于InnoDB表来说,rowid排序会要求回表多造成磁盘读,因此不会被优先选择。
10.3 联合索引
alter table t add index city_user(city, name);
执行过程:
- 从索引(city,name)找到第一个满足city='杭州’条件的主键id;
- 到主键id索引取出整行,取name、city、age三个字段的值,作为结果集的一部分直接返回;
- 从索引(city,name)取下一个记录主键id;
-
重复步骤2、3,直到查到第1000条记录,或者是不满足city='杭州’条件时循环结束。
联合索引少了排序过程
10.4 覆盖索引
alter table t add index city_user_age(city, name, age);
执行过程:
- 从索引(city,name,age)找到第一个满足city='杭州’条件的记录,取出其中的city、name和age这三个字段的值,作为结果集的一部分直接返回;
- 从索引(city,name,age)取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;
-
重复执行步骤2,直到查到第1000条记录,或者是不满足city='杭州’条件时循环结束。
查询后直接返回结果,回表也不需要,速度最快,但相应的维护代价也最大。
十一、随机数据
11.1 内存临时表
select word from words order by rand() limit 3;
执行过程:
- 创建一个临时表。这个临时表使用的是memory引擎,表里有两个字段,第一个字段是double类型,为了后面描述方便,记为字段R,第二个字段是varchar(64)类型,记为字段W。并且,这个表没有建索引。
- 从words表中,按主键顺序取出所有的word值。对于每一个word值,调用rand()函数生成一个大于0小于1的随机小数,并把这个随机小数和word分别存入临时表的R和W字段中,到此,扫描行数是10000。
- 现在临时表有10000行数据了,接下来你要在这个没有索引的内存临时表上,按照字段R排序。
- 初始化 sort_buffer。sort_buffer中有两个字段,一个是double类型,另一个是整型。
- 从内存临时表中一行一行地取出R值和位置信息(我后面会和你解释这里为什么是“位置信息”),分别存入sort_buffer中的两个字段里。这个过程要对内存临时表做全表扫描,此时扫描行数增加10000,变成了20000。
- 在sort_buffer中根据R的值进行排序。注意,这个过程没有涉及到表操作,所以不会增加扫描行数。
- 排序完成后,取出前三个结果的位置信息,依次到内存临时表中取出word值,返回给客户端。这个过程中,访问了表的三行数据,总扫描行数变成了20003。
过程中需要使用临时表,在内存中建立memory临时表。对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,因此优化器会选择rowid排序。
其中pos代表行在表中的定位:
- 对于有主键的InnoDB表来说,这个rowid就是主键ID;
- 对于没有主键的InnoDB表来说,这个rowid就是由系统生成的6字节数据;
- MEMORY引擎不是索引组织表。在这个例子里面,你可以认为它就是一个数组。因此,这个rowid其实就是数组的下标。
order by rand()使用了内存临时表,内存临时表排序的时候使用了rowid排序方法。
11.2 磁盘临时表
如果临时表大小超过了tmp_table_size(默认16M),那么内存临时表就会转成磁盘临时表。磁盘临时表使用的引擎默认是InnoDB。
优先队列排序算法
因为最终去除的数据较少,对所有数据排序失去了意义,因此采用优先队列排序算法。堆内维护三条数据(R,rowid),比较排序即可。
但如果取出数据过多,如limit 1000,就不会使用这种算法,因为得不偿失。
select city,name,age from t where city='杭州' order by name limit 1000
11.3 ###优化算法
id连续
- 取得这个表的主键id的最大值M和最小值N;
- 用随机函数生成一个最大值到最小值之间的数 X = (M-N)*rand() + N;
- 取不小于X的第一个ID的行。
select max(id),min(id) into @M,@N from t ;
set @X= floor((@M-@N+1)*rand() + @N);
select * from t where id >= @X limit 1;
id不连续优化
- 取得整个表的行数,并记为C。
- 取得 Y = floor(C * rand())。 floor函数在这里的作用,就是取整数部分。
- 再用limit Y,1 取得一行。
select count(*) into @C from t;
set @Y = floor(@C * rand());
set @sql = concat("select * from t limit ", @Y, ",1");
prepare stmt from @sql;
execute stmt;
DEALLOCATE prepare stmt;
service层组装
在java中组装sql代码
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;