进阶篇(8) 索引的创建与设计原则

259 阅读14分钟

1. 索引的声明与使用

1.1 索引的分类

MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。

  • 按照功能逻辑划分 4种:普通索引、唯一索引、主键索引、全文索引。
  • 按照 物理实现方式划分 2种:聚簇索引和非聚簇索引。
  • 按照 作用字段个数 划分 2种:分成单列索引和联合索引。

1.普通索引
不附加任何限制条件,可以创建在任何数据类型

2.唯一性索引
使用UNIQUE关键字设置,该索引的值唯一,但允许有空值

3.主键索引
特殊的唯一性索引,即NOT NULL + UNIQUE。一张表最多只有一个主键索引。 Why?主键索引的物理实现方式决定的,数据存储在文件中只能按一种顺序存储。

4.单列索引
单个字段上创建索引

5.多列(组合、联合)索引
多个字段组合创建一个索引。可以通过这几个字段进行查询,但只有查询条件使用第一个字段时才会被使用。
例:对id,name,gender建立索引,查询时必须带上id字段该索引才会被使用。使用组合索引时遵循最左前缀集合

6.全文索引
也称全文检索,是搜索引擎使用的一种关键技术。能够利用【分词技术】等多种智能算法分析出文本文字中关键词的频率和重要性,并按一定的算法规则智能地筛选出想要的结果。适合大型数据集。
使用参数FULLTEXT设置全文索引。只能创建在CHAR、VARCHAR或TEXT及其系列类型的字段上。查询数据量较大的字符串类型时使用全文索引能提高查询速度。
两种类型:自然语言的全文索引和布尔全文索引

  • 自然语言搜索引擎将计算每一个文档对象和查询的相关度(基于匹配的关键词的个数),以及关键词在文档中出现的次数。整个索引中出现次数越少的词语,匹配的相关度越高。相反,非常常见的单词不会被搜索。

MySQL从3.23开始支持全文索引(MyISAM),但InnoDB在5.6.4后才支持。

1.2 创建索引

1.2.1 创建表的时候创建索引
CREATE TABLE table_name [col_name data_type] 
[UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC | DESC]
  • UNIQUEFULLTEXTSPATIAL为可选参数,分别表示唯一索引、全文索引和空间索引;
  • INDEXKEY为同义词,两者的作用相同,用来指定创建索引;
  • index_name指定索引的名称,为可选参数,如果不指定,那么MySQL默认col_name为索引名;
  • col_name为需要创建索引的字段列,该列必须从数据表中定义的多个列中选择;
  • length为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度;
  • ASCDESC指定升序或者降序的索引值存储。

0.查看索引

SHOW INDEX FROM test1 \G

1.创建普通索引

CREATE TABLE book( 
    book_id INT , 
    book_name VARCHAR(100), 
    INDEX(book_name) 
);

2.创建唯一索引

CREATE TABLE book( 
    book_id INT , 
    book_name VARCHAR(100) ,# 隐式声明:直接在字段后+UNIQUE 
    UNIQUE INDEX(book_name) 
);

3.创建主键索引
设定为主键后数据库会自动建立索引,innodb为聚簇索引

CREATE TABLE book( 
    book_id INT NOT NULL, 
    book_name VARCHAR(100), 
    PRIMARY KEY(book_id) 
);

# 删除主键索引
ALTER TABLE student drop PRIMARY KEY ;
# 修改主键索引:必须先删除掉(drop)原索引,再新建索引

4.创建组合索引

CREATE TABLE book( 
    book_id INT , 
    book_name VARCHAR(100), 
    INDEX multi_index(book_id,book_name) 
);

5.创建全文索引

CREATE TABLE book( 
    book_id INT , 
    book_name VARCHAR(100), 
    FULLTEXT INDEX(book_name(50)) // 拿前五十个创建全文索引 
)ENGINE = MyISAM;

在MySQL5.7及之后版本中可以不指定最后的ENGINE了,因为在此版本中InnoDB支持全文索引。

不同于like方式的的查询:

SELECT * FROM papers WHERE content LIKE%查询字符串%’;

全文索引用match+against方式查询:

SELECT * FROM papers WHERE MATCH(title,content) AGAINST (‘查询字符串’)

注意点

  1. 使用全文索引前,搞清楚版本支持情况;
  2. 全文索引比 like + % 快 N 倍,但是可能存在精度问题;
  3. 如果需要全文索引的是大量数据,建议先添加数据,再创建索引。
1.2.2 在已经存在的表上创建索引

