一、存储引擎
1、SQL语句执行流程
MySQL大体上可分为Server层和存储引擎层两部分。
Server层:
- 连接器:TCP握手后服务器来验证登陆用户身份,A用户创建连接后,管理员对A用户权限修改了也不会影响到已经创建的链接权限,必须重新登陆。
- 查询缓存:查询后的结果存储位置,MySQL8.0版本以后已经取消,因为查询缓存失效太频繁,得不偿失。
- 分析器:根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。
- 优化器:多种执行策略可实现目标,系统自动选择最优进行执行。
- 执行器:判断是否有权限,将最终任务提交到存储引擎。
存储引擎:
- Innodb(默认):innodb里面完整数据都在主键索引的叶子节点上面。主键索引:存储索引和数据;辅助索引:存储索引和主键值
- (1)支持事务(redo log和undo log);
- (2)行级锁;支持外键;
- (3)支持崩溃后的安全恢复(bin log);
- (4)支持MVCC;
- (5)不保存表的具体行数,执行select count(*) from table时需要全表扫描;
- MyISAM:有两个存储文件,data和index分开;主键索引和辅助索引的方式相同,需要再index文件上面拿到数据的磁盘地址
- (1)不支持事务;
- (2)默认表级锁;不支持外键;
- (3)不支持崩溃后的安全恢复;
- (4)支持MVCC;
- (5)MyISAM用一个变量保存了整个表的行数,执行select count(*) from table时只需要读出该变量即可,速度很快;
- Memory:也叫 HEAP(堆内存),使用存在内存中的内容来创建表。
- 不允许使用BLOB或TEXT字段
- 只能使用比较运算符=,<,>,=>,= <
- HEAP表不支持AUTO_INCREMENT
- 索引不可为NULL
2、一张表里面有ID自增主键,当insert了17条记录之后,删除了第15,16,17条记录,再把mysql重启,再insert一条记录,这条记录的ID是18还是15 ?
一般情况下,我们创建的表的类型是InnoDB,如果新增一条记录(不重启mysql的情况下),这条记录的id是18;但是如果重启(文中提到的)MySQL的话,这条记录的ID是15。因为InnoDB表只把自增主键的最大ID记录到内存中,所以重启数据库或者对表OPTIMIZE操作,都会使最大ID丢失。
但是,如果我们使用表的类型是MylSAM,那么这条记录的ID就是18。因为MylSAM表会把自增主键的最大ID记录到数据文件里面,重启MYSQL后,自增主键的最大ID也不会丢失。
二、索引
1、索引为什么使用B+树而不是B树?
- IO次数少:B+树的中间结点只存放索引,数据都存在叶结点中,因此中间结点可以存更多的数据,让索引树更加矮胖;
- 范围查询效率更高:B树需要中序遍历整个树,只B+树需要遍历叶结点中的链表;
- 查询效率更加稳定:每次查询都需要从根结点到叶结点,路径长度相同,所以每次查询的效率都差不多;
2、索引的几个概念:
- 聚簇索引:B+树的非叶子节点只存储索引,而叶子节点存储索引和数据,这种索引和数据在一起存储的索引方式叫做聚簇索引。一张表只能有一个聚簇索引。
(1)如果表定义了主键,则之间就是聚集索引;
(2)如果表没有定义主键,则第一个非空唯一索引是聚集索引;
(3)如果没有这样的非空索引,InnoDB会创建一个隐藏的row_id作为聚集索引; - 非聚簇索引:非聚簇索引(二级索引)保存的是主键值,这一点和myisam保存的是数据地址是不同的。
- 覆盖索引:覆盖索引指的是在一次查询中,如果一个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,而不再需要回表查询。
- 回表:先通过普通索引的值定位聚簇索引值,再通过聚簇索引的值定位行数据,去取普通索引中未能提供的数据,即为回表。
explain select * from user where age=1; //查询的name无法从索引数据获取 (回表)
explain select id,age from user where age=1; //可以直接从索引获取(覆盖索引) - 联合索引:相对单列索引,组合索引是用多个列组合构建的索引。
- 最左匹配原则:对多个字段同时建立的组合索引(有顺序,ABC,ACB是完全不同的两种联合索引) 以联合索引(a,b,c)为例,建立这样的索引相当于建立了索引a、ab、abc三个索引。另外组合索引实际还是一个索引,并非真的创建了多个索引,只是产生的效果等价于产生多个索引。
3、索引的缺点:
- (1)创建跟维护索引都需要消耗时间
- (2)创建索引时,需要对表加锁,在锁表的同时,可能会影响到其他的数据操作
- (3)索引文件会占用磁盘空间
- (4)当对表中的数据进行CRUD的时,也会触发索引的维护,而维护索引需要时间,可能会降低数据操作性能
4、索引设计的原则:
- (1)频繁增删改的列不要建立索引
- (2)有大量重复的列不要建立索引
- (3)数据量小的表不要建立索引
5、索引的失效场景:
- (1)模糊搜索:左模糊或全模糊都会导致索引失效,比如'%a'和'%a%'。但是右模糊是可以利用索引的,比如'a%' 。
- (2)隐式类型转换:比如select * from t where name = xxx , name是字符串类型,但是没有加引号,所以是由MySQL隐式转换的,所以会让索引失效
- (3)当语句中带有or的时候:比如select * from t where name=‘sw’ or age=14
- (4)不符合联合索引的最左前缀匹配:(A,B,C)的联合索引,你只where了C或B或只有B,C
- (5)如果mysql估计使用全表扫描要比使用索引快,则不使用索引。例如
① 某些范围查询 select * from table_name where key>1 and key<90
② 或者数据量极少的表
6、InnoDB引擎的行锁是通过加在什么上实现的?
答:基于索引;
7、使用like 'a%' 、like'%a'、like'%a%'查询时是否会使用索引
答:'a%'会,其他两个不会;
8、为什么用自增列作为主键?
各条数据记录按主键顺序存放于主索引的叶子节点上(大小为一个内存页或磁盘页),每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。
如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。
如果使用非自增主键,由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置。此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销。同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。
三、事务
1、说一下事务的基本特性和隔离级别?
基本特性:ACID(原子性、一致性、隔离性、持久性)
- 原子性:原子性指的是一个事务中的操作要么全部成功,要么全部失败。
- 一致性:一致性指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设中间sql执行过程中系统崩溃A也不会损失100块,因为事务没有提交,修改也就不会保存到数据库。
- 隔离性:隔离性指的是一个事务的修改在最终提交前,对其他事务是不可见的。
- 持久性:持久性指的是一旦事务提交,所做的修改就会永久保存到数据库中。
隔离级别:而隔离性有4个隔离级别,分别是:
- 读未提交:read uncommit ,可能会读到其他事务未提交的数据,也叫做脏读。
- 读已提交:read commit ,两次读取结果不一致,叫做不可重复读。(不可重复读现象指的是,在一个事务内,连续两次查询同一条数据,查到的结果前后不一样)
- 可重复读:repeatable read ,这是mysql的默认级别,就是每次读取结果都一样,但是有可能产生幻读。
- 串行化:serializable ,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。
2、那ACID靠什么保证的呢?
- 原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
- 一致性一般由代码层面来保证
- 隔离性由MVCC来保证
- 持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复
3、那你说说什么是幻读?
幻读指当用户读取某一范围的数据时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的幻影行。InnoDB存储引擎通过多版本并发控制(MVCC)机制解决了该问题。注:其实多版本只是解决不可重复读问题,而加上间隙锁(也就是这里所谓的并发控制)才解决了幻读问题。
4、什么是MVCC?
MVCC叫做多版本并发控制,实际上就是保存了数据在某个时间节点的快照。我们每行数实际上隐藏了两列,创建时间版本号,过期(删除)时间版本号,每开始一个新的事务,版本号都会自动递增。
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
- 快照读:简单的select操作,属于快照读,不加锁。
select * from table where ?
- 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)
select * from table where ? lock in share mode
select * from table where ? for update
insert into table values (…)
update table set ? where ?
delete from table where ?
5、InnoDB的行锁模式及加锁方法?
- 共享锁(s):又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
- 排他锁(X):又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。
对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
意向锁:另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
- 意向共享锁(IS):事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
- 意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
加锁方法:意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁。
事务可以通过以下语句显式给记录集加共享锁或排他锁:
共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE
行锁实现方式:InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
(1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
(2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决 定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。
6、那你知道什么是间隙锁吗?
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的 索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。
(1)举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:
Select * from emp where empid > 100 for update;
是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
(2)InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!
假设emp表中只有101条记录,其empid的值分别是1,2,……,100,101。 在一个事务中执行如下语句加锁:
Select * from emp where empid > 100 for update;
在另一个事务中,执行如下语句会出现锁等待的情况
insert into emp(empid,...) values (201,...);
7、InnoDB使用间隙锁的目的:
- (1)防止幻读,以满足相关隔离级别的要求;
- (2)满足恢复和复制的需要:
MySQL 通过 BINLOG 录入执行成功的 INSERT、UPDATE、DELETE 等更新数据的 SQL 语句,并由此实现 MySQL 数据库的恢复和主从复制。MySQL 的恢复机制(复制其实就是在 Slave Mysql 不断做基于 BINLOG 的恢复)有以下特点:
一是 MySQL 的恢复是 SQL 语句级的,也就是重新执行 BINLOG 中的 SQL 语句。
二是 MySQL 的 Binlog 是按照事务提交的先后顺序记录的, 恢复也是按这个顺序进行的。
由此可见,MySQL 的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读。
四、Mysql优化
1、SQL优化主要分5个方向:SQL语句跟索引、表结构设计、系统配置、硬件配置、数据库架构扩展
- (1)SQL语句跟索引:①最大化利用索引 ②尽可能避免全表扫描 ③减少无效数据的查询
- (2)表结构设计:①表设计符合三范式 ②选择合适的数据类型
- (3)系统配置:①选择合适的存储引擎 ②系统内核优化(Mysql主要运行在Linux上)
- (4)硬件配置:①加大物理内存 ②使用SSD硬盘代替SAS硬盘 ,毕竟数据库的压力主要来自磁盘I/O方面
- (5)数据库架构扩展:①读写分离 ②分库分表分区 ③给数据库增加缓存系统
2、你们数据量级多大?分库分表分区怎么做的?
分区:就是把一张表的数据分成N个区块,在逻辑上看最终只是一张表,但底层是由N个物理区块组成的。
分表:就是把一张表按一定的规则分解成N个具有独立存储空间的实体表。
分库:一旦分表,一个库中的表会越来越多。
首先分库分表分为垂直和水平两个方式,一般来说我们拆分的顺序是先垂直后水平。
3、那分表后的ID怎么保证唯一性的呢?
(1)设定步长,比如1-1024张表我们分别设定1-1024的基础步长,这样主键落到不同的表就不会冲突了。
(2)分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法这种。
(3)分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,比如订单表订单号是唯一的,不管最终落在哪张表都基于订单号作为查询依据,更新也一样。
4、数据库三大范式
- 第一范式:保证数据的原子性,数据库表中的字段不可再分
- 第二范式:消除部分依赖,非主键列完全依赖于主键,而不能是依赖于主键的一部分(主要针对联合主键而言)
- 第三范式:消除传递依赖,非主键列只依赖于主键,不依赖于其他非主键
五、Mysql主从同步(读写分离、数据备份)
1、说说mysql主从同步怎么做的吧?
(1)master数据发生变化,提交完事务后,写入binlog
(2)slave连接到master,获取binlog
(3)master创建dump线程,推送binglog到slave
(4)slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中
(5)slave再开启一个sql线程读取relay log事件并在slave执行,完成同步
(6)slave记录自己的binglog
.Master 数据库只要发生变化,立马记录到Binary log 日志文件中
2.Slave数据库启动一个I/O thread连接Master数据库,请求Master变化的二进制日志
3.Slave I/O获取到的二进制日志,保存到自己的Relay log 日志文件中。
4.Slave 有一个 SQL thread定时检查Realy log是否变化,变化那么就更新数据
由于mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念。
- 全同步复制:主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。
- 半同步复制:和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。
2、主从同步/复制的好处
- (1)实现服务器负载均衡(读写分离)
- (2)通过复制实现数据的异地备份
- (3)提高数据库系统的可用性
六、执行计划
使用 MySQL 的执行计划来查看 SQL 语句的执行效率
EXPLAIN SELECT * FROM test_aa WHERE id = 1
1、select_type:查询的类型,主要是用于区分普通查询、联合查询、子查询等。简单的 select 查询,查询中不包含子查询或者 union
- PRIMARY:查询中包含子部分,最外层查询则被标记为 primary
- SUBQUERY:SUBQUERY 表示在 select 或 where 列表中包含了子查询,
- UNION:表示 union 中的第二个或后面的 select 语句
2、table:查询涉及到的表。
3、type :访问类型,SQL 查询优化中一个很重要的指标,结果值从好到坏依次是:system > const > eq_ref > ref > range > index > ALL。
- system:系统表,少量数据,往往不需要进行磁盘IO
- const:常量连接
- eq_ref:主键索引(primary key)或者非空唯一索引(unique not null)等值扫描
- ref:非主键非唯一索引等值扫描
- range:范围扫描
- index:索引树扫描
- ALL:全表扫描(full table scan)
4、possible_keys:查询过程中有可能用到的索引。
5、key:实际使用的索引,如果为 NULL ,则没有使用索引。
6、rows:根据表统计信息或者索引选用情况,大致估算出找到所需的记录所需要读取的行数。
7、filtered:表示返回结果的行数占需读取行数的百分比, filtered 的值越大越好。
8、Extra :十分重要的额外信息。
- Using index:表示 SQL 操作中使用了覆盖索引(Covering Index),避免了访问表的数据行,效率高。
- Using index condition:表示 SQL 操作命中了索引,但不是所有的列数据都在索引树上,还需要访问实际的行记录。
- Using where:表示 SQL 操作使用了 where 过滤条件。