1. 对象命名规范
- 不同类型数据库对象,以类型关键字缩写作为对象名前缀。如数据表
t_xxx_xxx
、视图v_xxx_xxx
; - 数据库对象名称用
小写字母
,并用下划线_
分割单词,以达到见名识意,且对象名长度不超 32 个字符; - 临时库表以
tmp_
为前缀,并以日期_YYYYMMDD
为后缀;备份表以bak_
为前缀,并以日期_YYYYMMDD
为后缀; - 存储相同数据的列名和列类型需保持一致(尤其作为表连接关联列时,若关联列类型不一致会自动进行数据类型隐式转换,将会造成关联列上的索引失效,导致查询效率降低)。
2. 环境使用规范
2.1. 禁止在线上做数据库压力测试
2.2. 禁止从开发环境,测试环境直连生产环境数据库
开发环境、测试环境直连生产环境数据库,可能因误操作,而导致误删数据等无法挽回的错误。
2.3. 专库专用(连接不同的数据库使用不同的账号),禁止跨库查询
- 为数据库迁移和分库、分表留出余地
- 专库专用,条理清晰,降低业务耦合度
- 避免权限过大而产生的安全风险
针对每个业务数据库,创建独立的数据库用户(通常与数据库同名),该数据库用户仅可操作与其同名的业务数据库。禁止在多个数据库之间跨库查询。
如下示例中,禁止授予 storemgr
用户访问 ordermgr
数据库。若业务需要,可通过多数据源等方式实现。
新创建的数据库用户,默认遵循最小授权原则,仅授予INSERT, DELETE, UPDATE, SELECT, CREATE
权限。
业务名称 | 数据库名 | 数据库用户名 | 授权语句 |
---|---|---|---|
订单管理子系统 | ordermgr |
ordermgr |
GRANT INSERT, DELETE, UPDATE, SELECT, CREATE ON ordermgr.* TO ordermgr |
仓储子系统 | storemgr |
storemgr |
GRANT INSERT, DELETE, UPDATE, SELECT, CREATE ON ordermgr.* TO ordermgr |
2.4. 用户权限遵循最小原则
默认新建的数据库用户,只分配 DML
权限,如 INSERT, DELETE, UPDATE, SELECT
,不分配 DDL 权限。
在MySQL 中 truncate table
需要 DROP
权限。
3. 数据库设计规范
3.1. 所有表必须使用 InnoDB 存储引擎
在特殊要求(即 InnoDB 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL 5.5 之前默认使用 Myisam,5.6 以后默认的为 InnoDB)。InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能更好
3.2. 统一字符集 UTF8 及排序规则
- 库、表字符集统一为 utf8mb4
utf8mb4
: 使用 1-4 个字节表示一个字符的UTF-8
编码。utf8mb3
: 使用 1-3 个字节表示一个字符的UTF-8
编码。utf8
: 同utf8mb3
,是utf8mb3
的别名
兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前,需要进行转换会造成索引失效。
- 统一排序规则
general_ci
更快,unicode_ci
更准确,ci
不区分大小写,cs
区分大小写
utf8mb4_general_ci
不区分大小写。排序速度略快,排序准确性略差。
utf8mb4_unicode_ci
不区分大小写。排序速度略慢,排序准确性略高。
utf8mb4_bin
区分大小写。将字符串每个字符用二进制数据编译存储,区分大小写,而且可以存二进制的内容。
utf8mb4_general_cs
区分大小写
3.3. 专业的事情交给专业的人做
避免在使用数据库函数、存储过程实现业务逻辑,以减少对数据库类型的依赖。便于日后迁移至其他数据库,如 PostgreSQL
、达梦
等。
4. 表设计规范
4.1. 所有表和字段都需要添加注释
使用 comment
子句添加 表和列
的备注
4.2. 控制单表数据量的大小
尽量控制单表数据量在 500W
以内,最高不超过 2000W
。过多的数据量会造成修改表结构,备份,恢复都会有很大的问题;
可以用历史数据归档(应用于日志数据)、分库分表(应用于业务数据)、分区表等手段来控制单表数据量大小;
分区表中的每个分区相当于一个物理表;
4.3. 谨慎使用 MySQL 分区表
分区表在逻辑上表现为一个表,但是在物理上表现为多个文件; 谨慎选择分区字段
(分区字段需要包含在 唯一键
中,即主键或唯一索引
中),跨分区查询效率可能更低。推荐采用 物理分表
的方式管理大数据。
4.4. 尽量做到冷热数据分离,减小表的宽度
MySQL 限制每个表最多存储 4096
列,并且每一行数据的大小不能超过 64K,以减少磁盘IO,保证热数据的内存缓存命中率(表越宽,把表装载进内存缓冲池时所占用的内存也就越大,也会消耗更多的 IO)。
更有效的利用缓存,避免读入无用的冷数据。经常一起使用的列放到一个表中(避免更多的表连接操作)。
4.5. 禁止在表中建立预留字段
预留字段的命名很难做到见名识义,预留字段无法确认数据类型。
4.6. 禁止在数据库中存储二进制数据
图片、文件通常较大,会短时间内造成数据量快速增长。数据库在进行数据读取时,会进行大量的随机IO操作,文件很大时,IO操作很耗时。通常将图片或文件存储于文件服务器,在数据库中存储其文件地址信息。
4.7. 为每个表都要指定递增数值主键
-
推荐使用业务不相关的递增数值型作为表主键,可用
AUTO_INCREMENT
自增或SHORT_UUID()
(类似雪花算法的结构)。因为 InnoDB 通过聚簇索引来组织表数据的存储。主键递增,可避免数据插入的随机性,避免 page 分裂,减少表碎片提升空间和内存的使用效率。避免使用 UUID(随机字符串)作为主键。 -
由于表没有主键,副本库的回放效率极差,大概率会引起主从复制的延迟,继而影响业务的读取,容灾切换的时效性等各种问题。
5. 字段设计规范
5.1. 优先选择符合存储需要的最小的数据类型
- 原因
列的字段越大,建立索引时所需要的空间也就越大,这样一页中所能存储的索引节点的数量也就越少也越少,在遍历时所需要的IO次数也就越多, 索引的性能也就越差。 - 方法
- 将字符串转换成数字类型存储,如:将 IP 地址转换成整形数据。
MySQL提供了两个方法来处理ip地址:
inet_aton 把ip转为无符号整型(4-8位)
inet_ntoa 把整型的ip转为地址
插入数据前,先用inet_aton
将 IP 地址转为整型,可以节省空间。显示数据时,使用inet_ntoa
将整型的 IP 地址转为地址显示即可。 - 对于非负型的数据(如自增 ID、整型 IP)来说,要优先使用无符号整型来存储。因为,无符号相对于有符号可以多出
1
倍的存储空间
VARCHAR(N)
中的N
代表的是字符数,而不是字节(Byte)数
- 将字符串转换成数字类型存储,如:将 IP 地址转换成整形数据。
使用 UTF8 存储 255 个汉字 VARCHAR(255) = 765
个字节,过大的长度会消耗更多的内存
5.2. 避免使用 TEXT、BLOB 数据类型,最常见的TEXT类型可以存储64k的数据
- 建议将
BLOB
或是TEXT
列分离到单独的扩展表中
MySQL 内存临时表不支持TEXT
、BLOB
这样的大数据类型。如果查询中包含这样的数据,在排序等操作时,无法使用内存临时表,必须使用磁盘临时表进行,影响性能。
而且对于这种数据,MySQL 还要进行二次回表查询,会使 SQL 性能变得很差。
如果一定要使用TEXT
、BLOB
,建议将TEXT
、BLOB
列分离到单独的扩展表中。查询时一定不要使用SELECT *
,而名确列出必要的列。不需要TEXT
列的数据时,不要对该列进行查询。 TEXT
、BLOB
类型只能使用前缀索引
因为 MySQL 对索引字段长度是有限制的,所以TEXT
类型只能使用前缀索引,并且TEXT
列上不能有默认值。
5.3. 避免使用ENUM类型
- 修改
ENUM
值需要使用ALTER
语句 ENUM
类型的ORDER BY
操作效率低,需要额外操作- 禁止使用数值作为
ENUM
的枚举值
5.4. 尽可能将列定义为 NOT NULL
原因:
- 索引
NULL
列需要额外的空间来保存,所以要占用更多的空间; - 进行比较和计算时,需要对
NULL
值做特别的处理; - 含
NULL
值的符合索引无效。
5.5. 使用DATETIME存储时间
类型 | 占用空间 | 表示范围 | 其他说明 |
---|---|---|---|
DATE | ‘1000-01-01’ to ‘9999-12-31’ | ||
DATETIME | 8字节 | ‘1000-01-01 00:00:00.000000’ to ‘9999-12-31 23:59:59.999999’ | |
TIMESTAMP | 4字节 | ‘1970-01-01 00:00:01.000000’ to ‘2038-01-19 03:14:07.999999’ | MySQL 将 TIMESTAMP 值从当前时区转换为 UTC 进行存储,并从 UTC 转换回当前时区进行检索。 |
注意
禁止用字符串存储日期
、时间
类型:
- 无法用
日期时间函数
进行计算比较- 字符串比
datetime
、timestamp
占用更多的存储空间
5.6. 财务金额用 decimal 类型
- 非精准浮点类型:
float
、double
- 精准浮点类型:
decimal
Decimal
类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。可用于存储比bigint
更大的整型数据。
6. 索引设计规范
6.1. 限制每张表上的索引数量,建议单张表索引不超过 5 个
索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。
因为优化器在选择如何优化查询时,会根据统计信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划。如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
6.2. 禁止给表中的每一列都建立单独的索引
5.6 版本之前,一个 SQL 只能使用到一个表中的一个索引。5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询效率高。
6.3. 每个 Innodb 表必须有个主键
Innodb 是一种索引组织表:数据存储的逻辑顺序和索引顺序是相同的。
每个表都可以有多个索引,但是表的存储顺序只能有一种。Innodb 是按照主键索引的顺序来组织表的。
不要使用更新频繁的列作为主键;不要用多列主键(相当于联合索引); 不要使用 UUID
、MD5
、HASH
、字符串列作为主键(无法保证数据的顺序增长)。
主键建议使用自增 ID
值,如 AUTO_INCREMENT
或 SHORT_UUID()
。
6.4. 常见索引列建议
- 出现在
SELECT
、UPDATE
、DELETE
语句的WHERE
从句中的列 - 包含在
ORDER BY
、GROUP BY
、DISTINCT
中的字段 - 并不要将符合
1
和2
中的字段的列都建立一个索引,通常将1
、2
中的字段建立联合索引效果更好 - 多表
join
的关联列
6.5. 如何选择索引列的顺序
建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能。索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
- 区分度最高的放在联合索引的最左侧(
区分度=列中不同值的数量/列的总行数
); - 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好);
- 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)。
- 需要用范围扫描的列,放联合索引的最右侧。
6.6. 避免建立冗余索引和重复索引
因为这样会增加查询优化器生成执行计划的时间。
- 重复索引示例:primary key(id)、index(id)、unique index(id)
- 冗余索引示例:index(a,b,c)、index(a,b)、index(a)
6.7. 优先考虑覆盖索引
对于频繁的查询优先考虑使用覆盖索引。
覆盖索引: 就是包含了所有查询字段(WHERE
,SELECT
,ORDER BY
,GROUP BY
包含的字段)的索引。
覆盖索引的好处:
-
避免 Innodb 表进行索引的二次回表
Innodb 是以聚集索引的顺序来存储的,对于 Innodb 来说,二级索引在叶子节点中所保存的是行的主键信息,
如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次回表才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次回表查询 ,减少了 IO 操作,提升了查询效率。 -
可将随机 IO 变成顺序 IO 加快查询效率
由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,比随机从磁盘读取每一行的数据 IO 要少的多。因此,利用覆盖索引在访问时也可将磁盘的随机读取 IO 转变成索引查找的顺序 IO。
6.8. 尽量避免使用外键约束
- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引;
- 外键可用于保证数据的参照完整性,但建议在业务端实现;
- 外键会影响父表和子表的写操作(因为有锁)从而降低性能。
7. 数据库 SQL 开发规范
7.1. 建议使用预编译语句进行数据库操作
预编译语句可以重复使用这些计划,减少 SQL 编译所需要的时间,还可以解决动态 SQL 所带来的 SQL 注入的问题。只传参数,比传递 SQL 语句更高效。相同语句可以一次解析,多次使用,提高处理效率。
7.2. 避免数据类型的隐式转换
隐式转换会导致索引失效。如:
select name,phone from customer where id = '111';
复制
7.3. 充分利用表上已经存在的索引
WHERE
条件避免使用左%
。
如a like '%123%'
,(如果无前置%
,只有后置%
,可以用到列上的索引)- 一个 SQL 只能利用到复合索引中的一列进行范围查询
如:有a,b,c
列的联合索引,在查询条件中有a
列的范围查询,则在b,c
列上的索引将不会被用到。在定义联合索引时,如果a
列要用到范围查找的话,就要把a
列放到联合索引的右侧。 - 使用
left join
或not exists
来优化not in
操作
因为not in
也通常会使用索引失效。 - 范围查询时,尽量用
>=
替代>
;用<=
替代<
7.4. 禁止使用 SELECT *
,必须使用 SELECT <字段列表>
查询
原因:
- 消耗更多的
CPU
和IO
以网络带宽资源 - 无法使用覆盖索引
- 可减少表结构变更带来的影响
7.5. 禁止使用不含字段列表的 INSERT
语句
如:
INSERT INTO VALUES('a','b','c');
复制
应使用:
INSERT INTO T(c1,c2,c3) VALUES ('a','b','c');
复制
7.6. 避免使用子查询,可将子查询优化为 join 操作
通常子查询在 in
子句中,且子查询中为简单 SQL
(不包含 union、group by、order by、limit
从句)时,才可将 子查询
转化为 表连接
进行优化。
子查询性能差的原因:
* 子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能 会受到一定的影响;
* 特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大;
* 由于子查询会产生大量的临时表也没有索引,所以会消耗过多的CPU和IO资源,产生大量的慢查询。
7.7. 避免使用 JOIN
关联太多的表,建议不超 5
个
对于 MySQL 来说,是存在关联缓存的,缓存的大小可以由 join_buffer_size
参数进行设置。
在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越高。
如果程序中大量的使用了多表关联的操作,同时 join_buffer_size
设置的不高的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
同时对于关联操作来说,会产生临时表操作,影响查询效率 MySQL 最多允许关联 61
个表,建议不超过 5
个。,尤其对于 OLTP 系统,建议不超过 3 个。
7.8. 减少与数据库的交互次数
数据库更适合处理批量操作,合并多个相同的操作到一起,可以提高处理效率
7.9. 使用 in
代替 or
-
对同一列进行
or
判断时,可使用in
代替or
。in
可以更有效的利用索引,or
大多数情况下很少能利用到索引。但in
的值不要超过500
个。 -
对不同字段进行
or
判断时,可将or
改写为union
。
7.10. LIMIT 高效分页
传统分页,offset 偏移量越大,速度越慢
SELECT * FROM table LIMIT <offset>,<rows>;
复制
可用如下推荐分页示例改写
SELECT * FROM table WHERE id>=23423 LIMIT 11;
SELECT * FROM table WHERE id >= ( SELECT id FROM table LIMIT 10000,1 ) LIMIT 10;
SELECT * FROM table INNER JOIN (SELECT id FROM table LIMIT 10000,10) USING (id) ;
复制
7.11. 禁止使用 order by rand()
进行随机排序
会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值。如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。
7.12. WHERE
从句中禁止对列进行函数转换和计算
对列进行函数转换或计算时会导致无法使用索引。
* 不推荐: where date(create_time)='20190101' * 推荐: where create_time >= '20190101' and create_time < '20190102'
复制
7.13. 使用 UNION ALL
而不是 UNION
在明显不会有重复值时,使用 UNION ALL
而不是 UNION
- UNION 会将两个结果集的数据放到临时表中,再进行去重操作。
- UNION ALL 将两个结果集取并集,不会进行去重操作。
7.14. 拆分复杂的大 SQL
为多个小 SQL
大 SQL,指逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL。在 MySQL 中,一个 SQL
只能使用一个 CPU
进行计算。将大 SQL 拆分后,可以通过并行执行来提高处理效率。
8. 数据库操作规范
8.1. 超 100W 行的批量 DML 操作,要分批多次进行操作
-
大批量 DML 操作可能会造成严重的主从延迟
主从环境中,大批量操作可能会造成严重的主从延迟
,大批量的 DML 操作一般都需要执行一定长的时间。而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况。 -
binlog 日志为 ROW 格式时会产生大量的日志
大批量DML
操作会产生大量binlog
,特别是对于ROW
格式的binlog
而言,由于在ROW
格式中会记录每一行数据的修改。我们一次修改的数据越多,产生的binlog
日志量也就会越多。日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因。 -
避免产生大事务操作
一个大事务中的大批量修改数据,会造成表中大批量数据进行锁定。从而导致大量的阻塞,阻塞会对 MySQL 的性能产生非常大的影响。
特别是长时间的阻塞会占满所有数据库的可用连接,会使生产环境中的其他应用无法连接到数据库。因此,要注意大批量写操作要进行拆分。
8.2. 对于大表使用 pt-online-schema-change 修改表结构
- 避免大表修改产生的主从延迟
- 避免在对表字段进行修改时进行锁表
对大表的结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。
推荐使用percona
公司percona-toolkit
工具包的pt-online-schema-change
进行表结构修改。修改原理:- 原表需有主键或唯一索引。
pt-online-schema-change
它会首先建立一个与原表结构相同的新表,并且在新表上进行表结构的修改- 然后再把原表中的数据复制到新表中,并在原表中增加一些触发器(DELETE/UPDATE/INSERT),通过原表的触发器,将原表中新增的数据也复制到新表中。
- 在行所有数据复制完成之后,将新表命名成原表,并删除原表。将原来一个
DDL
操作,分解成多个小的操作,分批次进行。