likes
comments
collection
share

在工作中 MySQL 的一些开发规范(干货)!

作者站长头像
站长
· 阅读数 12

前言

大家好,我是路由器没有路

今天跟大家聊下关于在工作中,MySQL 的一些开发规范,如有不当的地方,欢迎指正。

数据库设计规范

【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:

  • 不是频繁修改的字段。
  • 不是 varchar 超长字段,更不能是 text 字段。

正例: 商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联的表中冗余存 储类目名称,避免关联查询。

【推荐】单表行数超过 600 万行或者单表容量超过 5GB,才推荐进行分库分表。

说明:如果预计 3 年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。

【推荐】一个表的字段个数控制在 50 个字段以内;如果字段超过 50 个,可考虑将字段按冷热程度分表。

说明:这样做虽然会给应用带来更多的代码开发量,但对于热表来说,这样做可以提升 buffer 利用率,减少 IO,提升查询的效率。

【强制】字段字符集与表保持一致,不单独设置字符集。

【强制】 相同含义的字段在不同表中应使用相同的名称,数据类型及长度必须保持一致。

【推荐】id 必须是主键,每个表必须有主键,且保持增长趋势的, 小型系统可以依赖于 MySQL 的自增主键,大型系统或者需要分库分表时才使用 ID 生成器,如雪花生成器等。

【推荐】id 类型没有特殊要求,必须使用 bigint unsigned,禁止使用 int,即使现在的数据量很小。id 如果是数字类型的话,必须是 8 个字节。

  • 方便对接外部系统,还有可能产生很多废数据
  • 避免废弃数据对系统 id 的影响
  • 未来分库分表,自动生成 id,一般也是 8 个字节

【推荐】字段尽量设置为 NOT NULL, 为字段提供默认值。

  • 如字符型的默认值为一个空字符值串"";
  • 数值型默认值为数值 0;
  • 逻辑型的默认值为数值 0;

【强制】每个字段和表必须提供清晰的注释。同时,如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。

【推荐】关于时间的格式,推荐统一使用 Unix 时间戳格式

  • unix 时间戳(Unix Timestamp)是从 1970 年 1 月 1 日(UTC/GMT 的午夜)开始所经过的秒数,不考虑闰秒。
  • 因为 MySQL 关于日期的类型有 Date/ Datetime/ Timestamp 三种类型。对于时间的随便选择会导致数据库开发和业务逻辑中遇到不同类型的转换时经常出现混乱或者难以察觉的
  • unix 时间戳在 MySQL 上可以直接使用 bigint unsigned 存储,避免各种时间类型的不一致。也方便后台和数据库关于时间的比较。
  • 前端业务可以根据 unix 时间戳转换成自己需要的时间格式。

【强制】表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint( 1 表示是, 0 表示否)。

  • 任何字段如果为非负数,必须是 unsigned。

正例: 表达逻辑删除的字段名 is_deleted, 1 表示删除, 0 表示未删除。

【强制】表名、字段名必须使用小写字母或数字, 禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。

正例: t_admin,rdc_config,level3_name

反例: Admin, rdcConfig, level_3_name

【推荐】表名不使用复数名词。

  • 表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。

正例: t_user, t_config

反例: t_users, t_configs

【强制】禁用保留字,如 desc、 range、 match、 delayed 等, 请参考 MySQL 官方保留字。

【推荐】 主键索引名为 pk 字段名; 唯一索引名为 uk 字段名; 普通索引名则为 idx 字段名。

说明: pk 即 primary key; uk* 即 unique key; idx* 即 index 的简称。

【强制】小数类型为 decimal,禁止使用 float 和 double。

说明: float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不 正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。

【推荐】如果存储的字符串长度几乎相等,使用 char 定长字符串类型。

【推荐】varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

【强制】表必备三字段:id, f_create_time, f_modify_time。

其中 ID 必为主键,类型为 bigint unsigned。 f_create_time, f_modify_time 的类型均 unix 时间戳,前者现在时表示主动创建,后者过去分词表示被动更新。

