8-2 MySQL 索引的设计原则(超详细说明讲解)

8-2 MySQL 索引的设计原则(超详细说明讲解)

@

目录

1. 测试数据准备

第1步:创建数据库、创建表


CREATE DATABASE dbtest3;

USE dbtest3;


#1.创建学生表和课程表
CREATE TABLE `student_info` (
`id` INT(11) NOT NULL 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;



第2步:创建模拟数据必需的存储函数

说明: 创建函数,假如报错:

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

由于开启过慢查询日志bin-log, 我们就必须为我们的 funC++tion 指定一个参数。

主从复制,主机会将写操作记录在bin-log日志中。从机读取bin-log日志,执行语句来同步数据。如果使 用函数来操作数据,会导致从机和主键操作时间不一致。所以,默认情况下,mysql不开启创建函数设 置。

  • 查看mysql是否允许创建函数:
SHOW VARIABLES LIKE 'log_bin_trust_function_creators';

  • 命令开启:允许创建函数设置:
set global log_bin_trust_function_creators=1;    # 不加global只是当前窗口有效。

  • mysqld重启,上述参数又会消失。永久方法:

    • windows下:my.ini[mysqld]加上:
    log_bin_trust_function_creators=1 # 1 表示真-开启,0 表示假-关闭
    
    • Linux下:/etc/my.cnfmy.cnf[mysqld]加上:
    log_bin_trust_function_creators=1 # 1 表示真-开启,0 表示假-关闭
    


#函数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;

第3步:创建插入模拟数据的存储过程


# 存储过程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 ;

第4步:调用存储过程

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

0

第5步:查看是否含有这么多数据,数据是否插入成功

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

1

2. 哪些情况适合创建索引

2.1 字段的数值有唯一性的限制

索引本身可以起到约束的作用,比如:唯一索引,主键索引都是可以起到唯一性约束的,因此在我们的数据表中,如果某个字段是唯一性的 ,就可以直接创建唯一性索引 ,或者主键索引。这样可以更快速地通过该索引来确定某条记录。

例如:学生表中学号 是具有唯一性的字段,为该字段建立唯一性索引可以很快确定某个学生的信息,如果使用姓名 的话,可能存在同名现象,从而减低查询速度。

业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)

说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显s的。

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

某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在 数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。

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

没有给 student_id 字段添加索引,执行的速度是:

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

2

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

3

2.3 经常 GROUP BY 和 ORDER BY 的列

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

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

4

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

5

如果同时有 GROUP By 和 ORDER BY 的情况:比如我们按照 student_id 进行分组,同时按照创建时间降序的方式进行排序,这时我们就需要同时进行 GROUP BY 和 ORDER BY,那么是不是需要单独创建 student_id 的索引和 create_time 的索引呢?

当我们对 student_id 和 create_time 分别创建索引,执行下面的SQL查询

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

6

当我们添加对 student_id 和 create_time 组合创建联合索引,执行下面的SQL查询

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

7

再次执行该SQL语句:

2.4 UPDATE、DELETE 的 中的WHERE 条件列添加索引

对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就 能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或 删除。 如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更 新不需要对索引进行维护。

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

8

为 name 添加上索引

This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

9

2.5 对于经常 DISTINCT 字段需要创建索引

有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。 比如,我们想要查询课程表中不同的 student_id 都有哪些,如果我们没有对 student_id 创建索引,执行 SQL 语句:

SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
0

运行结果(600637 条记录,运行时间 0.683s ):

如果我们对 student_id 创建索引,再执行 SQL 语句:

SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
0

运行结果(600637 条记录,运行时间 0.010s ):

你能看到 SQL 查询效率有了提升,同时显示出来的 student_id 还是按照 递增的顺序 进行展示的。这是因 为索引会对数据按照某种顺序进行排序,所以在去重的时候也会快很多。

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

  1. 首先, 连接表的数量尽量不要超过 3 张 ,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增 长会非常快,严重影响查询的效率。
  2. 其次, 对 WHERE 条件创建索引 ,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下, 没有 WHERE 条件过滤是非常可怕的。
  3. 最后, 对用于连接的字段创建索引 ,并且该字段在多张表中的 类型必须一致 。比如 course_id 在 student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。

举个例子,如果我们只对 student_id 和 name 创建索引,执行 SQL 语句:

SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
2

SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
3

2.7 使用列的类型小的创建索引

使用列小的类型,创建的索引占用的磁盘空间就比较小一些,因为MySQL8 是将索引和数据都是存放再一起的。

