数据库设计规范

【MySQL】MySQL数据库设计规范

1. 规范背景与⽬的

MySQL数据库与 Oracle、 SQL Server 等数据库相⽐,有其内核上的优势与劣势。我们在使⽤MySQL数据库的时候需要遵循⼀定规范,扬长避短。本规范旨在帮助或指导RD、QA、OP等技术⼈员做出适合线上业务的数据库设计。在数据库变更和处理流程、数据库表设计、SQL编写等⽅⾯予以规范,从⽽为公司业务系统稳定、健康地运⾏提供保障。

2. 设计规范

2.1 数据库设计

以下所有规范会按照【⾼危】、【强制】、【建议】三个级别进⾏标注,遵守优先级从⾼到低。

对于不满⾜【⾼危】和【强制】两个级别的设计,DBA会强制打回要求修改。

2.1.1 库名

  1. 【强制】库的名称必须控制在32个字符以内,相关模块的表名与表名之间尽量提现join的关系,如user表和user_login表。
  2. 【强制】库的名称格式:业务系统名称_⼦系统名,同⼀模块使⽤的表名尽量使⽤统⼀前缀。
  3. 【强制】⼀般分库名称命名格式是库通配名编号,编号从0开始递增,⽐如wenda_001以时间进⾏分库的名称格式是“库通配名时间”
  4. 【强制】创建数据库时必须显式指定字符集,并且字符集只能是utf8mb4。创建数据库SQL举例:create database db1 default character set utf8mb4;。

2.1.2 表结构

  1. 【强制】表和列的名称必须控制在32个字符以内,表名只能使⽤字母、数字和下划线,⼀律⼩写。
  2. 【强制】表名要求模块名强相关,如师资系统采⽤”sz”作为前缀,渠道系统采⽤”qd”作为前缀等。
  3. 【强制】创建表时必须显式指定字符集为utf8或utf8mb4。
  4. 【强制】创建表时必须显式指定表存储引擎类型,如⽆特殊需求,⼀律为InnoDB。当需要使⽤除InnoDB/MyISAM/Memory以外的存储引擎时,必须通过DBA审核才能在⽣产环境中使⽤。因为Innodb表⽀持事务、⾏锁、宕机恢复、MVCC等关系型数据库重要特性,为业界使⽤最多的MySQL存储引擎。⽽这是其他⼤多数存储引擎不具备的,因此⾸推InnoDB。
  5. 【强制】建表必须有comment
  6. 【建议】建表时关于主键:(1)强制要求主键为id,类型为int或bigint,且为auto_increment(2)标识表⾥每⼀⾏主体的字段不要设为主键, 建议设为其他字段如user_id,order_id等, 并建⽴unique key索引(可参考cdb.teacher表设计)。因为如果设为主键且主键值为随机插⼊, 则会导致innodb内部page分裂和⼤量随机I/O,性能下降。
  7. 【建议】核⼼表(如⽤户表,⾦钱相关的表)必须有⾏数据的创建时间字段create_time和最后更新时间字段update_time,便于查问题。
  8. 【建议】表中所有字段必须都是NOT NULL属性,业务可以根据需要定义DEFAULT值。因为使⽤NULL值会存在每⼀⾏都会占⽤额外存储空间、数据迁移容易出错、聚合函数计算结果偏差等问题。
  9. 【建议】建议对表⾥的blob、text等⼤字段,垂直拆分到其他表⾥,仅在需要读这些对象的时候才去select。
  10. 【建议】反范式设计:把经常需要join查询的字段,在其他表⾥冗余⼀份。如user_name属性在user_account,user_login_log等表⾥冗余⼀份,减少join查询。
  11. 【强制】中间表⽤于保留中间结果集,名称必须以tmp_开头。备份表⽤于备份或抓取源表快照,名称必须以bak_开头。中间表和备份表定期清理。
  12. 【强制】对于超过100W⾏的⼤表进⾏alter table,必须经过DBA审核,并在业务低峰期执⾏。因为alter table会产⽣表锁,期间阻塞对于该表的所有写⼊,对于业务可能会产⽣极⼤影响。

