索引和事务

普通索引,唯一索引,主键索引,联合索引

索引和数据存储在引擎层。

**二叉查找树:**会退化成链表

**自平衡二叉树:**会自动平衡,时间复杂度会一直在O(logn),但是还是二叉树。节点只能有2个子节点,所以树的层高会很高,每访问一个节点就会有一次磁盘操作,层高=磁盘IO次数(适用于读多写少)

**B树vsB+树:**他们都是平衡多叉树。

1、 B+树只有叶子节点存数据,非叶子节点只有索引,所以非叶子节点容纳更多键。树层高变低,I/O减少。

2、B+树的查询效率更加稳定:每个查询都要查到叶子节点。

3、B+树更便于遍历:B+树直接扫叶子结点进行查询。B树每层每个节点都存数据,需要中序遍历按序扫。

4、B+树更适合基于范围的查询:B+树叶子节点间双向链表相连。

**自适应hash索引:**某些索引值使用频繁时会创建哈希索引,可快速哈希查找。用户无法干涉其创建与配置。

Hash表vsB+树:

  • hash表适合做等值查询,O(1),不适合做范围查询。
  • 相邻key1,key2经过哈希映射后的位置具随机性。
  • 当需按索引进行order by时,hash不支持排序,因为数据是按哈希值排列的,而B+树是按主键排列的。
  • B+树支持部分索引查询,如(a,b,c)组合索引,查询中只用了a和b也能查询到,但hash是用整个(a,b,c)索引计算hash值。不支持部分索引查询。
  • 数据量很大时,hash冲突的概率也会很大。用链表,红黑树解决,空间换时间。

红黑树(二叉树)vsB+树:

红黑树只能二叉,层高,IO多。B+树适用于大规模存储和范围查询,红黑树适用于插入删除操作多的情况

红黑树每个节点都存数据,一个节点里能存的索引变少,层数就会变高。IO多。

B+树叶子节点间双向链表相连,范围查询占优。

主键索引的B+树索引:

在innodb中,每加一个索引,就要多创建一个B+树,所以索引不是越多越好。

是一颗平衡多叉树。N个叶子节点,节点允许的最大子结点个数为d个。O(logdN)

1.叶子节点才存放数据,非叶子节点存放索引。2.每个节点数据按主键顺序存放。3.叶子节点间连成双向链表。4.叶子节点中记录连成单向链表。

二级索引的B+树索引:

1.叶子节点存的是主键值,不是实际数据。2.叶子节点间同样是双向链表。3.节点顺按主键顺序排放。

因为聚簇索引的叶子节点中已经存放了具体数据了,如果二级索引叶子节点也存,数据量就翻倍了。

当我要查询实际数据时,还需回表操作。

为什么B+树非叶子节点不存数据?

非叶子节点仅存储索引,使每个节点可以容纳更多的键。更多键意味着树高度更低。减少磁盘I/O。

B+树找数据的话都在一层里,而B树找数据需要穿梭在不同层之间找,增加了磁盘I/O。

B+树叶子节点双向链表相连,范围查询只需定位到起始叶子节点,然后遍历。

非叶子节点密集存储键,适合磁盘预读(一次性读取连续块),提高缓存命中率。

B+树非叶子节点缓存在内存中,键的密集存储使得内存缓存更有效,加速索引查找。

聚簇索引 VS 非聚簇索引:

聚簇索引一个表只能有一个,非聚簇索引一个表可以有多个。

非聚簇索引存储的是主键值而不是数据地址值,因为聚簇索引中有可能会分页或重排操作,数据可能会移动。

聚簇索引已经把数据存了一份了,所以非聚簇索引叶子节点就直接存主键值就行,如果叶子节点也存了数据的话,存储的数据就会成倍的增长,所以直接存主键值回表就行。

索引覆盖:

当不需要回表操作时,就是索引覆盖,可以在二级索引中直接找到结果,比如只需要查找主键值时。或者我要查询名称、价格时,我只要建立一个包含名称和价格的联合索引就行了,不需要进行回表聚簇索引。

索引下推:

后面的字段不会在 Server 层进行条件判断,而是会被下推到「存储引擎层」进行条件判断(因为c字段的值是在 (a,b,c)联合索引里的),然后过滤出符合条件的数据后再返回给 Server 层。由于在引擎层就过滤掉大量的数据无需再回表读取数据来进行判断,减少回表次数,减少了从存储引擎到数据库服务器的数据传输量。这不仅降低了网络传输开销,还减少了数据库服务器的处理负担,从而提高了查询性能。