【推荐】表的命名最好是加上“业务名称_表的作用”。

正例: t_task / force_project/trade_config

【推荐】库名与应用名称尽量一致。

【推荐】所有命名必须使用全名,有默认约定的除外,如果超过 30 个字符,使用缩写,请尽量名字易懂简短。如

information --> info;

address --> addr 等

【强制】业务应用禁止有 super/root 账号的存在

super 权限很大,一般是 DBA 才会用到,会导致 read only 失效,原则上不提供给应用账号使用。

【推荐】IP 地址的存储,ipv4 尽量使用 int unsigned 来存储,而不要使用 varchar(15)来存储,可以节省 11 字节,如果包括索引,可以节省 22 字节。

【强制】不允许不同业务模块的表连接查询。

【强制】禁止在数据库中存储明文密码。

【推荐】比较重要的数据删除操作使用逻辑删除(UPDATE table SET is_deleteflag=1)代替物理删除(DELETE FROM table WHERE ..)

【强制】InnoDB 表数据量特别大的避免使用 COUNT(*)操作,推荐计数统计实时要求较强可以使用 redis,非实时统计可以使用单独统计表,定时更新。

【强制】事务语句执行完成之后必须及时提交。

事务长时间不提交,如果这时对事务相关表执行 DDL 操作,会出现等待元数据锁的提示。DDL 语句被阻塞后,其他所有表上的正常操作(DML、SELECT)都会被阻塞。

【推荐】尽量避免或者拆分执行大事务。

无论在业务程序中,还是手动数据调整中都需要尽量避免执行大事务。对于影响行数过万的记录建议 DBA 审核后通过工具或脚本分批执行。

大事务的执行会给数据库稳定性带来很多问题。例如引起从库复制延迟、导致锁等待、系统脏数据 checkpoint 写入的性能抖动。

【推荐】对于历史数据、日志数据等数据量特别大的表(通常数据量超过千万,占用空间超过 10G),需要应用根据业务特点,预估业务数据增涨速度,提前在应用代码中进行分表操作(同一 MySQL 实例下的分表操作实现不难)。

通常可以按照时间(按年、按月)进行数据分表,存在明显的数据热点,比如最近一年或者最近三个月的数据是热点数据。其他数据是历史数据。历史数据很少进行修改,一般只要提供给业务进行分析查询即可。这样可以把历史数据归档到其他实例上,提供可供实时查询的接口便可。

DBA 有成熟的实现方案,主要是减少业务库中大量历史数据造成的磁盘空间紧张、数据备份、恢复慢的运维问题。

对于大表按规则进行分表也可提升业务并发读写性能。

【强制】严格禁止单条记录超过 8K

目前我们的 DB 一个 page 大小都设置为 16K,当一条记录超过 page 的一半(8K)时,记录中的 blob/varchar 会在行外存储。存取时会有额外 IO 消耗。插入操作会锁住整个聚簇索引(X Index Lock),直到插入完成才释放。

【推荐】字段约束,对于字典类型的表,因数据量小,修改少,影响面大,应依赖数据库约束来确保数据质量。对于日志或流水型表,为了提升效率,可以释放放宽限制。

【强制】库名、表名、字段名禁止超过 32 个字符。

库名、表名、字段名支持最多 64 个字符,但为了统一规范、易于辨识以及减少传输量,禁止超过 32 个字符。

【推荐】Mysql 数据库统一使用 innodb 存储引擎

原则上,业务范畴内的表都统一使用 innodb 存储引擎;如需使用其它存储引擎,需说明原因,并征得 DBA 同意。集团标准化部署设置默认使用 innodb 引擎。

【推荐】数据库设计不建议使用这样扩展字段:attr1,attr2,attr3…,业务需要什么字段使用时再添加。

使用扩展字段,扩展字段名字和注释不好维护。

新版本的数据库新增和删除字段成本相对比较低。

