现代软件架构的复杂性要求需要多人或多团队协同完成开发。在这种背景下,如何高效地协同完成软件的开发呢?对软件工程来说,数据库设计规范是在数据库设计层面对软件开发者的规范或标准。对软件来说,适当的规范和标准绝不是为了消除代码内容的创造性、优雅性,而是限制过度个性化,并以一种普遍认可的统一方式一起做事,提升协作效率,降低沟通成本。代码的字里行间流淌的是软件系统的血液,质量的提升是尽可能少踩坑,杜绝踩重复的坑,切实提升系统稳定性,码出质量。
接下来将从多个方面介绍数据库设计规范。如命名风格、注释风格、SQL编写等。 需要说明的是,这里的数据库特别指MySQL数据库,但其设计思想可用于其他数据库。
好的命名风格可以让代码更容易理解、维护、使用。数据库设计先从命名风格上进行培养。
这个规范是标识符定义规则。建议控制标识符长度不超过31个。如果太长,则需考虑换种角度定义标识符,如使用缩写。
Windows 操作系统不区分大小写,而 Linux 和 Unix 等操作系统是区分大小写。因此,数据库名、表名、字段名等,都不允许使用大写字母。正例:user,money;反例:User,Money。
在定义数据库名、表明、字段名、索引名等时,统一使用蛇形命名法(snake case),即用下划线将单词连接。示例:user_name, create_time,update_time等。
禁止使用制表符。不同操作系统对制表符的转码不同。不建议使用Tab缩进。如果使用 Tab 缩进,必须设置 1 个 Tab 为 4 个空格。
基于英文编码已是事实标准。在定义标识符时,不应将拼音与英文混合或直接使用中文。对国际通用的拼音,可以视同为英文,如BeiJing等。
数据库名、表名、字段名等仅表示实体或实体的属性,不应该表示实体数量。正例:user、apple;反例:users、apples。
禁止在数据库名、表名、字段名等命名时,直接使用关键字、保留字。关键字在SQL中具有重要意义的词。某些关键字,如SELECT, DELETE等,被保留,需要用作标识符。保留字则是预留的一些字段,可能会在未来的版本中使用。MySQL的保留字很多,具体可以参考官网。(在定义数据库名、表名、字段名、索引名等时,通常情况下,SQL编辑器会高亮这些特殊单词)。
注意,如果将保留字拼接到名称中,是允许的。如internal是保留字,但是在字段定义时,是可以定义类似"internal_time"这种字段名称的。
随着种族意识的弱化,不应使用使用种族歧视性词语。正例:block_list、allow_list、secondary等;反例:black_list、white_list、slave 等。
在定义表名、主键、外键、唯一索引、普通索引等,需要使用同一的缩写(不同厂商使用的缩写不一定相同,只要保证内部统一即可)。常见的缩写有:
名称 | 缩写 | 说明 |
---|---|---|
表名 | t | 英文table,表名一般定义为"t_系统名称缩写_业务名称"。示例:t_xxx_user,t_xxx_money |
主键 | pk | 英文primary key,主键名一般定义为"pk_表名"。示例:示例:pk_user,pk_money |
外键 | fk | 英文foreign key,外键名一般定义为"fk_+从表名+_+主表名"。示例:t_money_user |
唯一键 | uk | 英文unique key,唯一键一般定义为"uk_+索引名"。示例:uk_money |
普通索引 | idx | 英文index,索引名一般定义为"index_+索引名"。示例:idx_money |
触发器 | trg | 英文index,索引名一般定义为"trg_+索引名"。示例:trg_money_xxx_year |
视图 | view | 英文index,索引名一般定义为"view_+索引名"。示例:view_sale_money |
函数 | func | 英文index,索引名一般定义为"func_+索引名"。示例:func_update_money |
存储过程 | proc | 英文index,索引名一般定义为"proc_+索引名"。示例:proc_store_money |
一般情况下,一个大的业务系统包含多个子系统,每个子系统使用独立的数据库。对于这种情况,需要统一子系统的数据库的命名规则。其规则如下:业务系统名称_子系统名,同一模块使用的表名尽量使用统一前缀。示例:health、health_log、health_trade。
数据库存储引擎统一使用 INNODB,除非读写比率<1%,才考虑使用 MYISAM 存储引擎。相比MYISAM,INNODB 在可靠性和性能方面体现出极大优势。其他存储引擎或其他数据库的存储引擎请在数据库专家的建议下使用。
尽管在MySQL 5.5 版本以后,默认存储引擎就是 InnoDB,但是在DDL中尽量显式指定存储引擎。示例:
CREATE TABLE IF NOT EXISTS tab_user(
pk_id int NOT NULL AUTO_INCREMENT COMMENT '唯一主键',
user_name varchar(128) NOT NULL COMMENT '用户名称',
create_time datetime NOT NULL COMMENT '创建时间',
update_time datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`pk_id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息';
统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效,如果数据库中有存储emoji(表情)的需要,字符集需要采用utf8mb4字符集。示例如下:
CREATE DATABASE IF NOT EXISTS db_name DEFAULT CHARSET utf8mb4;
以 UUID(),USER() 等函数为例,其对于复制来说是很危险的,会导致主备数据不一致。
如果数据存在外键关系,请在程序层面实现。尽管外键能够保证主从表的数据完整性,但会给系统带来一定影响:(1) 性能方面,外键约束会降低从表的写入速度(在写入从表前,还需确认主表是否存在对应的数据);(2) 并发方面,外键约束会增大高并发场景下出现死锁的情况(使用外键约束后,需要额外查询主表,增加出现死锁的可能);(3) 可扩展性方面,对于平台迁移场景(如从Mysql迁移到Oracle),不同数据库平台的实现有差异。对于分库分表场景,在水平拆分和分库的情况下,外键是无法生效的。
将外键约束在应用程序中实现,虽然增大了开发人员的工作量,但是却减少了数据库层面的压力。而且应用程序的处理能力还可以通过水平扩展的方式增强,这种还是很推荐的。
当必须使用外键的话,必须在外键列创建索引。
除非有明确的需求,否则不建议使用视图、存储过程、函数、触发器。对 MYSQL 来说, 视图、存储过程、函数、触发器 还不是很成熟,没有完善的出错记录处理。此外,视图、存储过程、函数、触发器会对读写性能产生影响,应明确风险后,再使用。
MySQL在处理大表时,性能就开始明显降低。如果一个表超过 500w 行,那么该表的读写速度将大打折扣。如果一个表预计两年后的数据量根本达不到这个级别,请不要在创建表时就分库分表(避免过度设计)。
500w 行只是一个参考值,不同厂商定义的规格不要求完全一样,根据实际测试情况来规定阈值。如阿里的标准就是500W,百度的确是2000W。
以 alter table为例,会产生表锁,执行期间会阻塞对于该表的所有写入,对于业务可能会产生极大影响。
冗余字段应遵循:(1) 不是频繁修改的字段;(2) 不是 varchar 超长字段,更不能是 text 字段。
当一个表的字段过多时(如大于50个),可以考虑拆分为两个表。其中主表包含常用的一些字段,子表包含使用频率低或占用存储较多的字段(如text、blob、clob等)。
一些特殊场景允许反范式化设计,但在评审时需要对冗余字段的设计给出解释。
保证字段非空。如为字段提供默认值。因为使用空值会带来聚合函数计算结果偏差等问题。此外,不同数据库对空值的处理不同,使用空值还需在数据迁移时额外处理。
如果字段可能为空,在执行判空时,要使用IS NULL等函数,不能使用=或==判断。
id为主键,类型为unsigned bigint(禁止使用int)、单表时自增、步长为1。小型系统可以依赖于 MySQL 的自增主键,大型系统或者需要分库分表时才使用内置的 ID 生成器。主键的优势是唯一标识、有效引用、高效检索,所以一般情况下尽量有主键字段。
create_time, update_time 的类型均为 date_time,前者表示创建时间,后者表示更新时间。对表的记录进行更新的时候,必须包含对 update_time 字段的更新。
过度的宽表对读写性能产生很大的影响。
如关联的字段都是int类型。
不要引入复合字段。字段的含义应该是明确的,保证字段的原子性。
相比char,varchar是变长存储,更省空间。注意,使用varchar类型时,字符数不要超过2700。MySQL 规定一行所有文本最多存65535字节,因此在utf8字符集下最多存21844个字符,超过会自动转换为mediumtext字段。而text在utf8字符集下最多存21844个字符,mediumtext最多存224/3个字符,longtext最多存232个字符。
如果存储长度大于2700,可将该字段定义为 text,并独立出一张表,避免影响其它字段索引效率。
float 和 double 在存储的时候,存在精度损失的问题。decimal本质上字符串存储。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
enum 或 set 类型不仅浪费空间,且枚举值写死了,变更不方便。推荐使用smallint或varchar类型。
clob, blob, text 等类型比较浪费硬盘和内存空间。在加载表数据时,会读取大字段到内存里从而浪费内存空间,影响系统性能。在使用这类字段前,建议做好相关评审工作,明确是否真的需要这类字段。
Innodb中当一行记录超过8098字节时,会将该记录中选取最长的一个字段将其768字节放在原始page里,该字段余下内容放在overflow-page里。不幸的是在compact行格式下,原始page和overflow-page都会加载。
InnoDB和MyISAM存储引擎表,索引类型必须为BTRER,MEMORY表可以根据需要选择HASH或者BTREE类型索引。
索引在加速查询的同时,也会带来写入速度降低的问题(写入数据的同时,要更新索引)。应限制单表上索引个数。
索引在sql中的where子句、order by子句、join子句、select子句中会生效。但是,也要注意索引不生效的情况,如:
(1) 查询条件中有or。此时,必须保证所有or相关的字段都有索引才能生效。所以要谨慎使用or语句。(2) like语句以%开头。模糊查询时,使用%且将其放在开头,会导致索引失效。(3) 如果存在类型转换(如存储是字符串,查询的时候未用引号,则会进行类型转换),索引会失效。(4) 索引列参与计算会导致索引失效(如执行算数运算或使用函数)。(5) 违背最左匹配原则。(6) 如果Mysql 评估全表扫描要比使用索引要快,则索引失效(数据量很少,使用索引不会提升查找性能)。
select * 会返回该表的所有字段。对于宽表来说,查询所有字段是一种灾难,因为获取一些根本不需要的字段是没有必要的。此外,join语句使用select *可能导致只需要访问索引即可完成的查询需要回表取数。
不加where条件,会变成全表扫描。
大量的计算和转换会造成导致索引失效,当数据量很大时,会带来性能问题。
or 会导致索引失效(or关联的所有字段存在没有索引的时候),为保证效率,可以使用in或union。注意 in 会带来性能问题,如果包含的元素是不固定的时候。建议in中元素的个数小于100(经验值)。
在like子句中使用%做前缀会导致索引失效。可以使用%模糊后缀查询。
使用 insert时,要指定插入的列。示例:insert into t_name(col1,col2,col3) values (‘a’,1,‘text’)
大批量操作可能会造成严重的主从延迟,特别是主从模式下,大批量操作可能会造成严重的主从延迟,因为需要slave从master的binlog中读取日志来进行数据同步。binlog日志为row格式时会产生大量的日志。
基于count()函数统计行数时,count(*)和count(1)都可以用来统计表中的行数,基于SQL规范考虑,推荐使用count(*)。注意,在使用count(*)时,要注意不同存储引擎的支持事务的水平,如MyISAM不支持事务,使用的锁是表级锁,不会有并发的行操作,所以查询的结果是准确的。InnoDB支持事务,并且支持行级锁,行可能被并行修改,那么缓存记录不准确。且不加where条件时,MyISAM 引擎会直接返回这个总数。
不推荐使用 count(column_name),因为count(column_name)只能统计列值非NULL的行数。且会基于该字段是否使用了索引,决定是否基于索引统计,还是全表扫描统计。
注释主要有以下三方面的作用:1.能够反映数据库的设计思想和主要功能;2.能够描述业务含义,方便其他程序员了解代码背后的含义;3.提示示例代码,方便调用者使用。对于注意的中英文没有特别要求,对于面向国内开发者或公司内部的项目,其注释推荐使用中文,对于面向国际环境或开源的项目,其注释推荐使用英文。
在定义表时,使用COMMENT关键字添加字段说明。示例如下:
CREATE TABLE IF NOT EXISTS tab_user(
pk_id int NOT NULL AUTO_INCREMENT COMMENT '唯一主键',
user_name varchar(128) NOT NULL COMMENT '用户名称',
create_time datetime NOT NULL COMMENT '创建时间',
update_time datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`pk_id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息';
对于其他场景,使用SQL注释。SQL 接受的注释有三种:
(1) -- 这儿是注释 (注意,第 2 个破折号后面至少跟一个空格符)
(2) /* 这儿是注释 */
(3) # 这儿是注释
数据库设计开发规范 阿里云.数据库服务部 2010
https://cloud.tencent.com/developer/article/1475852 MySQL数据库设计规范
https://www.mysql.com/products/enterprise/techspec.html MySQL Technical Specifications
https://www.itworkman.com/mysql-database-design-specifications/ MySQL database design specifications
https://blog.csdn.net/dear_little_bear/article/details/108511836 MySQL设计规范及性能优化
https://zhuanlan.zhihu.com/p/171764482 InnoDB存储引擎简介
https://www.jianshu.com/p/f2db3010938f Mysql 关键字 保留字
https://blog.csdn.net/qq_43842093/article/details/127168632 mysql 命名规则
https://juejin.cn/post/7073649358498955295 数据库规范
https://zhuanlan.zhihu.com/p/391673897 索引设计(索引设计详细规范)
https://zhuanlan.zhihu.com/p/62020571 为什么不推荐使用外键约束
https://blog.csdn.net/sy_white/article/details/122112440 索引失效的情况及解决(超详细)