Files
rikako-note/mysql/mysql文档/mysql_表.md
2025-02-15 16:28:43 +08:00

714 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

- [](#表)
- [索引组织表](#索引组织表)
- [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会按照如下方式创建主键
- 首先判断表中是否存在非空的唯一索引unique not null字段如果有则其为主键
- 如果不存在非空唯一索引那么innodb会自动创建一个6字节大小的指针作为主键
如果有多个非空唯一索引innodb存储引擎将会选择第一个定义的非空唯一索引作为主键。
## innodb逻辑存储结构
在innodb的存储逻辑结构中所有的数据都被逻辑存放在表空间table space中。表空间则由`段segementextentpage`组成。
组成如图所示:
<img src="https://pic2.zhimg.com/v2-7a3fe8eb03c68e1378f24847e464e139_1440w.jpg" data-caption="" data-size="normal" data-rawwidth="421" data-rawheight="275" data-original-token="v2-d7e41f080ece2820138fd0331f965a79" class="origin_image zh-lightbox-thumb" width="421" data-original="https://pic2.zhimg.com/v2-7a3fe8eb03c68e1378f24847e464e139_r.jpg">
### 表空间
表空间为innodb存储引擎逻辑结构的最高层所有数据都存放于表空间中。innodb存在一个默认的共享表空间`ibdata1`,在开启`innodb_file_per_table`参数后,每张表内的数据可以单独存放到一个表空间。
#### innodb_file_per_table
`innodb_file_per_table`参数启用会导致每张表的`数据、索引、插入缓冲bitmap页`存放到单独的文件中;但是其他数据,例如`回滚undo信息插入缓冲页系统事务信息double write buffer`等还是存放在默认的共享表空间中。
### 段segment
如上图所示表空间是由段segment所组成的常见的段分为`数据段,索引段,回滚段`等。
在innodb存储引擎中数据即索引索引即数据。`数据段即为B+树的叶子节点(Leaf node segment)`,`索引段即为B+树的非叶子节点(Non-leaf node segment)`
### 区Extent
区是由连续页组成的空间在任何情况下每个区的大小都为1MB。为了保证区中页的连续性innodb存储引擎会一次性从磁盘申请4~5个区。在默认情况下innodb存储引擎中页的大小为`16KB`,一个区中包含64个页。
在新建表时新建表的大小为96KB小于一个Extent的大小1MB因为每个段Segmenet开始时都会有至多32个页大小的碎片页等使用完这些页后才会申请64个连续页作为Extent。
都与一些小表或是undo这样的段可以在开始时申请较少的空间节省磁盘容量开销。
### 页Page
innodb存储引擎中页的大小默认为`16KB`,默认的页大小可以通过`innodb_page_size`参数进行修改。通过该参数可以将innodb的默认页大小设置为4K8K。
页是innodb磁盘管理的最小单位在innodb中常见的页有:
- 数据页B-tree node
- undo页undo log page
- 系统页system page
- 事务数据页transaction system page
- 插入缓冲位图页insert buffer bitmap
- 插入缓冲空闲列表页insert buffer free list
- 未压缩的二进制大对象页uncompressed blob page
- 压缩的二进制大对象页compressed blob page
### 行
innodb存储引擎是面向行的数据按行进行存放。每个页中至多可以存放`16KB/2 - 200`行的记录即7992行记录。
## innodb行记录格式
innodb存储引擎以行的形式进行存储可以通过`show table status like '{table_name}'`的语句来查询表的行格式,示例如下:
```sql
show table status like 'demo_t1'
```
| Name | Engine | Version | Row\_format | Rows | Avg\_row\_length | Data\_length | Max\_data\_length | Index\_length | Data\_free | Auto\_increment | Create\_time | Update\_time | Check\_time | Collation | Checksum | Create\_options | Comment |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| demo\_t1 | InnoDB | 10 | Dynamic | 0 | 0 | 16384 | 0 | 32768 | 0 | 1 | 2025-01-30 15:18:57 | null | null | utf8mb4\_0900\_ai\_ci | null | | |
上述示例中表的row_format为dynamic。
### Compact
在使用Compact行记录格式时一个页中存放数据越多其性能越高。
Compact格式下行记录的存储格式如下
<table>
<tr>
<td>变长字段长度列表</td>
<td>NULL标志位</td>
<td>记录头信息</td>
<td>列1数据</td>
<td>列2数据</td>
<td>......</td>
</tr>
</table>
#### 变长字段长度列表
Compact行记录格式其首部是一个`非Null变长字段长度`列表,其按照列的顺序逆序放置,变长列的长度为:
- 如果列的长度小于255字节用1字节表示
- 如果列的长度大于255字节用2个字节表示
变长字段的长度不能小于2字节因为varchar类型最长长度限制为65535。
#### NULL标志位
NULL标志位为bitmap代表每一列是否为空。如果行中存在n个字段可为空那么NULL标志位部分的长度为ceiling(n/8)。
#### 记录头信息
record header固定占用5字节其中record header的各bit含义如下所示
| 名称 | 大小(bit) | |
| :-: | :-: | :-: |
| () | 1 | 未知 |
| () | 1 | 未知 |
| deleted_flag | 1 | 该行是否已经被删除 |
| min_rec_flag | 1 | 该行是否为预订被定义的最小记录行 |
| n_owned | 4 | 该记录拥有的记录数 |
| heap_no | 13 | 索引堆中该条记录的排序记录 |
| record_type | 3 | 记录类型000代表普通001代表B+树节点指针, 010代表infimum011代表supremum1xx保留 |
| next_record | 16 | 页中下一条记录相对位置 |
除了上述3个部分之外其他部分就是各个列的实际值。
> 在Compact格式中NULL除了占有NULL标志位外不占用任何实际空间。
> 每行数据中,除了有用户自定义的列外,还存在两个隐藏列,即`事务id列`和`回滚指针列`长度分别为6字节和7字节。
>
> 如果innodb表没有自定义主键每行还会增加一个rowid列。
### 行溢出数据
innodb存储引擎可能将一条记录中某些数据存储在真正的存储数据页面之外。一般来说blob、lob这类大对象的存储位于数据页面之外。
当行数据的大小特别大导致一个页无法存放2条行数据时innodb会自动将占用空间大的`blob`字段或`varchar`字段值放到额外的uncompressed blob page中。
### dynamic
dynamic格式几乎和compact格式相同但是对于每个blob字段其存储只消耗20字节用于存储指针。
而对于Compact格式其会在blob格式中存储768字节的前缀字节。
### char存储结构
`char(n)`字段中n代表`字符长度`而非字节长度故而在不同字符集下char类型字段的内部存储可能不是定长的。
例如在utf8字符集下`ab``我们`两个字符串其字符数都是2个但是`ab`其占用2字节`我们`占用4字节即使同样是`char(2)`类型的字符串,其占用字节数量仍然有可能不同。
## innodb数据页结构
innodb中页是磁盘管理的最小结构页类型为B-tree Node的页存放的即是表中行的实际数据。
innodb数据页由如下7个部分组成
- File Header文件头
- Page Header页头
- Infimum和Supremum Records
- user records用户记录即行记录
- free space空闲空间
- page directory页目录
- file trailer文件结尾信息
file headerpage headerfile trailer的大小是固定的分别为38568字节这些空间用于标记页的一些信息例如checksum数据页所在B+树的层数等。
<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d066e690d22484ebe33bbb4977c3cfb~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp" alt="image.png" loading="lazy" class="medium-zoom-image">
### File Header
File Header用于记录页的一些头信息由8个部分组成共占38字节
| 名称 | 大小 | 说明 |
| :-: | :-: | :-: |
| FIL_PAGE_SPACE_OR_CHKSUM | 4 | 代表页的checksum值 |
| FIL_PAGE_OFFSET | 4 | 表空间中页的偏移位置 |
| FIL_PAGE_PREV | 4 | 当前页的上一个页B+树决定叶子节点为双向列表 |
| FIL_PAGE_NEXT | 4 | 当前页的下一个页 |
| FIL_PAGE_LSN | 8 | 代表该页最后被修改的日志序列位置LSN |
| FIL_PAGE_TYPE | 2 | 存储引擎页类型 |
| FIL_PAGE_FILE_FLUSH_LSN | 8 | 该值仅在系统表空间的一个页中定义代表文件至少被更新到了该LSN值对于独立的表空间该值为0 |
| FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 代表页属于哪个表空间 |
### Infimum和Supremum record
在innodb存储引擎中每个数据页都有两行虚拟的行记录用于限定记录边界。Infimum是比该页中所有主键值都要小的值Supremum是比任何可能值都要大的值。`这两个值在页创建时被建立,并且在任何情况下都不会被删除。`
### user record 和 free space
user reocrd代表实际存储行记录中的内容free space则是代表空闲空间同样是链表数据结构在一条记录被删除后该空间会被加入到空闲列表中。
### page directory
page directory页目录中存放了记录的相对位置这些记录指针被称为目录槽directory slots`innodb中槽是一个稀疏目录一个槽中可能包含多个记录`记录Infimum的n_owned总为1记录Supremum的n_owned取值范围为`[1,8]`其他用户记录的n_owned为`[4,8]`。当记录被插入或删除时,需要对槽进行分裂或平衡的维护操作。
在slots中记录按照索引键值顺序存放可以通过二叉查询迅速找到记录的指针。
由于在innodb存储引擎中page directory是稀疏目录二叉查找结果只是一个粗略的结果innodb存储引擎必须通过record header中的next_record来继续查找相关记录。
#### B+树索引
<img src="//cloud.mo4tech.com/images/ldWYtlmLrJXYtJXZ0F2dtA3YmBnYmFTdzsWL2xGc05XYhR2MwIjZycTMyczN4gDOmBTY0Y2Y5QmYygjY1YTNk9CcjZGciZWM1NzatkWLuNWLz9Gdv02bj5yZtlWZ0lnYu4WaqVWdq1iNw9yL6MHc0RHa/cefbe79c0791cd0781f7c6aa3a923e21.image" data-cdn="">
如上图所示innodb中的数据是按照索引来进行组织的。但是通过B+树索引,其无法直接查询到指定的记录,其只能查询到记录所位于的页。
例如,在上图示例中,其在查询`ID=32`的时只能查询到该记录位于page 17中。
> 在B+树的叶子节点每个页的File Header中都存有指向前一个页和后一个页的指针`故而每个页之间是通过双向列表结构来维护的`;但是对于页中的记录,`记录与记录之间维维护的时单向列表的关系`。
>
> 对于Compact或Dynamic行格式的页其每条记录的record header中都包含一个next_record字段指向下一条记录。故而位于同一个页中的记录可以单向访问。
但是在一个页内查找某条记录时沿着单向链表进行查找其效率很低。故而page中的数据时机被分为了多个组被分为的组构成了一个subdirectory故而通过子目录能够缩小查询范围提高查询性能。
page directory是一个能够存储多个slots的部分每个slot中存储了group中最后一条记录的相对位置。假设slot中最大一条记录为`r`那么group中记录的条数被存储在`r`记录record header的`n_owned`字段中。
group中record的数量约束如下
- infimum group中records数量限制为`[1,8]`
- supremum group中records数量限制为`[1,8]`
- 其他group中records限制为`[4,8]`
page directory生成过程如下所示
- 最开始时页中只有infimum和supremum两条记录它们分别属于两个group。page directory中有两个slots指向这两条记录两个slot的n_owned都为1
- 后续当新的记录被插入页时系统会查找page directory中主键值大于待插入记录的第一个slot。slot对应最大记录的`n_owned`字段会增加1代表group中新插入了记录直到group中的记录数量到达8
- 当新纪录被插入到的group中记录数大于8时group中的记录被分为2个group一个group包含4条记录另一个group包含5条记录。该过程将会向page directory中新增一个新的slot
- 当记录被删除时slot最最大一条记录的`n_owned`将会减少1当n_owned字段小于4时会触发再平衡操作平衡后的page directory满足上述要求
page directory中的slots数量如page header中的`PAGE_N_DIR_SLOTS`所示。
一旦innodb中页包含page directory后其会通过二分查找快速的定位slot并且从group中最小记录开始通过`next_record`指针来遍历页中的记录列表。这样能够快速的定位记录位置。
### File Trailer
#### 完整性校验
在innodb中页的大小为16KB可能和磁盘的扇区大小不同。通常磁盘的扇区大小为512字节故而在写入一个页到磁盘中时需要分32个扇区进行写入。
在写入一个页时可能会发生宕机场景这时一个页只写入了一部分可能会发生脏写。此时可以通过double write buffer机制对脏写的页进行恢复。
在一个页被写入到磁盘中时首先会被写入的是File Header的`FIL_PAGE_SPACE_OR_CHKSUM`,该部分是page的checksum。
innodb设置了File Trailer部分来校验page是否被完全写入到磁盘中File Trailer中只包含一个`FIL_PAGE_END_LSN`部分占用8字节前4字节代表该页的checksum值后4字节和File Header中的`FIL_PAGE_LSN`相同,`代表最后一次修改该页关联的LSN位置`。将上述两个字段和File Header中的`FIL_PAGE_SPACE_OR_CHKSUM``FIL_PAGE_LSN`值进行比较,查看是否一致,从而保证页的完整性。
默认情况下在innodb每次从磁盘读取page时都会检查页面的完整性。
## 分区表
innodb存储引擎支持分区功能分区过程将一个表或索引分为多个更小、更可管理的部分。
对于访问数据库的应用而言,逻辑上的一个表或索引,其物理上可能由多个物理分区组成,每个分区都是独立的对象,既可以独自进行处理,又可以作为一个更大对象的一部分进行处理。
> mysql仅支持水平分区将同一张表中的不同行记录分配到不同的物理文件中并不支持垂直分区将同一张表中的不同列分配到不同的物理文件中
目前mysql支持如下几种类型的分区
- `RANGE`: 行数据基于一个给定连续区间的列值被放入分区
- `LIST`: 和`RANGE`类似,但`LIST`分区面对的是更加离散的值
- `HASH`: 根据用户自定义的表达式返回值来进行分区,返回值不能为负数
- `KEY`:根据mysql提供的哈希函数来进行分区
`不论创建何种类型的分区,如果表中存在唯一索引或主键,分区列必须是唯一索引的一个组成部分。`
### partition keys & primary keys & unique keys
对于分区表而言,`所有在partition expression中使用到的col其必须被包含在分区表所有的唯一索引中`
> `所有唯一索引`中包含主键索引。
正确建立分区表的声明示例如下:
```sql
CREATE TABLE t1 (
col1 INT NOT NULL,
col2 DATE NOT NULL,
col3 INT NOT NULL,
col4 INT NOT NULL,
UNIQUE KEY (col1, col2, col3)
)
PARTITION BY HASH(col3)
PARTITIONS 4;
CREATE TABLE t2 (
col1 INT NOT NULL,
col2 DATE NOT NULL,
col3 INT NOT NULL,
col4 INT NOT NULL,
UNIQUE KEY (col1, col3)
)
PARTITION BY HASH(col1 + col3)
PARTITIONS 4;
CREATE TABLE t7 (
col1 INT NOT NULL,
col2 DATE NOT NULL,
col3 INT NOT NULL,
col4 INT NOT NULL,
PRIMARY KEY(col1, col2)
)
PARTITION BY HASH(col1 + YEAR(col2))
PARTITIONS 4;
CREATE TABLE t8 (
col1 INT NOT NULL,
col2 DATE NOT NULL,
col3 INT NOT NULL,
col4 INT NOT NULL,
PRIMARY KEY(col1, col2, col4),
UNIQUE KEY(col2, col1)
)
PARTITION BY HASH(col1 + YEAR(col2))
PARTITIONS 4;
create table t2_partition (
col1 int ,
col2 date,
col3 int null,
col4 int null,
unique index `idx_t2_partition_cols` (col1, col2,col3),
primary key (col2, col1)
)
partition by hash(col1)
partitions 4;
```
#### 表中不存在唯一索引
如果表中没有唯一索引也未定义primary key那么上述要求并不会生效可以在partition expression中使用任意cols。
#### 后续向分区表添加唯一索引
如果想要向分区表中添加唯一索引那么新增的唯一索引中必须包含partition expression中所有的列。
#### 对非分区表进行分区
可以参照如下示例对非分区表进行分区:
```sql
CREATE TABLE np_pk (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(50),
added DATE,
PRIMARY KEY (id)
);
```
可以按`id`进行分区
```sql
ALTER TABLE np_pk
PARTITION BY HASH(id)
PARTITIONS 4;
```
### 分区类型
#### RANGE
RANGE为较常见的分区类型如下示例为创建RANGE类型分区表的示例
```sql
create table t1 (
id bigint auto_increment not null,
tran_date datetime(6) not null default now(6) on update now(6),
primary key (id, tran_date)
) engine=innodb
partition by range (year(tran_date)) (
partition part_y_let_2024 values less than (2025),
partition part_y_eq_2025 values less than (2026)
);
```
上述创建了表`t1`t1主键为`(id, tran_date)`,且按照`tran_date`字段的年部分进行分区分区类型为RANGE。
表t1的分区为两部分分区1为`tran_date位于2024或之前年份`的分区`part_y_let_2024`分区2是`tran_date位于2025年`的分区`part_y_eq_2025`
##### information_schema.partitions
如果要查看表的分区情况,可以查询`information_schema.partitions`表,示例如下
```sql
select * from information_schema.partitions where table_schema = 'learn_innodb' and table_name = 't1';
```
查询结果如下所示:
| TABLE\_CATALOG | TABLE\_SCHEMA | TABLE\_NAME | PARTITION\_NAME | SUBPARTITION\_NAME | PARTITION\_ORDINAL\_POSITION | SUBPARTITION\_ORDINAL\_POSITION | PARTITION\_METHOD | SUBPARTITION\_METHOD | PARTITION\_EXPRESSION | SUBPARTITION\_EXPRESSION | PARTITION\_DESCRIPTION | TABLE\_ROWS | AVG\_ROW\_LENGTH | DATA\_LENGTH | MAX\_DATA\_LENGTH | INDEX\_LENGTH | DATA\_FREE | CREATE\_TIME | UPDATE\_TIME | CHECK\_TIME | CHECKSUM | PARTITION\_COMMENT | NODEGROUP | TABLESPACE\_NAME |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| def | learn\_innodb | t1 | part\_y\_eq\_2025 | null | 2 | null | RANGE | null | year\(\`tran\_date\`\) | null | 2026 | 0 | 0 | 16384 | 0 | 0 | 0 | 2025-02-08 01:05:53 | null | null | null | | default | null |
| def | learn\_innodb | t1 | part\_y\_let\_2024 | null | 1 | null | RANGE | null | year\(\`tran\_date\`\) | null | 2025 | 0 | 0 | 16384 | 0 | 0 | 0 | 2025-02-08 01:05:53 | null | null | null | | default | null |
为分区表预置如下数据
```sql
insert into t1(tran_date) values ('2023-01-01'), ('2024-12-31'), ('2025-01-01');
```
可以看到分区表中数据分布如下:
```sql
select partition_name, table_rows from information_schema.partitions where table_schema = 'learn_innodb' and table_name = 't1';
```
| PARTITION\_NAME | TABLE\_ROWS |
| :--- | :--- |
| part\_y\_eq\_2025 | 1 |
| part\_y\_let\_2024 | 2 |
易知`('2023-01-01'), ('2024-12-31')`数据位于`part_y_let_2024`分区,故而该分区存在两条数据;而`('2025-01-01')`数据位于`part_y_let_2024`分区,该分区存在一条数据。
##### 看select语句查询了哪些分区
数据分布可以通过如下方式验证:
```sql
explain select * from t1 where tran_date >= '2023-01-01' and tran_date <= '2024-12-31';
```
| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | SIMPLE | t1 | part\_y\_let\_2024 | index | PRIMARY | PRIMARY | 16 | null | 2 | 50 | Using where; Using index |
```sql
explain select * from t1 where tran_date >= '2025-01-01'
```
| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | SIMPLE | t1 | part\_y\_eq\_2025 | index | PRIMARY | PRIMARY | 16 | null | 1 | 100 | Using where; Using index |
##### 插入超过分区范围的数据
如果尝试向分区表中插入超过所有分区范围的数据,会执行失败,示例如下:
```sql
insert into t1(tran_date) values ('2026-01-01');
```
由于分区表`t1`中现有分区最大只支持2025年的分区故而当尝试插入2026年分区时会抛出如下错误
```bash
[2025-02-08 01:32:51] [HY000][1526] Table has no partition for value 2026
```
##### 向分区表中添加分区
mysql支持向分区表中添加分区语法如下
```sql
alter table t1 add partition (
partition part_year_egt_2026 values less than maxvalue
);
```
添加分区后mysql支持`tran_date大于等于2026`的分区,执行如下语句
```sql
insert into t1(tran_date) values ('2026-01-01'), ('2027-12-31'), ('2028-01-01');
```
之后,再次分区分区中的数据分区,结果如下
| PARTITION\_NAME | TABLE\_ROWS |
| :--- | :--- |
| part\_y\_eq\_2025 | 1 |
| part\_y\_let\_2024 | 2 |
| part\_year\_egt\_2026 | 3 |
> `information.partitions`表中,`table_rows`字段可能存在不准确的情况,如果想要获取准确的值,需要执行`analyze table {schema}.{table_name}`语句
##### 向分区表中删除分区
当想要删除分区表中现存分区时,可以通过执行如下语句
```sql
alter table t1 drop partition part_year_egt_2026;
```
在删除分区后,分区中的数据也会被删除,执行查询语句后
```sql
select * from t1;
```
可知`part_year_egt_2026`被删除后分区中数据全部消失
| id | tran\_date |
| :--- | :--- |
| 1 | 2023-01-01 00:00:00.000000 |
| 2 | 2024-12-31 00:00:00.000000 |
| 3 | 2025-01-01 00:00:00.000000 |
#### LIST
LIST分区类型和RANGE分区类型非常相似但是LIST分区列的值是离散的RANGE分区列的值是连续的。
创建LIST类型分区表的示例如下
```sql
create table p_list_t1 (
id bigint not null auto_increment,
area smallint not null,
primary key (id, area)
)
partition by list (area) (
partition p_ch_beijing values in (1),
partition p_ch_hubei values in (2),
partition p_ch_jilin values in (3)
);
```
之后可向`p_list_t1`表中预置数据,执行语句如下:
```sql
insert into p_list_t1(area) values (1),(1), (2), (3), (3);
```
此时,数据分布如下:
| PARTITION\_METHOD | PARTITION\_NAME | TABLE\_ROWS |
| :--- | :--- | :--- |
| LIST | p\_ch\_beijing | 2 |
| LIST | p\_ch\_hubei | 1 |
| LIST | p\_ch\_jilin | 2 |
如果想要向分区表中插入不位于现存分区中的值,那么插入语句同样会执行失败,示例如下
```sql
insert into p_list_t1(area) values (4);
```
由于目前没有area为4的分区故而插入语句执行失败报错如下
```bash
[2025-02-09 20:30:03] [HY000][1526] Table has no partition for value 4
```
#### HASH
HASH分区方式是将数据均匀分布到预先定义的各个分区中期望各个分区中散列的数据数量大致相同。
在通过hash来进行分区时建表时需要指定一个分区表达式该表达式返回整数。
创建hash分区的示例如下
```sql
create table p_hash_t1 (
id bigint not null auto_increment,
content varchar(256),
primary key (id)
)
partition by hash (id)
partitions 4;
```
上述ddl创建了一个按照`id`列进行hash分区的分区表分区表包含4个分区。
预置数据sql如下
```sql
insert into p_hash_t1(content) values ('asahi'), ('maki'), ('katahara');
```
预置数据后数据分布如下
| PARTITION\_METHOD | PARTITION\_NAME | TABLE\_ROWS |
| :--- | :--- | :--- |
| HASH | p0 | 0 |
| HASH | p1 | 1 |
| HASH | p2 | 1 |
| HASH | p3 | 1 |
预置的三条数据id分别为`1, 2, 3`被hash放置到分区`p1, p2, p3`中。
> 当使用hash分区方式时例如存在`4`个分区,对于待插入数据其分区表达式的值为`2`,那么待插入数据将会被插入到`2%4 = 2`,第`3`个分区中(0,1,2分区排第三),也就是`p2`。
>
> 可以通过`explain select * from p_hash_t1 where id = 2;`语句来进行验证,得到如下结果。
>
> | id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra |
> | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
> | 1 | SIMPLE | p\_hash\_t1 | p2 | const | PRIMARY | PRIMARY | 8 | const | 1 | 100 | null |
##### 新增HASH分区
对于hash分区方式可以通过如下方式来新增分区
```sql
alter table p_hash_t1 add partition partitions 3;
```
其会新增3个分区执行后分区数量为7。
此时,原本分区中的数据会被重新散列到新分区中
##### 减少HASH分区
对于hash分区在尝试减少分区数量的同时通常不希望删除任何数据故而无法使用`drop`
故而,可以使用`coalesce`,其只会减少分区数量,不会删除数据。
示例如下:
```sql
alter table p_hash_t1 coalesce partition 4;
```
上述语句会将分区数量减少4个故而减少后分区数量为`3`
#### LINEAR HASH
创建linear hash分区表的方式和hash类似可以通过如下方式
```sql
create table p_linear_hash_t1 (
id bigint not null auto_increment,
content varchar(256),
primary key(id)
)
partition by linear hash (id)
partitions 5;
```
相较于hash分区在使用linear hash分区时添加、删除、合并、拆分分区将会变得更加快捷但是相比于hash分区其数据分区可能会不太均匀。
#### KEY & LINEAR KEY
KEY分区方式和HASH分区方式类似但是HASH分区采用用户自定义的函数进行分区而KEY分区方式则是采用mysql提供的函数进行分区。
创建key分区表的示例如下
```sql
create table p_key_t1 (
id bigint not null auto_increment,
content varchar(256),
primary key(id)
)
partition by key(id)
partitions 5;
```
> key分区方式同样可以使用linear进行修饰效果和hash分区方式类似。
> 对于key分区可以指定除了integer之外的col类型而`range, list, hash`等分区类型只支持integer类型的col。
#### COLUMNS
COLUMNS分区方式为`RANGE``LIST`分区的变体。
COLUMNS分区方式支持在分区时使用多个列在插入新数据或查询数据决定分区时所有的列都会被考虑。
COLUMNS分区方式分为如下两种
- RANGE COLUMNS
- LIST COLUMNS
上述两种方式都支持非整数类型的列COLUMNS支持的数据类型如下
- 所有integers类型DECIMAL, FLOAT则不受支持
- DATE和DATETIME类型
- 字符串类型CHAR, VARCHAR, BINARY, VARBINARY(TEXT, BLOB类型不受支持)
##### range columns
创建range columns分区表的示例如下
```sql
create table p_range_columns_t1 (
id bigint not null auto_increment,
name varchar(256),
born_ts datetime(6),
primary key (id, born_ts)
)
partition by range columns (born_ts) (
partition p0 values less than ('1900-01-01'),
partition p1 values less than ('2000-01-01'),
partition p2 values less than ('2999-01-01')
);
```
##### list columns
创建list columns分区表的示例则如下
```sql
create table p_list_columns_t1 (
id bigint not null auto_increment,
province varchar(256),
city varchar(256),
primary key (id, province, city)
)
partition by list columns (province, city) (
partition p_beijing values in (('china', 'beijing')),
partition p_wuhan values in (('hubei', 'wuhan'))
);
```
> 通过range columns和list columns可以很好的替代range和list分区而无需将列值都转换为整型。
### 子分区
`子分区`代表在分区的基础上再次进行分区mysql允许`在range和list分区的基础`上再次进行`hash或key`的子分区,示例如下:
```sql
create table p_list_columns_t1 (
id bigint not null auto_increment,
province varchar(256),
city varchar(256),
primary key (id, province, city)
)
partition by list columns (province, city)
subpartition by key(id)
subpartitions 3
(
partition p_beijing values in (('china', 'beijing')),
partition p_wuhan values in (('hubei', 'wuhan'))
);
```
上述示例先按照`list columns`分区方式进行了分区,然后在为每个分区都按`key`分了三个子分区,查看分区和子分区详情可以通过如下语句:
```sql
select partition_name, partition_method, subpartition_name, subpartition_method from information_schema.partitions where table_name = 'p_list_columns_t1';
```
| PARTITION\_NAME | PARTITION\_METHOD | SUBPARTITION\_NAME | SUBPARTITION\_METHOD |
| :--- | :--- | :--- | :--- |
| p\_wuhan | LIST COLUMNS | p\_wuhansp0 | KEY |
| p\_wuhan | LIST COLUMNS | p\_wuhansp1 | KEY |
| p\_wuhan | LIST COLUMNS | p\_wuhansp2 | KEY |
| p\_beijing | LIST COLUMNS | p\_beijingsp0 | KEY |
| p\_beijing | LIST COLUMNS | p\_beijingsp1 | KEY |
| p\_beijing | LIST COLUMNS | p\_beijingsp2 | KEY |
### 分区中的NULL值
mysql允许对null值做分区`mysql数据库中的分区总是视null值小于任何非null的值`,该逻辑`和order by处理null值的逻辑一致`
故而在使用不同的分区类型时对于null值的处理逻辑如下
- 对于range类型分区当插入null值时其会被放在最左侧的分区中
- 对于list类型的分区如果要插入null值必须在分区的`values in (...)`表达式中指定null值否则插入语句将会报错
- hash和key的分区类型将会将null值当作`0`来散列
### 分区和性能
对于OLTP类型的应用使用分区表时应该相当小心因为分区表可能会带来严重的性能问题。
例如对于包含1000w条数据的表`t`,如果包含主键索引`id`和非主键索引`code`,分区和不分区,其性能分析如下
#### 不分区
如果不对表t进行分区那么根据`id`(唯一主键索引)或`code`非unique索引进行查询例如`select * from t where id = xxx``select * from t where code = xxx`可能只会进行2~3次磁盘io1000w数据构成的B+树其层高为2~3
#### 按id进行分区
如果对表`t`按照`id`进行`hash`分区分为10个分区那么
- 对于按`id`进行查找的语句`select * from t where id = xxx`
- 将t按id分为10个区后如果分区均匀那么每个分区数据大概为100w对分区的查询开销大概为2次磁盘io
- 根据`id`的查询只会在一个分区内进行查找故而磁盘io会从3次减少为2次可以提升查询效率
- 对于按`code`进行查找的语句`select * from t where code = xxx`
- 对于按code进行查找的语句需要扫描所有的10个分区每个分区大概需要2次磁盘io故而总共的磁盘io大约为20次
> 在使用分区表时应尽量小心不正确的使用分区将可能会带来大量的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
```