【推荐】关于建模中遇到字段命名规范有疑问请参考数字办已经制定规范。

数字办已经有规范相关字段命名的工具

数据类型规范

【强制】表示状态字段(0-255)的使用 TINYINT UNSINGED,禁止使用枚举类型,注释必须清晰地说明每个枚举的含义,以及是否支持多选等。

【强制】表示 boolean 类型的都使用 TINYINT(1)

因为 MySQL 本身是没有 boolean 类型的,在自动生成代码的时候,DO 对象的字段就是 boolean 类型,例如 is_delete;其余所有时候都使用 TINYINT(4)。

TINYINT(4),这个括号里面的数值并不是表示使用多大空间存储,而是最大显示宽度,并且只有字段指定 zerofill 时有用,没有 zerofill,(m)就是无用的,例如 id BIGINT ZEROFILL NOT NULL,所以建表时就使用默认就好了,不需要加括号了,除非有特殊需求,例如 TINYINT(1)代表 boolean 类型。

TINYINT(1),TINYINT(4)都是存储一个字节,并不会因为括号里的数字改变。例如 TINYINT(4)存储 22 则会显示 0022,因为最大宽度为 4,达不到的情况下用 0 来补充。

【参考】合适的字符存储长度,不但节约数据库表空间节约索引存储,更重要的是提升检索速度。

【参考】非负的数字类型字段,都添加上 unsigned

如可以使用 INTUNSINGED 字段存 IPV4

【参考】时间字段存储首先推荐 7,如果确实要使用其他时间日期类型,不要使用字符串类型存储,日期使用 DATE 类型,年使用 YEAR 类型,日期时间使用 DATETIME.

【参考】字符串 VARCHAR(N), 其中 N 表示字符个数,请尽量减少 N 的大小,参考:code VARCHAR(32);name VARCHAR(32);memo VARCHAR(512);

【参考】Blob 和 Text 类型所存储的数据量大,删除和修改操作容易在数据表里产生大量的碎片,如果可以,应该尽量避免使用 Blob 或 Text 类型

SQL 语句规范

【强制】不要使用 count(列名)或 count(常量)来替代 count(),count()是 SQL92 定义的 标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。

count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。

【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,