建表规约:

  1. 【强制】表达是与否概念的字段,必须使⽤ is_xxx的⽅式命名,数据类型是 unsigned tinyint( 1表⽰是,0表⽰否)。
    1. 说明:任何字段如果为⾮负数,必须是 unsigned。
    2. 正例:表达逻辑删除的字段名 is_deleted,1 表⽰删除,0 表⽰未删除。
  2. 【强制】表名、字段名必须使⽤⼩写字母或数字,禁⽌出现数字开头,禁⽌两个下划线中间只出现数字。数据库字段名的修改代价很⼤,因为⽆法进⾏预发布,所以字段名称需要慎重考虑。
    1. 说明:MySQL 在 Windows 下不区分⼤⼩写,但在 Linux 下默认是区分⼤⼩写。因此,数据库名、表名、字段名,都不允许出现任何⼤写字母,避免节外⽣枝。
    2. 正例:aliyun_admin,rdc_config,level3_name
    3. 反例:AliyunAdmin,rdcConfig,level_3_name
  3. 【强制】表名不使⽤复数名词。
    1. 说明:表名应该仅仅表⽰表⾥⾯的实体内容,不应该表⽰实体数量,对应于 DO类名也是单数
    2. 形式,符合表达习惯。
  4. 【强制】禁⽤保留字,如 desc、range、match、delayed等,请参考 MySQL官⽅保留字。
  5. 【强制】主键索引名为 pk_字段名;唯⼀索引名为 uk_字段名;普通索引名则为 idx_字段名。
    1. 说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index的简称。
  6. 【强制】⼩数类型为 decimal,禁⽌使⽤ float和 double。
    1. 说明:float和 double在存储的时候,存在精度损失的问题,很可能在值的⽐较时,得到不正确的结果。如果存储的数据范围超过decimal的范围,建议将数据拆成整数和⼩数分开存储。
  7. 【强制】如果存储的字符串长度⼏乎相等,使⽤ char定长字符串类型。
  8. 【强制】varchar是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度⼤于此值,定义字段类型为text,独⽴出来⼀张表,⽤主键来对应,避免影响其它字段索引效率。
  9. 【强制】表必备三字段:id, gmt_create, gmt_modified。
    1. 说明:其中 id必为主键,类型为 unsigned bigint、单表时⾃增、步长为 1。gmt_create,gmt_modified的类型均为 date_time类型,前者现在时表⽰主动创建,后者过去分词表⽰被动更新。
  10. 【推荐】表的命名最好是加上“业务名称_表的作⽤”。
  11. 正例:alipay_task / force_project / trade_config
  12. 【推荐】库名与应⽤名称尽量⼀致。
  13. 【推荐】如果修改字段含义或对字段表⽰的状态追加时,需要及时更新字段注释。
  14. 【推荐】字段允许适当冗余,以提⾼查询性能,但必须考虑数据⼀致。冗余字段应遵循:
  15. 1)不是频繁修改的字段。
  16. 2)不是 varchar超长字段,更不能是 text字段。
  17. 正例:商品类⽬名称使⽤频率⾼,字段长度短,名称基本⼀成不变,可在相关联的表中冗余存储类⽬名称,避免关联查询。
  18. 【推荐】单表⾏数超过 500万⾏或者单表容量超过 2GB,才推荐进⾏分库分表。
  19. 说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
  20. 【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。

其它:

  1. INT 类型不使⽤ unsigned ⽆符号属性,容易引⼊额外的计算问题。
  2. ⾃增⽤ 8 字节 BIG INT,不要使⽤ 4 字节 INT,且⾃增在 MySQL 8.0 版本前有回溯问题,请考虑是否业务有影响。
  3. 字符集使⽤ UTF8MB4 字符编码,不推荐 GBK、UTF-8 等其他字符集。
  4. ⽇期类型⽤ DATETIME 类型,需要精确到毫秒⽤ DATETIME(6),不要使⽤ INT、TIMESTAMP。
  5. 类型 JSON 可⽤于存储⾮结构化数据,典型场景为⽤户标签,不要将 JSON ⽤于频繁更新的字段场景。
  6. 每张表⼀定要有⼀个主键,这样⾄少满⾜⼀范式的要求,核⼼业务表⽤全局唯⼀字段(雪花算法、有序UUID)做主键,不要使⽤⾃增做主键。
  7. 对于⽇志类的流⽔表、报警表、⽇志表,可以使⽤压缩设计,提升存储效率。MySQL 5.7 版本开始推荐使⽤透明页压缩,不要使⽤传统的 KEY_BLOCK_SIZE 的页压缩。
  8. 类别设计,⽤ ENUM+CHECK 约束,不要使⽤ INT 类型的设计。
  9. 敏感字段需加密,如账户密码、信⽤卡号等存储使⽤:动态盐 + ⾮固定加密算法(MD5/AES256等) + 多轮加密,不要简单使⽤ MD5算法加密,容易被暴⼒破解。
  10. MySQL 可以通过 KV 的⽅式访问表中的数据,若业务只是简单的 SET、GET 请求,可考虑将其转化为 Memcached 的 KV 访问⽅式,减少 SQL 解析的开销,性能可以有⾄少 50% 的提升。

2.1.3 列数据类型优化

  1. 【建议】表中的⾃增列(auto_increment属性),推荐使⽤bigint类型。因为⽆符号int存储范围为-2147483648~2147483647(⼤约21亿左右),溢出后会导致报错。
  2. 【建议】业务中选择性很少的状态status、类型type等字段推荐使⽤tinytint或者smallint类型节省存储空间。
  3. 【建议】业务中IP地址字段推荐使用int类型,不推荐⽤char(15)。因为int只占4字节,可以⽤如下函数相互转换,而char(15)占⽤⾄少15字节。⼀旦表数据⾏数到了1亿,那么要多⽤1.1G存储空间。 SQL:select inet_aton('192.168.2.12'); select in et_ntoa(3232236044); PHP: ip2long(‘192.168.2.12’); long2ip(3530427185);
  4. 【建议】不推荐使⽤enum,set。因为它们浪费空间,且枚举值写死了,变更不⽅便。推荐使⽤tinyint或smallint。
  5. 【建议】不推荐使⽤blob,text等类型。它们都⽐较浪费硬盘和内存空间。在加载表数据时,会读取⼤字段到内存⾥从⽽浪费内存空间,影响系统性能。建议和PM、RD沟通,是否真的需要这么⼤字段。Innodb中当⼀⾏记录超过8098字节时,会将该记录中选取最长的⼀个字段将其768字节放在原始page⾥,该字段余下内容放在overflow-page⾥。不幸的是在compact⾏格式下,原始page和overflow-page都会加载。
  6. 【建议】存储⾦钱的字段,建议⽤int,程序端乘以100和除以100进⾏存取。因为int占⽤4字节,⽽double占⽤8字节,空间浪费。
  7. 【建议】⽂本数据尽量⽤varchar存储。因为varchar是变长存储,⽐char更省空间。MySQL server层规定⼀⾏所有⽂本最多存65535字节,因此在utf8字符集下最多存21844个字符,超过会⾃动转换为mediumtext字段。⽽text在utf8字符集下最多存21844个字符,mediumtext最多存224/3个字符,longtext最多存232个字符。⼀般建议⽤varchar类型,字符数不要超过2700。
  8. 【建议】时间类型尽量选取timestamp。因为datetime占⽤8字节,timestamp仅占⽤4字节,但是范围为1970-01-01 00:00:01到2038-01-01 00:00:00。更为⾼阶的⽅法,选⽤int来存储时间,使⽤SQL函数unix_timestamp()和from_unixtime()来进⾏转换。