我们这里所说的类型大小 指的就是该类型表示的数据范围的大小。

我们在定义表结构的时候要显式的指定列的类型,以整数类型为例,有TINYINT,MEDIUMINT,INT,BIGINT 等,它们占用的存储空间依次递增,能表示的整数范围当然也是依次递增,如果我们想要对某个整数列建立索引的话,在表示的整数范围允许的情况下,尽量让索引列使用较小的类型,比如我们能使用 INT 就不要使用 BIGINT ,能使用 MEDIUMINT 就不要使用 INT

2.8 使用字符串前缀创建索引

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

  1. 创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引
SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
4

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

先看一下字段在全部数据中的选择度:

SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
5

通过不同长度去计算,与全表的选择性对比:

公式:

SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
6

例如:

SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
7

引申另一个问题:索引列前缀对排序的影响

拓展:Alibaba《Java开发手册》

  • 【 强制 】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本 区分度决定索引长度。
  • 说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会 高达 90% 以上 ,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。

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

列的基数 指的是某一列中不重复数据的个数,比方说某个列包含值2,5,8,2,5,8,2,5,8 ,虽然有9 条记录,但该列的基数却是3。也就是说,在记录行数一定的情况下,列的基数越大,该列中的值越分散;列的基数越小,该列中值越集中 。这个列的基数指标非常重要,直接影响我们是否能够有效的利用索引。最好为列的基数大的列建立索引,为基数太小的列建立索引效果可能不好。

可以使用公式:select count(distinct a) / count(*) from t1 计算区分度,越接近 1 越好,一般超过 33% 就算是比较高效的索引了。

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

2.10 使用最频繁的列放到联合索引的左侧——索引最左侧匹配

索引最左侧匹配的原则,索引会优先判断最左侧的字段是否,建立的索引,建立了索引就会走索引,如果左侧的字段没有走索引,就算后面的字段有索引,也不会走索引的。

3. 哪些情况不适合创建索引

3.1 在 where 筛选条件当中使用不到的字段,不要设置索引

你都不对该字段,进行筛选过滤,那么索引你没有意义,因为你索引也是会增加磁盘空间大小的。

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

举例:创建表1:

SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
8

提供存储过程1:

SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
9

创建表2:

set global log_bin_trust_function_creators=1;    # 不加global只是当前窗口有效。
0

创建存储过程2:

set global log_bin_trust_function_creators=1;    # 不加global只是当前窗口有效。
1

查询对比:

set global log_bin_trust_function_creators=1;    # 不加global只是当前窗口有效。
2

你能看到运行结果相同,但是在数据量不大的情况下,索引就发挥不出作用了。

结论:在数据表中的数据行数比较少的情况下,比如不到 1000 行,是不需要创建索引的。

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

举例1:要在 100 万行数据中查找其中的 50 万行(比如性别为男的数据),一旦创建了索引,你需要先 访问 50 万次索引,然后再访问 50 万次数据表,这样加起来的开销比不使用索引可能还要大。

举例2:假设有一个学生表,学生总数为 100 万人,男性只有 10 个人,也就是占总人口的 10 万分之 1。 学生表 student_gender 结构如下。其中数据表中的 student_gender 字段取值为 0 或 1,0 代表女性,1 代 表男性。

set global log_bin_trust_function_creators=1;    # 不加global只是当前窗口有效。
3

如果我们要筛选出这个学生表中的男性,可以使用:

set global log_bin_trust_function_creators=1;    # 不加global只是当前窗口有效。
4

运行结果(10 条数据,运行时间 0.696s ):

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

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

因为你不断更新表的同时,索引也是在同步更新的,索引更新是会消耗大量的时间。

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

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

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

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

冗余索引

举例:建表语句如下

set global log_bin_trust_function_creators=1;    # 不加global只是当前窗口有效。
5

我们知道,通过 idx_name_birthday_phone_number 索引就可以对 name 列进行快速搜索,再创建一 个专门针对 name 列的索引就算是一个 冗余索引 ,维护这个索引只会增加维护的成本,并不会对搜索有 什么好处.。

重复索引:

另一种情况,我们可能会对某个列 重复建立索引 ,比方说这样:

set global log_bin_trust_function_creators=1;    # 不加global只是当前窗口有效。
6

我们看到,col1 既是主键、又给它定义为一个唯一索引,还给它定义了一个普通索引,可是主键本身就 会生成聚簇索引,所以定义的唯一索引和普通索引是重复的,这种情况要避免。

4. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”