From ed5f2297cf5b519a326c2231884c461124961c7e Mon Sep 17 00:00:00 2001 From: asahi Date: Sat, 15 Feb 2025 16:28:43 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=85=E8=AF=BBmysql=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mysql/mysql文档/mysql_索引.md | 309 ++++++++++++++++++++++++++++++ mysql/mysql文档/mysql_表.md | 74 ++++++- 2 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 mysql/mysql文档/mysql_索引.md diff --git a/mysql/mysql文档/mysql_索引.md b/mysql/mysql文档/mysql_索引.md new file mode 100644 index 0000000..50398cd --- /dev/null +++ b/mysql/mysql文档/mysql_索引.md @@ -0,0 +1,309 @@ +- [innodb索引与算法](#innodb索引与算法) + - [索引算法](#索引算法) + - [二分查找](#二分查找) + - [B+树索引](#b树索引) + - [聚簇索引](#聚簇索引) + - [非数据页](#非数据页) + - [辅助索引](#辅助索引) + - [辅助索引查找](#辅助索引查找) + - [辅助索引查找分析](#辅助索引查找分析) + - [索引创建](#索引创建) + - [algorithm](#algorithm) + - [lock](#lock) + - [online DDL](#online-ddl) + - [Cardinality](#cardinality) + - [Cardinality](#cardinality-1) + - [innodb存储引擎的Cardinality](#innodb存储引擎的cardinality) + - [Cardinality采样](#cardinality采样) + - [索引使用](#索引使用) + - [联合索引](#联合索引) + - [覆盖索引](#覆盖索引) + - [覆盖索引对于统计场景的好处](#覆盖索引对于统计场景的好处) + - [优化器选择不使用索引](#优化器选择不使用索引) + - [辅助索引不能覆盖](#辅助索引不能覆盖) + - [索引提示](#索引提示) + - [Use index](#use-index) + + +# innodb索引与算法 +innodb存储引擎支持如下集中常见的索引: +- B+树索引 + - 对于B+树索引,其只能找到被查找数据所在的页,然后数据库把页读入到内存中,再在内存中进行查找 +- 全文索引 +- 哈希索引 + - innodb中哈希索引是自适应的,innodb存储引擎会根据目前表的使用情况自动生成哈希索引 + + +## 索引算法 +### 二分查找 +如前所示,mysql数据页中存在page directory,而page directory中slots是按照主键顺序来进行排放的。 + +`对于数据的查找,是通过对page directory中的槽进行二分查找得到的`。 + +## B+树索引 +inoodb中索引大多是B+树的实现,`且具有高扇出的特性`。故而,在数据库中,`B+树的高度一般都在2~4层,即在查找某一行记录时,最多只需要2~4次IO。` + +innodb中B+树索引可以分为聚簇索引和辅助索引,其都是基于B+树实现,`所有的数据都存放在叶子节点中`。 + +> 聚簇索引和辅助索引的区别在于叶子节点中是否存放的是整行的记录信息。 + +### 聚簇索引 +在innodb中,表是由索引组织的,即表中的数据按照主键的顺序存放。 + +聚簇索引是按照每张表的主键构建一颗B+树,而叶子节点存放的则是整张表的行记录数据,`故而,聚簇索引的叶子节点也被称为数据页`。 + +> 和B+树一样,每个数据页(叶子节点)都通过双向链表来进行链接。 + +> `每张表只能拥有一个聚簇索引`,并且,在多数情况下,查询优化器倾向于采用聚簇索引。`因为聚簇索引能够直接在叶子节点获取到行记录。` + +#### 非数据页 +在非数据页中,存放的是key以及指向数据页的页号。 + +聚簇索引的存储并非是物理上连续的,`而是通过双向链表来维护逻辑上的顺序`。 +> 数据页与数据页之间通过双向链表来维护顺序排序,而位于同一个页中的记录之间也是通过双向链表来维护排序的。 + +对于聚簇索引,其`按照主键排序进行查找`或`按主键进行范围查找`时,查询速度很快。 + +### 辅助索引 +对于辅助索引,叶子节点并不包含行记录的全部数据。在辅助索引的叶子节点中,叶子节点除了包含辅助索引的key外,`每个叶子节点中的索引行还包含一个书签(bookmark)`。bookmark可以告知inodb存储引擎哪里能找到和索引相对应的行数据。 + +> 实际上,innodb存储引擎的辅助索引其bookmark就是相应行数据的聚簇索引key`。 + +辅助索引并不会影响行数据在聚簇索引上的组织,故而每张表可以含有一个聚簇索引和多个辅助索引。 + +#### 辅助索引查找 +当通过辅助索引来查找数据时,innodb会遍历辅助索引的B+树数据结构,并从叶子节点中获取指向的主键索引值,之后再通过主键索引来查找到完整的行记录。 + +#### 辅助索引查找分析 +如果聚簇索引和辅助索引的B+树高度同样为3,那么当根据辅助索引来查找关联的主键时,需要花费3次IO。此时,再根据主键来从聚簇索引中获取对应行数据,需要另外3次IO。 + +故而,根据辅助索引来获取行记录,大概需要6次IO。 + +### 索引创建 +通过alter table语法,可以选择索引的创建方式: +```sql +alter table tbl_name +| add {index|key} [index_name] +[index_type] (index_col_name, ...) +algorithm [=] {default|inplace|copy} +lock [=] {default|none|shared|exclusive} +``` + +#### algorithm +通过`algorithm`可以指定创建或删除索引的算法: +- copy:通过创建临时表的方式,定义新的表结构,并将原表中的数据移动到新的表,最后删除原表,并将临时表重命名 + - 该算法将会耗费大量事件,并且在执行时表不可被访问 +- inplace:索引创建和删除操作不采用临时表 +- default: 按照`old_alter_table`参数来判断是通过inplace还是copy来创建或删除索引,默认`old_alter_table`为off,即是采用inplace算法 + + +#### lock +lock部分代表索引创建或删除时,对表的加锁情况: +- none:执行索引的创建或删除操作时,对目标表不添加任何的锁,在执行过程中事务仍然能够进行读写操作,不会阻塞。该加锁情况能够获取最大程度的并发度 +- share:在执行过程中,对表添加s锁,`s锁会和x锁以及ix锁发生冲突,但是能够兼容s锁和is锁`。 + - 故而,在加锁方式为share的场景下,执行索引的创建或删除时,其他事务仍然能够执行读取操作,但是写操作则是会被阻塞。 + - 如果存储引擎不支持share模式,会返回相应的错误信息 +- exclusive:在exclusive模式下,执行索引的创建和删除操作时,会对目标表添加一个x锁,此时,其他事务对该表的读和写操作都不会被执行 +- default: + - default模式下首先会判断当前操作能否使用none + - 如果不能,则判断是否能够使用shared + - 如果仍然不能,那么最后会判断能否使用exclusive + +#### online DDL +innodb存储引擎实现online DDL的原理是在执行索引的创建或删除操作时,将`insert, update, delete`这些DML写操作的日志写入到一个缓存中,等到索引创建完成后,再将这些DML语句重做到表上(日志内容类似于redo log)。 + +该缓存的大小可以通过`innodb_online_alter_log_max_size`来进行控制,默认大小为`128M`。 + +如果再执行索引创建和更新的表比较大,并且在`online alter`执行过程中存在大量的写事务,那么当`innodb_online_alter_log_max_size`指定的空间不足以存放时,会抛出错误。 + +如果发生上述错误,可以考虑增加`inodb_online_alter_log_max_size`参数的值;也可以设置`alter table`的`lock`模式为`share`,那么在执行过程中不会有写事务的发生,不需要进行DML日志的记录。 + + +### Cardinality +并不是所有的字段都适合创建B+树索引。例如对于性别字段、类型字段等,它们的选择范围都很小,这种字段不适合创建B+树索引。 + +> 性别字段中只有`M`和`F`两种可选值,故而执行`select * from student where sex = 'F'`这类sql可能会得到表中50%的数据,这种情况下为性别字段添加B+树索引完全没有必要。 + +而对于`bigint`类型的id字段,其选择范围很广,且没有重复,这类字段适合使用B+树索引。 + +#### Cardinality +通过`show index`语句可以查看索引是否拥有高选择性,`Cardinality`列代表索引中`不重复记录数量的预估值`。 + +`Cardinality`是一个预估值,并不是准确的值,且`Cardinality / rows_in_table`的值应该尽可能的接近1。如果该值非常小,那么用户应考虑该索引的创建是否有必要。 + +#### innodb存储引擎的Cardinality +在生产环境下,索引的操作可能十分频繁,如果每次操作都对cardinality进行统计,那么将会带来巨大的开销。`故而,数据库针对Cardinality的统计都是通过采样来进行的`。 + +在innodb中,针对cardinality统计信息的更新发生在两个操作中:`insert, update`。innodb针对cardinality的更新策略为: +- 表中1/16的数据已经发生变化 + - 自上一次统计cardinality信息时起,表中1/16的数据已经发生过变化,此时需要重新统计cardinality +- stat_modified_counter >= 2000,000,000 + - 如果针对表中某一行数据进行频繁的更新,那么该场景不会适配第一种更新策略,故而在innodb内部维护了一个计数器stat_modified_counter,用于标识发生变化的次数,当计数器的值大于`2000,000,000`时,也需要更新cardinality + +#### Cardinality采样 +默认情况下,innodb会针对8个叶子节点进行采样,采样过程如下: +- 取得B+树索引中叶子节点的数量,记为A +- 随机获取索引8个叶子节点,统计每个页不同记录的个数,记为P1, P2, ... , P8 +- 根据采样信息给出Cardinality的预估值`(P1 + P2 + ... + P8) * A/8` + +## 索引使用 +### 联合索引 +联合索引代表对表上的多个列进行索引。 + +示例如下: +```sql +create table t ( + id bigint not null auto_increment, + a int not null, + b int not null, + primary key(id), + index idx_a_b (a, b) +); +``` +对于上述联合索引`(a,b)`,各查询语句使用索引的情况如下: +- `where a = xxx and b = xxx`: + - 该语句会使用联合索引 +- `where a = xxx`: + - 该语句同样可以使用联合索引 +- `where b = xxx`: + - 该语句无法使用联合索引 + +### 覆盖索引 +innodb存储引擎支持覆盖索引,即通过辅助索引即可得到查询记录,无需再次查询聚簇索引。 + +> 通常来说,辅助索引中叶子节点的记录大小要远小于聚簇索引中叶子节点的记录大小。故而,在范围统计的场景下(`select count(1)`),辅助索引叶子节点中,一个页包含更多的记录,表扫描时需要读取的页数页更少。 +> +> 故而,在部分场景下,使用覆盖索引能够节省大量的io操作。 + +对于辅助索引而言,其叶子节点的记录中包含`(primary key1, primary key2, ..., key1, key2)`等信息,故而,如下语句都可以通过`一次辅助联合索引查询来完成`: +- `select key2 from table where key1 = xxx` + - 会命中辅助索引,且辅助索引中包含key2的值,故而无需再次查询聚簇索引 +- `select primary key2,key2 from table where key1 = xxx` +- `select primary key1,key2 from table where key1 = xxx` +- `select primary key1, primary key2, key2 from key1 = xxx` + +#### 覆盖索引对于统计场景的好处 +例如,对于表buy_log +```sql +create table buy_log ( + userid int unsigned not null, + buy_date date, + index `idx_userid` (userid), + index `idx_userid_buy_date` (userid, buy_date) +); +``` + +预置如下数据: +```sql +insert into buy_log(userid, buy_date) +values (1, '2022-01-01'), + (2,'2022-03-04'), + (3, '2024-12-31'), + (4, '2024-07-11'), + (5, '2025-01-01'); +``` + +执行`explain select count(*) from buy_log;`语句时,由于where条件为空,其并不会命中索引,但是语句执行的结果如下所示: + +| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| 1 | SIMPLE | buy\_log | null | index | null | idx\_userid | 4 | null | 5 | 100 | Using index | + +上述返回结果中,`possible_keys`列为null,但是实际执行时优化器却选择了`idx_userid`索引,Extra列值为`Using index`,代表优化器选择了覆盖索引。 + +> 由于覆盖索引中记录大小更小,故而使用覆盖索引能够节省io,提升性能。 + +并且,对于`explain select count(*) from buy_log where buy_date >= '2023-01-01' and buy_date <= '2024-01-01';`语句,其返回结果如下 +| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| 1 | SIMPLE | buy\_log | null | index | idx\_userid\_buy\_date | idx\_userid\_buy\_date | 8 | null | 5 | 20 | Using where; Using index | + +正常来说,`idx_userid_buy_date`索引组成为`(userid, buy_date)`,当根据`buy_date`来进行查找时,不应该命中索引。 + +但是,由于辅助索引中同一页包含记录更多,故而上述语句仍然使用了`idx_userid_buy_date`覆盖索引。 + +### 优化器选择不使用索引 +在某些情况下,当使用explain语句进行分析时,会发现优化器没有选择索引去查找数据,而是通过全盘扫描聚簇索引。这种情况通常发生在范围查找,join连接等操作时。 + +#### 辅助索引不能覆盖 +对于辅助索引不能进行覆盖的场景,优化器仅当`查询少量数据`时选择辅助索引。 + +优化器不选择辅助索引的原因是,`根据辅助索引再次查询聚簇索引时,会带来大量的随机读`。由于辅助索引和聚簇索引的排序不同,在辅助索引中连续的数据,很可能在聚簇索引中是分散的。如果使用辅助索引,那么在根据主键查询聚簇索引时,可能带来大量的随机读,主键可能随机分布在各个数据页中,这样会带来大量的io。 + +故而,`当辅助索引需要读取大量的记录,并且辅助索引不能覆盖时,优化器会倾向直接在聚簇索引中进行顺序查找。`顺序读通常要远快于随机读。 + +> 如果mysql server实例部署在固态硬盘上,随机读取的速度很快,同时确认使用辅助索引的性能更佳,可以使用`force index`来强制使用某个索引,使用示例如下: +> +> ```sql +> select * from orderdetails force index (orderID) where orderid > 10000 and order id < 102000 +> ``` + +### 索引提示 +mysql支持索引提示,如下两种场景可能会用到索引提示: +- mysql错误的选择了某个索引,导致sql语句运行较慢 +- 某个sql可选择索引非常多,此时优化器选择执行计划的耗时可能大于sql语句执行的耗时 + +mysql数据库中,索引提示的语法如下; +```sql +table_name [[as] alias] [index_hint_list] + +-- 其中,index_hint_list的含义如下 +index_hint_list: +index_hint[, index_hint]... + +-- index_hint的含义则如下: +index_hint: +USE {INDEX|KEY} +[{FOR {JOIN | ORDER BY | GROUP BY}}] ([index_list]) + +| IGNORE {INDEX | KEY} +[{FOR {JOIN | ORDER BY | GROUP BY}}] (index_list) + +| FORCE {INDEX|KEY} +[{FOR {JOIN | ORDER BY | GROUP BY}}] (index_list) + +-- index_list含义如下: +index_list: +index_name, [, index_name]... +``` + +创建`t_demo`示例如下: +```sql +create table t_demo ( + a int not null, + b int not null, + key idx_a (a), + key idx_b (b) +); + +insert into t_demo(a,b) +values + (1,1), + (1,2), + (2, 3), + (2, 4), + (1,2); +``` +### Use index +执行如下语句`explain select * from t_demo where a=1 and b = 2;`,其返回执行计划如下: +| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| 1 | SIMPLE | t\_demo | null | index\_merge | idx\_a,idx\_b | idx\_b,idx\_a | 4,4 | null | 1 | 83.33 | Using intersect\(idx\_b,idx\_a\); Using where; Using index | + +易知优化器选择了使用`idx_a`和`idx_b`两个索引来完成该查询,且Extra列包含了`Using intersect(b,a)`,代表查询结果根据两个索引得到的结果进行求交集的数学运算。 + +可以通过`Use Index`来提示使用`idx_a`索引: +```sql +explain select * from t_demo use index (idx_a) where a=1 and b = 2; +``` +得到结果如下: +| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| 1 | SIMPLE | t\_demo | null | ref | idx\_a | idx\_a | 4 | const | 3 | 20 | Using where | + +上述结果显示执行方案选用了`idx_a`。 + +> 在使用`use index`语法之后,仍然有可能出现优化器没有使用该索引的情况,此时可以使用`force index`语法,来强制指定使用的索引。 + + + diff --git a/mysql/mysql文档/mysql_表.md b/mysql/mysql文档/mysql_表.md index a4971d2..ea737b3 100644 --- a/mysql/mysql文档/mysql_表.md +++ b/mysql/mysql文档/mysql_表.md @@ -1,3 +1,57 @@ +- [表](#表) + - [索引组织表](#索引组织表) + - [innodb逻辑存储结构](#innodb逻辑存储结构) + - [表空间](#表空间) + - [innodb\_file\_per\_table](#innodb_file_per_table) + - [段(segment)](#段segment) + - [区(Extent)](#区extent) + - [页(Page)](#页page) + - [行](#行) + - [innodb行记录格式](#innodb行记录格式) + - [Compact](#compact) + - [变长字段长度列表](#变长字段长度列表) + - [NULL标志位](#null标志位) + - [记录头信息](#记录头信息) + - [行溢出数据](#行溢出数据) + - [dynamic](#dynamic) + - [char存储结构](#char存储结构) + - [innodb数据页结构](#innodb数据页结构) + - [File Header](#file-header) + - [Infimum和Supremum record](#infimum和supremum-record) + - [user record 和 free space](#user-record-和-free-space) + - [page directory](#page-directory) + - [B+树索引](#b树索引) + - [File Trailer](#file-trailer) + - [完整性校验](#完整性校验) + - [分区表](#分区表) + - [partition keys \& primary keys \& unique keys](#partition-keys--primary-keys--unique-keys) + - [表中不存在唯一索引](#表中不存在唯一索引) + - [后续向分区表添加唯一索引](#后续向分区表添加唯一索引) + - [对非分区表进行分区](#对非分区表进行分区) + - [分区类型](#分区类型) + - [RANGE](#range) + - [information\_schema.partitions](#information_schemapartitions) + - [看select语句查询了哪些分区](#看select语句查询了哪些分区) + - [插入超过分区范围的数据](#插入超过分区范围的数据) + - [向分区表中添加分区](#向分区表中添加分区) + - [向分区表中删除分区](#向分区表中删除分区) + - [LIST](#list) + - [HASH](#hash) + - [新增HASH分区](#新增hash分区) + - [减少HASH分区](#减少hash分区) + - [LINEAR HASH](#linear-hash) + - [KEY \& LINEAR KEY](#key--linear-key) + - [COLUMNS](#columns) + - [range columns](#range-columns) + - [list columns](#list-columns) + - [子分区](#子分区) + - [分区中的NULL值](#分区中的null值) + - [分区和性能](#分区和性能) + - [不分区](#不分区) + - [按id进行分区](#按id进行分区) + - [在表和分区之间交换数据](#在表和分区之间交换数据) + + # 表 ## 索引组织表 innodb存储引擎中,表都是根据主键顺序组织存放的,这种存储方式被称为索引组织表(index organized table)。在innodb存储引擎表中,每张表都有主键(primary key),如果在创建表时没有显式指定主键,那么innodb会按照如下方式创建主键: @@ -638,4 +692,22 @@ mysql允许对null值做分区,但`mysql数据库中的分区总是视null值 - 对于按`code`进行查找的语句`select * from t where code = xxx` - 对于按code进行查找的语句,需要扫描所有的10个分区,每个分区大概需要2次磁盘io,故而总共的磁盘io大约为20次 -> 在使用分区表时,应尽量小心,不正确的使用分区将可能会带来大量的io,造成性能瓶颈 \ No newline at end of file +> 在使用分区表时,应尽量小心,不正确的使用分区将可能会带来大量的io,造成性能瓶颈 + +### 在表和分区之间交换数据 +mysql支持在表分区和非分区表之间交换数据: +- 在非分区表为空的场景下,相当于将分区中的数据移动到非分区表中 +- 在表分区为空的场景下,相当于将非分区表中的数据移动到表分区中 + +在表和分区之间交换数据,可以通过`alter table ... exchange partition`语句,且必须满足如下条件: +- 要交换的非分区表和分区表必须要拥有相同的结构,但是,非分区表中不能够含有分区 +- 非分区表中的数据都要位于表分区的范围内 +- 被交换的表中不能含有外键,或是其他表含有被交换表的外键引用 +- 用户需要拥有`alter, insert, create, drop`权限 +- 使用`alter table ... exchange parition`语句时,不会触发交换表和被交换表上的触发器 +- `auto_increment`将会被重置 + +`exchange partition ... with table`的示例如下: +```sql +alter table p_range_columns_t exchange partition p_2024 with table np_t +```