2.1.4 索引设计

  1. 【强制】InnoDB表必须主键为id int/bigint auto_increment,且主键值禁⽌被更新。
  2. 【建议】主键的名称以“pk_”开头,唯⼀键以“uk_”或“uq_”开头,普通索引以“idx_”开头,⼀律使⽤⼩写格式,以表名/字段的名称或缩写作为后缀。
  3. 【强制】InnoDB和MyISAM存储引擎表,索引类型必须为BTREE;MEMORY表可以根据需要选择HASH或者BTREE类型索引。
  4. 【强制】单个索引中每个索引记录的长度不能超过64KB。
  5. 【建议】单个表上的索引个数不能超过7个。
  6. 【建议】在建⽴索引时,多考虑建⽴联合索引,并把区分度最⾼的字段放在最前⾯。如列userid的区分度可由select count(distinct userid)计算出来。
  7. 【建议】在多表join的SQL⾥,保证被驱动表的连接列上有索引,这样join执⾏效率最⾼。
  8. 【建议】建表或加索引时,保证表⾥互相不存在冗余索引。对于MySQL来说,如果表⾥已经存在key(a,b),则key(a)为冗余索引,需要删除。

索引规约:

  1. 【强制】业务上具有唯⼀特性的字段,即使是多个字段的组合,也必须建成唯⼀索引。
    1. 说明:不要以为唯⼀索引影响了 insert速度,这个速度损耗可以忽略,但提⾼查找速度是明显的;另外,即使在应⽤层做了⾮ 常完善的校验控制,只要没有唯⼀索引,根据墨菲定律,必然有脏数据产⽣。
  2. 【强制】超过三个表禁⽌ join。需 要 join的字段,数据类型必须绝对⼀致;多表关联查询时,保证被关联的字段需要有索引。
    1. 说明:即使双表 join也要注意表索引、SQL性能。
  3. 【强制】在 varchar字段上建⽴索引时,必须指定索引长度,没必要对全字段建⽴索引,根据实际⽂本区分度决定索引长度可。
    1. 说明:索引的长度与区分度是⼀对⽭盾体,⼀般对字符串类型数据,长度为 20的索引,区分度会⾼达 90%以上,可以使⽤ count(distinct left(列名, 索引长度))/count(*)的区分度来确定。
  4. 【强制】页⾯搜索严禁左模糊或者全模糊,如果需要请⾛搜索引擎来解决。
    1. 说明:索引⽂件具有 B-Tree的最左前缀匹配特性,如果左边的值未确定,那么⽆法使⽤此索引。
  5. 【推荐】如果有 order by的场景,请注意利⽤索引的有序性。order by 最后的字段是组合索引的⼀部分,并且放在索引组合顺序的最后,避免出现 file_sort的情况,影响查询性能。
    1. 正例:where a=? and b=? order by c; 索引:a_b_c
    2. 反例:索引中有范围查找,那么索引有序性⽆法利⽤,如:WHERE a>10 ORDER BY b; 索引a_b⽆法排序。
  6. 【推荐】利⽤覆盖索引来进⾏查询操作,避免回表。
    1. 说明:如果⼀本书需要知道第 11章是什么标题,会翻开第 11章对应的那⼀页吗?⽬录浏览⼀下就好,这个⽬录就是起到覆盖索引的作⽤。
    2. 正例:能够建⽴索引的种类:主键索引、唯⼀索引、普通索引,⽽覆盖索引是⼀种查询的⼀种效果,⽤ explain的结果,extra列会出现:using index。
  7. 【推荐】利⽤延迟关联或者⼦查询优化超多分页场景。
    1. 说明:MySQL并不是跳过 offset⾏,⽽是取 offset+N⾏,然后返回放弃前 offset⾏,返回N⾏,那当 offset特别⼤的时候,效率就⾮常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进⾏ SQL改写。
    2. 正例:先快速定位需要获取的 id段,然后再关联:
      1. SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id
  8. 【推荐】SQL性能优化的⽬标:⾄少要达到 range 级别,要求是 ref级别,如果可以是 consts最好。
    1. 说明:
      1. consts 单表中最多只有⼀个匹配⾏(主键或者唯⼀索引),在优化阶段即可读取到数据。
      2. ref 指的是使⽤普通的索引(normal index)。
      3. range 对索引进⾏范围检索。
    2. 反例:explain表的结果,type=index,索引物理⽂件全扫描,速度⾮常慢,这个 index级别⽐较 range还低,与全表扫描是⼩巫见⼤巫。
  9. 【推荐】建组合索引的时候,区分度最⾼的在最左边。
    1. 正例:如果 where a=? and b=? ,a列的⼏乎接近于唯⼀值,那么只需要单建 idx_a索引即可。
    2. 说明:存在⾮等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where a>?and b=? 那么即使 a的区分度更⾼,也必须把 b放在索引的最前列。
  10. 【推荐】防⽌因字段类型不同造成的隐式转换,导致索引失效。
  11. 【参考】创建索引时避免有如下极端误解:
  12. 宁滥勿缺。认为⼀个查询就需要建⼀个索引。
  13. 宁缺勿滥。认为索引会消耗空间、严重拖慢更新和新增速度。
  14. 抵制惟⼀索引。认为业务的惟⼀性⼀律需要在应⽤层通过“先查后插”⽅式解决。

