高性能MySQL - 创建高性能的索引(上)(B-Tree,Hash)

前言

索引在Mysql中也叫作‘键(key)’。
基本功能是用于存储引擎快速找到记录的一种数据结构。

Question:使用ORM,是否还需要关心索引
即使使用对象关系映射(ORM)工具,仍然要理解索引。除非只是生产非常基本的查询(例如仅是根据主键查询),否则它很难生成适合索引的查询。

Mysql中,索引是在存储引擎层而不是服务层实现的。所以没有统一的索引标准。

Mysql支持的索引

1. B-Tree 索引

如果没有特别指明类型,多半是说B-Tree索引。
存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值上限和下限。

假设有如下数据表:

CREATE TABLE Poople(
    last_name  varchar(50)   not null,
    first_name varchar(50)   not null,
    dob        date          not null,
    gender     enum('m','f') not null,
    key(last_name,first_name,dob)
);

索引包括last_name,first_name,dob, 该索引存储形式:

高性能MySQL - 创建高性能的索引(上)(B-Tree,Hash)_第1张图片

上述索引对如下类型的查询有效:
1. 全值匹配
全值匹配指的是和索引中的所有列进行匹配。即key的值一并给出,然后查找。
2. 匹配最左前缀
前面提到的索引可用于查找所有姓位Allen的人,即只使用索引的第一列。
3. 匹配列前缀
也可以只匹配某一列的值的开头部分。如查找所有以J开头的姓的人。
4.匹配范围值
可用于查找姓在Allen和Barrymore之间的人。
5.精确匹配某一列并范围匹配另外一列
6.只访问索引的查询
索引树的节点是有序的,所以除了按值可查找之外,索引还可以用于查询中的ORDER BY。

(重点)B-Tree索引的限制:
· 如果不是按照索引的最左列开始查找,则无法使用索引。例如上面的例子,无法用于查找名字为Bill的人,也无法查找某个特定的生日的人,因为这两列都不是最左数据列。
· 不能跳过索引中的列。
· 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。例如查找WHERE last_name='Smith' AND first_name LIKE 'J%' AND dob = '1976-12-23', 这个查询只能使用索引的前两列。

所以,重点在于索引列的顺序!!在优化性能的时候,可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求

2.哈希排序

哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。
在Mysql中,只有Memory引擎显式支持哈希索引。
假设有如下表:

CREATE TABLE test hash (
    fname VARCHAR(50) NOT NULL,
    lname VARCHAR(50) NOT NULL,
    KEY USING HASH(fname)
) ENGINE=MEMORY;

表中包含如下数据:
fname lname
Arijen Lentz
Baron Schwartz
Peter Zaitsev
Vadim Tkachenko

哈希函数f()
f(‘Arijen’) = 2323;
f(‘Baron’) = 7437;
f(‘Peter’) = 8784;
f(‘Vadim’) = 2458;

哈希索引的数据结构:
slot value
2323 指向第1行的指针
2458 指向第4行的指针
7437 指向第2行的指针
8784 指向第3行的指针

运行如下查询时:

SELECT lname FROM test hash WHERE fname='Peter';

先计算’Peter’的哈希值,并使用该值寻找对应的记录指针。因为f(‘Peter’) = 8784,所以在索引中查找8784,可以找到指向第3行的指针,最后一步是比较第三行的值是否为‘Peter’,以确保就是要查找的行。
因为索引自身只需要存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。然而,哈希索引也有它的限制:
· 哈希索引只包含哈希值和航指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不影响。
· 哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序。
· 哈希索引始终是使用索引列的全部内容来计算哈希值的。
· 不支持任何范围查询,只支持等值比较查询
· 访问哈希索引的数据非常快,除非有很多哈希冲突,当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行。
· 如果哈希冲突很多的话,一些索引维护操作的代价非常高。

所以,哈希索引只适用于某些特定的场所。而一旦适合哈希索引,它带来的性能提升也是非常显著的。

InnoDB引擎有一个特殊的功能叫做“自适应哈希索引”。当InnoDB注意到某些索引值被使用得非常频繁时,它会在内存中基于B-Tree索引之上再创建一个哈希索引,这样就让B-Tree索引也具有哈希索引的一些优点,比如快速的哈希查找。这是一个完全自动的、内部的行为,用户无法控制或者配置,但可以选择关闭

自定义哈希索引

SELECT id FROM url WHERE url="http://www.mysql.com" AND url_crc = CRC32("http://www.mysql.com");

MySQL 优化器会使用这个选择性很高而体积很小的基于url_crc列来完成查找。即使有多个记录有相同的索引值,查找仍然很快。只需要根据哈希值做快速的整数比较就能找到索引条目。
这样实现的缺陷是需要维护哈希值,可以手动维护,也可以使用触发器实现。

CREATE TABLE pseudohash(
    id int unsigned NOT NULL auto_increment,
    url varchar(255) NOT NULL,
    url_crc int unsigned NOT NULL DEFAULT 0,
    PRIMARY KEY(id)
);
DILIMITER//
CREATE TRIGGER pseudohash_crc_ins BEFORE INSERT ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url);
END;

CREATE TRIGGER pseudohash_crc_upd BEFORE UPDATE ON pseudo hash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url);
END;

DELIMITER;

记住不要使用SHA1()和MD5()作为哈希函数。因为这两个函数计算出来的哈希值是非常长的字符串,会浪费大量空间。SHA1()和MD5()是强加密函数,设计目标是最大限度消除冲突,但这里并不需要这样高的要求。

全文索引

是一种特殊类型的索引,查找的是文本中的关键词,而不是直接比较索引中的值。全文搜索和其他几类索引匹配的方式完全不一样。更类似于搜索引擎做的事情,而不是简单的WHERE条件匹配。

其他索引

分形树索引,新开发的数据结构
聚族索引
覆盖索引

索引是最好的解决方案吗

索引并不一定是最好的解决方案,在小型表中,全表扫描更加高效。对于中到大型表,索引就非常有效。但对于特大型的表,建立和使用索引就非常有效。但到特大型的表,建立和使用索引的代价将随之增长。
如果表的数量特别多,可以建立一个元数据信息表,用来查询需要用到的某些特性。例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录“哪些用户的信息存储在哪个表中”的元数据,这样在查询时就可以直接忽略那些不含指定用户信息的表,对于大型系统,这是常用的技巧。

高性能的索引策略

通常有一些查询不当地使用索引,或者使得MySQL无法使用已有的索引。如果查询中的列不是独立的,则MySQL就不会使用索引。

“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。
例如:

SELECT actor_id FROM skill.actor WHERE actor_id + 1 = 5

MySQL无法自动解析这个方程式。这完全是用户行为。

前缀索引和索引选择性
索引很长的字符串时,会让索引变得大而慢。通常可以索引开始的部分字符,这样可以大大节约索引的选择性。

“索引的选择性”:不重复的索引值和数据表的记录总数的比值,范围从1/记录总数 到1 之间。索引的选择型越高,查询效率越高。

对于BLOB、TEXT或很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整长度。

所以我们要做的就是选择足够长的前缀以保证较高的选择性。

例子:

SELECT COUNT(*) AS intercity FROM skill.city_demo GROUP BY city ORDER BY int DESC LIMIT 10;

结果:
cnt city
65 London
49 Hiroshima
48 Teboksary
……

现在查找到最频繁出现的城市前缀,从3个字母开始:

SELECT COUNT(*) As cnt,LEFT(city,3) AS pref FROM skill.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;

得出来的指400多到100多不等,继续增加前缀,实验后发现前缀长度为7的时候比较合适,得出来的数值跟原来的城市出现的次数差不多。
还有一个方法是直接计算完整列的选择性

SELECT COUNT(DISTINCT city)/COUNT(*) FROM skilla.city_demo;

找到合适的前缀长度之后,开始创建前缀索引:

ALTER TABLES skill.city_demo ADD KEY(city(7));

前缀索引的好坏:
好处:使索引更小,更快的有效方法。
坏处:MySQL无法使用前缀索引做ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描。

如何选择索引列顺序?
当不需要考虑排序和分组时,将选择性高的列放在索引最前列通常是好的。 可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。

比如说

SELECT * FROM payment WHERE staff_id = 2 AND customer_id = 584;  

运行一下

SELECT SUM(staff_id = 2),SUM(customer_id = 584) FROM payment 

得到:SUM(staff_id = 2): 7992 ; SUM(customer_id) = 30
对于这条语句来说,将customer_id放在前面,对应的staff_id列的选择性会变低。

但是这个方法依赖于选定的具体值,可能对其他一些条件值的查询不公平,导致服务器整体性能可能不是很好。除非是从诸如: pt_query_digest等工具提取的“最差”查询,再按上述的方法进行改动可能才有好的效果。

你可能感兴趣的:(数据库)