5.1 索引基础
索引是存储引擎用于快速找到记录的一种数据结构。
索引能将查询性能提高几个数量级。
select first_name from actor where id=5;
如果id列上建有索引,则mysql使用该索引找到id为5的行。先在索引上按值查找,然后返回所有包含该值的数据行。
如果索引包含多个列,列顺序也很重要,mysql只能高效使用索引的最左前缀列。
5.1.1 索引的类型
B-Tree索引
mysql的InnoDB索引默认使用B树索引,确切是B+Tree
MyISAM存储引擎使用前缀压缩技术使得索引更小,但InnoDB按原数据格式进行存储。
MyISAM索引通过数据的物理位置引用被索引的行,而InnoDB则根据主键引用被索引的行。
BTree所有的值都是按顺序存储的,每一个叶子页到根的距离相同。
B-Tree索引列使顺序阻止存储的,适合查找范围数据。
如“查I到K开头的名字”这样查找效率很高。
CREATE TABLE People ( last_name VARCHAR(50) NOT NULL,-- 姓 first_name VARCHAR(50) NOT NULL,-- 名 dob DATE NOT NULL, -- 生日 KEY(last_name,first_name,dob) );
B-Tree索引,适用于全键值、键值范围或键前缀查找,前缀查找只适用于根据最左前缀查找。
1. 全值匹配
指的是和索引中所有列进行匹配,例如查找姓名是Cuba Allen,出生于1994-1-1的人
2. 匹配最左前缀
查找姓为Allen的人,就是只是哟红索引的第一列
3. 匹配列前缀
只匹配某一列的值的开头部分,用索引查找以J开头的姓的人,这也只使用了索引的第一列
4. 匹配范围值
索引可用于查找姓在Allen和Barry之间的人,也只使用了索引第一列
5. 精确匹配某一列并范围匹配另外列
索引用于查找姓为Allen,然后名字以字母K开头的人。
第一列last_name精确匹配,第二列first_name范围匹配
只访问索引的查询
B-Tree支持只访问索引的查询,查询只要访问索引,无需访问数据行,这种叫覆盖索引。
索引树中节点是有序的,除了按值查找外,索引还可用于查询的ORDER BY操作。
B-Tree索引限制:
- 如果不是按索引最左列开始查找,无法使用索引。例如索引无法查找名字叫Bill的人,无法查找特定生日的人
- 不能跳过索引中的列,例如索引无法用于查找姓为Smith且在某个特定生日的人,把名跳过去了。
- 查询某个列的范围查询,其右边所有列无法使用索引优化查询。如last_name使用精确查找,first_name使用like,则dob则无法使用索引。如果范围查询列值有限,可以用多个等于来代替like。
哈希索引
基于哈希表实现的,只有精确匹配索引所有列才有效。
存储引擎会对所有的索引列计算一个哈希码,不同的行计算出来的哈希码不一样。哈希索引将所有哈希码存在索引中,并在哈希表中保存指向每个数据行的指针。
mysql中,只有memory引擎显式支持哈希索引,同时也支持BTree索引。
CREATE TABLE testhash( fname VARCHAR(50) NOT NULL, lname VARCHAR(50) NOT NULL, KEY USING HASH(fname) )ENGINE = MEMORY;
- 哈希索引结构十分紧凑,这让索引查找速度很快,索引只包含哈希值和行指针。
- 哈希索引是按哈希值查找,所以无法用于排序
- 哈希索引不支持部分索引匹配查找,例如在列(A,B)上建哈希索引,如果只查询列A则不能使用索引
- 哈希索引只支持等值比较,包含=,IN(),<=>.也不支持范围查询,比如>,<
- 如果有哈希冲突,存储引擎必须遍历链表中所有行指针,逐行比较,直到找到所有符合条件的行
- 哈希冲突很多的话,索引维护代价很高。当从表中删除一行,存储引擎要遍历哈希值链表中的每一行,找到并删除对应行的引用,冲突和代价成正比
InnoDB也有叫“自适应哈希索引”。当某些索引值使用很频繁的话,就在B-Tree索引上再创建一个哈希索引,让BTree也有哈希索引的有点,比如快速查找。这种哈希索引是对用户不可兼得。但可以控制开关闭。
当有哈希冲突,使用哈希索引进行查询的时候,需在where子句中包含常量值。
当索引一个列有大量字符串的,就不用正常用BTree索引,这样会导致索引非常的大,可以用CRC32()函数将内容转换成哈希值存放在表中一个字段里。并将该字段进行哈希索引,这样就会很快。
如果表数据量很大,CRC32()函数会导致大量的哈希冲突,可以使用MD5()函数作为自定义哈希函数存入到列中。
空间数据索引R-Tree
MyISAM支持空间索引,用作地理数据存储。无需前缀查询,该索引会从所有维度来索引数据。
必须使用MySQL的GIS相关函数如MBRCONTAINS()来维护数据。
全文索引
特殊类型索引,查找的是文本中的关键词,不是直接比较索引中的值。第七章详细描述
相同列上同时有全文索引和BTree索引不冲突,全文索引适用于MATCH AGAINST操作,不是WHERE操作。
5.2 索引的优点
索引可以让服务器快速定位到表的指定位置。
但也有其他的作用。
BTree索引,按顺序存储数据,所以可用ORDER BY和GROUP BY。
因为数据有序,BTree会将列值都存在一起。因为存了实际的列值,有些查询只需要索引就能完成查询任务了。
索引三个优点:
- 索引减少了服务器扫描的数据量
- 索引帮助服务器避免排序和临时表
- 索引可将随机IO变成顺序IO
5.3.2 前缀索引和索引选择
有时候索引很长的字符列,索引会变得很大而且很慢。
通常可以索引开始的部分字符,可以节约索引空间。
对于BLOB、TEXT或很长的VARCHAR列,必须使用前缀索引,MySQL不允许索引这些列的完整长度。
比如有一个city列,列中有各个城市Shanghai,London,Tokyo,Teboksary等等。
先从前3个前缀字母开始,可能统计出来的比原来城市数量较多。不符合真实情况。
前缀字符长度是7时比较合适。
select count(*) as cnt,left(city,7) as pref from city_demo group by pref order by cnt desc limit 10;
有公式可以计算合适的前缀长度:
select count(DISTINCT city)/count(*) from city_demo;
如果结果接近0.031基本上就可以用了,如:
select count(DISTINCT LEFT(city,num))/COUNT(*) from city_demo;
num就是你输入的数,从3到7,不接近0.031就再往上加
创建前缀索引:
ALTER TABLE city_demo ADD KEY(city(7));
前缀索引是使其更小、更快的有效方法;缺点就是,无法用前缀索引做ORDER BY和GROUP BY
5.3.3 多列索引
常见错误就是,为每个列创建独立索引,或按错误的顺序创建多列索引
在多个列上建立独立的单列索引大部分情况下不能提供MySQL的查询性能。尽管MySQL5.0之后引入叫“索引合并”的策略,可以使用多个单列索引来定位行。
- 对多个索引做相交操作(多个AND),需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。
- 对多个索引做联合操作(多个OR),需要消耗大量CPU内存和缓存等。
- 优化器不会把索引合并策略所需要时间和性能计算到“查询成本”中,导致性能还不如直接全表扫描。
5.3.4 选择合适的索引列顺序
如果选择索引顺序一般法则:将选择性最高的列放到索引列最前列。
例如:
select * from payment where staff_id=2 and customer_id=584;
对于这个查询要预测下where条件分支对应数据基数有多大:
select SUM(staff_id=2),SUM(custoemr_id=584) from patment -- SUM(staff_id=2): 7992 -- SUM(custoemr_id=584): 30
可以看出应该讲customer_id放在前面。
注意:这样的结果很依赖于选定的具体值。
更好的办法:
select count(DISTINCT staff_id),count(DISTINCT customer_id),count(*) from payment;
-- staff_id:0.0001
-- customer_id:0.0373
customer_id的选择性更高。
5.3.5 聚簇索引
聚簇索引不是一种单独索引类型,而是一种数据存储方法。InnoDB的聚簇索引实际在同一结构中保存了B-Tree索引和数据行。
表有聚簇索引时,数据行实际上存放在索引的叶子页中。数据行和相邻的键值紧凑存储在一起。一张表只有一个聚簇索引,覆盖索引可以模拟多个聚簇索引。
叶子页包含行的全部数据,节点页只包含索引列。索引列包含的是整数值。
InnoDB通过主键聚集数据,被索引的列就是主键列。
如果没有定义主键,InnoDB选择唯一的非空索引代替。如果没有这样索引,就会隐式定义一个主键作为聚簇索引。
缺点:可能会导致严重性能问题,尤其将表的存储引擎切换成其他的。
- 如果数据全在内存中,且访问顺序不重要,聚簇索引没优势。
- 插入速度严重依赖插入顺序。按主键顺序插入到数据是InnoDB表中最快方式。如果不是按主键插入,用OPTIMIZE TABLE重新组织一下表
- 更新聚簇索引列代价很高,强制InnoDB将每个被更新行移动到新位置
- 在插入新行,或主键被更新导致移动行,可能面临“页分裂”问题。当行主键值要求必须将这一行插入到某个已满的页。引擎会将该页分裂成两个页来容纳该行,页分裂导致表占用更多磁盘空间。
- 聚簇索引可能导致全表扫描变慢,尤其行比较稀疏,或页分裂导致数据存储不连续时。
- 二级索引(非聚簇索引)比想象更大,因为二级索引的叶子节点包含了引用行的主键列
- 二级索引方法访问需要两次索引查找(注),不是一次
优点:
- 可把相关数据存在一起,如邮箱,根据用户ID聚集数据,只需从磁盘读取少量数据就可获得用户全部邮件。如果没有聚簇可能每个都导致一次IO
- 数据访问更快,聚簇索引将索引和数据存在同一B-Tree中,从该索引获得数据比其他索引要快
- 覆盖索引扫描查询可直接用页节点的主键值
二级索引方法访问需要两次索引查找,二级索引中保存的“行指针”实质。二级索引叶子节点保存的是行的主键值。
存储引擎在二级索引的叶子节点获得对应的主键值,根据值去聚簇索引中查到对应的行。做个重复工作,两次B-Tree查找。
在InnoDB表中按主键顺序插入行
InnoDB表没什么数据要聚集,可定义一个代理键作为主键,这种主键的数据应和应用无关,最简单是使用自增列。可以保证行是按顺序写入。
避免随机的聚簇索引,特别是IO密集型应用。
例如使用UUID作为聚簇索引,这使索引插入完全随机,数据没有任何聚集特性。
使用UUID主键插入行不仅花费时间更长,索引占用的空间也更大。原因是主键字段更长。
5.3.6 覆盖索引
设计优秀的索引应考虑到整个查询,而不单是WHERE条件部分。
MySQL也可使用索引来直接获得列的数据,这样就不需要读取数据行。
如果一个索引包含或覆盖所有需要查询的字段的值,就称为覆盖索引
就是索引KEY(< cols >),cols是SELECT之后的字段,也是WHERE之后的字段
好处:
- 索引条目一般远小于数据行大小,如果只读索引,MySQL会吉大减少数据访问量。对缓存的负载很重要,对于数据拷贝可以缩短响应时间。对IO密集应用更好,索引比数据小,可全部放入内存,对MyISAM最有利,因为其能压缩索引。
- 索引是按列值顺序存储的(至少在单个页内),对IO密集型的范围查询比随机从磁盘读一行的IO少得多。MyISAM可以通过OPTIMIZE使索引完全顺序排列,就使范围查询可以使用完全顺序的索引访问。
- MyISAM在内存中只缓存索引,数据依赖于操作系统缓存。会导致性能问题,访问数据需要一次系统调用。
- 由于InnoDB的聚簇索引,覆盖索引对InnoDB表很有用。InnoDB的二级索引在叶子节点保存了行的主键值,如果二级主键能够覆盖查询,则可避免对主键索引的二次查询。
覆盖索引必须要存储索引列的值,因此哈希、空间、全文等索引都不存储索引列的值。
MySQL只能用B-Tree做覆盖索引。
关于索引的博客参考:
覆盖索引有何用?
mysql高效索引之覆盖索引
5.3.7 使用索引扫描来做排序
MySQL两种方式生成有序结果:
通过排序操作;或按索引顺序扫描。
如果EXPLAIN出来的type值“index”,说明MySQL使用索引扫描做排序;Extra的“Using index”是正在使用覆盖索引的意思,二者不一样。
扫描索引本身是很快的,但如果索引不能覆盖查询所需的全部列,那就必须扫描每一条索引记录就回表查询一次对应的行,这个就是随机IO了,影响性能。
MySQL可使用一个索引既满足排序,又可以查找行。设计索引时,应该尽可能满足这两种任务,这是最好的。
只有当索引的列顺序和ORDER BY子句顺序完全一致,且所有列的排序方向都一样,MySQL才用索引对结果做排序。
如果关联多张表,只有当ORDER BY子句引用字段全部是第一个表时,采用索引做排序。
CREATE TABLE rental( ... PRIMARY KEY(rental_id), UNIQUE KEY rental_date (rental_date,inventory_id,customer_id), KEY index_inventory_id (inventory_id), KEY index_customer_id (customer_id), KEY index_staff_id (staff_id), ... );
可以用rental_date索引为下面的查询做排序。
select rental_id,staff_id from tental where rental_date = '2005-5-5' order by inventory_id,customer_id
即使ORDER BY 子句不满足索引最左前缀要求,也可以,因为索引第一列是一个常数字符串,日期嘛
... where rental_date = '2005-5-5' order by inventory_id DESC;
还可以这样查询,因为where后是rental_date,order by后是inventory_id,二者组合在一起,就是索引rental_date的最左前缀。
... where rental_date > '2005-5-5' order by rental_date,inventory_id;
这样的查询也没问题。
5.3.8前缀压缩索引
MyISAM使用前缀压缩来减少索引的大小。可以让更多索引放入内存中。默认只压缩字符串,但设置后也可以压缩整数。
压缩每个索引块方法是,先保存索引块的第一个值,然后将其他值和第一个值比较,相同前缀的字节数和不同的后缀部分,把不同的存储起来。
例如,第一个值是“perform”,第二个值是“performance”,那么第二个值前缀压缩后存储的就是(7,mance)的形式。
压缩块占用更少空间,代价是操作更慢。
5.3.11 索引和锁
索引可以让查询锁定更少的行。
InnoDB只有在访问行的时候才会对其加锁,索引能减少InnoDB访问的行数,从而减少锁的数量。
尽管InnoDB行锁效率高,但锁定行仍然会带来额外开销。
关于InnoDB、索引和锁的细节:
InnoDB在二级索引上使用共享锁(读锁),但在访问主键索引上需要排他锁(写锁)。这消除了使用覆盖索引的可能性,并且使SELECT FOR UPDATE比LOCK IN SHARE MODE或非锁定查询要慢很多。
5.4 索引案例学习
设计一个在线约会网站,用户信息表有多列,国家、地区、城市、性别、眼睛色等。根据上面各种特征组合查询,还必须根据用户最后在线时间、用户个人评分等对用户进行排序。
第一个考虑的事:需要使用索引来排序;还是先检索数据再排序。
使用索引排序会限制索引和查询的设计。
例如:希望使用索引根据评分对用户排序,那么where age between 18 and 25就无法使用索引。
如果对某个索引进行范围查询,就无法使用另一个索引或该索引的后续字段进行排序。
5.4.1 支持多种过滤条件
需要看哪些列有不同的取值,哪些列在WHERE子句出现最频繁。
country列选择不高但很多查询会用到。
sex列选择不高但很多查询会用到。
建议不同组合索引的时候讲(sex,country)列作为前缀。
之前建议说的是不该在选择性低的列创建索引嘛,这次的原因是:
几乎所有查询都会用的sex列,甚至会设计每次按性别搜索用户。
加上sex列没坏处,即使查询没用sex列也可以用某方法绕过。
绕过sex列的索引查询:
在查询条件中新增 AND SEX IN(‘man’,’female’)让MySQL选择该索引,这样不会过滤任何行,和没有这个条件时返回结果相同。
但如果该列有太多的值,就会让IN()列表太长。
考虑其他常见WHERE条件的组合:
(sex,country,age)索引
(sex,country,region,age)和(sex,country,region,city,age)这样的组合索引
这样会要大量的索引,如果想尽可能重用索引而不是建立大量的组合索引,可以像刚才那样的IN()来避免。
为什么一直将age放在索引的最后面?
原因:因为age多半是范围查询,范围查询之后的字段索引不可用了。
注意:要避免多个范围条件,也就是多个 > < between and
5.4.3 优化排序
对于选择性很低的列,比如sex,只有男女两种选择,就可以创建(sex,rating)索引用于下面查询:
SELECT <cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 10;
这个查询同时使用了ORDER BY 和LIMIT,偏移量很高的时候查询也会慢,例如:LIMIT 100000,10
解决办法就是:限制用户翻页的数量,对用户影响不大,很少会真搜到第10000页。
更好的办法:延迟关联
使用覆盖索引查询返回需要的主键,再根据主键关联原表获得需要的行。这样可以减少MySQL扫描丢弃的行数。
SELECT <cols> FROM profiles INNER JOIN( SELECT <primary key cols> FROM profiles WHERE x.sex='M' ORDER BY rating LIMIT 10000,10 ) AS x USING(<primary key cols>);
5.6 总结
选择索引和编写利用这些索引的查询,三个原则:
1. 单行访问是很慢的。最好读取的块中包含尽可能多的行。使用索引可以创建位置引用以提升效率
2. 按顺序访问范围数据是很快的。第一,顺序IO不需多次磁盘寻道;第二,顺序读取不再需要额外排序。
3. 索引覆盖查询是很快的。