其它:

  1. 不要陷入设置单表行数、列数限制的固有印象,其他关系型数据库没有⾏数、列数限制,MySQL 也没有,⼤表的缺点不是性能,⽽是后续的 DDL 管理问题,随着 MySQL 8.0 快速加列功能的上线,⼤表 DDL 问题基本已解决。
  2. MySQL 是索引组织表,表中的数据以 B+ 树索引结构,根据主键逻辑排序,由于 B+ 树索引的特点是树的⾼度为 3~4 层,所以从数⼗亿的记录中,通过主键查询⼀条记录只需要 3、4 次 I/O,当前到 SSD 存储设备设置每秒⾄少能完成 10000 次的 I/O 查询,不要担⼼通过索引查询⼀条或⼏条记录的性能,每秒百万次查询并不难。
  3. MySQL 是索引组织表,⼆级索引只存储(键值、主键值),因此需要再通过⼀次主键索引查询得到记录,这种⽅式成为回表。在核⼼业务中,使⽤索引覆盖技术,提升索引查询性能,对于回表记录数⽐较⼤的场景,甚⾄可以有 10 倍的性能提升;
  4. 对类似 WHERE a = ? ORDER BY b 这样的查询,⼀定要创建(a、b)组合索引,这样可以避免⼀次额外排序,提升查询性能。
  5. MySQL 优化器是 CBO(Cost-based Optimizer),所有查询基于成本而不是规则,若发现 SQL 执行计划发⽣变化,不要怀疑 MySQL出错,请先分析数据特点、索引创建是否合理,是否可以通过直⽅图校准数据。
  6. MySQL JOIN ⽀持 NLJ(Nested Loop Join)和 NHJ(Nested Hash Join)两种⽅式。对于 OLTP 业务,放⼼⼤胆使⽤ JOIN,但⼀定要做好索引的设计和索引覆盖的考虑(不考虑分布式数据库场景);对于 OLAP 业务,MySQL 8.0 版本开始,⽀持 Hash Join,对于⼤数据量的关联,性能提升⾮常多,可以在不超过 10T 的数仓场景中考虑使⽤,超过 10T数据量,请⼀定使⽤⼤数据产品,如 Hive、Spark、麒麟等产品。
  7. MySQL 5.7 版本开始⼦查询优化已经做得不错,但是编写的⼦查询不能是关联⼦查询,上线前⼀定需要确认,若发现关联⼦查询,请改写⼦查询为 JOIN 或其他⽅式。
  8. 不要因为数据量⼤,使⽤分区表,MySQL 是索引组织表,数据量再⼤,定位记录也只需要3、4 次 I/O。可以考虑分区表唯⼀的应⽤场景是:需要定期清理历史流⽔类数据,但如果业务可以按⽉、按天做分表,那么当前 MySQL 8.0 版本,分区表也不推荐使用。
  9. 业务上线或新版本发布前,DBA ⼀定要进行所有 SQL Review,确保 SQL ⾛索引,否则不予上线,或由业务以邮件等正式⽅式,通知DBA 该 SQL 不会引起线上事故,业务⽅承担后续责任。
  10. DBA 每天要对数据库进行巡检,及早发现慢查询或潜在数据库风险,将任何潜在问题尽早抛出,否则后续自己承担相关责任。

2.1.5 分库分表、分区表

  1. 【强制】分区表的分区字段(partition-key)必须有索引,或者是组合索引的⾸列。
  2. 【强制】单个分区表中的分区(包括⼦分区)个数不能超过1024。
  3. 【强制】上线前RD或者DBA必须指定分区表的创建、清理策略。
  4. 【强制】访问分区表的SQL必须包含分区键。
  5. 【建议】单个分区⽂件不超过2G,总⼤⼩不超过50G。建议总分区数不超过20个。
  6. 【强制】对于分区表执⾏alter table操作,必须在业务低峰期执⾏。
  7. 【强制】采⽤分库策略的,库的数量不能超过1024
  8. 【强制】采⽤分表策略的,表的数量不能超过4096
  9. 【建议】单个分表不超过500W⾏,ibd⽂件⼤⼩不超过2G,这样才能让数据分布式变得性能更佳。
  10. 【建议】⽔平分表尽量⽤取模⽅式,⽇志、报表类数据建议采⽤⽇期进⾏分表。

