文章目录
- [1. 初见索引](#1. 初见索引)
- [2. MySQL与磁盘的交互](#2. MySQL与磁盘的交互)
-
- [2.1 page](#2.1 page)
- [2.2 为何是16KB?](#2.2 为何是16KB?)
- [2.3 缓冲池](#2.3 缓冲池)
- [3. 索引的理解](#3. 索引的理解)
-
- [3.1 一个现象](#3.1 一个现象)
- [3.2 单个page和多个page](#3.2 单个page和多个page)
- [3.3 页目录](#3.3 页目录)
- [3.4 为何不选用其它的数据结构?](#3.4 为何不选用其它的数据结构?)
- [3.5 聚簇索引和非聚簇索引](#3.5 聚簇索引和非聚簇索引)
- [4. 索引的操作](#4. 索引的操作)
-
- [4.1 创建索引](#4.1 创建索引)
-
- [4.1.1 主键索引](#4.1.1 主键索引)
- [4.1.2 唯一键索引](#4.1.2 唯一键索引)
- [4.1.3 普通索引](#4.1.3 普通索引)
- [4.2 查询索引](#4.2 查询索引)
-
- [4.2.1 查询主键索引](#4.2.1 查询主键索引)
- [4.2.2 查询唯一键索引](#4.2.2 查询唯一键索引)
- [4.2.3 查询普通索引](#4.2.3 查询普通索引)
- [4.3 删除索引](#4.3 删除索引)
-
- [4.3.1 删除主键索引](#4.3.1 删除主键索引)
- [4.3.2 删除唯一键索引](#4.3.2 删除唯一键索引)
- [4.3.3 删除普通索引](#4.3.3 删除普通索引)
1. 初见索引
定义:
索引是一种数据库结构,用于快速查找和访问表中的数据行。它就像是一本书的目录,通过索引,数据库管理系统(DBMS)可以快速定位到表中符合条件的数据,而不必逐行扫描整个表。在 MySQL 中,索引是存储在磁盘上的一种数据结构,它包含了表中一列或多列的值以及对应的行位置信息。
例子:
创建一个表,该表有5000000条记录,表结构如下。注意empno
没有主键约束
sql
mysql> desc emp;
+----------+--------------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------------------+------+-----+---------+-------+
| empno | int(6) unsigned zerofill | NO | | NULL | |
| ename | varchar(10) | YES | | NULL | |
| job | varchar(9) | YES | | NULL | |
| mgr | int(4) unsigned zerofill | YES | | NULL | |
| hiredate | datetime | YES | | NULL | |
| sal | decimal(7,2) | YES | | NULL | |
| comm | decimal(7,2) | YES | | NULL | |
| deptno | int(2) unsigned zerofill | YES | | NULL | |
+----------+--------------------------+------+-----+---------+-------+
8 rows in set (0.01 sec)
ename
列,depton
列都是随机的
sql
mysql> select * from emp limit 5;
+--------+--------+----------+------+---------------------+---------+--------+--------+
| empno | ename | job | mgr | hiredate | sal | comm | deptno |
+--------+--------+----------+------+---------------------+---------+--------+--------+
| 100002 | hlKSjs | SALESMAN | 0001 | 2024-11-30 00:00:00 | 2000.00 | 400.00 | 135 |
| 100003 | icPPFH | SALESMAN | 0001 | 2024-11-30 00:00:00 | 2000.00 | 400.00 | 221 |
| 100004 | iCmEkl | SALESMAN | 0001 | 2024-11-30 00:00:00 | 2000.00 | 400.00 | 253 |
| 100005 | PAhmMZ | SALESMAN | 0001 | 2024-11-30 00:00:00 | 2000.00 | 400.00 | 372 |
| 100006 | IfDCZp | SALESMAN | 0001 | 2024-11-30 00:00:00 | 2000.00 | 400.00 | 293 |
+--------+--------+----------+------+---------------------+---------+--------+--------+
5 rows in set (0.00 sec)
现在想查找某一个人,观察时间
sql
mysql> select * from emp where empno=4100002;
+---------+--------+----------+------+---------------------+---------+--------+--------+
| empno | ename | job | mgr | hiredate | sal | comm | deptno |
+---------+--------+----------+------+---------------------+---------+--------+--------+
| 4100002 | NlSJBQ | SALESMAN | 0001 | 2024-11-30 00:00:00 | 2000.00 | 400.00 | 244 |
+---------+--------+----------+------+---------------------+---------+--------+--------+
1 row in set (2.61 sec)
创建索引后,观察时间,发现速度大幅提升。
sql
mysql> alter table emp add index(empno);
Query OK, 0 rows affected (14.11 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> select * from emp where empno=4100002;
+---------+--------+----------+------+---------------------+---------+--------+--------+
| empno | ename | job | mgr | hiredate | sal | comm | deptno |
+---------+--------+----------+------+---------------------+---------+--------+--------+
| 4100002 | NlSJBQ | SALESMAN | 0001 | 2024-11-30 00:00:00 | 2000.00 | 400.00 | 244 |
+---------+--------+----------+------+---------------------+---------+--------+--------+
1 row in set (0.00 sec)
2. MySQL与磁盘的交互
2.1 page
MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的IO场景,所以,为了提高基本的IO效率, MySQL 进行IO的基本单位是 16KB (使用 InnoDB 存储引擎),每次读取或写入较大的单位可以减少 I/O 操作的次数。
实际看一下大小
sql
-- 查看全局状态变量innodb_page_size的值。这个变量表示 InnoDB 存储引擎的页大小。
mysql> show global status like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.00 sec)
MySQL 和磁盘进行数据交互的基本单位是 16KB 。这个基本数据单元,在 MySQL 这里叫做页(page)
MySQL 中要管理很多数据表文件,而要管理好这些文件,就需要先描述,在组织
,目前理解成一个个独立文件是有一个或者多个page构成的
2.2 为何是16KB?
问题:为何MySQL和磁盘进行IO交互的时候,要采用page的方案进行交互呢,不能采用"用多少,加载多少"的策略吗?
回答:
- 磁盘 I/O 操作是相对较慢的操作,每次磁盘寻道和读写都有一定的延迟。如果采用 "用多少,加载多少" 的方式,比如按字节或者较小的单位来读取数据,那么对于连续的数据访问需求,就会频繁地触发磁盘 I/O 操作。以页为单位进行交互,例如 InnoDB 存储引擎默认页大小为 16KB,一次读取一页的数据,当需要访问的数据在同一页内时,就无需多次进行磁盘 I/O。你怎么保证,用户一定下次找的数据,就在这个Page里面?我们不能严格保证,但是有很大概率,因为有局部性原理。
- 例如,在查询一个包含多条记录的表时,如果这些记录存储在相邻的位置并且在同一页中,一次读取该页就可以获取所有这些记录,而不是为每条记录分别进行磁盘 I/O。这种批量读取的方式大大减少了磁盘 I/O 的次数,提高了数据访问效率。
ps: 局部性原理的概念:如果一个存储单元被访问,那么在不久的将来,它很可能会被再次访问。这是因为程序中存在循环结构、函数调用等情况。在循环中,相同的指令或者数据会在短时间内被多次使用。
2.3 缓冲池
缓冲池(Buffer Pool) :缓冲池是 InnoDB 存储引擎用于缓存从磁盘读取的页的内存区域。当从磁盘读取数据时,首先会查看缓冲池中是否已经存在所需的页。如果存在,就直接从缓冲池中获取数据,避免了一次磁盘 I/O 操作。只有当缓冲池中没有所需的页时,才会从磁盘以页为单位进行读取,并将读取到的页放入缓冲池中。缓冲池的大小对于数据库性能有重要影响,合理设置缓冲池大小可以提高数据库对频繁访问数据的读取效率。
看一下mysql的配置文件
bash
vim /etc/my.cnf
--------------------------
...
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M
...
翻译:去掉行首的 "#" 符号,将其设置为 MySQL 中最重要的数据缓存所占用的内存量。如果是专用服务器(即该服务器主要是用来运行 MySQL 数据库,没有太多其他杂项任务占用资源),那么建议从服务器总内存的 70% 开始设置这个缓存的内存大小;如果不是专用服务器(比如服务器上还运行着其他很多不同的应用程序等),那么建议设置为服务器总内存的 10%。
默认行为 :从 MySQL 5.7 开始,innodb_buffer_pool_size
的默认大小是根据服务器的内存情况自动调整的。它会基于服务器的物理内存总量分配一个相对合理的值,但最小值仍为128MB。
3. 索引的理解
3.1 一个现象
建立一张表,注意该表有主键,随机插入数据,查找
sql
mysql> create table if not exists user (
id int primary key,
age int not null,
name varchar(16) not null
);
Query OK, 0 rows affected (0.07 sec)
mysql> insert into user values(3, 18, '张三');
Query OK, 1 row affected (0.06 sec)
mysql> insert into user values(1, 18, '李四');
Query OK, 1 row affected (0.06 sec)
mysql> insert into user values(6, 18, '王五');
Query OK, 1 row affected (0.06 sec)
mysql> insert into user values(4, 18, '赵六');
Query OK, 1 row affected (0.07 sec)
mysql> insert into user values(2, 18, '田七');
Query OK, 1 row affected (0.06 sec)
mysql> insert into user values(8, 18, '张飞');
Query OK, 1 row affected (0.06 sec)
mysql> select * from user;
+----+-----+------+
| id | age | name |
+----+-----+------+
| 1 | 18 | 李四 |
| 2 | 18 | 田七 |
| 3 | 18 | 张三 |
| 4 | 18 | 赵六 |
| 6 | 18 | 王五 |
| 8 | 18 | 张飞 |
+----+-----+------+
6 rows in set (0.13 sec)
发现mysql自动根据主键id进行排序了
3.2 单个page和多个page
对于3.1中的数据,单个page的结构如下
为了加快查找速度,引入了目录,这也是为什么会排序的原因。示意图如下
此时不必线性查找,而是根据目录查找,比如要查id为6的王五,先查目录1(记录了1-3的id),不在,继续查目录2(记录了大于4的id),在。
然后找id为4的赵六,发现不是,继续往下找,就找到了id为6的王五。
一个page只有16KB,随着数据量增大,16KB不能存下所有的数据,于是就会有多个page来存取数据,结构示意如下(每个page存的数据不一样,这里只是为了方便表示,所以直接cv了。)
3.3 页目录
当page逐渐增多,挨个查找page也不现实,所以又增加了一层(页目录),该层存的是普通page的地址,示意图如下
当上面的页目录层逐渐增大时,也可以再增加一层页目录
该结构是B+树(去掉第二层中各个page的连接),当我们建立完主键索引后,mysqld
就会帮我们构建一颗这样的树。InnoDB 在建立索引结构来管理数据的时候,都是采用如上的方式的。查找任意id,由于不是线性查找,而是自顶向下的查找,查找次数变少,与磁盘io的次数也减少,所以效率就提高了。
该B+树的特点是:
- 叶子节点保存数据,路上别的节点没有数据,只有目录项(不存数据可以保存更多的目录项,从而管理更多的叶子节点)。是一颗"矮胖型"的树
- 叶子节点都用链表链接起来,如果需要范围查找,就会很方便,通过指针很容易找到下一个page
3.4 为何不选用其它的数据结构?
- 链表:线性查找,速度慢
- 二叉搜索树:可能成为"歪脖子树"
- avl or 红黑树:树会比b树要高。
- hash:有些引擎也是使用hash的,能做到O(1)的查找,但是范围查找效果不好
- b树:b树的节点存储data,树会更高,io次数会更多。而且叶子节点没有相连,范围查找效果不好。
3.5 聚簇索引和非聚簇索引
聚簇索引:(如innodb引擎主键索引)
- 定义 :聚簇索引是一种对表中的数据进行物理排序的索引。在聚簇索引中,索引的叶子节点直接包含了数据记录本身。也就是说,数据行的物理存储顺序与聚簇索引的顺序相同。例如,在一个以学生学号为聚簇索引的学生表中,数据在磁盘上的物理存储顺序是按照学号从小到大排列的。
- 特点 :
- 数据存储顺序:数据按照聚簇索引列的值进行物理排序。这使得范围查询(如查询学号在 100 - 200 之间的学生)非常高效,因为数据在磁盘上是连续存储的,在进行此类查询时,磁盘 I/O 操作可以顺序读取数据,减少了寻道时间。
- 一个表只能有一个聚簇索引 :这是因为数据的物理存储顺序只能有一种。通常,在关系型数据库中,如果没有指定聚簇索引,会根据主键(如果有)自动创建聚簇索引。例如,在 MySQL 的 InnoDB 存储引擎中,主键索引就是聚簇索引。如果表没有主键,InnoDB 会选择一个唯一的非空索引作为聚簇索引;如果没有这样的索引,InnoDB 会隐式地定义一个隐藏的聚簇索引。
非聚簇索引:(如myisam引擎主键索引)
- 定义 :非聚簇索引的索引叶子节点并不包含数据记录本身,而是包含指向数据记录的指针。这些指针可以是物理地址(如磁盘上的存储位置)或者是逻辑指针(如通过行标识符来定位数据)。例如,在一个学生表中,有一个以学生姓名为非聚簇索引的索引结构,其叶子节点存储的是指向具有相应姓名的学生记录的指针。
- 特点 :
- 数据存储与索引分离:数据的物理存储顺序与非聚簇索引的顺序没有直接关系。这意味着可以为表创建多个非聚簇索引,每个索引基于不同的列,用于不同的查询目的。例如,在一个包含产品信息的表中,可以创建一个以产品名称为非聚簇索引的索引用于按名称查询产品,还可以创建一个以产品价格为非聚簇索引的索引用于价格查询。
- 查询性能:对于非聚簇索引,在查询时,首先通过索引找到指向数据记录的指针,然后根据指针再去获取数据记录。这比聚簇索引多了一步间接访问数据的过程。但是,对于单个值的查询(如查询某个特定姓名的学生),非聚簇索引仍然可以快速定位到指针,然后获取数据,性能也比较好。不过,对于范围查询,如果没有特殊的优化措施,可能需要多次通过指针去获取数据,性能可能不如聚簇索引。
- 索引维护成本相对较低:由于非聚簇索引的叶子节点不包含数据本身,所以在数据插入、更新或删除时,对非聚簇索引的维护相对简单,通常只需要更新索引中的指针信息,而不需要像聚簇索引那样可能涉及数据的物理重新排序。
一张表实际上就是多个文件,现在见两张表,使用不同的引擎
sql
mysql> create database test_index;
Query OK, 1 row affected (0.00 sec)
mysql> use test_index;
Database changed
mysql> create table t1( id int primary key, name varchar(20)) engine innodb;
Query OK, 0 rows affected (0.02 sec)
mysql> create table t2( id int primary key, name varchar(20)) engine myisam;
Query OK, 0 rows affected (0.01 sec)
-------------------------------
[root@hcss-ecs-3db9 mysql]# cd test_index/
[root@hcss-ecs-3db9 test_index]# ll
total 128
-rw-r----- 1 mysql mysql 61 Nov 30 21:30 db.opt
-rw-r----- 1 mysql mysql 8586 Nov 30 21:32 t1.frm
-rw-r----- 1 mysql mysql 98304 Nov 30 21:32 t1.ibd
-rw-r----- 1 mysql mysql 8586 Nov 30 21:33 t2.frm
-rw-r----- 1 mysql mysql 0 Nov 30 21:33 t2.MYD
-rw-r----- 1 mysql mysql 1024 Nov 30 21:33 t2.MYI
可以看到t1表只有一个t1.ibd
该文件不仅有节点,而且有数据。t2表有t2.MYD
和t2.MYI
,分别存储了数据和节点。
问题:实际上,不仅只有主键索引,其他键也可以作为索引,称之为辅助(普通)索引。实际上每次建立一个索引都是创建一颗b+树,innodb引擎和myisam引擎创建辅助索引时,各自的操作是什么样的呢?
回答:
- 在 InnoDB 存储引擎中,辅助索引(Secondary Index)也是基于 B + 树构建的。与聚簇索引(主键索引)不同的是,辅助索引的叶子节点存储的是主键值(而不是数据行本身)和一个指向聚簇索引中对应数据行的指针,获得主键后,然后用主键到主索引中检索获得记录。这种过程,就叫做回表查询 。这是因为 InnoDB 的数据存储是基于聚簇索引的,数据行按照主键的顺序存储在聚簇索引的叶子节点中。
- MyISAM 存储引擎的辅助索引同样是 B + 树结构。与 InnoDB 不同的是,MyISAM 的辅助索引叶子节点直接存储了数据记录的地址(在 MyISAM 存储格式下的数据文件中的位置)和索引列的值。这意味着在 MyISAM 中,辅助索引可以直接定位到数据记录,而不需要像 InnoDB 那样通过聚簇索引进行二次查找。
4. 索引的操作
4.1 创建索引
4.1.1 主键索引
sql
-- 在创建表的时候,直接在字段名后指定 primary key
CREATE TABLE t1(id int primary key, name varchar(20));
-- 在创建表的最后,指定某列或某几列为主键索引,这里是复合索引
CREATE TABLE t2(id int, name varchar(20), primary key(id, name));
-- 创建表后再添加主键
CREATE TABLE t3 (id int, name varchar(20));
ALTER TABLE t3 add primary key(id);
主键索引的特点:
- 唯一性:主键列的值不能重复
- 非空性:主键列不允许为空值(NULL)
- 索引结构高效性(b+树结构,树的高度低,查询效率高)
4.1.2 唯一键索引
sql
-- 在创建表的时候,直接在字段名后指定 unique
CREATE TABLE t4(id int unique, name varchar(20));
-- 在创建表的最后,指定某列或某几列为唯一键索引
CREATE TABLE t5(id int, name varchar(20), unique(name));
-- 创建表后再添加唯一键
CREATE TABLE t6 (id int, name varchar(20));
ALTER TABLE t6 add unique (name);
唯一键索引的特点:
- 值的唯一性:确保索引列的值在表中是唯一的。
- 允许为空:唯一键索引允许索引列包含一个空值(NULL)。
- 索引结构高效性:与上面的主键索引一样。
4.1.3 普通索引
sql
-- 在表的定义最后,指定某列为索引,这里是复合索引
CREATE TABLE t7 (id int, name varchar(20), email varchar(30), index(id, name));
-- 创建完表以后指定某列为普通索引
CREATE TABLE t8 (id int, name varchar(20), email varchar(30));
ALTER TABLE t8 add index(name);
-- 给索引重命名,这里命名为my_index
CREATE TABLE t9 (id int, name varchar(20), email varchar(30));
CREATE INDEX my_index on t9(name);
普通索引的特点:
- 一个表中可以有多个普通索引
- 索引列的值可以重复,可以为空
4.2 查询索引
4.2.1 查询主键索引
sql
mysql> show keys from t1\G;
*************************** 1. row ***************************
Table: t1
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: id
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
1 row in set (0.00 sec)
sql
-- 虽然这里两行,但只有是一棵b+树,key_name的value()是一样的。
mysql> show index from t2\G;
*************************** 1. row ***************************
Table: t2
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: id
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
*************************** 2. row ***************************
Table: t2
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 2
Column_name: name
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
2 rows in set (0.00 sec)
sql
mysql> desc t3;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| name | varchar(20) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
4.2.2 查询唯一键索引
sql
mysql> show keys from t4\G;
*************************** 1. row ***************************
Table: t4
Non_unique: 0
Key_name: id
Seq_in_index: 1
Column_name: id
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
1 row in set (0.00 sec)
sql
mysql> show index from t5\G;
*************************** 1. row ***************************
Table: t5
Non_unique: 0
Key_name: name
Seq_in_index: 1
Column_name: name
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
1 row in set (0.00 sec)
sql
mysql> desc t6;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int(11) | YES | | NULL | |
| name | varchar(20) | YES | UNI | NULL | |
+-------+-------------+------+-----+---------+-------+
2 rows in set (0.00 sec)
4.2.3 查询普通索引
sql
-- 虽然这里两行,但只有是一棵b+树,key_name的value()是一样的。
mysql> show keys from t7\G;
*************************** 1. row ***************************
Table: t7
Non_unique: 1
Key_name: id
Seq_in_index: 1
Column_name: id
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
*************************** 2. row ***************************
Table: t7
Non_unique: 1
Key_name: id
Seq_in_index: 2
Column_name: name
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
2 rows in set (0.00 sec)
sql
mysql> show index from t8\G;
*************************** 1. row ***************************
Table: t8
Non_unique: 1
Key_name: name
Seq_in_index: 1
Column_name: name
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
1 row in set (0.00 sec)
sql
mysql> desc t9;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int(11) | YES | | NULL | |
| name | varchar(20) | YES | MUL | NULL | |
| email | varchar(30) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
4.3 删除索引
删除看的是Key_name的value()值。
4.3.1 删除主键索引
sql
mysql> alter table t1 drop primary key;
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> show keys from t1\G;
Empty set (0.00 sec)
4.3.2 删除唯一键索引
sql
mysql> alter table t4 drop index id;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> show keys from t4;
Empty set (0.00 sec)
=====================
mysql> drop index name on t5;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> show keys from t5;
Empty set (0.00 sec)
4.3.3 删除普通索引
sql
mysql> alter table t7 drop index id;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> show keys from t7;
Empty set (0.00 sec)
========================
mysql> drop index name on t8;
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> show keys from t8;
Empty set (0.00 sec)