MySQL 分区表,为什么分区键必须是主键的一部分?
随着业务的不断发展,数据库中的数据会越来越多,相应地,单表的数据量也会越到越大,大到一个临界值,单表的查询性能就会下降。
这个临界值,并不能一概而论,它与硬件能力、具体业务有关。
虽然在很多 MySQL 运维规范里,都建议单表不超过 500w、1000w。
但实际上,我在生产环境,也见过大小超过 2T,记录数过亿的表,同时,业务不受影响。
单表过大时,业务通常会考虑两种拆分方案:水平切分和垂直切分。
水平拆分 VS 垂直拆分
水平切分,拆分的维度是行,一般会根据某种规则或算法将表中的记录拆分到多张表中。
拆分后的表既可在一个实例,也可在多个不同实例中。如果是后者,又会涉及到分布式事务。
垂直切分,拆分的维度是列,一般是将列拆分到多个业务模块中。这种拆分更多的是上层业务的拆分。
从改造的复杂程度来说,前者小于后者。
所以,在单表数据量过大时,业界用得较多的还是水平拆分。
常见的水平拆分方案有:分库分表、分区表。
虽然分库分表是一个比较彻底的水平拆分方案,但一方面,它的改造需要一定的时间;另一方面,它对开发的能力也有一定的要求。相对来说,分区表就比较简单,也无需业务改造。
分区表
很多人可能会认为 MySQL 的优势在于 OLTP 应用,对于 OLAP 应用就不太适合,所以,也不太推荐分区表这种偏 OLAP 的特性。
但实际上,对于某些业务类型,还是比较适合使用分区表的,尤其是那些有明显冷热数据之分,且数据的冷热与时间相关的业务。
下面,我们看看分区表的优点:
提升查询性能
对于分区表的查询操作,如果查询条件中包含分区键,则这个查询操作就只会被下推到符合条件的分区内进行,无关分区将自动过滤掉。
在数据量比较大的情况下,能提升查询速度。
对业务透明
将表从一个非分区表转换为分区表,业务端无需做任何改造。
管理方便
在对单个分区进行删除、迁移和维护时,不会影响到其它分区。
尤其是针对单个分区的删除(DROP)操作,避免了针对这个分区所有记录的 DELETE 操作。
遗憾的是,MySQL 分区表不支持并行查询。理论上,当一个查询涉及到多个分区时,分区与分区之间应进行并行查询,这样才能充分利用多核 CPU 资源。
但 MySQL 并不支持,包括早期的官方文档,也提到了这个问题,也将这个功能的实现放到了优先级列表中。
These features are not currently implemented in MySQL Partitioning, but are high on our list of priorities. - Queries involving aggregate functions such as SUM() and COUNT() can easily be parallelized. A simple example of such a query might be SELECT salesperson_id, COUNT(orders) as order_total FROM sales GROUP BY salesperson_id;. By “parallelized,” we mean that the query can be run simultaneously on each partition, and the final result obtained merely by summing the results obtained for all partitions. - Achieving greater query throughput in virtue of spreading data seeks over multiple disks.
MySQL 8.0 中分区表的变化
在 MySQL 5.7 中,对于分区表,有个很重大的更新,即 InnoDB 存储引擎原生支持了分区,无需再通过 ha_partition 接口来实现。
所以,在 MySQL 5.7 中,如果要创建基于 MyISAM 存储引擎的分区表,会提示 warning 。
The partition engine, used by table 'sbtest.t_range', is deprecated and will be removed in a future release. Please use native partitioning instead.
而在 MySQL 8.0 中,则更为彻底,server 层移除了 ha_partition 接口代码。
如果要使用分区表,只能使用支持原生分区的存储引擎。在 MySQL 8.0 中,就只有 InnoDB。
这就意味着,在 MySQL 8.0 中,如果要创建 MyISAM 分区表,基本上就不可能了。
这也从另外一个角度说明了为什么生产上不建议使用 MyISAM 表。
mysql> CREATE TABLE t_range (
-> id INT,
-> name VARCHAR(10)
-> ) ENGINE = MyISAM
-> PARTITION BY RANGE (id) (
-> PARTITION p0 VALUES LESS THAN (5),
-> PARTITION p1 VALUES LESS THAN (10)
-> );
ERROR 1178 (42000): The storage engine for the table doesn't support native partitioning
为什么分区键必须是主键的一部分?
在使用分区表时,大家常常会碰到下面这个报错。
mysql> CREATE TABLE opr (
-> opr_no INT,
-> opr_date DATETIME,
-> description VARCHAR(30),
-> PRIMARY KEY (opr_no)
-> )
-> PARTITION BY RANGE COLUMNS (opr_date) (
-> PARTITION p0 VALUES LESS THAN ('20210101'),
-> PARTITION p1 VALUES LESS THAN ('20210102'),
-> PARTITION p2 VALUES LESS THAN MAXVALUE
-> );
ERROR 1503 (HY000): A PRIMARY KEY must include all columns in the table's partitioning function (prefixed columns are not considered).
即分区键必须是主键的一部分。
上面的 opr 是一张操作流水表。其中,opr_no 是操作流水号,一般都会被设置为主键,opr_date 是操作时间。基于操作时间来进行分区,是一个常见的分区场景。
为了突破这个限制,可将 opr_date 作为主键的一部分。
mysql> CREATE TABLE opr (
-> opr_no INT,
-> opr_date DATETIME,
-> description VARCHAR(30),
-> PRIMARY KEY (opr_no, opr_date)
-> )
-> PARTITION BY RANGE COLUMNS (opr_date) (
-> PARTITION p0 VALUES LESS THAN ('20210101'),
-> PARTITION p1 VALUES LESS THAN ('20210102'),
-> PARTITION p2 VALUES LESS THAN MAXVALUE
-> );
Query OK, 0 rows affected (0.04 sec)
但是这么创建,又会带来一个新的问题,即对于同一个 opr_no ,可插入到不同分区中。如下所示:
mysql> insert into opr values(1,'2020-12-31 00:00:01','abc');
Query OK, 1 row affected (0.00 sec)
mysql> insert into opr values(1,'2021-01-01 00:00:01','abc');
Query OK, 1 row affected (0.00 sec)
mysql> select * from opr partition (p0);
+--------+---------------------+-------------+
| opr_no | opr_date | description |
+--------+---------------------+-------------+
| 1 | 2020-12-31 00:00:01 | abc |
+--------+---------------------+-------------+
1 row in set (0.00 sec)
mysql> select * from opr partition (p1);
+--------+---------------------+-------------+
| opr_no | opr_date | description |
+--------+---------------------+-------------+
| 1 | 2021-01-01 00:00:01 | abc |
+--------+---------------------+-------------+
1 row in set (0.00 sec)
这实际上违背了业务对于 opr_no 的唯一性要求。
既然这样,有的童鞋会建议给 opr_no 添加个唯一索引,But,现实是残酷的。
mysql> create unique index uk_opr_no on opr (opr_no);
ERROR 1503 (HY000): A UNIQUE INDEX must include all columns in the table's partitioning function (prefixed columns are not considered)
即便是添加唯一索引,分区键也必须包含在唯一索引中。
总而言之,对于 MySQL 分区表,无法从数据库层面保证非分区列在表级别的唯一性,只能确保其在分区内的唯一性。
这也是 MySQL 分区表所为人诟病的地方之一。
但实际上,这个锅让 MySQL 背并不合适,对于 Oracle 索引组织表( InnoDB 即是索引组织表),同样也有这个限制。
Oracle 官方文档( http://docs.oracle.com/cd/E11882_01/server.112/e40540/schemaob.htm#CNCPT1514),在谈到索引组织表(Index-Organized Table,简称 IOT)的特性时,就明确提到了 “分区键必须是主键的一部分”。
Note the following characteristics of partitioned IOTs:
- Partition columns must be a subset of primary key columns.
- Secondary indexes can be partitioned locally and globally.
- OVERFLOW data segments are always equipartitioned with the table partitions.
下面,我们看看刚开始的建表 SQL ,在 Oracle 中的执行效果。
SQL> CREATE TABLE opr_oracle (
2 opr_no NUMBER,
3 opr_date DATE,
4 description VARCHAR2(30),
5 PRIMARY KEY (opr_no)
6 )
7 ORGANIZATION INDEX
8 PARTITION BY RANGE (opr_date) (
9 PARTITION p0 VALUES LESS THAN (TO_DATE('20170713', 'yyyymmdd')),
10 PARTITION p1 VALUES LESS THAN (TO_DATE('20170714', 'yyyymmdd')),
11 PARTITION p2 VALUES LESS THAN (MAXVALUE)
12 );
PARTITION BY RANGE (opr_date) (
*
ERROR at line 8:
ORA-25199: partitioning key of a index-organized table must be a subset of the
primary key
同样报错。
注意,这里指定了 ORGANIZATION INDEX ,创建的是索引组织表。
看来,分区键必须是主键的一部分并不是 MySQL 的限制,而是索引组织表的限制。
之所以对索引组织表有这样的限制,个人认为,还是基于性能考虑。
假设分区键和主键是两个不同的列,在进行插入操作时,虽然也指定了分区键,但还是需要扫描所有分区才能判断插入的主键值是否违反了唯一性约束。这样的话,效率会比较低下,违背了分区表的初衷。
而对于堆表则没有这样的限制。
在堆表中,主键和表中的数据是分开存储的,在判断插入的主键值是否违反唯一性约束时,只需利用到主键索引。
但与 MySQL 不一样的是,Oracle 实现了全局索引,所以针对上面的,同一个 opr_no,允许插入到不同分区中的问题,可通过全局唯一索引来规避。
SQL> CREATE TABLE opr_oracle (
2 opr_no NUMBER,
3 opr_date DATE,
4 description VARCHAR2(30),
5 PRIMARY KEY (opr_no, opr_date)
6 )
7 ORGANIZATION INDEX
8 PARTITION BY RANGE (opr_date) (
9 PARTITION p0 VALUES LESS THAN (TO_DATE('20170713', 'yyyymmdd')),
10 PARTITION p1 VALUES LESS THAN (TO_DATE('20170714', 'yyyymmdd')),
11 PARTITION p2 VALUES LESS THAN (MAXVALUE)
12 );
Table created.
SQL> create unique index uk_opr_no on opr_oracle (opr_no);
Index created.
SQL> insert into opr_oracle values(1,to_date('2020-12-31 00:00:01','yyyy-mm-dd hh24:mi:ss'),'abc');
1 row created.
SQL> insert into opr_oracle values(1,to_date('2020-12-31 00:00:01','yyyy-mm-dd hh24:mi:ss'),'abc');
insert into opr_oracle values(1,to_date('2020-12-31 00:00:01','yyyy-mm-dd hh24:mi:ss'),'abc')
*
ERROR at line 1:
ORA-00001: unique constraint (SCOTT.SYS_IOT_TOP_87350) violated
但 MySQL 却无能为力,之所以会这样,是因为 MySQL 分区表只实现了本地分区索引(Local Partitioned Index),而没有实现 Oracle 中的全局索引(Global Index)。
本地分区索引 VS 全局索引
本地分区索引和全局索引的原理图如下所示:
结合原理图,我们来看看两种索引之间的区别:
本地分区索引同时也是分区索引,分区索引和表分区之间是一一对应的。
而全局索引,既可以是分区的,也可以是不分区的。
如果是全局分区索引,一个分区索引可对应多个表分区,同样,一个表分区也可对应多个分区索引。
对本地分区索引的管理操作只会影响到单个分区,不会影响到其它分区。
而对全局分区索引的管理操作会造成整个索引的失效,当然,这一点可通过 UPDATE INDEXES 子句加以规避。
本地分区索引只能保证分区内的唯一性,无法保证表级别的唯一性,但全局分区可以。
在 Oracle 中,无论是索引组织表还是堆表,如果要创建本地唯一索引,同样也要求分区键必须是唯一键的一部分。
SQL> create unique index uk_opr_no_local on opr_oracle(opr_no) local;
create unique index uk_opr_no_local on opr_oracle(opr_no) local
*
ERROR at line 1:
ORA-14039: partitioning columns must form a subset of key columns of a UNIQUE
index
总结
1. MySQL 分区表关于“分区键必须是唯一键(主键和唯一索引)的一部分”的限制,本质上是索引组织表的限制。
2. MySQL 分区表只实现了本地分区索引,没有实现全局索引,所以无法保证非分区列的全局唯一。
如果要保证非分区列的全局唯一,只能依赖业务实现了。
3. 不推荐使用 MyISAM 分区表。当然,任何场景都不推荐使用 MyISAM 表。
MySQL 分区表,为什么分区键必须是主键的一部分?的更多相关文章
- mysql 插入数据失败防止自增长主键增长的方法
mysql设置了自增长主键ID,插入失败的那个自增长ID也加一的,比如失败5个,下一个成功的不是在原来最后成功数据加1,而是直接变成加6了,失败次数一次就自动增长1了,能不能让失败的不增长的? 或者说 ...
- MySQL—概念,用户的创建,主键,外键,数据类型,表格创建
MySQL DBMS,MySQL的概念,数据库分类,以前MySQL的部署中的一些概念 #DBMS:数据库管理系统,用于管理数据库的大型软件.mysql就是dbms的一种 #Mysql:是用于管理文件的 ...
- MySQL 8 新特性之自增主键的持久化
自增主键没有持久化是个比较早的bug,这点从其在官方bug网站的id号也可看出(https://bugs.mysql.com/bug.php?id=199).由Peter Zaitsev(现Perco ...
- 踩坑记-java mysql 新增获取主键、DIY主键、UUID
java mysql 获取主键.DIY主键.UUID,简单粗暴,代码如下: mapper.xml insert id="add" parameterType="com.x ...
- MSSQL - 逻辑主键、业务主键和复合主键
转载自:http://blog.csdn.net/sunrise918/article/details/5575054 这几天对逻辑主键.业务主键和复合主键进行了一些思考,也在网上搜索了一下相关的讨论 ...
- Oracle 主键、联合主键的查询与创建
--查询某个表是否有唯一主键 select cu.* from user_cons_columns cu, user_constraints au where cu.constraint_name = ...
- 关于mybatis用mysql时,插入返回自增主键的问题
公司决定新项目用mybatis,虽然这个以前学过但是一直没用过都忘得差不多了,而且项目比较紧,也没时间去系统点的学一学,只好很粗略的百度达到能用的程度就行了. 其中涉及到插入实体要求返回主键id的问题 ...
- EF架构~mysql中时间戳字段被认为是主键自增
回到目录 如果在mysql中添加了自增字段,用来维护行的版本,那么在EF中会有一个问题,会把它当成是数据表主键,当你的真正主键是自曾时,就会把默认值0拼到生成的SQL语句里,导致你的insert出错, ...
- Mysql 表约束 非空、唯一、主键、自增长、默认、外键约束(基础6)
非空(not null).唯一(unique key).主键(primary key).自增长(auto_increment).默认约束(default) 准备基础环境: mysql> crea ...
随机推荐
- 日志审计功能-appent多个日志
public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1"); jedis.setnx ...
- Vue自定义组件实现v-model指令
Tips: 本文所描述的Vue均默认是Vue2版本 在我们初次接触Vue的时候,一定会了解到一个语法糖,那就是v-model指令,它带给我们的第一印象就是它可以实现双向绑定 那么,什么是双向绑定?通俗 ...
- [Bzoj 1432] [ZJOI2009]Function(结论推导题)
我们先看一下题目: (有没有和我一样的朋友看到这道题以为是几何不可做题 这个题目真的很难理解,并且样例也给得太水了吧! 理解题目是必不可少的(这并不是你看了半小时题目的理由)--首先我们先简化题目 1 ...
- Codeforces 1442D - Sum(找性质+分治+背包)
Codeforces 题面传送门 & 洛谷题面传送门 智商掉线/ll 本来以为是个奇怪的反悔贪心,然后便一直往反悔贪心的方向想就没想出来,看了题解才发现是个 nb 结论题. Conclusio ...
- sprint-boot 日志
市面上的日志框架: JUL.JCL.Jboss-logging.logback.log4j.log4j2.slf4j.... SpringBoot:底层是Spring框架,Spring框架默认是用JC ...
- Redis高并发处理常见问题及解决方案
1. 大型电商系统高流量系统设计 场景: 大量电商系统每天要处理上亿请求,其中大量请求来自商品访问.下单.商品的详情是时刻变化,由于请求量过大,不会频繁去服务端获取商品信息,导致服务器压力极大.需要用 ...
- Docker实用命令介绍
Docker实用命令介绍 1. docker启动.关闭.停止 ╭─wil-xz in ~ 12:15:44 ╰─٩(ŏ﹏ŏ.)۶ service docker restart Redirecting ...
- 毕业设计之zabbix+微信企业号报警
需要自己申请一个微信企业号 创建应用 AgentId 1000003 Secret SOI8b20G96yUVM29K02-bP5N5o6dovwSF2RrDaXHJNg 企业ID(自己再企业信息里面 ...
- perl 数组快速去除重复元素
这里记录两种perl数组去重的办法,一种利用哈希(hash),一种直接利用perl自带的模块List::MoreUtils内部的函数uniq. 一.利用hash去重 示例代码如下: 1 #!/usr/ ...
- 模拟串口UART的实现
我所祷告的,就是要你们的爱心,在知识和见识上,多而又多,使你们能分辨是非,做诚实无过的人,直到基督的日子.--腓立比书[1:9~10] 最近在调的MCU的型号为STM32F030,配置芯片相较之前的M ...