2.1.6 字符集

  1. 【强制】数据库本⾝库、表、列所有字符集必须保持⼀致,为utf8或utf8mb4。
  2. 【强制】前端程序字符集或者环境变量中的字符集,与数据库、表的字符集必须⼀致,统⼀为utf8。

2.1.7 程序层DAO设计建议

  1. 【建议】新的代码不要⽤model,推荐使⽤⼿动拼SQL+绑定变量传⼊参数的⽅式。因为model虽然可以使⽤⾯向对象的⽅式操作db, 但是其使⽤不当很容易造成⽣成的SQL非常复杂,且model层自己做的强制类型转换性能较差,最终导致数据库性能下降。
  2. 【建议】前端程序连接MySQL或者redis,必须要有连接超时和失败重连机制,且失败重试必须有间隔时间。
  3. 【建议】前端程序报错⾥尽量能够提⽰MySQL或redis原⽣态的报错信息,便于排查错误。
  4. 【建议】对于有连接池的前端程序,必须根据业务需要配置初始、最小、最大连接数,超时时间以及连接回收机制,否则会耗尽数据库连接资源,造成线上事故。
  5. 【建议】对于log或history类型的表,随时间增长容易越来越⼤,因此上线前RD或者DBA必须建⽴表数据清理或归档⽅案。
  6. 【建议】在应⽤程序设计阶段,RD必须考虑并规避数据库中主从延迟对于业务的影响。尽量避免从库短时延迟(20秒以内)对业务造成影响,建议强制⼀致性的读开启事务走主库,或更新后过⼀段时间再去读从库。
  7. 【建议】多个并发业务逻辑访问同⼀块数据(innodb表)时,会在数据库端产生行锁甚⾄表锁导致并发下降,因此建议更新类SQL尽量基于主键去更新。
  8. 【建议】业务逻辑之间加锁顺序尽量保持⼀致,否则会导致死锁。
  9. 【建议】对于单表读写比大于10:1的数据⾏或单个列,可以将热点数据放在缓存里(如mecache或redis), 加快访问速度,降低MySQL压力。

2.1.8 ⼀个规范的建表语句⽰例

⼀个较为规范的建表语句为:

