什么是索引?上篇笔者写到,可以简单理解为书的目录。术语一点,我们可以说索引是一种数据结构,是将特定字段取出来,按照某种特殊的内在组织方式(MySQL的索引类型)存储起来的数据结构。有了索引,当我要在数据库中的某个表中找数据时,MySQL无须任何扫描,即准确可找到,反之,则需要全表扫描,试想一个百万行以上的表,这要多久?顺便说一下每种索引对应的使用这种索引的方式都是有限的,就是说我们要查的东西,必须在索引中定义了才可以,如果定义了A字段的索引,但是给的条件却不是A字段的条件,是不会使用A字段的索引的。
索引的列最好是出现在WHERE子句中的列,或者连接子句指定的列,而不是出现在SELECT关键字后选择列表中的列。
索引可以帮助我们在海量的数据中快速找到所需数据。但是也并非索引带来的都是好处,不当或者过多的索引都会带来负面效果,因为每修改数据记录,索引就必须刷新一次。
为了弥补这一缺陷,MySQL可以暂时制止MySQL在该命令每插入一条新记录和每修改一条现有之后立刻对索引进行刷新,对索引的刷新将等到全部记录插入/修改完毕之后再进行。此功能在大量数据修改的场景非常有效,需要注意的是,此功能仅对MyISAM类型表有效,若要检查此项功能当前状态,则如下:
那么此时,又有问题了,如果数据修改完成,还未来得及同步索引,系统或者其他原因,关闭了,那么下次启动服务时,数据没问题,但是没有索引,这也不行。因此,MySQL还有另外一个选项,保证在每次读取MyISAM表时,检查其标记是否为非正常关闭或损坏,若不一致,则更新索引或尝试修复,其值为{OFF,DEFAULT|BACKUP|FORCE|QUICK},其中BACKUP是如果先备份,再做操作;而FORCE则直接强制恢复,那么弄坏数据;QUICK则是如果没有删除块,不要检查表中的行。默认为OFF,此项查询如下:
现在又有问题了,恢复依然会带来性能成本而且还可能损坏数据,不恢复吧,又不能保证索引的时效性,到底怎么选呢?哈哈,看实际需求吧,好多事儿,终究是不能两全。另外,索引还会在硬盘上占用相当大的空间。因此应该只为最经常查询和最经常排序的数据列建立索引。注意,如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果
上篇也提到了,建立在单个字段上的索引,我们称为简单索引;而建立在多个字段上的所有,我们称为组合索引;值得一提的是,这种归类并不完善,只是简单定义哪种索引更简单,哪种更复杂(网上也有更详细的分类,但笔者认为其仅是对具体到使用时的归类,而非其原理的索引类型分类)。而事实上,MySQL支持的索引类型有以下几种:
B-Tree: Balance Tree 平衡树索引。我们可以将其理解成一个倒状的树的数据结构,每个树叶就是一个指针,指向了所要查找的数据真正所在的位置;每个分叉的节点通常是指一个索引的查找范围;简单画图如下:
假设上图中,根节点的索引值是公司今年全部的营业额,而在node级上切开,一个前六月,一个后六月,然后下一级别再分,分别具体各分几个月,再下一级别分每个月哪几天在一个节点,又或者在第二级别分为6个,依次类推…当然实际上,具体到怎么分是由MySQL自行决定的,我们仅在此需要了解的是其工作原理。如果没有这样的一个索引,我们如果要查哪天的营业额数据,则就需要全表扫描了,这时间上的节约,明显不是一个级别不是吗?还有,试想一下,我在公司员工的性别上做索引有意义么?当然没有,所以,我们建立的索引,一定要建立在用于条件比较或者是具有唯一性字段上。
B-Tree是MySQL默认用到的索引,而在MySQL中索引也可通常直接称为键,而表在创建的时候指定主键的话,默认就指定其为B-Tree索引,而唯一键也是。可以简单理解为,键就是索引。
索引本身是有限的字段,而非完整的字段,因此,叶子节点上存的是指针,指向数据真正的磁盘位置。当我们找的字段被找到后,还要通过磁盘IO寻道取出目标数据,而且,我们目标字段其对应的其他字段也会根据我们的需求一一取出,这样,在数据查找阶段固然很快,但是,由于受限于磁盘IO,所以我们的实际的查找给用户的感觉,仍然是无法有质量的提升。
因此有没有一种方法,在找到索引的时候,就找到数据的呢?有的B-Tree+,又叫聚簇索引(cluster index),可以将其理解为B-Tree的升级版,InnoDB引擎默认使用就是这种索引,InnoDB默认索引使用主键建立的,为了避免多个索引的复杂存储请求,索引跟主键是一起存储的,也就是说,其索引没有用指针指向磁盘块,而是其数据磁盘块就真正的存储在索引节点背后---只要找到这个节点,就能找到其对应的数据。
所以,我们可以看到,在MyISAM引擎中,每建立一个表,会产生三个文件:.frm(结构);.MYI(索引);.MYD(数据),看到索引和数据是分开存放的,也就是如我们刚才所说,找到索引,还要再找到数据,产生的大量的磁盘I/O。而InnoDB引擎,则是使用表空间存储数据的,默认情况下,所有的InnoDB的所有表都在一个表空间中,这管理起来是非常不理想的,因此我们安装完InnoDB引擎后,都会开启一个选项,就是所谓的每秒一个表空间,这样,看起来就是MyISAM挺像,此时,InnoDB表,每个表都有俩文件:.frm(在指定数据库中) ;*.ibd(在datadir中,存放数据和索引,如果没有设定单个表单个空间,则直接存储在数据目录中,且名字为ibdata*)。
在InnoDB中,由于其数据存储格式的特性,考虑多个索引时,我们又不可能将其分开存放或者在每个数据块中存储一样的但不同类型的数据,这样也是不合理的。因此,设计人员将主键索引设置的至关重要,其他所有的非主键索引,都依赖于主键索引,都指向主键索引,并不在存储节点存实际数据,这也意味着,如果在innodb的表中使用非主键索引,那么查找过程最少有两次,而最后一次一定是找到主键索引找到数据。这种非主键索引的索引,我们将其称之为第二索引。我们将InnoDB的表中的主键建立在最常用的字段上,那对于索引的速度提升是非常有帮助的。
在MySQL中,B-Tree又将其称之为最左前缀索引:也就是说,如果我们建立了组合索引,那么其在被调用时,是以从左到右的顺序依次调用的,如果我们不使用最左边的那个字段,直接想使用组合索引的第二个或者之后的字段,系统是不会调用的,因此,在建立组合索引时,尽量把最常用的字段放在最左边。这种查找方式适用场景也限定了其只能应用于左前缀、全键值(将索引的每个值都拿来做条件和索引中的所有列进行匹配)或键值范围(仍需满足最左前缀,跟所有的值进行运算比较)的查找。且其局限性也非常明显,譬如:从最左侧开始,否则索引无效;且不能跳过索引中的列;最重要的是,每当数据变动,那么组合索引的每个列都要随之更新。
值得注意的是,数据迁移时,索引是不需要迁移的,因为索引中存储的是指针,在本地磁盘和在对方磁盘,指针指向的数据块也是不同的。 索引,迁移后,索引只能重建。
还有一种索引利用方式,我们直接将其建立在数据字段上,当查找到索引时,索引指向的字段的所有数据内容也都被找出,这种利用方式我们称之为覆盖索引。
有了索引以后,我们的查找一般都会先根据有效索引来进行查找,那么,为了更高效的利用索引,我们应该讲索引载入内存以便于高效实用。在MyISAM中,有所谓的键缓存;在InnoDB中有buffercache。其中MyISAM的键缓存就是在内存中开辟一段空间将索引载入进来,用于在内存中完成查找匹配;在InnoDB中buffercache则不同,其不仅将索引缓存,其数据也缓存到内容,虽然占用空间更多,但是省掉了磁盘IO。
我们再来简单说一下MySQL中的另一种索引Hash索引,又叫键值对索引(key-value),其中key存储的是其hash码,value是指针,指向数据块;这种索引的速度是无与伦比的(在碰撞率低的情况下,因为其使用的循环冗余检验码),在上篇我们已经提到。这种索引在在字符串单串比较的时候是一种非常好用的索引。但是这种索引很独特,并非所有的存储引擎都支持这种索引,在MySQL中只有Memory引擎是支持显示hash索引;而MyISAM是不支持的,InnoDB的支持也不是可以定义的,而是其内部自行实现的。需要注意的是Hash索引只能支持对于数据的等于或不等于(=,IN,<=>),而对于覆盖索引、运算排序和部分键匹配(hash结果的不等)是不支持的,因为其内部存储的是键值对,比较的时候,仅是根据请求的数据的hash码和自己的hash码进行比较。
再说一种索引:R-Tree空间位置信息索引,此种索引是空间索引,就目前而言仅MyISAM支持,例如微信中的摇一摇,查找离你最近的有哪些人这种功能。这种索引只能使用MySQL内置的函数来使用,不能用于其他用法。
最后再说一个索引,全文索引Fulltext :可在整片文档中按某个关键字搜索。譬如搜索引擎或者淘宝,在搜索内容里面输入任何一个字符串,都以被搜索到;也就是说整片文档的任意字符串都可以用于做关键字索引。只有MyISAM支持。而其搜索机制也是调用特殊函数来实现,而InnoDB不能使用这种索引。而如果我们既需要使用InnoDB的事务,又要使用全文索引怎么办?因此又有第三方全文索引项目提供此种功能,分别是Lucene(java开发)和Sphinx(使用C++开发);其中Sphinx的速度几乎是Lucene的8倍。
那么,到底如何使用索引呢,且看如下:
1、 必须使用隔离列:使其不能参与运算
如下所示,第一种则无法调用索引,第二种则可以
WHERE AGE+20 > 60; ---> WHERE AGE > 60-20;
2、 前缀索引和索引选择性
前缀索引:当索引字段非常长,我们则需要截取有限的字段(长度),我们将其称之为前缀索引:指定使用固定长度的索引。
索引选择性:在能有效区分索引字段长度的基础上取最短值。索引越短,则其越高效;如果索引字段不能有限的进行区分,很有可能索引带来的并非是速度的提升,甚至拖累索引速度也不是没可能的。一般来讲,索引字段如果非常大的前提下,取其30%则就可以有效区分了,请注意,是一般来讲。
在选择是否要做索引的时候,我们可以判断,如果字段的每个行的值,有太多的重复,则这个字段就不必做索引,假设一个表,一个字段名字为性别,那么里面的值,最多也就俩,这字段自然是没有必要做索引的,笔者认为,在一个字段的不同超过80%(请注意,是笔者认为),则由必要,我们可以通过如下公式进行判断,见下图:
3、 按业务需求选择使用合适的覆盖索引
查找的数据,使用覆盖索引可以直接返回,这个是相当的快速。
4、 使用索引既完成查找,又同时完成排序
索引本身是有序的,而索引对应的数据是无序的,可以扫描有序索引进行数据排序;使用文件排序(Filesort),其略慢于扫描有序索引
5、 尽可能避免重复索引
ALTER TABLE students ADD INDEX(Name,Age);
在students表中,以Name,Age字段添加一个组合索引
CREATE INDEX create ON students(CreateTime);
在students表中的CreateTime字段上创建名为create的索引
ALTER TABLE students DROP INDEX createtime;
在students表中删除名为createtime的索引
DROP INDEX Name ON students;
在students表中删除名为name的索引;
MySQL的锁和事务
锁,什么是锁?当然,这里肯定不是家门的锁。这里的锁,是为了解决资源争用的产物,MySQL可用提供给多用户的,试想一下,当多个用户同时对一个数据进行修改时,怎么办?因此,加锁的必要性就有了,谁需要修改了,就会获取到修改数据的权限,然后此数据加锁并通知其他人,已经有人在修改了,其他用户只能查询或等待。
先说读锁和写锁,其又被称为共享锁和独占锁。一般情况下,读锁并不影响其他用户读,但不允许其写;但写操作对其他用户的读写都有影响。试想一个问题,一个表中,如果有百万行以上,但是我们仅修改了其中几行,而把全表加锁,那么在没有修改完的时间段内,这整个表的数据都被加上写锁,这是很严重的。因此,我们能不能仅对要修改的行加锁而不影响整个表呢?这就说MySQL对于锁的粒度控制了。
MySQL的锁粒度有:表锁,对整个表加锁;页锁,对整个页面加锁,MySQL的多行数据可能会存储在MySQL页面中,这个页面可以理解为MySQL管理存储数据的基本单位;行锁,顾名思义,仅锁定某行。
MySQL锁的实现又分两个级别:存储引擎级别、服务器级别。由于存储引擎更接近数据,更易于理解哪些数据是否需要加锁,因此笔者建议,有存储引擎自我实现加锁。除非特殊情况,我们才手动来给服务器加锁。
用户在修改数据的数据时,存储引擎会自动为其加锁和完成后解锁,这种锁,我们称为隐式锁。当然,作为用户而言,我们手动施加的锁,我们称为显式锁。
存储引擎也称作表类型。如果在创建表的时候没有指定,则会应用默认存储引擎。在MySQL中的两个最重要的存储引擎MyISAM和InnoDB,其中MyISAM在MySQL的5.5.3之前的版本中,是默认的存储引擎。通过如下两个命令,可查看当前数据所有的表的引擎或者单独某个表使用的存储引擎:
SHOW TABLE STATUS\G
SHOW TABLE STATUS LIKE 'table'\G
我们在简单说一下这两种存储引擎的区别:
MyISAM:
不支持事务
表级锁:导致其并发能力过弱
索引类型:B-Tree, Fulltext, R-Tree
支持延迟更新索引:在上篇提到的相关的内容
适用于数据仓库:读多写少,一般读写比例为9:1或者8:2,再多就不合适了
InnoDB:
事务:事务支持能力很强,基于多版本并发控制MVCC,将所有的数据放在表空间中,由引擎自身全权管理,
在其默认隔离级别上使用了间隙(gaps)锁,防止幻读。
行级锁:
自适应hash索引
预读:基于程序的局部性原理,局部性原理分空间局部性和时间局部性。空间局部性是指,一个数据被访问,那么离这个数据很近的数据都有可能被访问;时间局部性是指,一个数据被访问到,那么近期很可能还被访问到。这几乎可以理解为缓存的原理。
上文中提到了,那么,什么是事务(Transaction)?先想一个问题,假设两个人在网络上交易,一方付钱了,系统提示钱已经扣掉了,但是由于其它不可预知的原因,或许是网络,或许是对方数据的问题,对方并没有收到钱,从服务器的角度来看,就是A减去的数字,B并没有加上,但是这整个过程,却是为了一个目的:就是把A减去的数字,给B加上,数字总和是不变的。而事务的概念就是为了解决这种问题而产生的。
从数据库的角度,我们可以把事务理解成一组SQL语句,这组SQL语句很特别,因为只要其中一个执行失败,那么这组语句就全部失败。而判断数据库引擎是否支持事务,可通过ACID进行判定,所谓ACID就是指:
A : 原子性 Atomicity
整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
C : 一致性 Consistency
在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
I : 隔离性 Islation 操作时,检查状态,是否有并行操作,隔离越严格,数据越安全
隔离级别: 语句小,小事务,大则大事务,越小越好。
隔离级别有四种:
READ-UNCOMMITED : 读未提交,别的插入的数据,还没提交,就可以看到了。存在脏读
READ-COMMITTED : 读提交 :别人插入数据,提交了,才可以被看到。 不可重复读
REPEATABLE-READ : 可重读 :默认级别。别人提交,我看不到,我查询的数据一直一致。但其背后会有幻影行,但系统默认忽略。
SERIABLIZABLE : 可串行化 : 为避免幻读,让事务依次执行。 加锁读,安全但是慢
D : 持久性 Durability 一旦一个交易完成,这个数据要被永久存储下来。
通过如下命令,查看到当期事务的隔离级别:
mysql> SHOW VARIABLES LIKE 'tx%';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+