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% 的时候,也不需要对这个字段使用索引。
索引是一把双刃剑,可提高查询效率,但也会降低插入和更新的速度并占用磁盘空间。
选择索引的最终目的是为了使查询的速度变快,上面给出的原则是最基本的准则,但不能拘泥于上面的准则,大家要在以后的学习和工作中进行不断的实践,根据应用的实际情况进行分析和判断,选择最合适的索引方式。