CREATE TABLE user (`id` bigint(11) NOT NULL AUTO_INCREMENT,`user_id` bigint(11) NOT NULL COMMENT ‘⽤户id’`username` varchar(45) NOT NULL COMMENT '真实姓名',`email` varchar(30) NOT NULL COMMENT ‘⽤户邮箱’,`nickname` varchar(45) NOT NULL COMMENT '昵称',`avatar` int(11) NOT NULL COMMENT '头像',`birthday` date NOT NULL COMMENT '⽣⽇',`sex` tinyint(4) DEFAULT '0' COMMENT '性别',`short_introduce` varchar(150) DEFAULT NULL COMMENT '⼀句话介绍⾃⼰,最多50个汉字',`user_resume` varchar(300) NOT NULL COMMENT '⽤户提交的简历存放地址',`user_register_ip` int NOT NULL COMMENT ‘⽤户注册时的源ip’,`create_time` timestamp NOT NULL COMMENT ‘⽤户记录创建的时间’,`update_time` timestamp NOT NULL COMMENT ‘⽤户资料修改的时间’,`user_review_status` tinyint NOT NULL COMMENT ‘⽤户资料审核状态,1为通过,2为审核中,3为未通过,4为还未提交审核’,PRIMARY KEY (`id`),UNIQUE KEY `idx_user_id` (`user_id`),KEY `idx_username`(`username`),KEY `idx_create_time`(`create_time`,`user_review_status`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='⽹站⽤户基本信息';

2.2 SQL编写

2.2.1 DML语句

  1. 【强制】SELECT语句必须指定具体字段名称,禁⽌写成*。因为select *会将不该读的数据也从MySQL⾥读出来,造成⽹卡压⼒。且表字段⼀旦更新,但model层没有来得及更新的话,系统会报错。
  2. 【强制】insert语句指定具体字段名称,不要写成insert into t1 values(…),道理同上。
  3. 【建议】insert into…values(XX),(XX),(XX)…。这⾥XX的值不要超过5000个。值过多虽然上线很很快,但会引起主从同步延迟。
  4. 【建议】SELECT语句不要使⽤UNION,推荐使⽤UNION ALL,并且UNION⼦句个数限制在5个以内。因为union all不需要去重,节省数据库资源,提⾼性能。
  5. 【建议】in值列表限制在500以内。例如select… where userid in(….500个以内…),这么做是为了减少底层扫描,减轻数据库压⼒从⽽加速查询。
  6. 【建议】事务⾥批量更新数据需要控制数量,进⾏必要的sleep,做到少量多次。
  7. 【强制】事务涉及的表必须全部是innodb表。否则⼀旦失败不会全部回滚,且易造成主从库同步终端。
  8. 【强制】写⼊和事务发往主库,只读SQL发往从库。
  9. 【强制】除静态表或⼩表(100⾏以内),DML语句必须有where条件,且使⽤索引查找。
  10. 【强制】⽣产环境禁⽌使⽤hint,如sql_no_cache,force index,ignore key,straight join等。因为hint是⽤来强制SQL按照某个执⾏计划来执行,但随着数据量变化我们⽆法保证⾃⼰当初的预判是正确的,因此我们要相信MySQL优化器!
  11. 【强制】where条件⾥等号左右字段类型必须⼀致,否则⽆法利⽤索引。
  12. 【建议】SELECT|UPDATE|DELETE|REPLACE要有WHERE⼦句,且WHERE⼦句的条件必需使⽤索引查找。
  13. 【强制】⽣产数据库中强烈不推荐⼤表上发⽣全表扫描,但对于100⾏以下的静态表可以全表扫描。查询数据量不要超过表⾏数的25%,否则不会利⽤索引。
  14. 【强制】WHERE ⼦句中禁⽌只使⽤全模糊的LIKE条件进⾏查找,必须有其他等值或范围查询条件,否则⽆法利⽤索引。
  15. 【建议】索引列不要使⽤函数或表达式,否则⽆法利⽤索引。如where length(name)='Admin'或where user_id+2=10023。
  16. 【建议】减少使⽤or语句,可将or语句优化为union,然后在各个where条件上建⽴索引。如where a=1 or b=2优化为where a=1… union …where b=2, key(a),key(b)。
  17. 【建议】分页查询,当limit起点较⾼时,可先⽤过滤条件进⾏过滤。如select a,b,c from t1 limit 10000,20;优化为: select a,b,c from t1 where id>10000 limit 20;。

其它:

  1. 【强制】不要使⽤ count(列名)或 count(常量)来替代 count(),count()是 SQL92定义的
    1. 标准统计⾏数的语法,跟数据库⽆关,跟 NULL和⾮ NULL⽆关。
    2. 说明:count(*)会统计值为 NULL的⾏,⽽ count(列名)不会统计此列为 NULL值的⾏。
  2. 【强制】count(distinct col) 计算该列除 NULL之外的不重复⾏数,注意 count(distinctcol1, col2) 如果其中⼀列全为 NULL,那么即使另⼀列有不同的值,也返回为 0。
  3. 【强制】当某⼀列的值全是 NULL时,count(col)的返回结果为 0,但 sum(col)的返回结果为NULL,因此使⽤ sum()时需注意 NPE问题。
    1. 正例:可以使⽤如下⽅式来避免 sum的 NPE问题:SELECT IF(ISNULL(SUM(g)),0,SUM(g))FROM table;
  4. 【强制】使⽤ ISNULL()来判断是否为 NULL值。
    1. 说明:NULL与任何值的直接⽐较都为 NULL。
      1. NULL<>NULL的返回结果是 NULL,⽽不是 false。
      2. NULL=NULL的返回结果是 NULL,⽽不是 true。
      3. NULL<>1的返回结果是 NULL,⽽不是 true。
  5. 【强制】 在代码中写分页查询逻辑时,若 count为 0应直接返回,避免执⾏后⾯的分页语句。
  6. 【强制】不得使⽤外键与级联,⼀切外键概念必须在应⽤层解决。
    1. 说明:以学⽣和成绩的关系为例,学⽣表中的 student_id是主键,那么成绩表中的 student_id为外键。如果更新学⽣表中的
    2. student_id,同时触发成绩表中的 student_id更新,即为级联更新。外键与级联更新适⽤于单机低并发,不适合分布式、⾼并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插⼊速度。
  7. 【强制】禁⽌使⽤存储过程,存储过程难以调试和扩展,更没有移植性。
  8. 【强制】数据订正时,删除和修改记录时,要先 select,避免出现误删除,确认⽆误才能执⾏更新语句。
  9. 【推荐】in操作能避免则避免,若实在避免不了,需要仔细评估 in后边的集合元素数量,控制在 1000个之内。
  10. 【参考】如果有全球化需要,所有的字符存储与表⽰,均以 utf8mb4编码,注意字符统计函数的区别。
  11. 说明:
    1. SELECT LENGTH("轻松⼯作"); 返回为 12
    2. SELECT CHARACTER_LENGTH("轻松⼯作"); 返回为 4
  12. 如果需要存储表情,那么选择 utf8mb4来进⾏存储,注意它与 utf-8编码的区别。
  13. 【参考】TRUNCATE TABLE ⽐ DELETE 速度快,且使⽤的系统和事务⽇志资源少,但 TRUNCATE⽆事务且不触发trigger有可能造成事故,故不建议在开发代码中使⽤此语句。
  14. 说明:TRUNCATE TABLE 在功能上与不带 WHERE ⼦句的 DELETE 语句相同。

2.2.2 多表连接

  1. 【强制】禁⽌跨db的join语句。因为这样可以减少模块间耦合,为数据库拆分奠定坚实基础。
  2. 【强制】禁⽌在业务的更新类SQL语句中使⽤join,⽐如update t1 join t2…。
  3. 【建议】不建议使⽤⼦查询,建议将⼦查询SQL拆开结合程序多次查询,或使⽤join来代替⼦查询。
  4. 【建议】线上环境,多表join不要超过3个表。
  5. 【建议】多表连接查询推荐使⽤别名,且SELECT列表中要⽤别名引⽤字段,数据库.表格式,如select a from db1.table1 alias1 where …。
  6. 【建议】在多表join中,尽量选取结果集较⼩的表作为驱动表,来join其他表。

2.2.3 事务

  1. 【建议】事务中INSERT|UPDATE|DELETE|REPLACE语句操作的⾏数控制在2000以内,以及WHERE⼦句中IN列表的传参个数控制在500以内。
  2. 【建议】批量操作数据时,需要控制事务处理间隔时间,进⾏必要的sleep,⼀般建议值5-10秒。
  3. 【建议】对于有auto_increment属性字段的表的插⼊操作,并发需要控制在200以内。
  4. 【强制】程序设计必须考虑“数据库事务隔离级别”带来的影响,包括脏读、不可重复读和幻读。线上建议事务隔离级别为repeatable-read。
  5. 【建议】事务⾥包含SQL不超过5个(⽀付业务除外)。因为过长的事务会导致锁数据较久,MySQL内部缓存、连接消耗过多等雪崩问题。
  6. 【建议】事务⾥更新语句尽量基于主键或unique key,如update … where id=XX; 否则会产⽣间隙锁,内部扩⼤锁定范围,导致系统性能下降,产⽣死锁。
  7. 【建议】尽量把⼀些典型外部调⽤移出事务,如调⽤webservice,访问⽂件存储等,从⽽避免事务过长。
  8. 【建议】对于MySQL主从延迟严格敏感的select语句,请开启事务强制访问主库。

2.2.4 排序和分组

  1. 【建议】减少使⽤order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
  2. 【建议】order by、group by、distinct这些SQL尽量利⽤索引直接检索出排序好的数据。如where a=1 order by b可以利⽤key(a,b)。
  3. 【建议】包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000⾏以内,否则SQL会很慢。2.2.5 线上禁⽌使⽤的SQL语句
  4. 【⾼危】禁⽤update|delete t1 … where a=XX limit XX;这种带limit的更新语句。因为会导致主从不⼀致,导致数据错乱。建议加上order by PK。
  5. 【⾼危】禁⽌使⽤关联⼦查询,如update t1 set … where name in(select name from user where…);效率极其低下。
  6. 【强制】禁⽤procedure、function、trigger、views、event、外键约束。因为他们消耗数据库资源,降低数据库实例可扩展性。推荐都在程序端实现。
  7. 【强制】禁⽤insert into …on duplicate key update…在⾼并发环境下,会造成主从不⼀致。
  8. 【强制】禁⽌联表更新语句,如update t1,t2 where t1.id=t2.id…。

2.2.5 ORM映射

  1. 【强制】在表查询中,⼀律不要使⽤ * 作为查询的字段列表,需要哪些字段必须明确写明。
    1. 说明:1)增加查询分析器解析成本。2)增减字段容易与 resultMap配置不⼀致。
  2. 【强制】POJO类的布尔属性不能加 is,⽽数据库字段必须加 is_,要求在 resultMap中进⾏字段与属性之间的映射。
    1. 说明:参见定义 POJO类以及数据库字段定义规定,在中增加映射,是必须的。在 MyBatis Generator⽣成的代码中,需要进⾏对应的修改。
  3. 【强制】不要⽤ resultClass当返回参数,即使所有类属性名与数据库字段⼀⼀对应,也需要定义;反过来,每⼀个表也必然有⼀个与之对应。
    1. 说明:配置映射关系,使字段与 DO类解耦,⽅便维护。
  4. 【强制】sql.xml配置参数使⽤:#{},#param# 不要使⽤${} 此种⽅式容易出现 SQL注⼊。
  5. 【强制】MYBATIS⾃带的 queryForList(String statementName,int start,int size)不推荐使⽤。
    1. 说明:其实现⽅式是在数据库取到statementName对应的SQL语句的所有记录,再通过subList取 start,size的⼦集合。
    2. 正例:Map map = new HashMap();
      1. map.put("start", start);
      2. map.put("size", size);
  6. 【强制】不允许直接拿 HashMap与 Hashtable作为查询结果集的输出。
    1. 说明:resultClass=”Hashtable”,会置⼊字段名和属性值,但是值的类型不可控。
  7. 【强制】更新数据表记录时,必须同时更新记录对应的 gmt_modified字段值为当前时间。
  8. 【推荐】不要写⼀个⼤⽽全的数据更新接⼝传⼊为 POJO类,不管是不是⾃⼰的⽬标更新字段,都进⾏ update table set c1=value1,c2=value2,c3=value3; 这是不对的。执⾏SQL时,不要更新⽆改动的字段,⼀是易出错;⼆是效率低;三是增加 binlog存储。
  9. 【参考】@Transactional事务不要滥⽤。事务会影响数据库的 QPS,另外使⽤事务的地⽅需要考虑各⽅⾯的回滚⽅案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
  10. 【参考】中的 compareValue是与属性值对⽐的常量,⼀般是数字,表⽰相等时带 上此条件;表⽰不为空且不为 null时执⾏;表⽰不为 null值时执⾏。

