MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。
小结:不同的存储引擎支持的索引类型也不一样
MySQL支持多种方法在单个或多个列上创建索引:在创建表的定义语句CREATE TABLE中指定索引列,使用ALTER TABLE语句在存在的表上创建索引,或者使用CREATE INDEX语句在已存在的表上添加索引.
使用CREATE TABLE创建表时,除了可以定义列的数据类型外,还可以定义主键约束、外键约束或者唯一性约束,而不论创建哪种约束,在定义约束的同时相当于在指定列上创建了一个索引。
举例:
#隐式的方式创建索引。在声明有主键约束、唯一性约束、外键约束的字段上,会自动的添加相关的索引
CREATE TABLE dept(
dept_id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(20)
);
CREATE TABLE emp(
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20) UNIQUE,
dept_id INT,
CONSTRAINT emp_dept_id_fk FOREIGN KEY (dept_id) REFERENCES dept (dept_id)
);
但是,如果显式创建表时创建索引的话,基本语法格式如下
CREATE TABLE table_name [col_name data_type]
[UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC |DESC]
CREATE TABLE book(
book_id INT,
book_name VARCHAR(100),
authors VARCHAR(100),
info VARCHAR(100),
comment VARCHAR(100),
year_publication YEAR,
INDEX (year_publication)
);
#通过命令查看索引
#方式1:
SHOW CREATE TABLE book;
#方式2:
SHOW INDEX FROM book;
#性能分析工具:EXPLAIN
EXPLAIN SELECT * FROM book WHERE year_publication = 2023;
CREATE TABLE test1(
id INT NOT NULL,
name varchar(30) NOT NULL,
UNIQUE INDEX uk_idx_id (id)
);
SHOW INDEX FROM test1;
# 声明有唯一索引的字段,在添加数据时,要保证唯一性,但是可以添加null
CREATE TABLE student(
id INT(10) ,
student_no VARCHAR(200),
student_name VARCHAR(200),
PRIMARY KEY (id)
);
ALTER TABLE student
drop PRIMARY KEY;
CREATE TABLE test2(
id INT NOT NULL,
name CHAR(50) NULL,
INDEX single_idx_name (name(20))
);
SHOW INDEX FROM test2;
CREATE TABLE test3(
id INT(11) NOT NULL,
name CHAR(30) NOT NULL,
age INT(11) NOT NULL,
info VARCHAR(255),
INDEX multi_idx (id, name, age)
);
SHOW INDEX FROM test3;
CREATE TABLE test4(
id INT NOT NULL,
name CHAR(30) NOT NULL,
age INT NOT NULL,
info VARCHAR(255),
FULLTEXT INDEX futxt_idx_info (info)
) ENGINE = MyISAM;
SHOW INDEX FROM test4;
#在MySQL5.7及之后版本中可以不指定最后的ENGINE了,因为在此版本中InnoDB支持全文索引。
举例2:
CREATE TABLE articles(
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT index (title, body)
) ENGINE = INNODB;
SHOW INDEX FROM articles;
创建了一个给title和body字段添加全文索引的表。
CREATE TABLE `papers`(
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) DEFAULT NULL,
`content` text,
PRIMARY KEY (`id`),
FULLTEXT KEY `title` (`title`, `content`)
) ENGINE = MyISAM
DEFAULT CHARSET = utf8;
SHOW INDEX FROM papers;
不同于like方式的的查询:注意点
- 使用全文索引前,搞清楚版本支持情况;
- 全文索引比 like + % 快 N 倍,但是可能存在精度问题;
- 如果需要全文索引的是大量数据,建议先添加数据,再创建索引。
CREATE TABLE test5(
geo GEOMETRY NOT NULL,
SPATIAL INDEX spa_idx_geo (geo)
) ENGINE = MyISAM;
在已经存在的表中创建索引可以使用ALTER TABLE语句或者CREATE INDEX语句。
ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY]
[index_name] (col_name[length],...) [ASC | DESC]
CREATE TABLE book5(
book_id INT,
book_name VARCHAR(100),
AUTHORS VARCHAR(100),
info VARCHAR(100),
COMMENT VARCHAR(100),
year_publication YEAR
);
SHOW INDEX FROM book5;
ALTER TABLE book5
ADD INDEX idx_cmt (COMMENT);
ALTER TABLE book5
ADD UNIQUE uk_idx_bname (book_name);
ALTER TABLE book5
ADD INDEX mul_bid_bname_info (book_id, book_name, info);
CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name
ON table_name (col_name[length],...) [ASC | DESC]
CREATE TABLE book6(
book_id INT,
book_name VARCHAR(100),
AUTHORS VARCHAR(100),
info VARCHAR(100),
COMMENT VARCHAR(100),
year_publication YEAR
);
SHOW INDEX FROM book6;
CREATE INDEX idx_cmt ON book6 (COMMENT);
CREATE UNIQUE INDEX uk_idx_bname ON book6 (book_name);
CREATE INDEX mul_bid_bname_info ON book6 (book_id, book_name, info);
ALTER TABLE book5
DROP INDEX idx_cmt;
提示
添加AUTO_INCREMENT约束字段的唯一索引不能被制除,
DROP INDEX uk_idx_bname ON book5;
提示 删除表中的列时,如果要删除的列为索引的组成部分,则该列也会从索引中删除。如果组成索引的所有列都被删除,则整个索引将被删除。
降序索引以降序存储键值。虽然在语法上,从MySQL4版本开始就已经支持降序索引的语法了,但实际上该DESC定义是被忽略的,直到MySQL8.x版本才开始真正支持降序索引(仅限于InnoDB存储|擎),MySQL在8,0版本之前创建的仍然是升序索引,使用时进行反向扫描,这大大降低了数据库的效率。在某些场景下,降序索引意义重大。例如,如果一个查询,需要对多个列进行排序,且顺序要求不一致,那么使用降序索引将会避免数据库使用额外的文件排序操作,从而提高性能。
举例:分别在MySQL 5.7版本和MySQL 8.0版本中创建数据表ts1,结果如下:
CREATE TABLE ts1(a int,b int,index idx_a_b(a,b desc));
在MySQL 5.7版本中查看数据表ts1的结构,结果如下:
从结果可以看出,索引仍然是默认的升序。
在MySQL 8.0版本中查看数据表ts1的结构,结果如下:
从结果可以看出,索引已经是降序了。下面继续测试降序索引在执行计划中的表现。
分别在MySQL 5.7版本和MySQL 8.0版本的数据表ts1中插入800条随机数据,执行语句如下:
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();
在MySQL 5.7版本中查看数据表ts1的执行计划,结果如下:
EXPLAIN SELECT * FROM ts1 ORDER BY a,b DESC LIMIT 5;
从结果可以看出,执行计划中扫描数为799,而且使用了Using filesort。
提示 Using filesort是MySQL中一种速度比较慢的外部排序,能避免是最好的。多数情况下,管理员可以通过优化索引来尽量避免出现Using filesort,从而提高数据库执行速度。
在MySQL 8.0版本中查看数据表ts1的执行计划。从结果可以看出,执行计划中扫描数为5,而且没有使用Using filesort。
注意 降序索引只对查询中特定的排序顺序有效,如果使用不当,反而查询效率更低。例如,上述查询排序条件改为order by a desc, b desc,MySQL 5.7的执行计划要明显好于MySQL 8.0。
将排序条件修改为order by a desc, b desc后,下面来对比不同版本中执行计划的效果。 在MySQL 5.7版本中查看数据表ts1的执行计划,结果如下:
EXPLAIN SELECT * FROM ts1 ORDER BY a DESC,b DESC LIMIT 5;
在MySQL 8.0版本中查看数据表ts1的执行计划。
从结果可以看出,修改后MySQL 5.7的执行计划要明显好于MySQL 8.0。
在MySQL 5.7版本及之前,只能通过显式的方式删除索引。此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。
从MySQL 8.x开始支持 隐藏索引(invisible indexes) ,只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除 。
同时,如果你想验证某个索引制除之后的查询性能影响,就可以暂时先隐藏该索引:
注意:
主键不能被设置为隐藏索引。当表中没有显式主键时,表中第一个唯一非空索引会成为隐式主键,也不能设置为隐藏索引。
索引默认是可见的,在使用CREATE TABLE,CREATE INDEX或者ALTER TABLE等语句时可以通过VISIBLE或者INVISIBLE关键词设置索引的可见性,
CREATE TABLE tablename(
propname1 type1[CONSTRAINT1],
propname2 type2[CONSTRAINT2],
……
propnamen typen,
INDEX [indexname](propname1 [(length)]) INVISIBLE
);
CREATE TABLE book7(
book_id INT,
book_name VARCHAR(100),
AUTHORS VARCHAR(100),
info VARCHAR(100),
COMMENT VARCHAR(100),
year_publication YEAR,
#创建不可见的索引
INDEX idx_cmt (COMMENT) invisible
);
SHOW INDEX FROM book7;
EXPLAIN SELECT * FROM book7 WHERE COMMENT = 'mysql';
CREATE INDEX indexname
ON tablename(propname[(length)]) INVISIBLE;
CREATE INDEX idx_year_pub ON book7(year_publication) invisible;
ALTER TABLE tablename
ADD INDEX indexname (propname [(length)]) INVISIBLE;
ALTER TABLE book7
ADD UNIQUE INDEX uk_idx_bname(book_name) invisible;
ALTER TABLE tablename ALTER INDEX index_name INVISIBLE; #切换成隐藏索引
ALTER TABLE tablename ALTER INDEX index_name VISIBLE; #切换成非隐藏索引
ALTER TABLE book7 ALTER INDEX idx_year_pub invisible; #可见--->不可见
ALTER TABLE book7 ALTER INDEX idx_cmt visible; #不可见 ---> 可见
注意
当索引被隐意时,它的内容仍然是和正常索引一样实时更新的。如果一个索引需要长期被隐藏,那么可以将其删除,因为素引的存在会影响插入、更新和除的性能
mysql> set session optimizer_switch="use_invisible_indexes=on";
Query OK, 0 rows affected (0.00 sec)
SQL语句执行成功,再次查看查询优化器的开关设置。第1步:创建数据库、创建表
CREATE DATABASE atguigudb1;
USE atguigudb1;
#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步:创建模拟数据必需的存储函数
#函数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 ;
创建函数,假如报错:
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, 我们就必须为我们的function指定一个参数。主从复制,主机会将写操作记录在bin-log日志中。从机读取bin-log日志,执行语句来同步数据。如果使用函数来操作数据,会导致从机和主键操作时间不一致。所以默认情况下,mysql不开启创建函数设置。
第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步:调用存储过程
CALL insert_course(100);
CALL insert_stu(1000000);
业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的。
#查看当前stduent_info表中的索引
SHOW INDEX FROM student_info;
#student_id字段上没有索引的:
SELECT course_id, class_id, NAME, create_time, student_id
FROM student_info
WHERE student_id = 123110;#324ms
#给student_id字段添加索引
ALTER TABLE student_info
ADD INDEX idx_sid (student_id);
#student_id字段上有索引的:
SELECT course_id, class_id, NAME, create_time, student_id
FROM student_info
WHERE student_id = 123110; #41ms
#student_id字段上有索引的:
SELECT student_id, COUNT(*) AS num
FROM student_info
GROUP BY student_id
LIMIT 100; #41ms
#删除idx_sid索引
DROP INDEX idx_sid ON student_info;
#student_id字段上没有索引的:
SELECT student_id, COUNT(*) AS num
FROM student_info
GROUP BY student_id
LIMIT 100;#866ms
如果同时有GROUP BY和ORDER BY的情况:比如我们按照student_id进行分组,同时按照创建时间降序的方式进行排序,这时我们就需要同时进行GROUP BY和ORDER BY,那么是不是需要单独创建student
#添加单列索引
ALTER TABLE student_info
ADD INDEX idx_sid (student_id);
ALTER TABLE student_info
ADD INDEX idx_cre_time (create_time);
SELECT student_id, COUNT(*) AS num
FROM student_info
GROUP BY student_id
ORDER BY create_time DESC
LIMIT 100; #5.212s
#添加联合索引
ALTER TABLE student_info
ADD INDEX idx_sid_cre_time (student_id, create_time DESC);
SELECT student_id, COUNT(*) AS num
FROM student_info
GROUP BY student_id
ORDER BY create_time DESC
LIMIT 100;#0.257s
create table shop(address varchar(120) not null);
alter table shop add index(address(12))
问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的;截取得少了,重复内容太多,字段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?
select count(distinct left(address,10)) / count(*) as sub10, -- 截取前10个字符的选择度
count(distinct left(address,15)) / count(*) as sub11, -- 截取前15个字符的选择度
count(distinct left(address,20)) / count(*) as sub12, -- 截取前20个字符的选择度
count(distinct left(address,25)) / count(*) as sub13 -- 截取前25个字符的选择度
from shop;
引申另一个问题:索引列前缀对排序的影响
SELECT FROM shop
ORDER BY address
LIMIT 12;
mysql
因为二级索引中不包含完整的address列信息,所以无法对前12个字符相同,后边的字符不同的记录进行排序,也就是使用索引列前缀的方式无法支持使用素引排序,只能使用文件排序。
这样也可以较少的建立一些索引。同时,由于"最左前缀原则",可以增加联合索引的使用率。
在实际工作中,我们也需要注意平衡,索引的数目不是越多越好。我们需要限制每张表上的索引数量,建议单张表索引数量不超过6个。原因:
SELECT course_id,student_id,create_time
FROM student_info
WHERE student_id = 41251;
因为我们是按照student_id来进行检索的,所以不需要对其他字段创建索引,即使这些字段出现在SELECT字段中。
CREATE TABLE t_without_index(
a INT PRIMARY KEY AUTO_INCREMENT,
b INT
);
提供存储过程1:
#创建存储过程
DELIMITER //
CREATE PROCEDURE t_wout_insert()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 900
DO
INSERT INTO t_without_index(b) SELECT RAND() * 10000;
SET i = i + 1;
END WHILE;
COMMIT;
END //
DELIMITER ;
#调用
CALL t_wout_insert();
创建表2:
CREATE TABLE t_with_index
(
a INT PRIMARY KEY AUTO_INCREMENT,
b INT,
INDEX idx_b (b)
)
创建存储过程2:
#创建存储过程
DELIMITER //
CREATE PROCEDURE t_with_insert()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 900
DO
INSERT INTO t_with_index(b) SELECT RAND() * 10000;
SET i = i + 1;
END WHILE;
COMMIT;
END //
DELIMITER ;
#调用
CALL t_with_insert();
查询对比:结论:在数据表中的数据行数比较少的情况下,比如不到 1000 行,是不需要创建索引的。
结论:当数据重复度大,比如 高于 10% 的时候,也不需要对这个字段使用索引。
索引是一把双刃剑,可提高查询效率,但也会降低插入和更新的速度并占用磁盘空间。
选择索引的最终目的是为了使查询的速度变快,上面给出的原则是最基本的准则,但不能拘泥于上面的准则,大家要在以后的学习和工作中进行不断的实践,根据应用的实际情况进行分析和判断,选择最合适的索引方式。