1.使用ALTER TABLE语句创建索引

ALTER TABLE table_name 
ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name[length],...) [ASC | DESC]

2.使用CREATE INDEX创建索引

CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name 
ON table_name (col_name[length],...) [ASC | DESC]

1.3 删除索引

ALTER TABLE table_name DROP INDEX index_name;
# 或
DROP INDEX index_name ON table_name;

删除表中的列时,若删除部分为索引的组成部分,则该列也会从索引中删除。如果组成索引的所有列都被删除,则整个索引将被删除。

2. MySQL8.0索引新特性

2.1 支持降序索引

MySQL8.0之前创建的仍是升序索引,使用时进行反向扫描,大大降低了数据库的效率。 在某些场景下降序索引意义重大。例如:一个查询对多个列进行排序且顺序要求不一样,那么使用降序索引将会避免数据库使用额外的文件排序操作,从而提高性能。

CREATE TABLE ts1(a int,b int,index idx_a_b(a,b desc));

在MySQL 5.7版本中查看数据表ts1的结构,结果如下:

image.png
从结果可以看出,索引仍然是默认的升序。

在MySQL 8.0版本中查看数据表ts1的结构,结果如下:

image.png

DELIMITER // 
CREATE PROCEDURE ts_insert() 
BEGIN 
    DECLARE i INT DEFAULT 1; 
    WHILE i < 800 
    DO 
        insert into ts1 select rand()*80000,rand()*80000;
        SET i = i + 1;
    END WHILE; 
    commit;
END //
DELIMITER ;

CALL ts_insert();

查看数据表ts1的执行计划,结果如下:

EXPLAIN SELECT * FROM ts1 ORDER BY a,b DESC LIMIT 5;

MySQL 5.7版本中:执行计划中扫描数为799,而且使用了Using filesort。 image.png

Using filesort是MySQL中一种速度比较慢的外部排序,能避免是最好的。多数情况下,管理员 可以通过优化索引来尽量避免出现Using filesort,从而提高数据库执行速度。

MySQL 8.0版本中:执行计划中扫描数为5,而且没有使用 Using filesort。 image.png

注意 降序索引只对查询中特定的排序顺序有效,如果使用不当,反而查询效率更低。例如,上述查询排序条件改为 order by a desc, b desc,MySQL 5.7的执行计划要明显好于MySQL 8.0。

2.2 隐藏索引

MySQL5.7及以前,只能通过显式的方式删除索引。此时如果发现删除后出现错误,又只能显式的创建回来。若数据量非常大,则这种操作会消耗大量资源。

从MySQL 8.x开始支持隐藏索引(invisible indexes),只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除

想验证某个索引删除后的查询性能影响,可先隐藏该索引。

1. 创建表时直接创建

CREATE TABLE tablename( 
    colname1 type1[CONSTRAINT1], 
    colname2 type2[CONSTRAINT2], 
    ……
    colnamen typen, 
    INDEX [indexname](colname [(length)]) INVISIBLE 
);

2. 在已经存在的表上创建

CREATE INDEX indexname  ON tablename(colname[(length)]) INVISIBLE;
# 或
ALTER TABLE tablename  ADD INDEX indexname (colname [(length)]) INVISIBLE;

3. 切换索引可见状态

ALTER TABLE tablename ALTER INDEX index_name INVISIBLE; #切换成隐藏索引 
ALTER TABLE tablename ALTER INDEX index_name VISIBLE; #切换成非隐藏索引

注意: 当索引被隐藏时,它的内容仍然是和正常索引一样实时更新的。如果一个索引需要长期被隐 藏,那么可以将其删除,因为索引的存在会影响插入、更新和删除的性能。

4. 使隐藏索引对查询优化器可见

在MySQL 8.x版本中,为索引提供了一种新的测试方式,可以通过查询优化器的一个开关 (use_invisible_indexes)来打开某个设置,使隐藏索引对查询优化器可见。如果 use_invisible_indexes 设置为off(默认),优化器会忽略隐藏索引。如果设置为on,即使隐藏索引不可见,优化器在生成执行计 划时仍会考虑使用隐藏索引。

  1. 在MySQL命令行执行如下命令查看查询优化器的开关设置。
select @@optimizer_switch \G

在输出的结果信息中找到如下属性配置。

use_invisible_indexes=off # 隐藏索引默认对查询优化器不可见。
  1. 使隐藏索引对查询优化器可见,需要在MySQL命令行执行如下命令:
set session optimizer_switch="use_invisible_indexes=on";

3. 索引的设计原则