count(distinct col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。

【强制】当某一列 col 的值全是 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。

NULL<>NULL 的返回结果是 NULL,而不是 false。

NULL=NULL 的返回结果是 NULL,而不是 true。

NULL<>1 的返回结果是 NULL,而不是 true。

【强制】 在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。

【强制】不得使用外键与级联,一切外键概念必须在应用层解决。

说明:以学生和成绩的关系为例,学生表中的 studentid 是主键,那么成绩表中的 studentid 则为外键。如果更新学生表中的 studentid,同时触发成绩表中的 studentid 更新,即为 级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻 塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。

【强制】在 MySQL 中禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

【强制】数据订正时,删除和修改记录时,要先 select,避免出现误删除,确认无误才能执行更新语句。

【推荐】逻辑运算如果能在业务层解决,尽量不在数据库做运算,尽可能简单使用 MySQL。

md5() 或 Order by Rand()或计算字段等操作不在数据库表上进行

【推荐】不建议将数据字典表进行连接查询

数据字典表可以查询到 cache 或者在内存中对相关列进行替换。

【推荐】用 UNION ALL 代替 UNION

UNION ALL 不需要对结果集再进行排序。

【推荐】in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。

【推荐】减少与数据库交互次数,尽量采用批量 SQL 语句

使用下面的语句来减少和 db 的交互次数:

INSERT ... ON DUPLICATE KEY UPDATE

REPLACE INTO

INSERT IGNORE

INSERT INTO VALUES()

【推荐】SQL 为多个小 SQL,避免大事务

简单的 SQL 容易使用到 MySQL 的 QUERY CACHE;减少锁表时间特别是 MyISAM;可以使用多核 CPU。

【强制】禁止使用 HINT

HINT 给 DBA 维护带来很多的不便,MySQL 使用 CBO 优化器,会综合考虑如果执行 SQL。

【强制】禁止使用分区表

分区表对分区键有严格要求;分区表在表变大后,执行 DDL、SHARDING、单表恢复等都变得更加困难。因此禁止使用分区表,并建议业务端手动 SHARDING。

【参考】如果有全球化需要,所有的字符存储与表示,均以 utf8mb4 编码,注意字符统计函数 的区别。

SELECT LENGTH(“轻松工作”); 返回为 12

SELECT CHARACTER_LENGTH(“轻松工作”); 返回为 4

utf8mb4 字符集可以存储表情等字符。

【参考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE 无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。

说明: TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。

【推荐】不要写一个大而全的数据更新接口。

传入为 POJO 类,不管是不是自己的目标更新字 段,都进行 update table set c1=value1,c2=value2,c3=value3; 这是不对的。执行 SQL 时,不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储。

【推荐】如果可以放到业务逻辑里面,避免使用 GROUP BY、DISTINCT 、ORDER BY 等语句的使用,避免联表查询和子查询,以 GROUP BY 为列:

order by 的实现有两种方式,主要就是按用没用到索引来区分,

根据索引字段排序,利用索引取出的数据已经是排好序的,直接返回给客户端;

没有用到索引,将取出的数据进行一次排序操作后返回给客户端。这时会大量耗费数据库服务器的计算性能。

【推荐】针对索引字段使用 >, >=, =, <, <=, IF NULL 和 BETWEEN 将会 使用索引,如果对某个索引字段进行 LIKE 查询,使用 LIKE ‘%abc%’ 不能使用索引,使用 LIKE ‘abc%’ 将能够使用索引

【推荐】如果在 SQL 里使用了 MySQL 部分自带函数,而且自带函数使用到索引字段,索引将失效

【强制】避免直接使用 select *,只取需要的字段,增加使用覆盖索引使用的可能。

【推荐】连表查询的情况下,要确保关联条件的数据类型一致,避免嵌套子查询。

【强制】where 语句中尽量不要使用 CASE [WHEN]条件,

反例:

SELECT * FROM logs
WHERE CASE WHEN in_username = "" THEN true
ELSE username LIKE CONCAT("%",in_username,"%") END
AND CASE WHEN time = "" THEN true
ELSE time = in_time END
ORDER BY id
DESC LIMIT in_page,in_limit;

其中 in_username, in_time 是传入的参数,这样写的后果是当数据量很大的情况下查询会很慢,原因是 WHERE 条件语句中 case when 后面的 username 和 time 字段的索引将失效!

【推荐】当只要一行数据时使用 LIMIT 1,因为 LIMIT 1 能有效缩短查询时间。下面是查询的结果对比。

【强制】不能使用触发器

MySQL 中触发器是行触发的,每次增加、修改或者删除记录都会触发进行处理,编写过于复杂的触发器或者增加过多的触发器对记录的插入、更新、删除操作会有比较严重的影响,因此不要将应用的处理逻辑过多地依赖于触发器来处理。触发器的功能通常可以用其他方式实现,确实需要采用触发器,请联系 DBA 进行确认。

【推荐】避免使用视图

视图可能导致执行计划错乱,影响 SQL 运行效率。对视图的修改,数据库必须把它转化为对基本表的信息修改,不便于维护。

【强制】对同一个表的多次 alter 操作必须合并为一次操作

MySQL 对表的修改绝大部分操作都需要锁表并重建表,而锁表则会对线上业务造成影响。为减少这种影响,必须把对表的多次 alter 操作合并为一次操作。例如,要给表 t 增加一个字段 b,同时给已有的字段 aa 建立索引,

通常的做法分为两步:

alter table t add column b varchar(10);

然后增加索引:

alter table t add index idx_aa(aa);

正确的做法是:

alter table t add column b varchar(10),add index idx_aa(aa);

【推荐】避免多余的排序。使用 GROUP BY 时,默认会进行排序,当你不需要排序时,可以使用 order by null。

例如:

Select a.OwnerUserID,count(*) cnt from DP_MessageList a
group by a.OwnerUserID order by null;

SQL 索引规范

【推荐】业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。

说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。

【推荐】主键最好由一个字段构成,最多不要超过 3 个,禁止超过 3 个字段的组合主键。

多列主键会导致其他索引体积膨胀,占用更多的空间,并降低性能。

使用多列业务主键的话,对主键来说,一般可认为是随机插入。

【强制】多表关联查询中,关联字段必须创建索引,关联字段的字段类型、字符集、校验集 保持一致。where 子句后面必须带上关联条件。

多表关联查询,关联字段如果没有索引,或者关联字段类型不一致,则会对多表进行全表扫描。这样对 CPU 和磁盘 IO 消耗是比较大的。如果是百万级以上的大表关联,则会严重影响数据库性能。where 子句后不带关联条件,则会导致 cross-join, MySQL 将使用大量的磁盘空间进行数据中间结果处理。严重情况下,/tmp 目录下的临时表空间文件会直接把所有数据盘占满,导致数据库无法写入、进而崩溃。

【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。

索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。

【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。

索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。

【强制】禁止冗余索引。

索引是双刃剑,会增加维护负担,增大 IO 压力。(a,b,c)、(a,b),后者为冗余索引。可以利用前缀索引来达到加速目的,减轻维护负担。

【推荐】如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。

正例:where a=? and b=? order by c; 索引:a_b_c

反例:索引中有范围查找,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引 a_b 无法排序。

【推荐】利用覆盖索引来进行查询操作,避免回表

如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。

正例:能够建立索引的种类:主键索引、唯一索引、普通索引,而覆盖索引是一种查询的效果,用 explain 的结果,extra 列会出现:using index。

【推荐】利用延迟关联或者子查询优化超多分页场景。

MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。

正例:先快速定位需要获取的 id 段,然后再关联

select a.* from table_name a join (select id from table_name where condition LIMIT 100000,20 ) b where a.id = b.id;

【推荐】SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts 最好。

  • range 对索引进行范围检索。
  • ref 指的是使用普通的索引(normal index)。
  • consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。

反例: explain 表的结果,type=index,索引物理文件全扫描,速度非常慢,这个 index 级 别比较 range 还低,与全表扫描是小巫见大巫。

【推荐】建组合索引的时候,区分度最高的在最左边。

存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where a>? and b=? 那么即使 a 的区分度更高,也必须把 b 放在索引的最前列。

正例:如果 where a=? and b=? ,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。

【参考】创建索引时避免有如下极端误解

  • 宁滥勿缺。认为一个查询就需要建一个索引。
  • 宁缺勿滥。认为索引会消耗空间、严重拖慢更新和新增速度。
  • 抵制惟一索引。认为业务的惟一性一律需要在应用层通过“先查后插”方式解决。

【参考】其他注意事项

  • 索引占磁盘空间,不要重复的索引,尽量短 。
  • 只给常用的查询条件加索引。
  • 过滤性高的列建索引,区分度不高的列不建索引。
  • 唯一的记录添加唯一索引。
  • 频繁更新的列不要建索引。
  • 不要对索引列运算。
  • 同样过滤效果下,保持索引长度最小。
  • 合理利用组合索引,注意索引字段先后顺序。
  • 多列组合索引,过滤性高的字段最前。
  • order by 字段建立索引,避免 filesort。
  • 组合索引,不同的排序顺序不能使用索引。
  • <>!=无法使用索引。

总结

以上是自己在工作中总结的关于 MySQL 的一些开发规范,主要从【数据库设计规范】、【数据类型规范】、【SQL 索引规范】、【SQL 语句规范】这几个方面来描述,如对你有帮助,可以给个赞。

另外,MySQL 的使用需要结合实际业务场景,通过优化和管理来提高其性能和稳定性,我们需要根据特定的业务场景和使用需求来选择合适的方案。