没有索引下推:在存储引擎层用索引age过滤出age>30的数据后(最左匹配),每个数据都进行回表操作,比如查出四个age>30的数据,那么这四个数据都需要回表,接着找出salary>5000的数据。

有索引下推:对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。在(age,salary)索引内部就判断了 salary 是否大于5000,对于小于5000 的记录,直接判断并跳过,减少回表次数.

如果没有索引下推优化(或称ICP优化),当进行索引查询时,首先根据索引来查找记录,然后再根据where条件来过滤记录;判断是否可以进行where条件过滤再进行索引查询,也就是说在支持ICP优化后,MYSQL会在取出索引的同时提前执行where的部分过滤操作,在某些场景下,可以大大减少回表次数,从而提升整体性能。

前缀索引:

无法用做覆盖索引,order by不能用前缀索引。

联合索引:

联合索引失效:不使用最左匹配原则。故要使用最左匹配原则。

联合索引(a,b,c),a相同的情况下才去比b,b相同的情况下才去比c,都是需要前一个相同之后,后面的才能比,可以ab,不可以ac,因为c跳过了前一个b。就会变成全表扫描

索引在磁盘上的排列就是先按a排序,再按b排序,最后按c排序。如果没用a查找,直接用b查找,就会导致全表扫描。

利用索引的前提是索引里的key有序。b和c在全局无序,局部相对有序,只有在a相同情况下,b才有序。

联合索引范围查询:

使用范围查询当前字段还可以用联合索引,范围查询当前字段后面的字段无法用到联合索引。

由于查询优化器,所以a字段在where子句中的顺序不重要,如where b=1 and a=2,还是会被优化成先a再b,没有破坏最左匹配。故对于 >=、<=、between、like 前缀匹配的范围查询,并不会停止匹配。

索引区分度:

联合索引中靠前的字段更高概率被用于索引过滤,故靠前索引应有更高区分度,否则优化器会自动忽略索引进行全表扫描。

执行计划:

在sql语句前加explain:

通过使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析

查询语句或是表结构的性能瓶颈

|-------------------|-----------------------|--------------------------|
| 字段 | 描述 | 优化意义 |
| id | 查询的序列号,表示执行顺序 | 用于分析子查询、联合查询的执行顺序。 |
| select_type | 查询类型(简单查询、子查询、union)。 | 判断查询复杂度。 |
| table | 查询涉及的表或派生表。 | 是否访问了预期表,检查是否有不必要临时表。 |
| partitions | 匹配的分区(仅对分区表有效)。 | 确认分区裁剪是否生效。 |
| type | 访问类型(性能关键指标)。 | 尽可能达到const、ref,避免 ALL。 |
| possible_keys | 可能使用的索引。 | 检查是否漏掉可用索引,或未选择最佳索引。 |
| key | 实际使用的索引。 | 确认索引使用情况。 |
| key_len | 索引使用的字节数。 | 判断是否使用索引前缀,或联合索引的覆盖情况。 |
| ref | 与索引比较的列或常量。 | 检查索引是否有效匹配查询条件。 |
| rows | 预估扫描的行数。 | 行数过大需优化索引或查询条件,减少扫描量。 |
| filtered | 查询条件过滤后的行占比。 | 值越小,筛选效率越高。100% 表示未使用索引。 |
| Extra | **额外,**是否用临时表、文件排序 | 优化查询语句或索引。 |

其中"index全索引扫描"的意思是把索引的整张B+树都扫了一遍,而不是把整张表扫了一遍,扫的是索引B+树


索引优点缺点(索引一定能提升效率吗)

创建索引原则(哪些列可成为索引):

order by和group by经常用到的字段。

区分度比较高的字段。

优化索引的方法?

前缀索引优化:

减小了索引字段的大小之后,可以增加一个索引页中存储的索引值数量,有效提高索引的查询速度。

  1. order by不能用前缀索引原因:
  1. 前缀索引无法用作覆盖索引原因:

覆盖索引要求索引中包含我想要查询的字段所需的全部数据,而前缀索引只存储列的部分数据,因此前缀索引无法独立提供完整的信息,无法用作覆盖索引。

主键索引最好是自增的:

首先有个前提,B+树这种数据结构的叶子节点数据都是按主键顺序进行存放的。

  • 使用主键自增:插入的新数据会按顺序添加到当前索引节点位置,都是追加操作,不需移动已有数据,页面写满就会自动开辟一个新页面。
  • 不使用主键自增:每次生成的索引值随机,插入新数据时,为保证B+树顺序存储特性,不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面(页分裂)。可能会造成内存碎片,导致索引结构不紧凑,影响查询效率。
  • 读取可以由b+树的二分查找定位
  • 支持范围查找,范围数据自带顺序