3.1 准备数据

#1.创建学生表和课程表
CREATE TABLE `student_info` (
 `id` INT(11) AUTO_INCREMENT,
 `student_id` INT NOT NULL ,
 `name` VARCHAR(20) DEFAULT NULL,
 `course_id` INT NOT NULL ,
 `class_id` INT(11) DEFAULT NULL,
 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
 PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `course` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`course_id` INT NOT NULL ,
`course_name` VARCHAR(40) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

SET GLOBAL log_bin_trust_function_creators = 1;

#函数1:创建随机产生字符串函数
DELIMITER //
CREATE FUNCTION rand_string(n INT) 
	RETURNS VARCHAR(255) #该函数会返回一个字符串
BEGIN 
	DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
	DECLARE return_str VARCHAR(255) DEFAULT '';
	DECLARE i INT DEFAULT 0;
	WHILE i < n DO 
       SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
       SET i = i + 1;
    END WHILE;
    RETURN return_str;
END //
DELIMITER ;

#函数2:创建随机数函数
DELIMITER //
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN   
DECLARE i INT DEFAULT 0;  
SET i = FLOOR(from_num +RAND()*(to_num - from_num+1))   ;
RETURN i;  
END //
DELIMITER ;

# 存储过程1:创建插入课程表存储过程
DELIMITER //
CREATE PROCEDURE  insert_course( max_num INT )
BEGIN  
DECLARE i INT DEFAULT 0;   
 SET autocommit = 0;    #设置手动提交事务
 REPEAT  #循环
 SET i = i + 1;  #赋值
 INSERT INTO course (course_id, course_name ) VALUES (rand_num(10000,10100),rand_string(6));  
 UNTIL i = max_num  
 END REPEAT;  
 COMMIT;  #提交事务
END //
DELIMITER ;


# 存储过程2:创建插入学生信息表存储过程
DELIMITER //
CREATE PROCEDURE  insert_stu( max_num INT )
BEGIN  
DECLARE i INT DEFAULT 0;   
 SET autocommit = 0;    #设置手动提交事务
 REPEAT  #循环
 SET i = i + 1;  #赋值
 INSERT INTO student_info (course_id, class_id ,student_id ,NAME ) VALUES (rand_num(10000,10100),rand_num(10000,10200),rand_num(1,200000),rand_string(6));  
 UNTIL i = max_num  
 END REPEAT;  
 COMMIT;  #提交事务
END //
DELIMITER ;

#调用存储过程:
CALL insert_course(100);
CALL insert_stu(1000000);

3.2 哪些情况适合创建索引

1. 字段的数值有唯一性的限制

如果某个字段是唯一性的,可以创建唯一性索引或主键索引

例:学号

业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)
说明:唯一索引影响 insert 的速度损耗可以忽略,但提高查找速度是明显的。

2. 频繁作为 WHERE 查询条件的字段

例:student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。

我们先查看student_id上是否有索引

SHOW INDEX FROM student_info;

image.png

进行查询

SELECT course_id, class_id, NAME, create_time, student_id 
FROM student_info WHERE student_id = 123110;

image.png

对该字段添加索引:

ALTER TABLE student_info ADD INDEX (student_id);

速度增快的很明显啊! image.png

3. 经常 GROUP BY 和 ORDER BY 的列

索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据进行排序的时候,就需要对分组或者排序的字段进行索引。如果待排序的列有多个,那么可以在这些列上建立组合索引

举例:按学号分组,查找课最多的前100个同学: 注意:现在是有学号索引的状态

SELECT student_id, COUNT(*) AS num 
FROM student_info  GROUP BY student_id LIMIT 100;

image.png

删除索引后再试试:

DROP INDEX idx_sid ON student_info;

SELECT student_id, COUNT(*) AS num 
FROM student_info  GROUP BY student_id LIMIT 100;

一下子起飞了。。。 image.png

如果同时又 GROUP BY 和 ORDER BY的情况:比如我们按照学号分组,创建方式降序排序。

# 需改sql_mode,不用管它干嘛
SET @@sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';

# 添加两个单列索引
ALTER TABLE student_info ADD INDEX (student_id);
ALTER TABLE student_info ADD INDEX (create_time);

# 查询
SELECT student_id, COUNT(*) AS num FROM student_info 
GROUP BY student_id ORDER BY create_time DESC LIMIT 100; 

image.png

看看查询到底用的是哪个索引:

EXPLAIN SELECT student_id, COUNT(*) AS num FROM student_info 
GROUP BY student_id ORDER BY create_time DESC LIMIT 100;

