MySQL 数据库开发规范
一、前言
1. 目的
本文档旨在为使用 MySQL 数据库的应用和系统提供统一规范参考,标准化 MySQL 数据库的开发使用及SQL优化流程,提高业务开发系统的规范性和代码的可读性,减轻维护工作量,提高工作效率。
本文档的规范内容除命名约定外,许多理念也适用于其他关系型数据库。
2. 适用范围
- MySQL 数据库开发设计人员
- 数据库管理人员
- 运维人员
- 数据分析人员
- 研发人员
- 架构师
3. 规范说明
以下所有规范会按照【强制】、【建议】两个级别进行标注,遵守优先级从高到低,未标注的默认为【建议】级别。
二、数据库设计规范
用于规范数据库对象的设计,如数据库(DATABASE)、表(TABLE)、索引(INDEX) 等的设计约定。
1. 原则
- 【强制】命名建议使用具有意义的英文词汇,词汇中间以下划线分隔。
- 【强制】命名只能使用英文字母、数字、下划线。
- 【强制】避免用 MySQL 的保留字如:group,order 等作为单个字段名。
- 【强制】所有数据库对象使用小写字母。
2. 数据库设计
- 【强制】遵守以上全部一般命名规则。
- 【强制】使用单数。
- 【建议】库的名称格式:业务系统名称_子系统名。建议按照业务、产品线或者其它指标进行区分,一般不要超过 20 个字符。如:临时库(tmp_crm)、测试库(test_crm)、产品中心主数据库(product_main)、产品中心应用打包(product_apk)。
- 【强制】一般分库名称命名格式是
库通配名_编号
,编号从 0 开始递增,比如northwind_001
,以时间进行分库的名称格式是库通配名_时间
。 - 【强制】创建数据库时必须显式指定字符集,并且字符集只能是 utf8 或者 utf8mb4(优先使用)。创建数据库 SQL 举例:
create database db_name default character set utf8mb4;
3. 表设计
- 【强制】遵守以上全部一般命名规则。
- 【强制】使用单数。
- 【强制】相关模块的表名与表名之间尽量体现 join 的关系,如
user
表和user_login
表。 - 【强制】创建表时必须显式指定字符集为 utf8 或 utf8mb4。
- 【强制】创建表时必须显式指定表存储引擎类型,如无特殊需求,一律为 InnoDB。当需要使用除 InnoDB/MyISAM/Memory 以外的存储引擎时,必须通过 DBA 审核才能在生产环境中使用。因为 InnoDB 表支持事务、行锁、宕机恢复、MVCC 等关系型数据库重要特性,为业界使用最多的 MySQL 存储引擎。而这是其它大多数存储引擎不具备的,因此首推 InnoDB。
- 【强制】建表必须有 comment。
- 【强制】关于主键:命名为
id
,类型为 int 或 bigint,且为auto_increment
; - 【建议】核心表(如用户表,金钱相关的表)必须有行数据的创建时间字段
create_time
和最后更新时间字段update_time
,便于排查问题,后台系统所维护得表还需要有create_account
和update_account
字段,account 为管理账号,与 smart-sso 中的 account 命名保持一致。 - 【建议】表中所有字段尽量使用
NOT NULL
属性,除非NULL
含有明确含义,因为使用NULL
值会存在每一行都会占用额外存储空间、数据迁移容易出错、聚合函数计算结果偏差等问题。 - 【建议】尽量不要定义
DEFAULT
值,因为业务逻辑下放到了数据库层面,这是一种隐式行为,不利于开发和维护。 - 【建议】建议对表里的
blob
、text
等大字段,垂直拆分到其它表里,仅在需要读这些对象的时候才去 select。 - 【建议】反范式设计:当数据不怎么变化时,把经常需要 join 查询的字段,在其它表里冗余一份。如
username
属性在user_account
,user_login_log
等表里冗余一份,减少 join 查询。 - 【强制】中间表用于保留中间结果集,名称必须以
tmp_
开头。备份表用于备份或抓取源表快照,名称必须以bak_
开头。中间表和备份表定期清理。 - 【强制】对于超过 100W 行的大表进行
alter table
,必须经过 DBA 审核,并在业务低峰期执行。因为alter table
会产生表锁,期间阻塞对于该表的所有写入,对于业务可能会产生极大影响。 - 【强制】日志类型的表,应该从主数据库中分离出来,放入专门的数据库,并且做好数据清理或归档方案。
4. 字段设计
【强制】字段命名需要表示其实际含义的英文单词或简写;
【建议】建议各表之间相同意义的字段应同名;
【强制】字段需添加注释,枚举型需指明主要值的含义,如“0 - 离线,1 - 在线”;
【强制】布尔值列命名为
is_描述
。如 member 表上表示为 enabled 的会员的列命名为 is_enabled;【建议】字段名不建议超过 30 个字符,字段个数不建议大于 60。
【强制】避免使用保留字,如 order、from、desc 等,请参考官方保留字。
【强制】小数类型为 decimal,禁止使用 float 和 double。
说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。
【强制】如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
【强制】varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
【推荐】表的命名最好是加上“业务名称_表的作用”。
正例:alipay_task / force_project / trade_config
【推荐】库名与应用名称尽量一致。
【推荐】如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。
【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:
1)不是频繁修改的字段。
2)不是varchar超长字段,更不能是text字段。
正例:商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联的表中冗余存储类目名称,避免关联查询。
【推荐】单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。
正例:如下表,其中无符号值可以避免误存负数,且扩大了表示范围。
对象 年龄区间 类型 字节 人 150岁之内 unsigned tinyint 1 龟 数百岁 unsigned smallint 2 恐龙化石 数千万岁 unsigned int 4 太阳 约50亿年 unsigned bigint 8
5. 索引设计
- 【强制】主键索引:pk_[表名称或简写]_[字段名或简写]。
- 【强制】唯一索引:uk_[表名称或简写]_[字段名或简写]。
- 【强制】普通索引:idx_[表名称或简写]_[字段名或简写]。
- 多单词组成的 column_name,取尽可能有代表意义的缩写。
- 【强制】选择区分度大的列建立索引,不在低基数列上建立索引,例如:“性别”,“是否是 XXX”。
- 单张表的索引数量控制在 5 个以内,避免冗余索引。
- 索引中的字段数建议不超过 5 个。
- 唯一索引建议由 3 个或更少的字段组成。
- 尽量不要在频繁更新的列上创建索引。
- 对于确定需要组成组合索引的多个字段,建议将选择性高的字段靠前放。
- 最左前缀原则,使用联合索引时,从左向右匹配,比如索引 idx_c1_c2_c3 (c1,c2,c3,)相当于创建了 (c1)、(c1,c2)、(c1,c2,c3) 三个索引,where 条件包含上面三种情况的字段比较则可以用到索引,但像 where c1=a and c3=c 只能用到 c1 列的索引,像 c2=b and c3=c 等情况就完全用不到这个索引。
- 很长的 VARCHAR 字段建立索引时,指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。 idx_table_name (name(10))
- 定期删除一些长时间未使用过的索引。
- ORDER BY,GROUP BY,DISTINCT 的字段需要添加在索引的后面,形成覆盖索引。
- 新的 select,update,delete 上线,都要先 explain,确保索引的正确性。
- 不建议在 where 条件索引列上使用函数,会导致索引失效,如 lower(email)。
- 使用 like 模糊匹配,% 不要放首位,会导致索引失效。
6. 权限设计
生产环境需要调整 root 用户为强密码,且不对外开放。
线上所需用户建议按照用户或者业务场景划分,根据实际情况对每个用户授予相应权限,例如:
序号 | 用户名 | 涵义 | 用途 |
---|---|---|---|
1 | root | 超级用户 | 全局管理,禁止对外开放 |
2 | dba | 数据库管理员 | 数据库 DBA |
3 | app | 应用开发 | 应用开发 |
4 | developer | 开发者 | 开发环境使用 |
5 | tempuser | 临时统计 | 线上业务临时统计,只读用户 |
6 | other | 其他用户 | 第三方人员访问 |
三、数据模型设计
数据库模型设计是指对于一个给定的应用环境,构造合理的数据库模式,建立数据库及其应用系统,有效存储数据,满足用户信息要求和处理要求。数据库设计在开发过程中处于一个非常重要的地位。一个高效的数据库模型是非常重要和必要的。
1. 完整性
数据库完整性是指数据库中数据的正确性和相容性,数据库完整性是由完整性约束来保证的,数据库完整性对于数据库应用系统非常关键,其作用主要体现在以下几个方面:
- 利用完整性控制机制来实现业务规则,易于定义,容易理解,而且可以降低应用程序的复杂性,提高应用程序的运行效率。
- 合理的数据库完整性设计,能够同时兼顾数据库的完整性和系统的效能。在应用软件的功能测试中,完善的数据库完整性有助于尽早发现应用软件的错误。
- 为了在数据库和应用程序代码之间提供另一层抽象,可以为应用程序建立专门的视图而不必非要应用程序直接访问数据表。这样做还等于在处理数据库变更时给你提供了更多的自由。
2. 性能
性能是衡量一个系统的关键因素,在设计阶段就在性能方面就应该多关注,尽量减少后期的烦恼。在数据库设计阶段,性能上的考虑时需要注意:不能以范式作为唯一标准或者指导,在设计过程中,需要从实际需求出发,以性能提升为根本目标来展开设计工作,一些时候为了提升性能,甚至会做反范式设计。
另外还有一些设计上的方法和技巧:
- 设置合理的字段类型和长度。字段类型在满足需求后应尽量短,比如,能用int就尽量不要用 bigint。另外不同数据库在 varchar 和 text 类型在长度和性能上也是不同的,选择时要谨慎。
- 选择高效的主键和索引。由于对表记录的读取都是直接或者间接地通过主键或索引来获取,因此应该该根据具体应用特性来设计合理的主键或索引。同时索引长度的也应该关注,尽量减少索引长度。
- 适度冗余。适度的冗余可以避免关联查询,减少 join 查询。
3. 扩展性
在大规模系统中,除了性能,可扩展性也是设计的关键点,而数据库表扩展性主要包含
表逻辑结构、功能字段的增加、分表等。在扩展性上要把握的原则如下:
- 一表一实体。如果不同实体之间有关联时,可增加一个单独的表,不会影响以前的功能。
四、SQL 开发规范
1. SQL 语句
【强制】不要使用count(列名)或count(常量)来替代count( ),count( )是SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL和非NULL无关。
说明:count(*)会统计值为NULL的行,而count(列名)不会统计此列为NULL值的行。
【强制】count(distinct col) 计算该列除NULL之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为NULL,那么即使另一列有不同的值,也返回为0。
【强制】当某一列的值全是NULL时,count(col)的返回结果为0,但sum(col)的返回结果为NULL,因此使用sum()时需注意NPE问题。
正例:可以使用如下方式来避免sum的NPE问题:
SELECT IF(ISNULL(SUM(g)),0,SUM(g)) FROM table;
【强制】使用
ISNULL()
来判断是否为NULL值。 说明:NULL与任何值的直接比较都为NULL。1)
NULL<>NULL
的返回结果是NULL,而不是false
。2)
NULL=NULL
的返回结果是NULL,而不是true
。3)
NULL<>1
的返回结果是NULL,而不是true
。【强制】 在代码中写分页查询逻辑时,若count为0应直接返回,避免执行后面的分页语句。
【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
说明:以学生和成绩的关系为例,学生表中的student_id是主键,那么成绩表中的student_id则为外键。如果更新学生表中的student_id,同时触发成绩表中的student_id更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
【强制】数据订正(特别是删除、修改记录操作)时,要先select,避免出现误删除,确认无误才能执行更新语句。
【推荐】in操作能避免则避免,若实在避免不了,需要仔细评估in后边的集合元素数量,控制在1000个之内。
【参考】如果有全球化需要,所有的字符存储与表示,均以utf-8编码,注意字符统计函数的区别。
说明:
SELECT LENGTH("轻松工作")
返回为12SELECT CHARACTER_LENGTH("轻松工作")
返回为4如果需要存储表情,那么选择utf8mb4来进行存储,注意它与utf-8编码的区别。
【参考】 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但TRUNCATE无事务且不触发trigger,有可能造成事故,故不建议在开发代码中使用此语句。
说明:TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。
【建议】查询列表时若不含有分页和条件参数,建议增加 limit 参数,限制结果集数量,避免出现 OOM。
【强制】分页查询时若页数量参数可由客户端提供,必须限制
pageSize
最大值。
2. DML 语句
- 【强制】select 语句必须指定具体字段名称,禁止写成
*
。因为select *
会将不该读的数据也从 MySQL 里读出来,造成 I/O 压力。 - 【强制】insert 语句指定具体字段名称,不要写成
insert into t1 values(…)
,道理同上。 - 【建议】
insert into … values(xx),(xx),(xx)…
,这里 xx 的值不要超过 5000 个。值过多虽然上线很快,但会引起主从同步延迟。 - 【建议】select 语句不要使用
union
,推荐使用union all
,并且union
子句个数限制在 5 个以内。因为union all
不需要去重,节省数据库资源,提高性能。 - 【建议】in 值列表限制在 500 以内。例如
select … where user_id in(…500 个以内…)
,这么做是为了减少底层扫描,减轻数据库压力从而加速查询。 - 【建议】事务里批量更新数据需要控制数量,进行必要的 sleep,做到少量多次。
- 【强制】事务涉及的表必须全部是 InnoDB 表。否则一旦失败不会全部回滚,且易造成主从库同步终端。
- 【强制】写入和事务发往主库,只读 SQL 发往从库。
- 【强制】除静态表或小表(100 行以内),dml 语句必须有 where 条件,且使用索引查找。
- 【建议】生产环境避免使用
hint
,如sql_no_cache
,force index
,ignore key
,straight join
等。因为hint
是用来强制 sql 按照某个执行计划来执行,但随着数据量变化我们无法保证自己当初的预判是正确的,临时性的优化可以使用hint
,后期需要将语句优化成合适状态。 - 【强制】where 条件里等号左右字段类型必须一致,否则会引起类型转化,且可能导致索引失效。
- 【建议】
select|update|delete|replace
要有 where 子句,且 where 子句的条件必需使用索引查找。 - 【强制】生产数据库中强烈不推荐大表上发生全表扫描,但对于 100 行以下的静态表可以全表扫描。查询数据量不要超过表行数的 25%,否则不会利用索引。
- 【强制】where 子句中禁止只使用全模糊的 like 条件进行查找,必须有其它等值或范围查询条件,否则无法利用索引。
- 【建议】索引列不要使用函数或表达式,否则无法利用索引。如
where length(name) = 'admin'
或where user_id + 2 = 10023
。 - 【建议】分页查询,当
limit
起点较高时,可先用过滤条件进行过滤。如select a, b, c from t1 limit 10000, 20;
优化为:select a, b, c from t1 where id > 10000 limit 20;
。
3. 多表连接
- 【强制】禁止跨 DB 的 join 语句。因为这样可以减少模块间耦合,为数据库拆分奠定坚实基础。
- 【建议】不要在业务的更新类 SQL 语句中使用 join,比如
update t1 join t2 …
。 - 【建议】不建议使用子查询,建议将子查询 SQL 拆开结合程序多次查询,或使用 join 来代替子查询。
- 【建议】线上环境,多表 join 不要超过 3 个表。
- 【建议】多表连接查询推荐使用别名,且 select 列表中要用别名引用字段,数据库.表格式,如
select a from db1.table1 alias1 where …
。 - 【建议】在多表 join 中,尽量选取结果集较小的表作为驱动表,来 join 其它表。
4. 事务
- 【建议】事务中
insert|update|delete|replace
语句操作的行数控制在 2000 以内,以及 where 子句中 in 列表的传参个数控制在 500 以内。 - 【建议】批量操作数据时,需要控制事务处理间隔时间,进行必要的 sleep,一般建议值 5-10 秒。
- 【建议】对于有
auto_increment
属性字段的表的插入操作,并发需要控制在 200 以内。 - 【强制】程序设计必须考虑“数据库事务隔离级别”带来的影响,包括脏读、不可重复读和幻读。线上建议事务隔离级别为
repeatable-read
。 - 【建议】事务里包含 SQL 不超过 5 个(支付业务除外)。因为过长的事务会导致锁数据较久,MySQL 内部缓存、连接消耗过多等雪崩问题。
- 【建议】事务里更新语句尽量基于主键或
unique key
,如update … where id = XX;
,否则会产生间隙锁,内部扩大锁定范围,导致系统性能下降,产生死锁。 - 【建议】尽量把一些典型外部调用移出事务,如调用 Web Service,访问文件存储等,从而避免事务过长。
- 【建议】对于 MySQL 主从延迟严格敏感的 select 语句,请开启事务强制访问主库。
5. DAO 层注意事项
【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
说明:1)增加查询分析器解析成本。2)增减字段容易与resultMap配置不一致。
【强制】POJO类的布尔属性不能加is,而数据库字段必须加is_,要求在resultMap中进行字段与属性之间的映射。
【强制】
开发客户端接口
时不要用 DO 类当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个与之对应。说明:配置映射关系,使字段与DO类解耦,方便维护。
【强制】sql.xml配置参数使用:#{},#param# 不要使用${} 此种方式容易出现SQL注入。
【强制】iBATIS自带的queryForList(String statementName,int start,int size)不推荐使用。
说明:其实现方式是在数据库取到statementName对应的SQL语句的所有记录,再通过subList取start,size的子集合。
【强制】不允许直接拿HashMap与Hashtable作为查询结果集的输出。
说明:resultClass=”Hashtable”,会置入字段名和属性值,但是值的类型不可控。
【强制】更新数据表记录时,必须同时更新记录对应的 create_time 字段值为当前时间。
【推荐】不要写一个大而全的数据更新接口。传入为POJO类,不管是不是自己的目标更新字段,都进行update table set c1=value1,c2=value2,c3=value3; 这是不对的。执行SQL时,不要更新无改动的字段,一是易出错;二是效率低;三是增加binlog存储。
【参考】
@Transactional
事务不要滥用。事务会影响数据库的QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。【参考】
<isEqual>
中的compareValue是与属性值对比的常量,一般是数字,表示相等时带上此条件;<isNotEmpty>
表示不为空且不为null时执行;<isNotNull>
表示不为null值时执行。
五、流程规范
- 【强制】数据库的 scheme 变动,需要进行版本控制,对每个数据库建立专门的 liquibase 或 flyway 项目进行 SQL 版本管理。
- 【强制】在一个迭代中,开发阶段的第一个步骤为数据库设计,需要首先提交数据库版本控制代码的合并请求,合并通过后再进行接下来的功能开发。