MYSQL- 表结构设计

702 阅读12分钟

表结构设计

数字类型

整型类型

类型占用空间最小值-最大值[signed]最小值-最大值[unsigned]
TINYINT1-128 ~1270~255
SMALLINT2-32768~327670~65535
MEDIUMINT3-8388608 ~83886070~16777215
INT4-2147483448~21474836470~4294967295
BIGINT8-9223372036854775808 ~92233720368547758070~18446744073709551615

整型类型中,有signed和unsigned属性,表示整型取值范围,默认signed。 sql_mode 设置为 NO_UNSIGNED_SUBTRACTION,可以允许unsigned相减的结果为 signed

浮点和高精度

float和double不是高精度,也不是sql标准类型,不推荐使用。使用Decimal类型

业务设计实践

  • 整型结合属性auto_increment可以实现自增功能,但是一般要注意两点:

    • 用BIGINT做主键,不是INT
    • 自增值不持久化,可能有回溯现象(8.0版本前)

    当达到自增整型类型的上限值时,再次自增插入,MySQL 数据库会报重复错误;

  • 在海量互联网业务设计标准中,资金类型并不推荐使用DECIMAL类型, 而是更推荐将DECIMAL转换成整型类型。

字符串类型

mysql字符串类型有CHAR, VARCHAR, BINARY, BLOB, TEXT, ENUM, SET

CHAR(N) 用来保存固定长度的字符,N的范围是0 ~ 255。VARCHAR(N)用来保存边长字符,N的范围是0 ~ 65536。N表示字符。

超出65536个字符的情况下, 可以考虑用TEXT或BLOB,两者最大存储长度是4G,区别是BLOB没有字符集属性,纯属二进制存储。

字符集

字符集推荐使用UTF8MB4

SELECT CAST(0xF09F988E as char charset utf8mb4) as emoji 

包括8.0在内,字符集被默认设置成UTM8MB4, 8.0之前默认Latin1。可以在配置文件中进行配置:

[mysqld]
character-set-server=utf8mb4

GDK字符集,1个字符串最多存储两个字节, UTF8MB4最多存储4个字节。在多字节字符集下,CHAR和VARCHAR完全相同,都是变长存储。

排序规则

排序规则(Collation) 是比较和排序字符串的一种规则, 可以通过SHOW CHARSET来查看字符集默认的排序规则

SHOW CHARSET LIKE '%utf8%'
SHOW COLLATION  LIKE 'utf8mb4%'

排序规则以_ci 结尾, 表示不区分大小写, _cs表示大小写敏感, _bin表示通过存储字符的二进制进行比较

SELECT CAST('a' as char) COLLATE utf8mb4_0900_as_cs = CAST('A' as CHAR) COLLATE utf8mb4_0900_as_cs as result;

修改字符集需要执行

ALTER TABLE emoji_test CHARSET utf8mb4

这是将表的字符集修改成UTF8MB4, 下次新增列,不显式指定字符集, 新列的字符集会变成UTF8MB4, 但对已经存在的列, 默认字符集不做修改。

如果要彻底改变, 需要执行

ALTER TABLE emoji_test CONVERT TO CHARSET utf8mb4;

业务设计实践

性别字段设计

8.0 之前, 可以用ENUM字符串枚举, 只允许有限的定义值插入,如果将参数SQL_MODE设计为严格模式,插入非定义数据就会报错。

Create Table: CREATE TABLE `User` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `sex` enum('M','F') COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

SET sql_mode = 'STRICT_TRANS_TABLES';

在MySQL8.0.16后, 数据库原生提供CHECK约束功能