因为查询先 GROUP BY 再 ORDER BY,所以只会使用 student_id 索引: image.png

我们再创建两个联合索引试试查询速度:

ALTER TABLE student_info ADD INDEX idx_sid_crt (student_id, create_time DESC);
ALTER TABLE student_info ADD INDEX idx_crt_sid (create_time DESC, student_id);

SELECT student_id, COUNT(*) AS num FROM student_info 
GROUP BY student_id ORDER BY create_time DESC LIMIT 100;

很明显,快多了:
image.png

那么到底是哪个索引发挥了作用呢? 用的是学号在前的索引 image.png

那我们把 idx_sid_crt删除查询就会使用 idx_crt_sid了吗?

DROP INDEX dx_sid_crt ON student_info;

 EXPLAIN SELECT student_id, COUNT(*) AS num FROM student_info 
GROUP BY student_id ORDER BY create_time DESC LIMIT 100;

查询语句进行到 GROUP BY的时候发现只有student_id可用,就直接使用了 image.png

你可以自己试试把学号索引删了后查询会用idx_crt_sid吗

4. UPDATE、DELETE 的 WHERE 条件列

如果进行更新的字段非索引字段, 添加 WHERE 常用字段索引的效果会更明显。因为更新时不需要动态修改索引的B+树

5.DISTINCT 字段需要创建索引

有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。查询出来的数据还是按递增/减的形式展示的。

6. 多表 JOIN 连接操作时,创建索引注意事项

  • 连接表的数量尽量不要超过 3 张,每增加一张表相当于增加了一次嵌套的循环,严重影响查询的效率。
  • 对 WHERE 条件创建索引
  • 对用于连接的字段创建索引,并且该字段在多张表中的类型必须一致

7. 使用列的类型小的创建索引

8. 使用字符串前缀创建索引

创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引

create table shop(address varchar(120) not null);
alter table shop add index(address(12));

问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的;截取得少了,重复内容太多,字段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?

# 区分度 = 不重复的数量/总数
select count(distinct address) / count(*) from shop;

索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上

select count(distinct left(address,10)) / count(*) as sub10, -- 截取前10个字符的选择度 
select count(distinct left(address,15)) / count(*) as sub11, -- 截取前15个字符的选择度   
from shop;

9. 区分度高(散列性高)的列适合作为索引

列的基数指某一列中不重复数据的个数,比方说某列包含值2,5,8,2,5,8,2,5,8,虽然有9条记录,但该列的基数却是3。,在记录行数一定的情况下,列的基数越大/小,该列中的值越分散/集中。最好为列的基数大的列建立索引

区分度,越接近1越好,一般超过33%就算是比较高效的索引了。

拓展:联合索引把区分度高(散列性高)的列放在前面。

10. 使用最频繁的列放到联合索引的左侧

最左前缀原则

11. 在多个字段都要创建索引的情况下,联合索引优于单值索引

3.3 限制索引的数目

建议单张表索引数量不超过6个。原因:

  • 索引需要占用磁盘空间,索引越多,需要的磁盘空间就越大。
  • 索引会影响INSERT、DELETE、UPDATE等语句的性能,表中的数据更改的同时,索引也会进行调整和更新。
  • 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,会增加MySQL优化器生成执行计划时间,降低查询性能。

3.4 不适合创建索引的情况

1. 在where中使用不到的字段,不要设置索引

2. 数据量小的表最好不要使用索引

不到1000行,可能索引查询比没索引查询还慢。

3. 有大量重复数据的列上不要建立索引

当数据重复度大,比如高于 10% 的时候,也不需要对这个字段使用索引。

4. 避免对经常更新的表创建过多的索引

5. 不建议用无序的值作为索引

例如身份证、UUID(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字符串等。

6. 删除不再使用或者很少使用的索引

7. 不要定义冗余或重复的索引

① 冗余索引

CREATE TABLE person_info(
    id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    birthday DATE NOT NULL,
    PRIMARY KEY (id),
    KEY idx_name_birthday (name(10), birthday),
    KEY idx_name (name(10))
);

通过 idx_name_birthday 索引就可以对 name 列进行快速搜索,创建一个name的单列索引没什么用还会增加维护成本。

② 重复索引

CREATE TABLE repeat_index_demo (
    col1 INT PRIMARY KEY,
    col2 INT,
    UNIQUE uk_idx_c1 (col1),
    INDEX idx_c1 (col1)
);

col1 既是主键、又给它定义为一个唯一索引,还给它定义了一个普通索引