赞
踩
我们完全可以将索引可以看作是书本的目录吧,通过书本的目录查找对应的章节就会快一些,索引底层数据结构有b树、
b+树`、红黑树、哈希表等,对于MySQL而言,无论Innodb 还是MyIsam,使用的都是b+树。
不一定,若数据量不大的情况下,建立索引不一定会带来性能的提升。
上面说了几个底层数据结构,我们不妨来一个个进行介绍吧:
这就是一种键值对的结构,在保证哈希算法能够均摊散列的情况下,查询时间复杂度为O(1)
,至于什么是均摊散列,说白了每一个数据算出来的key都是唯一的,若重复了我们只能通过链地址法解决冲突,这样查询数据的时间复杂度就不一定是O(1)了。
你可能会问为什么MySQL
不适用哈希结构呢?原因很简单:
B树和B+树差不多,都是多路自平衡查找树,整体总结有以下几个区别:
(上面说了B树每个节点存放的都是key和data)
,而B+树必须到达叶子节点才能找到data
,而且B+
树叶子节点还存在引用链的原因,范围查询还是很有优势的。B树如下图所示
再看看B+树,可以看到每个非叶子节点存储的都是子节点的指针,而叶子节指向被索引的数据,而且这个树是按照顺序进行排序的,所以b+树是非常适合范围查询的。
需要补充一点,如果存储引擎为MyISAM ,我们会根据索引算法查找数据,若找到对应的data域,我们就可以从data域中获取到数据的地址,从而获取数据,这就是我们说的非聚簇索引。
相比之下InnoDB的底层数据结构就比较特殊了,它将数据和索引都存放到一个文件中,所以文件本身就是按照B+树组织的一种索引结构,这棵树的叶子节点的data域保存的都是完整的数据记录。
而其余的索引则称为辅助索引,所以当我们使用辅助索引查找数据时,会根据辅助索引找到对应的data域,从data域中获取到主键的值,然后用这个值在文件中找到对应的数据,这也是我们说的非聚簇索引。
所以我们在建立主键时,建议建立的主键不要太长,也不要是那种非单调的字段作为主键。
实际上在我们数据表中有创建主键索引的情况下,那么这个索引结构就是基于我们自定义的主键索引字段。若没有的话,InnoDB
会从表中找到不为null的唯一索引
字段作为主键索引。若还是没有那么InnoDB
就会自己生产一个6byte
的字段作为自增主键。
在InnoDB 索引结构就是基于主键索引构成的,主键索引要求索引字段不为null且唯一,如果我们设计的表中没有主键字段,则InnoDB 会从表中找到唯一且不为null的唯一索引作为主键索引。若没有唯一且不为null的索引,那么InnoDB 就会自动生成一个6byte的自增主键作为主键索引。
二级索引是一级索引下一级的索引,它的data域存放的是一级索引的值,我们可以通过二级节点的data找到一级索引的值,从而定位到一级索引进而获取到数据。
二级索引可以是:
MySQL5.6
之前只有innodb
支持,5.6
之后MyISAM
也支持了。二级索引如下图所示,可以看到非叶子节点data存放的都是叶子节点的页地址,而叶子节点的data
存放的都是主键的地址值。
聚簇索引说的其实一种将索引结构和数据放在一起的文件结构,用高性能MySQL的话说它就是将数据行和相邻的键值紧凑在一起。因为数据行不可以同时放在两个地方,所以一张表中只可能有一个聚簇索引。
它的文件后缀为.ibd,InnoDB 中的主键索引就属于聚簇索引。对于InnoDB而言,所有非叶子节点存放的都是索引,而叶子节点存放的则是索引以及索引对应的数据行。
上面的说法仿佛告诉我们使用聚簇索引使用主键自增是最好的解决方案,实际上在高并发写的场景下,聚簇索引的顺序插入很可能导致间隙锁或者AUTO_INCREMENT锁。所以读者在使用聚簇索引时一定要考虑到当前业务场景是否会出现并发写的情况。
如下图所示,我们的user表,有id和name两列,而id为主键,所以由主键生成的聚簇索引如下图所示,所有的非叶子节点指向叶子节点,叶子节点则存储着数据行。
而且非叶子节点11前面一列所指向的都是id小于11的数据行,需要注意的是数据行中不仅包含主键和其他列,还包含事务id和回滚指针,这些都是mvcc中的概念,笔者就不多做赘述了。
而且如果我们使用前缀索引作为主键列的话,MySQL的叶子节点不仅仅会存储主键,还会将完整的列放到叶子节点中。
在说说非聚簇索引吧,它也不是一种索引类型,是一种索引结构和数据分开的的索引。MyIsam
使用的就是聚簇索引,而常用的InnoDB
的二级索引用的也是非聚簇索引。非聚簇节点的叶子节点存放的不一定是数据的指针,以二级索引为例,它存放的就是索引以及索引对应主键的值。通过这个值我们可以通过回表查询到对应的数据。
和聚簇索引不同的是非聚簇索引数据和索引存放在不同的位置,它们分别存放在.myi
和.myd
中。
正是因为文件结构的特殊性,这就导致索引结构比较特殊,非聚簇索引会将建立的索引专门存放的索引以及数据的指针,我们之前所说的二级索引就是非聚簇索引,在非聚簇索引结构我们可以通过非叶子节点或者叶子节点定位到主键的指针,然后通过主键获取到真正的数据。
总的来说非聚簇索引有以下优点:
缺点:
首先是覆盖索引
,通俗一点来说我们查询的值包含在我们建立的索引中,这种查询操作就无需通过回表就能直接获取到数据的情况就属于索引覆盖。而我们一直说的回表意思就是通过二级索引定位到主键的值,然后拿着主键的值去主键索引上获取数据的过程。
这个就很简单了,说白了就是多个字段组合创建成一个索引,它使用原则是需要遵循最左匹配原则的。
为了更好的演示我们不妨创建一张实验表,并插入数据,可以看到这张数据表id为主键,使用k作为普通索引
create table T (
ID int primary key,
k int NOT NULL DEFAULT 0,
s varchar(16) NOT NULL DEFAULT '',
index k(k)
)engine=InnoDB;
insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');
先来看这段SQL,可以看到查询条件用的是索引k,他希望获取到k在3-5
之间的数据。
select * from T where k between 3 and 5
结合上面数据表,我们可以得到下面这样一段步骤:
由此我们得出,因为数据表中存在范围内的k分别有3和5,所以触发两次回表,这样的查询性能是很差的。
再来看看这段SQL
select ID from T where k between 3 and 5
这段SQL
要查询的刚刚好是id,而通过我们的索引k即可直接定位到id的值,无需进行回表,这就是我们所说的索引覆盖。
同样的联合索引我们可以通过建立一张实验表来讲述一下:
数据表如下所示,可以看到我们使用name、age、ismale
建立了一个联合索引,联合索引是遵循最左匹配原则的,在我们建立联合索引之后,相当于创建了name
、(name、age)
、(name、age、ismale)
三个索引
create table `user` (
ID int primary key,
`name` varchar(32) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`ismale` tinyint(1) DEFAULT NULL,
KEY `name_age_ismale`(`name`,`age`,`ismale`)
)engine=InnoDB
所以,下面这三种情况都会走索引的
select * from user where name = 'Jack'
select * from user where name = 'Jack' and age = 22
--这种SQL优化器会自动优化为name在前,先匹配name然后再匹配age
select * from user where age > 18 and name = 'Jack'
这种就不会走索引了
select * from user where age = 18;
答: 索引下推是MySQL5.6
增加的一种特性,说白了就是为了更好的利用索引减少没必要的回表操作的一种方式。
对此我们不妨举个例子,首先我们创建一张数据表
create table `user` (
ID int primary key,
`name` varchar(32) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`sex` int(11) DEFAULT NULL,
KEY `name_age`(`name`,`age`)
)engine=InnoDB;
然后我们使用下面这条SQL进行查询
select * from user where name like '张%' and age=10;
假如我们的索引结构如下所示
这就意味着在没有索引下推这个机制前,因为name字段不是等值匹配,所以查询当我们看到了张三、张五、张a这几个name匹配name条件时,就会拿着对应的id去主键索引中判断age是否符合条件,如下图所示,这就意味着我们需要进行3次回表操作,没有一条数据是有效的。
MySQL5.6
之后增加索引下推机制,它会对索引中包含的字段进行过滤,将没必要进行回表的数据直接过滤掉,从而减少回表次数,还是以上图为例,我们直接通过age过滤发现没有一条符合条件,直接不回表了。
最左匹配原则算是MySQL的一个重点了,它会基于联合索引最左的字段进行排序,例如我们用字段a、b、c
创建联合索引,他就会先通过a进行排序,如果a一样比较b,b再一样比较c这样,通过这样的比较机制生成一个有序的索引结构。
这也是为什么,我们直接直接用b就不走索引了,因为如果没有a,我们单单b这个查询条件得到的结果是散列的,根本无法快速定位数据。
为了更好的讲述这个技术点,我们不妨创建一张实验表,如下表所示,我们对这张学生表建立普通索引、联合索引、主键。
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`sname` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`s_code` int(100) NULL DEFAULT NULL,
`address` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`height` double NULL DEFAULT NULL,
`classid` int(11) NULL DEFAULT NULL,
`create_time` datetime(0) NOT NULL ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`id`) USING BTREE,
INDEX `普通索引`(`height`) USING BTREE,
INDEX `联合索引`(`sname`, `s_code`, `address`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
然后我们再插入一堆测试数据
INSERT INTO `student` VALUES (1, '学生1', 1, '上海', 170, 1, '2022-11-02 20:44:14');
INSERT INTO `student` VALUES (2, '学生2', 2, '北京', 180, 2, '2022-11-02 20:44:16');
INSERT INTO `student` VALUES (3, '学生3', 3, '京东', 185, 3, '2022-11-02 20:44:19');
INSERT INTO `student` VALUES (4, '学生4', 4, '联通', 190, 4, '2022-11-02 20:44:25');
我们不妨通过一个个SQL
来了解最左匹配原则。
先来看看这条SQL
,很明显它使用的条件是联合索引最左边的字段,所以它是走索引的
select create_time from student where sname = "学生3
通过执行计划我们也可以看到这个类型是ref
,说明它使用到了非唯一性索引
扫描。
再来看看这条SQL
,明显这就是我们前面说的直接使用联合索引第二个字段的情况,由于其排序结果依赖第一列字段,这就导致这样的查询没法利用有序的索引,进而索引失效
select create_time from student where s_code = 1
查看执行计划,可以发现type
用到了全表扫描,条件也只是using where
这两条SQL
和上面同理,不多赘述
explain select create_time from student where address = "上海";
explain select create_time from student where address = "上海" and s_code = 1 ;
再来看看这条,这就比较有趣了,你会发现sname
放在后面,可是观察执行计划还是走索引的,原因也很简单,SQL优化器会将name
排到前面,因为最左匹配原则的定律,这个SQL
虽然没有用到第2列,但是用到最左排序依赖项,所以它走索引了。
explain select create_time from student where address = "上海" and sname = "学生3";
这两条同理,走索引
select create_time from student where sname = "变成派大星" and address = "上海"
select create_time from student where sname = "变成派大星" and s_code = 1 and address = "上海"
补充说一下,最左匹配原则依赖最左排序项,而且一旦条件中同时遇到范围查询(>、<、between、like)
就会停止匹配,对此我们不妨举3个例子
先看看这条,很明显它是走索引的
EXPLAIN select create_time from student where sname = "变成派大星" and s_code = 1 and address = "上海"
查看执行计划发现都是等值匹配所以,三个索引都用上了,type也是ref
再看看这条
EXPLAIN select create_time from student where sname = "变成派大星" and s_code > 1 and address = "上海"
观察执行计划,如下图,是Using index condition
,type
也是范围查询,说明匹配到s_code
就停止进行索引查询了。
再看看最后一个例子
EXPLAIN select create_time from student where sname = "变成派大星" and s_code = 1 and address like "%上海"
查看其执行计划可以发现,由于第3个条件用了左边%
的like
导致索引匹配只用到了前两个条件,所以extra
是using index condition
MySQL8
提供了一种非最左匹配原则的走索引策略,索引跳跃扫描:
对此我们不妨建立一张实验表
DROP TABLE IF EXISTS `user`;
create table user(
id int not null auto_increment ,
name varchar(255) not null ,
gender tinyint not null ,
primary key (id),
key idx_gender_name (gender,name)
)ENGINE=InnoDB;
因为笔者的MySQL
版本就是8.0
,所以键入以下SQL
查看执行计划
explain select * from user where name='一灯';
可以看到它还是走索引的
索引跳跃扫描出现于联合索引最左一列唯一值较少时,若用户直接跳过第一列索引使用第二列时,一样可以用到联合索引。
为了更好的演示问题,我们不妨创建一张数据表,表中有主键、普通索引、普通列,并插入实验数据
DROP TABLE IF EXISTS `leftaffix`;
create table leftaffix(
a int(11) not null auto_increment,
b int(11) null default null,
c int(11) null default null,
d int(11) null default null,
e varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (a) USING BTREE,
INDEX `联合索引`(b, c, d) USING BTREE,
INDEX `idx_e`(e) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `leftaffix` VALUES (1, 1, 1, 1, '1');
INSERT INTO `leftaffix` VALUES (2, 2, 2, 2, '2');
INSERT INTO `leftaffix` VALUES (3, 3, 2, 2, '3');
INSERT INTO `leftaffix` VALUES (4, 3, 1, 1, '4');
INSERT INTO `leftaffix` VALUES (5, 2, 3, 5, '5');
INSERT INTO `leftaffix` VALUES (6, 6, 4, 4, '6');
INSERT INTO `leftaffix` VALUES (7, 8, 8, 8, '7');
虽然说select *
会走索引,例如下面这段SQL
explain select * from leftaffix where b=100;
但是,它会导致:
所以我们建议非必要情况下不要使用select *
。
如下这两句SQL
,第二句因为缩小了检索范围就走了索引(using index)
explain select * from leftaffix where b>1;
explain select * from leftaffix where b>7;
如下面这两句,第二句就不失效,工作原理很简单,因为第二句保留的索引的原值,我们的索引结构是基于字段原值建立,如果使用函数产生的计算结果可能就和原值不一样,进而导致索引失效。
当然MySQL8之后出现了函数索引,如果你的字段需要用到函数就可以为其创建函数索引。
-- 失效
explain select * from leftaffix l where length (b) =10;
-- 不失效
explain select length(b) from leftaffix l where b =10;
如下所示,工作原理也很上相同,不破坏索引原值的情况下是走索引的。
-- 失效
explain select * from leftaffix l where b-1=10;
-- 不失效
explain select * from leftaffix l where b=10-1;
如下所示,前者就走索引了,因为前者我们可以知晓一定范围的索引,不像后者那样,匹配的索引范围可能性几乎是全表,导致索引失效了。
-- like %要在左边
-- 走索引,但是级别比较低就是了
explain select * from leftaffix l where e like 'aa%';
-- 左边% 要匹配索引范围很大就没有走索引的必要了
explain select * from leftaffix l2 where e like '%aa';
这个例子我们不妨重建一张表格
DROP TABLE IF EXISTS `leftaffix`;
create table leftaffix(
a int(11) not null auto_increment,
b int(11) null default null,
c int(11) null default null,
d int(11) null default null,
e varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (a) USING BTREE,
INDEX `联合索引`(b, c, d) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `leftaffix` VALUES (1, 1, 1, 1, '1');
INSERT INTO `leftaffix` VALUES (2, 2, 2, 2, '2');
INSERT INTO `leftaffix` VALUES (3, 3, 2, 2, '3');
INSERT INTO `leftaffix` VALUES (4, 3, 1, 1, '4');
INSERT INTO `leftaffix` VALUES (5, 2, 3, 5, '5');
INSERT INTO `leftaffix` VALUES (6, 6, 4, 4, '6');
INSERT INTO `leftaffix` VALUES (7, 8, 8, 8, '7');
看看这条SQL,索引和普通列用or,索引失效
-- 用or左索引右普通裂,导致失效
explain select * from leftaffix l where a=1 or e='4';
从执行计划我们就能看出来,它走了全表扫描
如下所示,第一条SQL是走索引的,一旦查询结果集范围大于表中结果的30%,就会走全表扫描
explain select * from leftaffix l where b in(1);
-- in 在结果集大于百分之30时索引失效
explain select * from leftaffix l where b in(1,2,3,4,5,6);
如下所示,我们都知道这张表我们对b创建了索引,但是查看执行计划还是走了全表扫描
explain select e from leftaffix l order by b ;
如下图所示,原因很简单,使用索引进行order by因为有序自然效率高一些,但因为获取的字段要通过回表进行获取,如果回表次数过多性能也会受到影响,所以MySQL优化器直接走了全表扫描。
正常情况下合理使用是会走索引的。
我们不妨对explain每一个字段进行概括一下吧:
先说说select_type吧,这个字段决定了你的SQL涉及的查询类型,常见的有:
explain select * from `user` u ;
explain select * from `user` u where id =(select id from `user` where id=1);
我们查看执行计划的截图,可以看到涉及这种嵌套查询的SQL左边的SQL就是PRIMARY
explain select * from `user` u union select * from `user` ;
explain select * from `user` u where id in (select id from `user` u1 union select id from `user` u2)
效率从高到低如下图所示,这里就不多赘述了
这个字段也很重要,它表示当前SQL
进行排序时使用的规则:
Using filesort
:使用了外部的索引进行排序,并没有用到我们自己定义的索引,性能较差。using index
:这种方式性能就不错了,使用了索引并且不需要回表就得到了我们需要的数据,即用到了索引覆盖。Using temporary
:MySQL
查询排队时使用了临时表。using where
:排序时用的只是普通字段,没有走索引Using join buffer
:大量使用了表连接,所以会将某些结果放到缓存中进行排序,这种情况下我们建议加大joinbuffer缓存空间大小。impossible where
:说明where条件基本得不到需要的结果,筛选数据时一直处于false
的状态。表示当前查询可能用到的索引。如下这个执行计划,它就以为着可能用到了主键
表示用到的索引名称,如下所示下面这条sql可能就用到了这两个索引。
在MySQL的EXPLAIN语句中,key_len列表示使用索引的键部分的字节数。它是一个估计值,根据查询中使用的索引类型和数据类型来计算。通常,key_len越小,性能就越好,因为它意味着需要读取更少的数据块。
例如,如果你有一个使用VARCHAR(100)数据类型的列作为索引,并且查询中只使用了前10个字符作为搜索条件,则key_len将是10。如果你使用的是INT(10)数据类型的列作为索引,则key_len将是4,因为INT类型占用4个字节。
在优化查询时,理解key_len可以帮助你确定哪些索引可以更有效地支持查询,以及如何进一步优化索引设计。
以笔者的sql为例这里的长度为5,意为着本次索引用到了5字节。
表示查询时那一列被用到了。如下所示,这就意味着笔者rental_date中的三个索引列rental_date、inventory_id、customer_id 都被用到了。
当前查询查到的行数。如这个执行计划就意为着查到了1行。
表示选取的行和读取的行占比,例如100就代表选取的所有行就是读取的所有行。
这个问题我们不妨看个例子吧,首先我们创建一张表,如下所示,可以看到num1、num2都是key,一个是int,一个是varchar类型。
DROP TABLE IF EXISTS test1;
CREATE TABLE `test1` (
`id` int(11) NOT NULL,
`num1` int(11) NOT NULL DEFAULT '0',
`num2` varchar(11) NOT NULL DEFAULT '',
`type1` int(4) NOT NULL DEFAULT '0',
`type2` int(4) NOT NULL DEFAULT '0',
`str1` varchar(100) NOT NULL DEFAULT '',
`str2` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `num1` (`num1`),
KEY `num2` (`num2`),
KEY `type1` (`type1`),
KEY `str1` (`str1`),
KEY `str2` (`str2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后我们创建一个存储过程进行数据插入,如下所示,可以看出num1
和num2
值是一样的,但是类型不同的。
DROP PROCEDURE IF EXISTS pre_test1;
DELIMITER //
CREATE PROCEDURE `pre_test1`()
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
WHILE i < 10000000 DO
SET i = i + 1;
SET @str1 = SUBSTRING(MD5(RAND()),1,20);
-- 每100条数据str2产生一个null值
IF i % 100 = 0 THEN
SET @str2 = NULL;
ELSE
SET @str2 = @str1;
END IF;
INSERT INTO test1 (`id`, `num1`, `num2`,
`type1`, `type2`, `str1`, `str2`)
VALUES (CONCAT('', i), CONCAT('', i),
CONCAT('', i), i%5, i%5, @str1, @str2);
-- 事务优化,每一万条数据提交一次事务
IF i % 10000 = 0 THEN
COMMIT;
END IF;
END WHILE;
END;
// DELIMITER ;
-- 执行存储过程
CALL pre_test1();
然后我们进行如下查询
先来看看这句,num1为key,查询条件右边为int类型。从执行计划可以看出查到了4条数据,并且走了索引
explain SELECT * FROM `test1` WHERE num1 = 10000;
再来看看这句,左边为int,右边为字符串,从执行计划看出他也是走索引查到的数据也是4条
explain SELECT * FROM `test1` WHERE num1 = '10000';
接下来看看这条,左右都是num类型,走了索引,不多赘述
explain SELECT * FROM `test1` WHERE num2 = '10000';
最后看看神奇的一条,左边为字符类型,右边为int类型,走了全表扫描,查出来的数据有好几条,这正是我们说的索引失效问题
explain SELECT * FROM `test1` WHERE num2 = 10000;
MySQL隐式转换
<=>
对两个NULL
做比较时会返回1(即结果匹配,是我们要的数据)
这两种情况都不需要做类型转换。TIMESTAMP
或DATETIME
,并且另外一个参数是常量,常量会被转换为timestamp
。decimal
类型,如果另外一个参数是decimal
或者整数,会将整数转换为decimal
后进行比较。由此,我们上面所说就是第7种情况,我们先来分析这句SQL,左边是int,右边varchar,按照情况7都被转为浮点数,左右条件情况唯一,所以走索引。
SELECT * FROM `test1` WHERE num1 = '10000';
再看看这条,左边字符型右边int类型,都被转为浮点类型。
explain SELECT * FROM `test1` WHERE num2 = 10000;
这时候情况就不一样的,因为MySQL会将字符串类型转为数字的方式有很多种,情况不唯一,如下所示,这些SQL比较结果都为1(true),这就意味着字符串类型转浮点数会将英文抹去,高位的0也被抹去,字母后面的数字也被抹去。
-- 隐式转换比较
select 123='123abc';
select 12='012abc';
select 5.3='5.3a666';
所以上面那条SQL中的num2在和’10000’比较时,隐式转换的情况就特别多,左边条件不唯一确定,故不走索引。
答:
我说MySQL联合索引遵循最左前缀匹配原则,面试官让我回去等通知
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。