Create Table: CREATE TABLE `User` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `sex` char(1) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`),
  CONSTRAINT `user_chk_1` CHECK (((`sex` = _utf8mb4'M') or (`sex` = _utf8mb4'F')))
) ENGINE=InnoDB

日期类型

Mysql数据库中常见的日期类型有YEAR, DATE, TIME,DATETIME,TIMESTAMP。

DATETIME

DATETIME最终展现形式是: YYYY-MM-DD HH:MM:SS,固定占用8个字节。

从Mysql5.6开始, DATETIME类型支持毫秒,DATETIME(N)中的N表示毫秒的精度。

CREATE TABLE User (
    id BIGINT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    sex CHAR(1) NOT NULL,
    password VARCHAR(1024) NOT NULL,
    money INT NOT NULL DEFAULT 0,
    register_date DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
    last_modify_date DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
    CHECK (sex = 'M' OR sex = 'F'),
    PRIMARY KEY(id)
);

TIMESTAMP

实际存储内容为'1970-01-01 00:00:00'到现在的毫秒数, 占用4个字节, 存储上限只能到‘2038-01-19 03:14:07’.

从Mysql5.6开始, TIMESTAMP类型也能支持毫秒,不同的是有毫秒时,类型TIMESTAMP占用7个字节, DATESTAMP无论是否存储毫秒信息,都占用8个字节。

TIMESTAMP带有时区信息。参数time_zone指定了当前使用的时区, 默认SYSTEM操作系统时区, 用户可以根据该参数指定需要的时区。

SET time_zone = '-08:00'
SET time_zone = 'America/Los_Angeles'
SET time_zone = 'Asia/Shanghai'

中国时区'+08:00', 美国时区'-08:00', 也可以直接设置名字

业务设计实践

选型

TIMESTAMP 上限值很快到来,而且有潜在的性能问题。从毫秒数转换TIMESTAMP本身指令不多, 不会带来直接的性能问题,但是使用默认的操作系统时区,每次通过时区计算时间,需要调用操作系统底层函数_tz_convert()函数,需要额外的加锁操作,确保操作系统时区没有改变。所以性能不如DATETIME,海量并发时,存在性能抖动问题。优化策略是使用显式的时区, 在配置文件中设置时区, 不要使用系统时区。

[mysqld]
time_zone = "+08:00"

可以通过mysqlslap测试性能

mysqlslap --create-schema=test -uroot --number-of-queries=1000000 --concurrency=100 --query='SELECT NOW()'

非结构存储

JSON 类型是从 MySQL 5.7 版本开始支持的功能,而 8.0 版本解决了更新 JSON 的日志性能瓶颈。如果要在生产环境中使用 JSON 数据类型,强烈推荐使用 MySQL 8.0 版本。

JSON 类型比较适合存储一些修改较少、相对静态的数据

JSON分为JSON对象和JSON数组

业务设计实践

用户登录

CREATE TABLE UserLogin (
    userId BIGINT NOT NULL,
    loginInfo JSON,
    PRIMARY KEY(userId)
);

SET @a = '
{
	"cellphone" : "13918888888",
	"wxchat" : "破产码农",
    "QQ" : "82946772"
}
';

INSERT INTO UserLogin VALUES (1,@a);

常见的函数 JSON_EXTRACT,它用来从 JSON 数据中提取所需要的字段内容。JSON_UNQUOTE函数 是去掉引号的功能 将原json串的引号去掉转成string类型

SELECT
    userId,
    JSON_UNQUOTE(JSON_EXTRACT(loginInfo,"$.cellphone")) cellphone,
    JSON_UNQUOTE(JSON_EXTRACT(loginInfo,"$.wxchat")) wxchat
FROM UserLogin;

等价于

SELECT
    userId,
    loginInfo->>"$.cellphone" cellphone,
    loginInfo->>"$.wxchat" wxchat
FROM UserLogin;

给JSON创建索引方法:

ALTER TABLE UserLogin ADD COLUMN cellphone VARCHAR(255) AS (loginInfo->>"$.cellphone");
ALTER TABLE UserLogin ADD UNIQUE INDEX idx_cellphone(cellphone);

等价于

CREATE TABLE UserLogin (
    userId BIGINT,
    loginInfo JSON,
    cellphone VARCHAR(255) AS (loginInfo->>"$.cellphone"),
    PRIMARY KEY(userId),
    UNIQUE KEY uk_idx_cellphone(cellphone)
);

用户画像

CREATE TABLE UserTag (
    userId bigint NOT NULL,
    userTags JSON,
    PRIMARY KEY (userId)
);

INSERT INTO UserTag VALUES (1,'[2,6,8,10]');
INSERT INTO UserTag VALUES (2,'[3,10,12]');

mysql8.0.17开始支持Multi-Valued Indexes, 用于JSON数组上创建索引,并通过member of, json_contains,json_overlans快速检索数据

 ALTER TABLE UserTag ADD INDEX idx_user_tags ((cast((userTags->"$") as unsigned array)));
EXPLAIN SELECT * FROM UserTag 
WHERE 10 MEMBER OF(userTags->"$")\G
EXPLAIN SELECT * FROM UserTag 
WHERE JSON_CONTAINS(userTags->"$", '[2,10]')\G
EXPLAIN SELECT * FROM UserTag 
WHERE JSON_OVERLAPS(userTags->"$", '[2,3,10]')\G

忘记范式准则

三范式

  • 第一范式(1NF): 数据达到原子性, 数据不可再分
  • 第二范式(2NF): 每行数据具有唯一性,消除数据之间的部分依赖, 一个表中的非主键字段,完全依赖主键字段
  • 第三范式(3NF): 每个字段都独立的依赖于主键关系,消除部分非主键字段的内部依赖

业务设计实践

自增主键设计

使用BIGINT的自增类型作为主键的设计只适合非核心业务表,比如告警表,日志表等。真正的核心业务表,一定不用自增键做主键。

  • 自增存在回溯问题

  • 自增值在服务端产生,存在并发性能问题

    自增值在服务端产生, 需要有一把自增的AI锁保护,有大量插入请求,可能引起性能瓶颈。innodb_autoinc_lock_mode用于控制自增锁持有的时间。

    INSERT INTO ... VALUES (NULL,...),(NULL,...),(NULL,...);
    
    innodb_autoinc_lock_mode可能值持有自增锁时间持有/释放自增锁次数
    05.1版本以前的表锁实现,不推荐
    1每次SQL结束释放自增锁1次
    2每次自增释放自增锁3次
  • 自增值做主键, 只能在当前实例保证唯一, 不能保证全局唯一

  • 公开数据值, 容易引发安全问题

  • MGR(MySQL Group Replication) 可能引起性能问题

  • 分布式架构设计问题

表结构设计应遵循这样规范

  • 每张表一定要有一个主键;
  • 自增主键只推荐用在非核心业务表,甚至应避免使用;
  • 核心业务表推荐使用 UUID 或业务自定义主键;
  • 一份数据应尽可能保留一份,通过主键关联进行查询,避免冗余数据;
  • 在一些场景下,可以通过 JSON 数据类型进行反范式设计,提升存储效率;

表压缩

如果一个页中存放的记录数越多,数据库的性能越高。启用压缩后,存储空间占用也变小了,同样单位的存储能存放的数据也变多了。

若要启用压缩技术,数据库可以根据记录、页、表空间进行压缩,不过在实际工程中,我们普遍使用页压缩技术

  • 压缩每条记录: 因为每次读写都要压缩和解压,过于依赖 CPU 的计算能力,性能会明显下降;另外,因为单条记录大小不会特别大,一般小于 1K,压缩效率也并不会特别好。
  • 压缩表空间: 压缩效率非常不错, 但要求表空间文件静态不增长,这对基于磁盘的关系型数据库很难实现。

压缩并不意味着性能下降,或许能额外提升性能,因为大部分的数据库业务系统,CPU 的处理能力是剩余的,而 I/O 负载才是数据库主要瓶颈。

借助页压缩技术,MySQL 可以把一个 16K 的页压缩为 8K,甚至 4K,这样在从磁盘写入或读取时,就能将 I/O 请求大小减半,甚至更小,从而提升数据库的整体性能。

当然,压缩是一种平衡,并非一定能提升数据库的性能。这种性能“平衡”取决于解压缩开销带来的收益和解压缩带来的开销之间的一种权衡。但无论如何,压缩都可以有效整理数据原本的容量,对存储空间来说,压缩的收益是巨大的。

COMPRESS页压缩

COMPRESS 页压缩是 MySQL 5.7 版本之前提供的页压缩功能。只要在创建表时指定ROW_FORMAT=COMPRESS,并设置通过选项 KEY_BLOCK_SIZE 设置压缩的比例。 虽然是通过选项 ROW_FORMAT 启用压缩功能,但这并不是记录级压缩,依然是根据页的维度进行压缩。

CREATE TABLE Log (
  logId BINARY(16) PRIMARY KEY,
  ......
)
ROW_FORMAT=COMPRESSED
KEY_BLOCK_SIZE=8

COMPRESS 页压缩,适合用于一些对性能不敏感的业务表,例如日志表、监控表、告警表等,压缩比例通常能达到 50% 左右。虽然 COMPRESS 压缩可以有效减小存储空间,但 COMPRESS 页压缩的实现对性能的开销是巨大的,性能会有明显退化。主要原因是一个压缩页在内存缓冲池中,存在压缩和解压两个页。

Page1 和 Page2 都是压缩页 8K,但是在内存中还有其解压后的 16K 页。这样设计的原因是 8K 的页用于后续页的更新,16K 的页用于读取,这样读取就不用每次做解压操作了。这样的实现会增加对内存的开销,会导致缓存池能存放的有效数据变少,MySQL 数据库的性能自然出现明显退化。

为了 解决压缩性能下降的问题,从MySQL 5.7 版本开始推出了 TPC 压缩功能。

TPC压缩

Transparent Page Compress是 5.7 版本推出的一种新的页压缩功能,其利用文件系统的空洞(Punch Hole)特性进行压缩。

CREATE TABLE Transaction (
  transactionId BINARY(16) PRIMARY KEY,
  .....
)
COMPRESSION=ZLIB | LZ4 | NONE;

由于空洞是文件系统的一个特性,利用空洞压缩只能压缩到文件系统的最小单位 4K,且其页压缩是 4K 对齐的。它对数据库性能的侵入几乎是无影响的(小于 20%),甚至可能还能有性能的提升。不同于 COMPRESS 页压缩,TPC 压缩在内存中只有一个 16K 的解压缩后的页,对于缓冲池没有额外的存储开销。

所有页的读写操作都和非压缩页一样,没有开销,只有当这个页需要刷新到磁盘时,才会触发页压缩功能一次。但由于一个 16K 的页被压缩为了 8K 或 4K,其实写入性能会得到一定的提升。

对一些对性能不敏感的业务表,例如日志表、监控表、告警表等,它们只对存储空间有要求,因此可以使用 COMPRESS 页压缩功能。

在一些较为核心的流水业务表上,我更推荐使用 TPC压缩。因为流水信息是一种非常核心的数据存储业务,通常伴随核心业务。如一笔电商交易,用户扣钱、下单、记流水,这就是一个核心业务的微模型。

所以,用户对流水表有性能需求。此外,流水又非常大,启用压缩功能可更为有效地存储数据。

若对压缩产生的性能抖动有所担心,建议:由于流水表通常是按月或天进行存储,对当前正在使用的流水表不要启用 TPC 功能,对已经成为历史的流水表启用 TPC 压缩功能,如下所示

通过命令 ALTER TABLE xxx COMPRESSION = ZLIB 可以启用 TPC 页压缩功能,但是这只对后续新增的数据会进行压缩,对于原有的数据则不进行压缩。所以上述ALTER TABLE 操作只是修改元数据,瞬间就能完成。

若想要对整个表进行压缩,需要执行 OPTIMIZE TABLE 命令:

ALTER TABLE Transaction202102 COMPRESSION=ZLIB;
OPTIMIZE TABLE Transaction202102;