2.3 ⾼可⽤架构

  1. MySQL ⾼可⽤的基⽯是利⽤⼆进制⽇志的复制技术,核⼼业务⼀定要使⽤⽆损半同步⽅式,但凡不适⽤⽆损半同步的⾼可⽤架构,请业务⽅业务以邮件等正式⽅式回复,数据丢失等后续问题⾃⼰承担相关责任。
  2. MySQL 5.7 版本开始,⼀定要使⽤基于 WRITESET 的从机回放,避免主从延迟。
  3. 当前 MySQL 发⽣主从延迟的可能性主要是存在⼤事务,⽐如定期计算收益等操作,这类⼤事务,⼀定要通知业务⽅将⼤事务拆成⼩事务,否则不予上线;若要上线,请业务⽅以邮件等⽅式回复,⾃⼰承担后续主从延迟带来的后续⼀系列问题。
  4. MySQL 8.0 版本开始推荐⾦融业务使⽤ MGR(MySQL Group Replication),通过 Paxos 协议保证数据⼀致性,并⾃⼰完成选主的逻辑,也可以使⽤多主模式,数据不冲突的情况下,可以⼤幅提升写⼊性能。
  5. 对于核⼼业务,必须遵循互不信任原则,数据⼀致性不单单依赖 MySQL 复制本⾝,DBA 这⾥需要通过逻辑的⽅式,对主从数据进⾏核对,业务这⾥也需要⼀套业务层的逻辑进⾏“对账”。
  6. 核⼼业务,务必使⽤⼀地三中⼼,两地三中⼼的跨机房复制架构,这样发⽣机房级故障,可以切换到另⼀个机房,保证业务可⽤性。
  7. 同城容灾架构⼀定要评估切换到另⼀个机房后业务访问的性能,多次跨机房访问 DB,虽然每次只多了 2~3ms,但也存在业务雪崩问题,推荐 DB 切换机房,上层业务跟着⼀起联动切换。
  8. 对于有跨城容灾需求的业务,可以考虑使⽤三地五中⼼架构,但是由于 30ms 延迟,业务需要进⾏评估,对于核⼼业务,务必使⽤业务层的跨城机制,将数据层的多次⽹络耗时合并为⼀次,这样能⼤⼤提升业务的性能。
  9. 业界的 MHA、Ochestrator 等⾼可⽤套件都是基于 ssh 访问 MySQL,稳定性、安全性不⾼,不推荐⼤⼚使⽤;⾃⼰开发⼀个数据库管理平台,通过 agent 的模式管理⾼可⽤和 MySQL 数据库的⽇常操作更为安全、有效。
  10. ⼀定做好数据备份架构的设计,全备 + 增量备份 + 延迟备机(可选),做到可以基于任何⼀点恢复和回滚,同时,遵循互不信任原则,备份⽂件⼀定要进⾏检查,确保需要时⼀定能够进⾏恢复。