这边我原来想的是分成:|1,3,5,7|;|9|。这样的话页就不会被浪费,但是没考虑到之后还有可能会插入数据,如果插入的数据比如是6,那么第一页就没有空间了,又会触发页分裂,所以页分裂的规则就是尽量分的均匀,稍微空出一点位置, 这样可以保证后续插入时,有一段时间不用再次分裂 。

主键字段的长度不要太大,主键字段长度越小则二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小。

索引最好设置成not null:

1.null值会使优化器做索引选择时更复杂,如count会省略值为null的行。

2.null值无意义,但占物理空间(至少1字节存null值列表)。

防止索引失效:

发生索引失效的情况:

**1.模糊匹配:**如果使用xx%右模糊匹配就不会失效。

只有一种情况,就是覆盖索引的时候不会索引失效。

2.使用函数:

3.计算:

4.对索引隐式类型转化:

MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,再进行比较

两种情况:

①这种情况会索引失效:

select * from t_user where phone = 1300000001;

phone字段为字符串,mysql会把字符串转换为数字,所以这条语句相当于:

select * from t_user where CAST(phone As signed int)= 1300000001;

因为phone字段为字符串,所以cast函数作用在了phone字段上,而phone字段是索引,也就相当于对索引使用了函数,而对索引使用函数是会索引失效的。

②这种情况不会索引失效,直接走的索引扫描:

select * from t_user where id = "1";

这个语句中"1"为字符串,所以需要将"1"字符串转换为数字,语句相当于:

select * from t_user where id =CAST("1" As signed int);

直接走了索引扫描,因为cast其实并没有作用在字段上,而是作用在参数"1"上

只有对字段使用函数才会导致索引失效。对单纯的字符串使用不会导致索引失效。

5.联合索引非最左匹配

6.where子句中or两边都应是索引

OR含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会进行全表扫描。它会对两个索引分别进行扫描,再将两个结果集进行合并。

但是其实还有一些情况,就算触发了上面的六点,也不会索引失效:

如:表有两个字段id,name。

表中字段全是索引字段,select*相当于 select id,name ,查询的数据都在二级索引的B+树上,所以查二级索引的B+树就能查到全部结果,这个就是覆盖索引。

但是一旦表添加了非索引字段,再select *时就会全表扫描了,因为这样会增加一次回表操作,优化器认为这样的查询过程成本太高,还不如直接走全表扫描。

唯一索引性能:

使用唯一索引影响性能。

插入或更新数据时,数据库必须去磁盘检查唯一性约束。需要一次额外索引查找操作。

影响了 insert 速度的损耗可以忽略,但提高查找速度是明显的(阿里规范)

一个B+树能存储多少条数据?

order by是否走索引:

看情况,有的时候走,有的时候不走。

走索引的情况:

1.order by后面的字段就是索引:

2.where后的字段和order by后的字段是同一个:

3.有联合索引,且顺序一致:

4.升序降序一致,要么都是升序,要么都是降序,不能一个升一个降:

不能走索引的情况:

order by后不是索引,索引是B+树本来就排好序的,所以如果后面是索引的话,就直接拿过来用就行了, 但是如果order by后面不是索引,B+ 树根本没按它排过序 → 只能扫描后再自己排序 。

通过索引排序的内部流程:

count(*),count(1),count(主键字段),count(字段)

count():符合查询条件的记录中,函数指定的参数不为 NULL 的记录有多少个。

select count(name) from table:表中name字段不为null的数据有多少条。

select count(1) from table:表中1不为null的有多少条,1肯定不为null,所以就是统计全表有多少条数据。

count(主键字段):

①当只有主键索引,没有二级索引时:直接走主键索引。

②表里有二级索引时,选择二级索引:相同数量的二级索引记录可以比聚簇索引记录占用更少空间,二级索引树比聚簇索引树小,遍历二级索引I/O成本比遍历聚簇索引I/O成本小,优化器选择二级索引

count(1):

①当只有主键索引,没有二级索引时:循环遍历主键索引,但不会读取记录中任何字段的值,因为count函数的参数是 1,不是字段。server 层每从InnoDB读取到一条记录,就将 count 变量加 1。count(1)相比 count(主键字段)少一个步骤,就是不需要读取记录中的字段值。所以通常count(1)效率比 count(主键字段) 高。

