MySQL的索引是一种帮助 MySQL 高效地查询和检索数据 的数据结构,可以看作是 数据的目录。(就像书籍的目录)
索引是一种用空间换时间的设计思想。
SHOW INDEX FROM your_table_name;
MySQL的索引按照不同的类型和特点可以从三个角度进行分类:
MySQL索引从数据结构的角度可以分为B+树索引、Hash索引、全文索引等。
在这里只详细说一下B+树索引,因为InnoDB、MyISAM等常见的MySQL存储引擎都支持B+树索引,并且MySQL5.5后将InnoDB设为默认存储引擎,B+树索引也成为了MySQL存储引擎使用最多的所有类型。
MySQL的数据是 持久化 的,也就是说数据是存储在磁盘中的(即索引和记录是保存在磁盘中的)。
那么在需要使用索引进行高效检索数据时,就必须访问磁盘取得索引读入内存,再根据索引再次访问磁盘检索数据后读入内存,所以每次检索数据都需要进行多次的 I/O操作,I/O操作耗费大量时间。
读索引到内存 => 根据索引读数据到内存!
因此应该选择一种数据结构 使得 I/O操作 次数尽可能的少 。
此外还需要注意的是MySQL是支持范围查询的,也就意味着挑选的数据结构必须能够 支持高效地范围查询。
从磁盘读取索引到内存 => 根据索引再次从磁盘读取到目标数据到内存。 (先看目录,再找目标数据)
B+树是一种基于磁盘的 平衡多路查找树,它的高度通常很低(3~4层),这意味着访问效率很高,从千万或上亿数据里查询一条数据,只用 3到4 次 I/O操作。
B+树的 非叶子节点不存储实际的数据,只存储索引,相比于非叶子节点又存储索引又存储数据的B树而言,B+树的非叶子节点可以存放更多的索引,所以查询底层节点的磁盘I/O次数会更少。
B+树的所有叶子节点都存储了数据,并且叶子节点之间用双向链表连接,所以数据是有序的,范围查询通过链表可以快速实现,这样可以方便地进行范围查询和排序操作。
B+树每个节点都有大量的数据或索引(有大量冗余的节点),这些冗余的数据或索引可以保证B+树在插入和删除时效率更高(因为不会因为某个数据导致需要进行复杂的树变化,即B+树的树层结构很稳定不会经常变化)。
所以B+树更适合作为MySQL索引的数据结构。
MySQL的 InnoDB 存储引擎中每张表只能有一个聚簇索引,通常是主键索引;而非聚簇索引都是二级索引,可以有多个。
主键索引是建立在主键字段上的索引,主键索引的B+树结构中的叶子节点存放的是实际数据(所有完整的记录)。
关于InnoDB引擎确定 聚簇索引(主键索引) 的方式按照如下次序:
一张表,只能创建一个主键索引,这个主键索引可以包含一个或多个列。
001 在创建表时定义主键
使用CREATE TABLE语句时,可以在列的定义后面添加PRIMARY KEY来指定主键。例如:
CREATE TABLE students (
student_id INT PRIMARY KEY,
name VARCHAR(50),
age INT
);
002 使用ALTER TABLE语句添加主键
如果表已经存在,可以使用ALTER TABLE语句来添加主键。例如:
ALTER TABLE orders
ADD PRIMARY KEY (order_id);
003 创建表的同时定义自动递增主键
通常,可以使用AUTO_INCREMENT属性创建自动递增的主键。例如:
CREATE TABLE products (
product_id INT AUTO_INCREMENT,
product_name VARCHAR(50),
price DECIMAL(10, 2),
PRIMARY KEY (product_id)
);
004 使用CREATE TABLE时定义复合主键
如果需要复合主键,可以在CREATE TABLE语句中同时指定多个列作为主键。例如:
CREATE TABLE orders (
order_id INT,
customer_id INT,
order_date DATE,
PRIMARY KEY (order_id, customer_id)
);
二级索引又称为辅助索引,二级索引的B+树结构中的叶子节点 存放的数据是主键,索引分类中的唯一索引、普通索引、前缀索引等都属于二级索引。
char
、varchar
、binary
、varbinary
)等类型上。创建方式可以是CREATE INDEX 索引名 ON 表名(列名(指定前缀长度));
,前缀索引的目的是为了减少索引占用的存储空间,提高查询效率。001 在CREATE TABLE语句中创建唯一索引
在创建表的同时定义唯一索引,使用UNIQUE
关键字确保列中的值是唯一的。
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(255) UNIQUE,
username VARCHAR(50) UNIQUE
);
在上面的示例中,我们在users
表中创建了两个唯一索引,分别用于email
和username
列,以确保这两列中的值不重复。
建表语句末写法:
CREATE TABLE `sys_dict` (
`id` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '唯一ID',
`dict_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '字典名称',
`dict_code` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '字典编码',
`status` int DEFAULT '0' COMMENT '状态(0正常 1停用)',
`remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建用户',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '更新用户',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `dict_code_index` (`dict_code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='字典表';
-- 方法一:等效于方法二
UNIQUE INDEX dict_code_index (dict_code) USING BTREE
-- 方法二:等效于方法一
UNIQUE KEY `dict_code_index` (`dict_code`) USING BTREE
002 使用ALTER TABLE语句添加唯一索引
如果表已经存在,可以使用ALTER TABLE
语句添加唯一索引。例如,如果要为已存在的表mytable
添加唯一索引:
ALTER TABLE mytable
ADD UNIQUE (username);
003 使用CREATE INDEX语句创建唯一索引
使用CREATE INDEX
语句,您可以为表的一个或多个列创建唯一索引。例如:
CREATE UNIQUE INDEX idx_username ON mytable (username);
004 通过PRIMARY KEY创建唯一索引(主键索引)
在定义表结构时,通常使用PRIMARY KEY
来创建主键索引,它也是唯一的。例如:
CREATE TABLE mytable (
id INT PRIMARY KEY,
username VARCHAR(50)
);
这将为 id
列创建一个唯一索引,并将其定义为主键。
005 多列组合建立唯一索引(联合索引)
这个唯一索引要求三个列(column1
、column2
和 column3
)的组合是唯一的,而不是每个列都唯一。
UNIQUE INDEX unique_index_name (column1, column2, column3) USING BTREE
001 CREATE INDEX语句
使用CREATE INDEX
语句可以为表中的一个或多个列创建索引。例如,要为名为example_table
的表的column_name
列创建索引,可以使用以下语句:
CREATE INDEX index_name ON example_table (column_name);
这将创建一个名为index_name
的索引,用于提高column_name
列的检索性能。
002 ALTER TABLE语句
使用ALTER TABLE
语句也可以添加索引。例如,要为已经存在的表example_table
的column_name
列添加索引,可以使用以下语句:
ALTER TABLE example_table ADD INDEX index_name (column_name);
这将在example_table
表上添加一个名为index_name
的索引。
003 建表语句中写法
CREATE TABLE `sys_dict_item` (
`id` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '唯一ID',
`dict_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '字典ID',
`dict_code` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '字典编码',
`dict_label` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '字典标签',
`dict_value` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '字典键值',
`value_type` tinyint(1) DEFAULT '0' COMMENT '值类型(0字符 1数字 2布尔)',
`dict_sort` int DEFAULT '0' COMMENT '字典排序',
`color` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '配色(用于前端显示)',
`status` int DEFAULT '0' COMMENT '状态(0正常 1停用)',
`remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建用户',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '更新用户',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `dict_code_index` (`dict_code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='字典项';
-- 方法一:等效于方法二
INDEX dict_code_index (dict_code) USING BTREE
-- 方法二:等效于方法一
KEY `dict_code_index` (`dict_code`) USING BTREE
004 多列组合建立普通索引(联合索引)
INDEX index_name (column1, column2, column3) USING BTREE
001 使用CREATE INDEX语句
你可以使用CREATE INDEX语句来创建前缀索引。例如,如果你有一个名为text_column
的文本列,你可以创建一个前缀索引,仅索引文本列的前几个字符,如下所示:
CREATE INDEX prefix_index ON your_table (text_column(10)); -- 这将创建一个索引,仅索引前10个字符
002 使用ALTER TABLE语句
你也可以使用ALTER TABLE语句来添加前缀索引到已存在的表中。例如,如果你有一个名为your_table
的表,你可以执行以下操作来添加前缀索引:
ALTER TABLE your_table ADD INDEX prefix_index (text_column(10)); -- 这将添加一个索引,仅索引前10个字符
003 建表语句中写法
CREATE TABLE your_table (
id INT AUTO_INCREMENT PRIMARY KEY,
text_column VARCHAR(255), -- 假设你有一个文本列
-- 创建一个前缀索引,仅索引text_column的前10个字符
INDEX prefix_index (text_column(10)) USING BTREE
);
-- 方法一:等效于方法二
INDEX prefix_index (text_column(10)) USING BTREE
-- 方法二:等效于方法一
KEY `prefix_index` (`text_column(10)`) USING BTREE
正如上面所说的二级索引的 B+树 结构中的叶子节点存放的数据是主键,那么使用二级索引查找记录的过程是怎么样的呢?
二级索引查找记录的过程为:先获得二级索引中的B+树的索引值,检索 二级索引的B+树 找到对应的叶子节点后获取到对应的主键值,再通过主键值检索 主键索引的B+树 找到对应的叶子节点即可获得对应的数据(记录)。第一次检索获取主键值,再通过主键值再次检索获得记录的这个过程叫做 回表。
举个例子:
select * from products where name = 'iPhone 14';
如上面的代码,id为主键索引列,name为唯一索引列(二级索引)。执行这条语句时,会先拿'iPhone 14'
作为索引值检索二级索引的B+树找到对应的叶子节点,这里的叶子节点存储的是要找的这条记录的主键值1
,然后再拿1
作为索引值检索主键索引的B+树找到对应的叶子节点,此时这里的叶子节点存储的就是整条记录了。
答案是并不一定。
修改上面的例子:
select id from products where name = 'iPhone 14';
当 查询的数据能在二级索引的B+树的叶子节点上直接查询到 时,就不再需要进行回表查询,直接返回数据即可。正如这里例子中,因为需要查询的是 id
,二级索引的B+树的叶子节点上可以直接获取,这时就不需要回表操作了。
这种在二级索引的B+树的叶子节点能直接查询到数据的过程就叫做 覆盖索引。
扩展:如何确保或者如何实现:二级索引的叶子节点包含了 id 列的值?
要确保或实现二级索引的叶子节点包含了
id
列的值,你可以按照以下步骤操作:1、创建合适的二级索引:首先,你需要创建一个二级索引,确保该索引包含了你希望包含的列。在这种情况下,你希望
id
列的值包含在二级索引中。创建索引的SQL语句如下:CREATE INDEX idx_name_id ON products (name, id);
这将创建一个名为
idx_name_id
的二级索引,其中包含name
列和id
列的值。2、查询优化:确保查询语句中包含了需要的列。在你的查询中,你需要选择
id
列,以便数据库引擎知道你想要从索引中检索这一列的值。示例查询如下:SELECT id FROM products WHERE name = 'iPhone 14';
这个查询将从包含
name
列和id
列的二级索引idx_name_id
中检索id
列的值。3、维护索引:确保在插入、更新和删除操作后,二级索引保持一致。MySQL通常会自动处理索引的维护,但在大批量插入或更新数据时,可能需要手动重建索引以确保其正确性。
索引下推是在MySQL5.6之后引进的索引优化功能,可以让存储引擎在非聚簇索引检索数据时,对索引中包含的字段先做判断是否符合条件,过滤掉不符合条件的记录,在返回给MySQL数据库(Server层),减少回表次数,减少不必要的数据传输,提高查询效率和性能。
索引下推我的理解就是把 原本应该在MySQL数据库(Server层)进行判断是否符合条件的工作下推到了存储引擎层中进行,这样的好处就是存储引擎返回给Server层的数据少了,减少了不必要的数据传输同时也就减少了回表的次数。
这里需要注意的是 存储引擎层 只是判断 索引中包含的字段 是否符合条件,对于那些 没有索引的字段但又需要判断的 会返回到 Server层 之后再进行判断。
联合索引:由 多个字段 组合成一个索引。
语法:CREATE INDEX 索引名 ON 表名(列名1, 列名2, ...);
当前数据有:
通过 CREATE INDEX index_stockNo_stock ON products(stockNo, stock);
创建 stockNo
和 stock
的联合索引。
此时联合索引的B+树是这样的:
可以看到 B+树 是先按照 stockNo
字段进行排序,再 stockNo
字段相同的情况况下再按照 stock
字段进行排序。
所以 stockNo
字段是 全局有序 的,而 stock
字段是 全局无序,但局部有序 的。也就是说在创建联合索引时,创建顺序为(a, b, c)
,那么对应的就是 a
全局有序,b
、c
局部相对有序。
联合索引的最左前缀原则指的是 在MySQL建立联合索引时,会遵守最左前缀原则,在检索数据时从联合索引的最左边开始匹配。
通俗一点讲:好比现在创建了一个联合索引 (a, b, c)
,相当于建立了三个索引 (a)
、(a, b)
、(a, b, c)
, 也就是说当需要检索的是 where a = XXX;
或者是 where a = XXX and b = XXX;
或者是 where a = XXX and b = XXX and c = XXX
都是可以命中索引,也就是利用索引进行检索数据的。
但是如果需要检索的是 where b = XXX and c = XXX
那就无法命中索引了也就是无法利用索引检索数据。也就是说必须从最左边开始匹配,只要匹配不到了就无法使用索引(注意这里 where a = XXX and c = XXX
是可以匹配到的,具体在上面索引失效中介绍了)。
where
子句查询的字段:经常用于条件查询的字段可以建立索引,这样能提高查询的速度。oreder by
或者 group by
使用的字段可以建立索引,这样在查询时就无需再做一次排序。(a, b, c)
,则自然无需为字段 a
单独建立索引,此时单独建立字段 a
索引就是冗余索引。a + 1 = 5
这样的表达式变为 a = 5 - 1
使得索引不失效,但针对一些较为复杂的表达式,无疑会增加查询成本,所以不建议在需要计算的字段上建立索引。对于字符串类型的字段建立索引时,可以使用前缀索引替换普通索引,这样可以减少索引字段的大小,从而增大数据也存储索引的数量,可以提高索引的查询效率。
对于查询记录不要求全部的,可以考虑建立联合索引,这样使得二级索引的B+树中的叶子节点能找到所有需要查询的数据,这样就不再需要通过主键索引查询整条记录,也就是避免了回表操作,减少I/O次数,从而提高查询效率。
对于主键索引的主键最好可以设置为自增。
对于使用自增的主键值,在索引的B+树插入新数据时,都是顺序的追加操作,无需移动节点调整树结构,这样的插入效率会变得更高。
而对于使用非自增的主键值,在B+树插入新数据时,可能需要移动其他节点来满足新节点的插入,也可能出现页分裂(当前数据页由于新数据的插入需要将数据页的数据隔开,所以需要新建一张数据页并且把当前数据页的一部分数据复制进新数据页),页分裂可能造成大量的内存碎片从而导致索引结构不稳定,影响查询效率。
建立了索引,未必每次查询都能用到索引,也可能全表扫描!
建立了索引,是否意味着 所有查询都可以使用索引进行检索数据 呢?
当然不是,如果无法命中索引(索引失效)时,将会进行全表扫描,此时建立的索引就派不上用场了。
出现索引失效这种情况将会导致性能大大降低,所以需要了解 如何查看索引是否命中 以及一些 常见的索引失效问题,避免出现索引失效这样才能有效地利用索引提高查询效率。
MySQL中通过 explain
查看SQL的执行计划,通过执行计划可以查看是否命中索引。
语法:EXPLAIN + sql 语句
如下图这样:
执行计划中只需要重点关注 type
、key
、rows
、filtered
、Extra
即可。
type
连接类型可以看成是扫描方式
type
如下表,扫描方式的 速度(性能)从慢到快。
key
实际使用的索引表示实际使用到的索引(显示的是索引名,也就是在创建索引时给索引命名的名字)。
rows
扫描的行数表示MySQL估算找到所需的记录需要扫描的行数,注意这个值只是估算值,并不是一定准确。
filtered
返回的行数表示符合条件的记录数的百分比(注意这里是百分比值),百分比为 经过过滤后满足条件的记录数 / 存储引擎返回的记录数 。
Extra
额外信息WHERE
子句出现 OR
如果在查询语句的 WHERE
子句中出现 OR
并且 OR
的左右两边的字段中有一个字段不是索引列时,会出现索引失效,进行全表扫描。
如 select * from products where name = 'Apple Watch' or price = 999.99;
语句中 name
字段为唯一索引列,而 price
只是普通字段时,执行计划如下:
可以看到执行计划中的 type
为 ALL,说明进行了全表扫描,也就说明了对于 name
字段的唯一索引并没有使用,即 索引失效。
如何避免这种情况下的索引失效呢?
避免这种情况的方法是 OR
的左右两边的字段都需要是索引列。
如 select * from products where name = 'Apple Watch' or id = 1;
就使用了主键索引和唯一索引,这里的执行计划 type
为 index_merge,表示利用两个索引分别检索后合并得到结果。
如果在查询语句中 对索引列使用函数 或者 对索引列进行表达式计算,都会导致索引失效。
如 select * from products where LENGTH(name) = 5;
语句中看似使用了 name
的唯一索引,但是实际上并没有命中索引,而是进行了全表扫描。
为什么会出现这种情况呢?
因为 索引的B+树结构中索引键是原始的索引值(没有经过计算或函数的),如果经过函数或者表达式计算之后自然就无法在B+树结构找到对应的索引键了,那么就自然无法通过索引来检索到记录了。
如何避免这种情况呢?
其实只需要对代码做一些转换,像 id + 2 = 5
转换为 id = 5 - 2
,from_unixtime(create_time) = '2023-3-22'
转换为 create_time = unix_timestamp('2023-3-22')
这样即可实现索引命中,也就是让索引列比较时变得“干净些”。
如 select * from products where id = 5 - 2;
即可避免索引失效问题。
但是还是比较建议如果 需要进行计算的字段就不必再建立索引 了,因为当面对一些较为复杂的函数时无法做到的两边转换。
like
通配符模糊查询如果在查询语句中使用 like
模糊匹配时,对于 like %XXX
和 like %XXX%
这样的形式模糊匹配时是无法命中索引(索引失效)的。而对于 like%
这样的形式是可以命中索引进行索引检索的。
如 select * from products where name like '%Pro';
语句中执行计划的 type
为 ALL,表明这次执行是全表扫描。
如 select * from products where name like 'Apple%';
语句中执行计划的 type
为 range,表明这次执行是索引范围查询。
为什么会出现这样的情况呢?
由于索引的B+树的索引键是有序的,根据指定的值和索引键比较,最后找到所需的记录,在模糊查询时会根据指定的值 按照前缀的方式 比较索引键来找到所需的记录。
对于像 Apple%
这样的值因为前面有明确的 A
,所以完全可以使用索引快速缩小范围,进行索引范围查询。
而对于像 %Pro
或者 %Pro%
这类的值因为前面不具有明确的值,可以是任意值,这样索引就不知道从哪里开始检索,无法快速缩小范围,只能选择全表扫描,也就会导致索引失效。
如果在查询语句时,对字符串类型的索引列的输入参数类型为整型时,会导致索引失效。
如 select * from products where stockNo = 1001;
中将为 varchar
类型的 stockNo
比较时输入参数为整型 1001
时,虽然可以找出记录,但是使用的却是全表扫描,说明索引失效。
出现这种情况的原因
MySQL的隐式转换:当遇到字符串和数字进行比较时,会自动将字符串转为数字进行比较。
知道这个原则后,就比较好解释了,上面的例子中在 字符串类型的 stockNo
和数字类型的 1001
比较时,会自动把 stockNo
转换成数字,但是因为 隐式转换仍然是需要使用函数进行转换的,也就是说需要对 stockNo
索引列使用函数,所以自然无法使用索引检索了,这就导致索引失效。
也就是说如果例子为 select * from products where id = '2';
时,因为 id
是主键索引并且是整型,此时输入参数是字符串类型,但是通过隐式转换后会变为 id = 2
,所以并不影响主键索引的使用,也就不会导致索引失效。
如果在查询数据时使用联合索引进行检索数据时,需要注意按照最左优先的方式进行索引的匹配(遵循最左前缀原则)。
现在创建一个联合索引 (a, b, c)
,此时的索引结构是以 a
为全局有序, b
、c
为局部相对有序。
如果此时的查询为:where b = 2 and c = 3
或者 where b = 2
或者 where c = 3
会出现索引失效的问题
之所以出现这样的情况是因为查询中并没有遵守最左前缀原则,无法匹配到 a
字段,所以无法命中联合索引,导致索引失效问题。
对于 where a = 1 and c = 3
这种情况会出现索引失效吗?
答案是不会。
在没有出现 索引下推 前,这条语句的执行过程是 先去匹配 a
字段的索引键找到所有符合条件的主键后,通过回表操作将这些主键值经过主键索引找到对应的数据行,并且把这些数据都返回给Server层,然后再通过Server层的 where
比较 c
的值,过滤掉不符合的条件。
在出现 索引下推 后,这条语句的执行过程是 先去匹配 a
字段的索引键找到所有符合条件的主键时,因为 c
字段也在索引中,所以可以在存储引擎层就对 c
字段进行比较,过滤掉不符合 c
字段的值,然后再将剩余符合条件的主键进行回表操作,通过主键索引找到对应的数据返回给Server层。
通过这个例子也可以巩固之前文章中 索引下推 的定义,索引下推减少了回表的数量,提高了查询效率。
为什么不遵守最左前缀原则就无法命中索引呢?
因为在联合索引中,数据是按照索引第一列排序,在第一列相同的情况下才按照第二列进行排序,也就是最左边的列是全局有序的,而其余的列都是局部相对有序的,查询时如果连最左的列都无法匹配到,那么自然无法使用索引。
想要更多的列的使用到 联合索引,必须保证从最左边开始能连续匹配到对应列,像 where a = XXX and c = XXX
这种使用索引只能缩小范围到 a
字段,无法快速检索 c
字段,只能通过遍历进行 c
字段的比较。