Mysql的索引
- 普通索引是最基本的索引,它没有任何限制,值可以为空;仅加速查询。可以直接create index创建索引。
- 唯一索引与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。简单来说:唯一索引是加速查询 + 列值唯一(可以有null)。
- 主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。简单来说:主键索引是加速查询 + 列值唯一(不可以有null)+ 表中只有一个。
- 组合索引指在多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合。
- 全文索引主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配。
索引就是可以用来优化查询的,是一种帮助快速查找的特殊的数据结构。那为什么索引就可以家快查找速度呢?
如果我有一个SQL语句是:select*from Table,其中id=15,那么在没有索引的情况下,它实际要进行一个完整的表扫描,直到我找到id=15的记录,时间复杂度是O(N);如果是查询索引的情况。首先,根据ID=15,在索引值中执行二分查找,二分查找的效率非常高。其时间复杂度为O(logn);
这就是为什么索引可以提高查询效率,但是索引数据量也比较大,所以它一般不存储在内存中,直接存储在磁盘中。
我们上面说了,索引数据存储在磁盘中,但是我们操作数据还是得在内存当中操作的。如果磁盘中的数据量很大,不能一次性写入内存,就得涉及多次io操作,因此良好的索引数据结构,可以换来次数较少的磁盘IO次数。每个io读写操作都是以页为单位进行的。
Hash
MySQL实际有两种索引类型去选择,一个是B+Tree的类型,还有一个就是Hash类型。
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,定位速度非常快。如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;前提是键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据,也就是说在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在哈希碰撞问题。
如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索;哈希索引也没办法利用索引完成排序,以及like ‘xxx%’ 这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询)。
所以乡innoDB默认采用的存储结构是B+Tree。
B-Tree
B树是一个平衡的多叉树(避免了时间复杂度为O(N)的单向链表树),英文是balance binary tree. 从根节点到每个叶子节点的高度差值不超过1。
在一般B-树中,一个节点中有多少子节点,我们称为多少阶B-树。以上是一个4阶的b-tree。
接下来就看看b-tree是怎么查找数据的吧:
- 如果查询ID=7的数据,首先将关键字20的节点加载到内存中,然后它比较确定7比12小;
- 然后加载第一个子节点,如果查询数据等于12或17,则直接返回;不等于就继续查找,查找比12小的7数据;
- 然后,继续加载第一个子节点,在找到7后,返回7下面的数据DATA。
因此,整个操作已经执行了3个IO操作,但实际上,一般的B-树有很多分支(通常大于100)。
为了更好地利用磁盘的IO能力,Mysql设置每个节点的大小为16K。如果每个节点中的关键字为int类型,则为4字节,如果数据区域的大小为8字节,则节点指针占4字节,则B树的每个节点可保存的关键字数为:(16*1000)/(4+8+4)=1000。每个节点最多可存储1000个关键字,每个节点最多可有1001个分支节点。
这样,当查询索引数据时,一次性磁盘IO操作可以计算1000个关键字,读入内存。所以为什么说索引的类型要选字节小的类型。
需要注意的是:B-树为了保证数据的平衡,它会做一系列的操作。这保持了一个平衡的处理时间,因此创建索引太多时的更新索引的过程也很耗时。
还有就是问为啥不选择低区域索引字段值作为索引,例如一个性别字段,总共两个值,这样可能会导致B树的深度过大,索引效率降低。
B+Tree
B-Tree呢的树的高度还是可能很高,所以还可以采用B+Tree。
当查询数据时,b+tree采用左闭右开的原则,然后非叶子节点存储key,不会存储data的,叶子节点存储key和数据,而且要尽量减少key值的占用空间(同理上面所介绍的)。
让我们看看数据查询是如何在B+树中进行的:
现在删除ID=2的数据,然后先取根节点,加载到内存中,发现根节点上存在ID=2,因为它是左闭区间存储数据,所以id<=2的都在根节点的第一个子节点上;
因此,取第一个子节点,将其加载到内存中,发现当前节点存在。id=2这个关键字已到达叶子节点,然后直接删除叶子节点中的数据。
也就是说查找过程就是先通过目录项数据页定位真实数据存放在哪个叶子节点数据项。且B+tree的高度平均是3-4层,因为B+tree每个节点可以存放更多的节点,也就是可以存放很多key,因此就降低了树的高度,这不就减少了磁盘io读写。单个io读写读取一个node,一个node包含那么多key。所以与平衡二叉树相比,B+Tree的查询效率更高。
还有就是B+树的叶子节点是顺序的,相邻的两个叶子节点有顺序引用,可以更好地支持范围查询。B-树是不存在这种顺序关系的。
B+Tree的查询效果也最稳定,因为所有查询都需要扫描到叶子节点才能返回数据。但不一定是最优,因为如果直接查询B树的根节点数据,B树只需直接返回数据,效果最好。
经过分析,MySQL最终选择B+树作为其索引的数据结构。
补充:uuid是128位的通用唯一标识符,java里有UUID的类帮助把string生成标识符。mysql不要用uuid用自增主键,因为innodb的索引特性导致了自增id做主键是效率最好的。
Mysql的常见两种存储引擎
主要有MyisAM和innodb两种存储引擎。不同存储引擎就有不同的存储机制和索引技巧,我们可以SHOW ENGINES去查看系统所支持的引擎类型。像inooDB就是Mysql默认的存储引擎。
首先,打开查询可以先执行以下命令查询存储数据的目录:
show variables like '%datadir%'
我的电脑中MySQL的内存数据的目录为:
然后进入那个文件夹,随便进入一个数据库文件夹,可以看到许多后缀名为.idb的文件,也就是说我建表的时候使用的是innodb的引擎。
InnoDB引擎的表文件有两个:
- *.frm此类型的文件是表的定义文件。
- *.IBD这种类型的文件是数据和索引存储文件。表数据和索引聚合存储,直接按索引查询数据。
Myiasm引擎的表文件,有三个:
- *.frm此类型的文件是表的定义文件。
- *.Myd此类型的文件是表数据文件,表中的所有数据都保存在此文件中。
- *.Myi此类文件是表的索引文件,MyISAM存储引擎的索引数据单独存储。
要是不想使用默认的innodb,可以指定ENGINE的。
InnoDB vs MyISAM
- InnoDB有行级锁,行锁定对于具有多个用户的数据库很有用。行级锁定的唯一缺点是占用大量内存,查询和修改数据需要时间。MyISAM只有表级锁,只允许单会话修改表,很适合只读数据库,因为不需要那么多内存。
- InnoDB具有所谓的引用完整性,它包括支持外键(RDBMS)和关系约束,而MyISAM不支持(DMB)。
- InnoDB支持事务哟,就是可以提交事务、回滚事务,但是MyISAM不可以。
- InnoDB更可靠,因为它使用事务日志进行自动恢复。MyISAM没有。
- InnoDB支持ACID属性,而MyISAM不支持ACID属性。
- InnoDB具有更高的写入速度。与MyISAM相比,InnoDB对于大容量数据的性能更好。MyISAM不支持事务属性,读取速度更快。与InnoDB相比,高容量数据的性能更低。
- InnoDB引擎有buffer pool:缓存数据和索引;Myisam引擎有Key Cache:专门缓存索引,淘汰算法都是LRU。
- 如果涉及到大数据量的排序、全表扫描、count之类的操作的话,还是MyISAM占优势些,因为MyISAM有保存表的数据行数。还因为索引所占空间小,这些操作是需要在内存中完成的。
- InnoDB使用聚簇索引,MyISAM使用的是非聚簇索引。
最后说到的聚簇索引和非聚簇索引,也要详细总结一下。
使用MyISAM存储引擎时,索引和数据单独分开存储,索引结构B+树最终指向数据的物理地址,而不是特定数据。需要根据数据文件(*.myd)的物理地址查找特定数据。(我看到网上很多很多其他博客说M有ISAM使用的非聚簇索引,但是我学习了一下,感觉还是有区别的呀。)
如果有多个索引,会指向同一个物理地址。
InnoDB既有非聚簇索引又有聚簇索引。
聚簇索引:那么聚簇索引就是将数据存储与索引放到了一块、并且是按照一定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的。一个表只能有一个聚集索引,通常是主键作为聚簇索引。如果没有主键,INNODB默认会生成一个隐藏列表作为主键。如下图所示:
非聚簇索引:也叫二级索引。虽然关键字也保存在B+树的每个分支节点上,但叶节点不保存关键字,而是保存主键值。使用二级索引查询数据时,首先查询数据对应的主键,然后根据主键查询特定的数据行。如下图所示:
索引失效问题
使用索引存在一些细节和注意事项:
1)不要在列上使用函数,这将导致索引失效而进行全表扫描。
select * from news where year(publish_time) < 2017
为了使用索引,防止执行全表扫描,可以进行改造。
select * from news where publish_time < '2017-01-01'
2)不要在列上进行运算,这也将导致索引失效而进行全表扫描。
select * from news where id / 100 = 1
为了使用索引,防止执行全表扫描,可以进行改造。
select * from news where id = 1 * 100
3)尽量避免使用 负向条件索引!= 或 not in或 <> 等否定操作符,可以使用in啊
- 应该尽量避免在 where 子句中使用 != 或 not in 或 <> 操作符,因为这几个操作符都会导致索引失效而进行全表扫描。
- 应该尽量避免在 where 子句中使用 or 来连接条件,因为这会导致索引失效而进行全表扫描。
select * from news where id = 1 or id = 2
4)复合索引的最左前缀原则
复合索引遵守“最左前缀”原则,即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。如果不是按照索引的最左列开始查找,则无法使用索引。
假设,有一个场景只需要针对资讯的月份进行查询,那么,SQL 语句可以写成:
select * from news where news_month = 1
此时,无法使用 news_year_month_idx(news_year, news_month)
索引,因为遵守“最左前缀”原则,在查询条件中没有使用复合索引的第一个字段,索引是不会被使用的。
5)like 语句的索引失效问题
like 的方式进行查询,在 like “value%” 可以使用索引,因为由于B+树的索引顺序,是按照首字母的大小进行排序,前缀匹配又是匹配首字母,所以可以在B+树上进行有序的查找,查找首字母符合要求的数据。但是对于 like “%value%” 这样的方式,执行全表查询,
至于说索引为什么会失效,因为我们上面学习了b+树就知道了:索引是按照一定规则排好序的,如果对索引列使用函数,或者 like % 李,具体的值都不知道,它怎么在B+树上加速查询?
事务的四大特性
事务有四大特性(ACID):
- 原子性(要么一起执行,要么不执行)
- 一致性(就是要保护我们的数据库完整性,这个操作啊要符合数据库的设定,不能是一些不太合理的操作,还成功了)
- 隔离性(在事务开始和结束之间,预防多个并发事务同时对数据库的增改查这些操作导致的数据不一致了)
- 持久性(事务完成后,数据将会被持久化到数据库中)
并发安全问题及事务的隔离级别
安全问题:
- 脏读(读了一个回滚前的数据)
- 不可重复读(因为修改了所以第二次读不一致)
- 幻读(因为插入了所以第二次读不一样)
- 更新丢失(就是因为没有把并发事务隔离开,我的修改回滚了,把之前的修改也回滚撤销了)
解决安全问题的隔离级别:
MVCC(关于这个话题其实我都想单独开写一个文章了?)
MVCC 主要应用在RC、RR(READ COMMITTED和REPEATABLE READ)这两个隔离级别,是基于数据版本对并发事务的控制。学习MVCC的时候,会学习到一个UNDO_LOG版本链,是用于回滚操作的。还有一个ReadView,就是“快照读”的生成物,是SQL执行时MVCC提取数据的依据。
快照读就是最普通的不加锁的查询SQL语句,它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。ReadView当前读就是一个数据结构,会根据ReadView里存储的版本号信息去版本链里返回数据,因为就是通过ReadView里可以判断这个事务是不是自己修改的哒?是不是已经提交的还是没有提交哒?或者这个事务是ReadView之后开启哒?
而执行insert、update、select…for update就是当前读,当前读就是它读取的一定是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,所以会对读取的记录进行加锁的操作。
当前读是通过 next-key 锁(行记录锁+间隙锁)来是实现的。
这里补充下行锁的 3 种算法:
行锁(Record Lock):锁直接加在索引记录上面。
间隙锁(Gap Lock):是 Innodb 为了解决幻读问题时引入的锁机制,所以只有在 Read Repeatable 、Serializable 隔离级别才有。
Next-Key Lock :Record Lock + Gap Lock,锁定一个范围并且锁定记录本身 。
读已提交RC会在每一次快照读的时候都执行快照读,所以可以防止脏读,不会读到回滚前的事务。可重复读RR仅在第一次执行快照读时生成ReadView,后续会一直复用这一次ReadView,所以可防止不可重复读。
具体的操作理解:
READ COMMITTED读取最新的版本号记录,就是所有事务最新提交的结果。
REPEATABLE READ读取之前系统版本号的记录,保证同一个事务中多次读取结果一致。REPEATABLE READ隔离级别下,MVCC具体操作:
SELECT操作,InnoDB会根据以下两个条件检查每行记录:
a. InnoDB只查找创建版本号早于或等于当前系统版本号的数据行,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
b. 行的删除版本号要么未定义,要么大于当前的系统版本号(在当前事务开始之后删除的)。这可以确保事务读取到的行,在事务开始之前未被删除。
其他两个隔离级别和MVCC不兼容。READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。SERIALIZABLE会对所有读取的行都加锁。
redo log
redo log 和undo log相对应,它是将事务操作的最新数据存储起来。
redo log主要是为了实现事务的持久性而产生的。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。
MySQL InnoDB是使用undo log实现多版本并发控制(MVCC)。事务未提交之前,undo log保存未提交数据之前的数据版本,当读取某一行被其他事务操作时,可以从undo log中分析出该行之前记录的数据。
还有就是在执行事务的时候,我们知道当调用roll back命令的时候,数据就会还原,这里用到的原理就是undo,也是实现事务原子性的原理。
数据库三大范式(不知为啥就想加进来了)
- 所有字段值都是不可分解的原子值。
- 在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。
- 数据表中的每一列数据都和主键直接相关,而不能间接相关。
第一范式(确保每列保持原子性)
第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。
第一范式的合理遵循需要根据系统的实际需求来定。比如某些数据库系统中需要用到“地址”这个属性,本来直接将“地址”属性设计成一个数据库表的字段就行。但是如果系统经常会访问“地址”属性中的“城市”部分,那么就非要将“地址”这个属性重新拆分为省份、城市、详细地址等多个部分进行存储,这样在对地址中某一部分操作的时候将非常方便。这样设计才算满足了数据库的第一范式。
第二范式(确保表中的每列都和主键相关)
第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说 在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。
比如要设计一个订单信息表,因为订单中可能会有多种商品,所以要将订单编号和商品编号作为数据库表的联合主键。
第三范式(确保每列都和主键列直接相关,而不是间接相关)
第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。
比如在设计一个订单数据表的时候,可以将客户编号作为一个外键和订单表建立相应的关系。而不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段。
优点:可以尽量得减少数据冗余 缺点:对于查询需要多个表进行关联,更难进行索引优化
参考文章: