SQL索引, since 2022-04-14

2022-04-14  本文已影响0人  Mc杰夫

(2022.04.14 Thur)

SQL在执行检索或更新时,会对全表进行遍历操作。而这种操作会浪费大量时间。索引(index)的使用极大的提升了数据查询的效率。

索引也是一个表,只是这个表对用户不可见,仅仅在执行对关系的查询和修改时调用。索引这个表包含被索引的字段、该字段的不同值和指向不同值的指针(pointer)。

索引可以针对一个列,可以针对多列,也包括对不同列的组合。

索引可以极大的提高查询的性能,但是限制关系的更新速度。一个关系设定的索引越多,更新关系时的速度就越慢。一般来说一个关系的索引不超过6个。

MySQL支持的索引类型

(2022.06.17 Fri)
在MySQL中,索引是在存储引擎层而非服务器层实现,没有统一的索引标准,不同存储引擎的索引工作方式不同,不是所有存储引擎都支持所有类型的索引,同一种索引类型,不同存储引擎的底层实现也可能不同。

这里主要介绍MySQL中的B-tree索引和哈希索引,其他的诸如全文索引、空间数据索引等略。

B-tree索引

当人们谈论索引,如果没有特别指明,说的就是B-tree索引,它使用B-tree数据结构来存储数据。

B-tree意味着所有值都是按顺序存储的,每个叶子页到根的距离相同,这也是B-tree的特性。


Index based on B-tree

有了B-tree索引加持,检索不需要查询全表,仅需要从B-tree的根节点开始查询,B树的叶子节点定义了被索引量的上下限。最终存储引擎中找到查询值,或者不存在。

叶子节点存储的指针指向被索引的数据,而非其他叶节点。

B树的对被索引的序列按顺序组织存储,适合查找范围。而基于文本的索引树,按字母顺序传递连续的值进行查找也很合适。

除了单列,B树还可以对多列进行索引,比如下面这个表

CREATE TABLE people (
    last_name varchar(50) NOT NULL,
    family_name varchar(50) NOT NULL,
    dob date NOT NULL,
    gender enum('m', 'f') NOT NULL,
    key(last_name, first_name, dob)
);

索引中包含了三个列的值,在索引表中的表示如下


多列索引demo

索引中三个列的顺序就是CREATE TABLE时指定的列顺序。可以注意到最后两个key,他们的last_namefamily_name相同,而dob不同。

使用B-tree索引的查询类型

索引除了用于查询,也可用于ORDER BY操作。

在MySQL中查看索引键的组成,可通过

mysql> DESC table_name;
mysql> DESCRIBE table_name;
mysql> SHOW CREATE TABLE table_name;

查看建表信息。

B-tree实现的多键索引在查询时的限制:

上面的结果可以看到,索引中列的顺序对于搜索很重要。优化索引的过程中,可以使用相同列不同顺序的索引来满足不同查询需求。

哈希索引Hash index

基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎会对所有的索引列计算一个哈希码hash code。哈希索引将所有的哈希码存储在索引中,在哈希表中保存指向每个行数据的指针。在MySQL中Memory引擎默认支持哈希索引。如果多个列的哈希值相同,索引会以链表的方式存储多个记录指针到同一个哈希表中。考虑下面的关系

CREATE TABLE testhash(
    fname varchar(50) NOT NULL,
    lname varchar(50) NOT NULL,
    key using hash(fname)
) ENGINE MEMORY;

使用假想的哈希函数f(),对关系中的列做索引有如下值

f('Arjen') = 2323
f('Baron') = 7437
f('Peter') = 8784
f('Vadim') = 2458

注意在关系中,这四个名字是按序排列的。哈希索引的数据结构如下

slot 槽 value 值
2323 指向第1行的指针
2458 指向第4行的指针
7437 指向第2行的指针
8784 指向第3行的指针

注意slot的编号是按序排列,但数据不按此顺序。
哈希索引的查询过程如下,有查询mysql> SELECT lname from testhash where lname = "Peter"

哈希索引的速度很快,但也有如下限制:

因为限制多,哈希索引只适合特定情况,比如指向和关联多个数据表的星形schema。

创建自定义哈希索引

如果存储引擎不支持哈希索引,可以自己创建。基于B-tree创建哈希索引,使用B-tree进行查找,但是使用哈希值而不是键本身进行索引查找,需要在where从句中手动指定哈希函数。

比如一个关系url,其中存储了大量url,且对应的值非常长。

mysql> SELECT id from url where url='http://www.baidu.com';

删除url列上的索引,创建一个新的索引,使用CRC32做哈希函数,可用如下方式查询

mysql> SELECT from url 
       where url='http://www.baidu.com' and 
       url_crc = CRC32('http://www.baidu.com')

这样的查询结果速度提升明显。

这个案例中的url_crc列需要手动维护,或使用触发器维护。创建表格的方式如下

CREATE TABLE url (
    id int unsigned NOT NULL auto_increment,
    url varchar(255) NOT NULL,
    url_crc  int unsigned NOT NULL DEFAULT 0,
    PRIMARY KEY(id)
);

之后创建触发器,可以在数据insert和update(代码中未展示)时加入url_crc对应的值。

DELIMITER//
CREATE TRIGGER pseudohash_crc_ins BEFORE INSERT ON pseudohash FOR EACH ROW BEGIN 
SET NEW.url_crc = CRC32(NEW.url);
END;
DELIMITER;

则每一次插入url数据时会自动生成url_crc字段并赋值。

如果数据大,则CRC32会产生大量哈希冲突,可以自定义哈希函数,利用MD5函数返回值的一部分做哈希

mysql> SELECT CONV(RIGHT(MD5('http://www.baidu.com'), 16), 16, 10) as hash64;

处理哈希冲突,可以在WHERE从句中除了包含哈希索引字段,还加入常量字段,共同检索。

mysql> SELECT id from url where url_crc = CRC32('http://www.baidu.com') 
       and url = 'http://www.baidu.com';

B-tree和hash index各自适合的场景

(2022.07.16 Sat)
能够应用B-tree的检索场景

限制使用B-tree的场景

Hash索引适用:

Hash索引的限制:

索引类型

Unique索引

unique索引对应的单列,其值没有重复值,但可以有Null值。Unique索引对应的列对(column pair),其中的某个单列可以有重复值,但是作为pair,应该没有完全重合的值,即可以(1, 1)和(1, 2),而不可以(1, 1)和(1, 1)。

CREATE UNIQUE INDEX <index_name> ON <table_name>(<index_column1>, <index_column2>,…);
# or
CREATE TABLE <table_name>(<column1> CHAR (30) NOT NULL, UNIQUE INDEX (<column2>));
# or
ALTER TABLE <table_name> ADD UNIQUE INDEX (<column1>, <column2>);

Primary索引

Primary索引是一种unique索引,不同的是primary索引处理的列不能含有Null值,并且只能是单列(?)。一旦列的值确定,primary索引无法修改。

CREATE TABLE <table_name> (<column_name> <data_type> PRIMARY KEY );
# or
CREATE TABLE <table_name> (<column1> CHAR(30) NOT NULL, <column2> CHAR(30) NOT NULL, PRIMARY KEY (<column1>, <column2>));
# or
ALTER TABLE <table_name> ADD PRIMARY KEY(<column1>, <column2>);

Simple/Regular/Normal

这种常规的索引,被索引的列可以有重复值,可以有空值。这种索引目的在于提高查询速度。

CREATE INDEX <index_name> ON <table_name>  (<index_column1>, <index_column2>, …);
# or
CREATE TABLE <table_name>(<column1> CHAR(30) NOT NULL, INDEX(<column2>));
# or
ALTER TABLE <table_name> ADD INDEX(<column1>, <column2>);

Full-text

全文索引用于实现全文检索。适用的情况是在文本中所有特定关键词和特定substring,在搜索引擎和电子商务中应用广泛。这种索引仅适用于MySQLISAM和InnoDB,以及VARCHAR,CHAR和Text类型的列。

适用与不适用的情况

适用的情况

不适用的情况

(2022.04.15 Fri)

计算最佳索引

建立的索引越多,对给定的查询来说,更有可能提高访问速度,但同时会降低更新的速度。每个对于关系R的更新操作会迫使同时更新R修改过的属性或者属性集上的索引,即不仅要读取和回写修改的R的页,还需要花费额外代价读取和回写存储索引的页。实际上,更新操作本身页包含了对数据库的查询,比如带有select-from-where的子查询插入或者带条件的删除操作,也就是说索引的加入会影响更新的效率,但在建立索引时,仍需要考虑查询和更新的相对频度问题。

在做最佳索引分析之前,我们做如下假定

下面通过一个实例来实现寻找最佳索引的过程。有一个关系StarsIn(movieTitle, movieYear, starName)。对这个关系有如下假定:

下面分析不同操作的代价,一个代价代表着访问一次磁盘页面。考虑到访问记录和访问磁盘的时间相差不多,以访问磁盘页面为代价计算标准。

这里使用两种查询,1) 给定影星信息的查询Q1,2) 给定电影信息的查询Q2。一种修改,即对关系R做插入操作I。

不使用索引,Q1和Q2都需要对整个关系做扫描,代价均为10. I需要读取一个有空闲空间的硬盘页(n_I),再将新的元组写入,代价是2.

使用索引,仅对starName属性做索引,Q2仍需扫描整个关系,代价为10.而Q1通过访问1次索引页(n_{ind})快速找到给定影星对应的3个元组,再通过3次磁盘访问将包含3个元组的磁盘页读入主存,因此累计代价为4.插入操作I对索引页和数据页都进行一次读入操作和回写操作,需要4次磁盘访问,代价为4.

仅对movieName属性做索引的情况和前一种情况相同。

如果对starName和movieName都做索引,则Q1和Q2都需要4次访问磁盘其中含一次索引,即代价各为4。而插入操作需要分别对两个索引表进行访问和修改,外加对数据页做读写操作,累计6次访问。

同时,假定Q1在总的操作中的时间比例是p1,Q2的比例是p2,I的比例是1-p1-p2

每种情况对应的总时间代价在下面表中列出。

action No Index Star Index Movie Index Both Indexes
Q1 10: n_p 4: n_{ind}+n_s 10: n_p 4: n_{ind}+n_s
Q2 10: n_p 10: n_p 4: n_{ind} + n_m 4: n_{ind}+n_m
I 2: n_I \times 2 4: n_{ind}\times 2+n_I \times 2 4: n_{ind}\times 2+n_I \times 2 6: n_{ind}\times 2 \times 2+ n_I \times 2
cost 2+8p1+8p2 4+6p2 4+6p1 6-2p1-2p2

在不同的p1,p2值的情况下,每个场景的总代价不同。由此,可根据数据库的实际使用情况来决定选择如何做索引。直觉上,如果某类查询非常频繁,那么就仅仅创建有助于该查询的索引。

Reference

1 数据库系统基础教程,Jeffrey D. U.等著,岳丽华等译,机械工业出版社
2 高性能MySQL第三版,宁等翻译,电子工业出版社

上一篇下一篇

猜你喜欢

热点阅读