②如果表里有二级索引时,选择二级索引。

count(*):

count(*)=count(0),使用count(*)时,MySQL会将 *参数转化为参数0处理。所以count(*)和count(1)执行效率一样。而且 MySQL 会对 count(*)和 count(1)有个优化,如果有多个二级索引的时候,优化器会使用key_len 最小的二级索引进行扫描。只有当没有二级索引的时候,才会采用主键索引来进行统计。

count(字段):

效率最差,因为此字段不是索引,所以会走全表扫描。

优化count(*):

1.近似值:当请求只需要计算近似值时,可用show table status或explain命令估算。

2.额外表保存计数值:获取表记录总数时可将这个计数值存到单独的一张计数表中。

当我们在数据表插入一条记录的同时,将计数表中的计数字段 +1。也就是说,在新增和删除操作时,我们需要额外维护这个计数表。

事务遵守四个特性:ACID

  1. Atomicity原子性:所有操作要么全完成要么错误回滚到开始,无中间状态。undo log保证。
  2. Consistency一致性:事务操作前操作后满足完整性约束,如扣款后总数不变,通过A+I+D保证
  3. Isolation隔离性:防止多个事务并发执行导致数据不一致,每个事务都有独立数据空间,消费者购买商品这个事务不影响其他消费者购买。通过MVCC或锁机制保证
  4. Durability持久性:事务结束后对数据的修改时永久的,通过redo log重做日志保证。

并行事务会发生的4种问题:

**脏写:**一个事务修改了另一个未提交事务修改过的数据。

**脏读:**B事务修改数据但未提交,A事务去读,但B回滚,A读到了B修改但未提交的数据(读到过期数据)。

**不可重复读:**一个事务多次读取同一个数据,前后两次读到的数据不一样。

**幻读:**一个事务多次查询记录数量,前后查询到的记录数量不一样。

事务的4种隔离级别:

三种并发问题的严重性排序:脏读>不可重复读>幻读

**读未提交:**其他事务可读到事务没提交时做的修改;什么都解决不了。直接读取最新数据;

**读提交:**事务提交后,它做的变更才能被其他事务看到;解决脏读问题。每次读数据时,都会重新生成一个 Read View,每次都会读版本链中已提交的最新数据。

**可重复读:**执行过程中数据跟启动时看到的数据一致,解决脏读、不可重复读。可重复读很大程度上解决幻读问题,启动事务时生成一个 Read View,整个事务期间都用这个 Read View。(默认使用)

**串行化:**对记录加读写锁,冲突时后面的事务须等前一事务执行完,避免并行访问,全部解决

**解决幻读方案:**除了普通select,其他都是当前读(insert,update,delete...)

当前读时,事务A对记录加上范围为(2,+∞]的next-key lock(间隙锁+记录锁的组合)。

