mysql优化
现状:一串sql语句查询很慢,优化思路如下:
- where条件中字段是否需要建立索引?
答:经常查询的要建立索引;有and的可以创建复合索引;
- 那么索引建立好了?仍然慢?不妨通过explain执行计划查看分析情况
答:可以通过可能用到的索引字段和用到的索引字段,分析为什么没有使用此索引;宗旨使sql最大程度走索引
- 建立了索引为什么可能走但是实际没有走呢?
答:查看是否命中以下不会走索引的情况:
- 尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描;
- 尽量避免使用in 和not in,会导致引擎走全表扫描;
- 尽量避免使用 or,会导致数据库引擎放弃索引进行全表扫描;
- 尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描;
- 尽量避免在where条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描;
- 当数据量大时,避免使用where 1=1的条件。通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描;
- 查询条件不能用 <> 或者 !=;
- where条件仅包含复合索引非前置列(最左匹配原则);
- 隐式类型转换造成不使用索引;
- order by 条件尽量要与where中条件一致,否则order by不会利用索引进行排序;
- 上面都已经避免后仍然查询较慢?
答:可以尝试看下如下几点:
- 避免出现select *;
- 避免出现不确定结果的函数;
- 多表关联查询时,小表在前,大表在后;
- 用where字句替换having字句;
- 调整where字句中的连接顺序,mysql采用从左往右,自上而下的顺序解析where子句。根据这个原理,应将过滤数据多的条件往前放,最快速度缩小结果集;
存储引擎
innodb存储引擎
结构图如下:内存结构;磁盘结构;后台线程
内存结构
innodb 提供了自己的内存组件,主要包括缓冲池(buffer pool)、变更缓冲(change buffer)、日志缓冲(logbuffer)以及自适应哈希索引(adaptive hash index)技术
缓冲池
缓冲池是 innodb 在内存中的一个缓冲区域,主要用于缓存访问过的表和索引等数据。缓冲池利用内存直接处理数据,避免磁盘操作,从而加快了数据处理的速度。
缓冲池管理算法
为了提高大量读取操作时的效率,缓冲池被划分为页(page),每个页可能包含多行数据。为了提高缓存管理的效率,缓冲池被实现为页组成的链接列表。最终缓冲池使用特定的 lru(最近最少使用)算法进行管理,从而将频繁访问的数据保留在缓存中,将最少使用的缓存页移除。
变更缓冲
- 变更缓冲缓存了那些不在缓冲池中的二级索引(secondary index)页的修改操作。insert、update或者delete操作导致的变更将会在此缓冲,随后再合并(由其他读取操作引起)到缓冲池中。下图演示了变更缓冲的作用过程;
- 与聚集索引(clustered index)不同,二级索引通常是非唯一索引,索引的插入、新、删除通常是顺序随机的操作。将变更进行缓存,并且在随后读入缓冲池时进行合并,能够避免将辅助索引页从磁盘读入缓冲池所需的大量随机 i/o;
- 当系统处于空闲状态或在缓慢关闭期间运行清除操作,定期将更新后的索引页写入磁盘。相对于每次将数据即写入磁盘,这种清除操作可以更有效地写入多个连续的索引值;
日志缓冲
- 日志缓冲是重做日志(redo log)的内存缓冲,日志缓冲的大小由变量》》 innodb_log_buffer_size 决定,默认为 16
mb。日志缓冲的内容会定期刷新到磁盘文件。设置一个大的日志缓冲使得大型事务不必在提交之前将重做日志数据写入磁盘。因此,如果存在需要更新、插入或者删除大量数据的事务,可以通过增加日志缓冲的大小减少磁盘;
i/o。- 系统变量 innodb_flush_log_at_trx_commit 用于控制日志缓冲写入磁盘的方式。默认值为 1,即每次事务提交都会刷新缓冲到磁盘,满足 acid 特性;
- 系统变量 innodb_flush_log_at_timeout 用于控制日志缓冲刷新到磁盘的频率。默认值为 1 秒,即每隔 1 秒刷新一次;
自适应哈希索引
- innodb 包含了一个监控索引查找的机制,当 innodb
发现哈希索引可以提高查询的性能时会自动创建哈希索引。哈希索引基于索引键的一个前缀部分创建,可能只包含了
b 树索引中的一些值,通常时频繁访问的索引页
磁盘结构
innodb 提供的磁盘存储组件主要包括表空间(tablespace)、表(table)、索引(index)、重做日志(redolog)、回滚日志(undo logs)以及双写缓冲(doublewrite buffer)
表空间
- 表空间是一个逻辑上的存储概念,用于存储数据表、索引、回滚(undo)数据等。一个表空间对应操作系统上的一个或者多个文件。从逻辑概念上来说,表空间又是由段(segment)组成,段由区间(extent)组成,区间由页(page)组成,页最终由行(row)组成;
- innodb 提供的表空间包括:系统表空间(system tablespace)、独立表空间(file-per-tabletablespaces)、通用表空间(general tablespaces)、回滚表空间(undotablespaces)以及临时表空间(temporary tablespaces);
系统表空间:
系统表空间用于存储双写缓冲和变更缓冲。如果创建表和索引时不使用独立表空间或通用表空间,它们也会被存储到系统表空间;不推荐这种做法。在 mysql 8.0 之前,系统表空间中还包含了 innodb 数据字典信息;从 mysql 8.0 开始, innodb 使用统一的 mysql 数据字典存储元数据;
独立表空间:
独立表空间(file-per-table tablespaces)用于存储单个 innodb 表的数据和索引,每个表空间在文件系统中对应单个数据文件。举例来说,如果我们为 test 数据库创建一个表 t1,mysql 会在数据目录下的 test 子目录中创建一个数据文件 t1.idb
通用表空间
通用表空间是一种共享的 innodb 表空间,可以供多个表和索引使用。通用表空间比独立表空间具有更高的内存利用率。mysql 服务器将会缓存表空间的元数据,包含多个表的通用表空间需要的内存比多个独立表空间更少。通用表空间可以像独立表空间一样在 mysql 数据目录内部或者外部创建数据文件,从而为关键的表指定单独的存储,例如 raid 或者 drbd,提高数据访问的性能
回滚表空间
回滚表空间用于存储回滚日志,回滚日志记录中包含了撤销事务对聚集索引记录所作的最新修改所需的信息。回滚记录存储在回滚日志段中,回滚日志段存储在回滚段中
临时表空间
innodb 存在两种临时表空间:会话临时表空间(session temporary tablespaces)和一个全局临时表空间(global temporary tablespace)。
- 会话临时表空间用于存储用户创建的临时表;当 innodb 被设置为磁盘内部临时表的存储引擎时,会话临时表空间也用于优化器创建的内部临时表。从 mysql 8.0.16 开始,磁盘内部临时表的存储引擎永远都是 innodb;在此之前由参数 internal_tmp_disk_storage_engine 决定 。
- 全局临时表空间(ibtmp1)存储了用户临时表修改信息的回滚段数据。系统变量 innodb_temp_data_file_path 定义了全局临时表空间数据文件的相对路径、名称、大小以及属性。如果没有指定该参数,默认在 innodb_data_home_dir 目录中创建一个名为 ibtmp1 的自动扩展的数据文件,初始大小略微大于 12 mb
表
表是数据库中存储数据的主要对象,使用create table语句创建
索引
- innodb 表按照索引的组织方式存储数据,被称为聚簇索引(clustered index)
- 除了聚簇索引之外的索引被称为二级索引(secondary indexes)。innodb二级索引中的每个索引记录都包含了主键索引列的值,以及二级索引的字段。innodb使用主键值查找聚集索引中的数据行。因此,如果主键字段很长,二级索引就需要占用更多的磁盘空间,查找的效率就会更低。这也就是为什么 innodb 推荐使用简单的数字作为主键
双写缓冲
双写缓冲是系统表空间中的一个存储区域;在 innodb将缓冲池刷新到数据文件之前,会先将缓冲页写入该区域。如果在写入数据页的过程中,出现了操作系统、存储系统或者 mysqld进程崩溃,innodb 可以利用双写缓冲存储的缓冲页进行故障恢复。
由于 innodb 的数据页大小往往和操作系统数据页大小不一致,例如 innodb 为 16 kb,操作系统为 4 kb;此时 innodb 刷新一个数据页,操作系统需要刷新 4个数据页,在系统故障时可能只刷新了部分数据页。双写缓冲会先把缓冲池的数据写入共享表空间,然后再刷新数据页;如果在这个过程中发生系统崩溃,innodb可以从共享表空间获取到要刷新的数据,然后重新执行写入
重做日志
重做日志用于故障恢复时修复未完成事务的数据,它位于磁盘中,与内存中的日志缓冲相对应。在正常操作过程中,重做日志记录了表中的数据修改信息。当系统出现异常关闭后,重新启动时自动利用重做日志恢复未更新到数据文件中的修改。
回滚日志
回滚日志由一组回滚日志记录组成,这些记录属于单个读写事务。回滚日志记录包含了回滚一个事务对聚集索引记录的最新修改所需的信息。另外,如果另一个事务需要查看原始的数据(一致性读),将会从回滚日志记录中返回未修改前的数据。
回滚日志存储在回滚日志段中,后者包含在回滚段中;回滚段存储在回滚表空间以及全局临时表空间中
后台线程
从磁盘刷新内存池中的数据,保证缓冲池中缓存的数据是最新的;
将缓冲池中已修改的数据文件刷新到磁盘;
保证数据库异常时innodb能恢复到正常运行状态
其他知识点
事务的acid属性
原子性
原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。
实现原理:undo log 实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的 sql 语句。
当事务对数据库进行修改时,innodb 会生成对应的 undo log。如果事务执行失败或调用了 rollback,导致事务需要回滚,便可以利用 undo log 中的信息将数据回滚到修改之前的样子。undo log 属于逻辑日志,它记录的是 sql 执行相关的信息。当发生回滚时,innodb 会根据 undo log 的内容做与之前相反的工作:
持久性
持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
实现原理:redo log
innodb 作为 mysql 的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘 io,效率会很低。
为此,innodb 提供了缓存(buffer pool),buffer pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:
当从数据库读取数据时,会首先从 buffer pool 中读取,如果 buffer pool 中没有,则从磁盘读取后放入 buffer pool。当向数据库写入数据时,会首先写入 buffer pool,buffer pool 中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。buffer pool 的使用大大提高了读写数据的效率,但是也带来了新的问题:如果 mysql 宕机,而此时 buffer pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log 被引入来解决这个问题:当数据修改时,除了修改 buffer pool 中的数据,还会在 redo log 记录这次操作;当事务提交时,会调用 fsync 接口对 redo log 进行刷盘。
如果 mysql 宕机,重启时可以读取 redo log 中的数据,对数据库进行恢复。
redo log 采用的是 wal(write-ahead logging,预写式日志),所有修改先写入日志,再更新到 buffer pool,保证了数据不会因 mysql 宕机而丢失,从而满足了持久性要求。
既然 redo log 也需要在事务提交时将日志写入磁盘,为什么它比直接将 buffer pool 中修改的数据写入磁盘(即刷脏)要快呢?
主要有以下两方面的原因:
刷脏是随机 io,因为每次修改的数据位置随机,但写 redo log 是追加操作,属于顺序 io。刷脏是以数据页(page)为单位的,mysql 默认页大小是 16kb,一个 page 上一个小修改都要整页写入;而 redo log 中只包含真正需要写入的部分,无效 io 大大减少。redo log 与 binlog
我们知道,在 mysql 中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的。
作用不同:
redo log 是用于 crash recovery 的,保证 mysql 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制。层次不同:
redo log 是 innodb 存储引擎实现的,而 binlog 是 mysql 的服务器层(可以参考文章前面对 mysql 逻辑架构的介绍)实现的,同时支持 innodb 和其他存储引擎。内容不同:
redo log 是物理日志,内容基于磁盘的 page。binlog 是逻辑日志,内容是一条条 sql。写入时机不同:
redo log 的写入时机相对多元。前面曾提到,当事务提交时会调用 fsync 对 redo log 进行刷盘;这是默认情况下的策略,修改 innodb_flush_log_at_trx_commit 参数可以改变该策略,但事务的持久性将无法保证。除了事务提交时,还有其他刷盘时机:如 master thread 每秒刷盘一次 redo log 等,这样的好处是不一定要等到 commit 时刷盘,commit 速度大大加快。
binlog 在事务提交时写入。
隔离性
隔离性是指事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
实现原理:详情
通过readview生成策略的不同实现不同的隔离级别。
readview中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。假设当前列表里的事务id为[80,100]。如果你要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。
如果你要访问的记录版本的事务id为70,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。如果不在那说明事务已经提交,所以版本可以被访问。
如果你要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在readview生成之后才发生的,所以不能被访问。
这些记录都是去版本链里面找的,先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束。
一致性
一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。比如转账不会出现a账户扣了钱而b账户没有收到钱
事务的隔离性级别
mysql的默认隔离级别是可重复读,不是读已提交。
未提交读 (read uncommited)
在未提交读这种隔离级别中,对数据的修改,即使还未提交,对其他的事务也是可见的。事务可以读取未提交的数据,也称之为“脏读”,读到的数据被称之为“脏数据”
已提交读(read commited)
这个隔离级别是大多数操作系统的默认隔离级别,也是平时用的最多的隔离级别。常见的oracle、sqlserver等,但是mysql是个例外,mysql的默认隔离级别是可重复读。已提交读满足前面隔离性的简单定义。一个事务对数据所作的修改,只有到提交之后才能看到
可重复读(repeatable read)
该级别保证了在同一个事务中多次读取同样的记录的结果是一致的
可串行化(serializable)
串行化是最高的隔离级别。串行化会在读取的每一行上都加锁,所以可能会导致大量的锁超时和锁争用问题。在实际业务中我们很少使用这个隔离级别。除非是严格要求数据一致性,并且可以接受在没有并发的前提下,我们才会考虑使用这种隔离级别
mvcc
- mvcc(mutil-version concurrency control),就是多版本并发控制。mvcc 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问
- 在mysql的innodb引擎中就是指在已提交读(read committd)和可重复读(repeatable read)这两种隔离级别下的事务对于select操作会访问版本链中的记录的过程
- 我们先来理解一下版本链的概念。在innodb引擎表中,它的聚簇索引记录中有两个必要的隐藏列:
- 版本链: 我们先来理解一下版本链的概念。在innodb引擎表中,它的聚簇索引记录中有两个必要的隐藏列:
trx_id :
这个id用来存储的每次对某条聚簇索引记录进行修改的时候的事务id。
roll_pointer:
每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
事务实现原理
就是acid以及隔离级别实现原理
聚簇索引和非聚簇索引
聚簇索引又称主键索引 叶子节点存放的是id和数据页 非聚簇索引叶子节点存放的数据项指针或者主键id需要二次查找
在线大表加索引
先说下mysql加索引原理:
mysql默认online ddl的原理如下:
1、创建一个和原来表结构一样的临时表并ddl
2、锁住原表,所有数据都无法写入(insert,update,delete)
3、将原表数据写入到临时表中(通过insert …select方式)
4、写入完后,重命名临时表和原表名称
5、删除原表,释放锁
- ag真人游戏的解决方案 第一种方案:
- 先创建临时表t_sys_test_temp
- 向临时表t_sys_test_temp 添加索引
- 将目标表的数据查询出来插入到临时表,再将目标表t_sys_test重命名为temp2, 将临时表t_sys_test_temp 重命名为结果表t_sys_test
- 删除临时表temp2
- 此时会有一个问题在:操作插入临时表数据和rename表的时候,旧表有新数据插入;此情况可以将新建的临时表预留主键id 然后在用户访问较少时间进行切换将切的过程中新插入旧表的数据插入到预留逐渐id记录中 不过这个很low;下面有更好的
- ag真人游戏的解决方案 第二种方案:
通过pt工具pt-online-schema-change在线ddl原理如下:
1、创建一个和原来表结构一样的临时表并ddl
2、将原表数据写入到临时表中(通过insert…select方式),并且在原表上创建触发器,如果原表有数据写入,通过触发器方式将新增的数据写入临时表中(前提该表之前没有触发器)3、写入完后,重命名临时表和原表名称
4、删除原表
mysql为什么主键要用自增id
详情(详情)