2.4 分布式架构

  1. 分布式数据库的本质就是根据某⼏个列的规则,将数据⽔平打散,存在不同的实例中。数据拆分的列就成为分区键,分区键⼀定是业务⼤部分访问(超过 80%)的表都会使⽤的列。若选不出合适的分区键,那就⼀定不要进⾏分布式数据库架构的设计;互联⽹业务绝⼤部分分区键的选择是⽤户维度。
  2. 分区算法绝⼤部分场景使⽤ Hash算法,这样数据的存储和访问可以平均到下⾯多个实例,真正的做到可扩展性,Range 算法通常无法解决热点问题,会是灾难,但 Range 算法可以但实例中使⽤,作为⼆级拆分数据的规则。
  3. 分布式数据库分⽚时,⼀开始就设计为不少于 1000 个分⽚的规则,不⽤担⼼分⽚过多的问题,管理 1 个分⽚和 1000 个分⽚的成本是⼀样的,但为后续的扩展做好了充⾜的准备。
  4. 分布式数据库扩缩容就是通过部分过滤的复制技术,按库或按表进⾏数据同步,分库分表设计推荐库名、表都不同,做到全局唯⼀,⽅便后续拆分。
  5. 分布式数据库索引设计中,⾮分区键的唯⼀索引⼀定带⼊分区键信息,这样业务查询时可以直接定位到数据所在分⽚,提升查询效率。
  6. 分布式数据库索引设计中,数据库层的唯⼀约束只在单个实例中保证,若要保证全局唯⼀,⼀定要使⽤全局唯⼀的索引设计。
  7. 直接使⽤ JOIN 请确认⼀定可以单元化在⼀个分⽚中完成,如果涉及跨分⽚的 JOIN,请通知业务修改成多条 SQL 的访问⽅式,只访问指定分⽚⽽不是所有分⽚。
  8. 分布式数据库可以进⾏业务层的分库分表访问,和通过数据库中间件的访问,对于业务耗时敏感的业务,推荐业务层直接根据路由规则访问数据,否则使⽤数据库中间件,简单易⽤。
  9. 对于耗时敏感的核⼼业务,推荐使⽤最终⼀致的业务层柔性事务,数据库层的 2PC 分布式事务耗时较⼤,性能较为⼀般,但是 2PC使⽤简单,能满⾜⼤部分业务的使⽤。
  10. ⼀定要利⽤好分布式数据库架构的特点,设计多活架构,每个机房都可以有写⼊流量,提升资源使⽤率和业务连续性,请 DBA 和业务⽅⼀起做好全链路的架构设计。

你可能感兴趣的:(数据库设计规范)