事务B插入语句时,发现有next-key lock,于是事务B生成一个插入意向锁,进入等待状态,直到事务A提交。

  • 可重复读(a事务开启后挂起,b事务插入一条数据后提交,a事务能查到b事务提交的数据吗)
    • 普通快照读 (如普通 SELECT)查不到事务 B 提交的数据(基于事务 A 启动时的快照)。
    • 当前读 (如 SELECT ... FOR UPDATE能查到事务 B 提交的数据,因为当前读会绕过快照,直接读取最新已提交数据。

发生幻读的场景:

但是可重复读也不能完全解决幻读,有两种情况还会导致幻读:

1.事务 A 执行查询 id =5 的记录,此时表中是没有该记录的,查询不出来。然后事务 B插入一条 id =5 的记录,并提交了事务,此时,事务 A 更新 id =5 这条记录,事务 A 看不到 id=5 这条记录(因为只能读到事务开始时的readview,开始时没有这条记录),但是他去更新了这条记录,然后再次查询 id =5 的记录,事务 A 就能看到事务 B插入的纪录了,幻读就是发生在这种违和的场景。

这边事务B提交了id=5的事务之后,A事务查询的时候是快照读,使用的是MVCC机制,读到的是事务开始时的readview,那时还没有这条记录,所以事务A没读到这条记录,但是后面事务A又去修改了这条记录,修改是当前读,会直接绕过快照,直接读到最新的已提交数据,所以它可以修改这条数据,然后最后事务A为什么能读到这条记录了呢,因为一个事务总能读到自己所做的修改。

所如果要避免这类特殊情况导致的在可重复读隔离下发生的幻读,解决方法是:

在开启事务后,马上执行当前读语句,因为它会对记录加 next-key lock,避免其他事务插入新记录。

MVCC:

读提交和可重复读隔离级别这两个都是通过【readview的四个字段】+【聚簇索引记录的两个隐藏字段】的比对,来控制并发事务访问同一个同一个记录的行为。版本控制。

readview在MVCC中是如何工作的:

readview:

聚簇索引隐藏列:

创建了readview之后,可将隐藏列中的trx_id划分成三种情况:

可重复读隔离级别下使用MVCC的例子:

现有两个事务,A事务的事务id为51,B事务的事务id为52。

可重复读隔离级别下,有一条记录:trx_id保存着修改记录的事务id(规定此事务id为50的事务已经提交。)

1.事务B读到100万。2.事务A把记录改成200万,但还未提交。3.事务B再次读时,仍然读到100万。4.事务A将修改提交。5.事务B再次读时还是读到100万。

过程:将事务AB启动,事务分别创建了各自的readview:m_ids中存储的是当前活跃但还未提交的事务id。

1.事务B第一次读,找到记录后查看trx_id为50,比min_trx_id 值(51)还小,意味着id=50事务的修改已经提交,所以该版本记录对事务B可见,事务B可获取到这条记录,读到记录值为100万。

2.事务 A 通过 update 语句将这条记录修改了(还未提交事务),将小林的余额改成 200 万,这时MySQL 会记录相应的 undo log,并以链表的方式串联起来,形成版本链,如下图:

3.事务B第二次读取,发现trx_id 值为51,在min_trx_id和max_trx_id之间,进一步判断trx_id是否在m_ids范围内,发现在,说明这条记录还未提交,这时事务B不会读取这个版本记录。而是沿着undo log链条往下找旧版本记录,直到找到 trx_id小于min_trx _id值的第一条记录(其实就是找到上一条已提交的事务),所以事务B 能读取到的是 trx id 为50的记录,读到100万。

4.事物 A提交事务。

5.由于隔离级别为可重复读,所以事务B再读记录时,还是基于启动事务时创建的 Read View 来判断当前版本的记录是否可见。所以,即使事物 A 将小林余额修改为 200万并提交了事务, 事务 B第三次读取记录时,读到的记录都是小林余额是 100 万的这条记录。

读提交隔离级别下使用MVCC的例子:

对于「读提交」隔离级别的事务来说,每次读取数据时,都会重新生成一个 Read View,每次都会读版本链中已提交的最新的数据。

1.事务B读到100万。(创建readview)2.事务A把记录改成200万,但还未提交。3.事务B再次读时,仍然读到100万。(创建readview)4.事务A将修改提交。5.事务B再次读时读到了200万。(创建readview)

过程:除了每次读都会创建新的readview之外,前面四步都是一样的。最后一步因为每次读都会创建新readview,所以最后一步事务B的readview为:

而之前事务A提交过的记录为:

所以当事务B读取时查到当前记录trx_id是51,比事务B中min_trx_id值(52)小,说明修改这条记录的事务在创建Read View前提交过了,该版本记录对事务B可见。

mysql如何解决并发问题的?

相关推荐
正在走向自律2 小时前
电科金仓MySQL迁移实战:一个技术专家的深度踩坑与突围笔记
数据库·mysql·电科金仓·kfs·kdts
TDengine (老段)2 小时前
煤机设备每天 TB 级数据,天地奔牛用 TDengine 把查询提速到“秒级”
大数据·运维·数据库·struts·架构·时序数据库·tdengine
泯仲2 小时前
从零起步学习MySQL 第二章:DDL语句定义及常见用法示例
数据库·mysql
Leon-Ning Liu2 小时前
记录MySQL 主从架构切换双主(互为主从)操作步骤
数据库·mysql
@insist1232 小时前
数据库工程师核心 TCP/IP 协议栈知识:从软考考点到运维实战
运维·数据库·网络协议·tcp/ip·软考·数据库系统工程师·软件水平考试
!chen2 小时前
Oracle数据库物理备份工具支持本机+异机
数据库
前进的李工3 小时前
数据库视图:数据安全与权限管理利器
开发语言·数据库·mysql·navicat
what丶k3 小时前
深度解析 Canal 数据同步:原理、实操与生产级最佳实践
数据库·后端
白鲸开源3 小时前
(三)ODS/明细层落地设计要点:把数据接入层打造成“稳定可运维”的基础设施
大数据·数据结构·数据库