数据库设计及架构优化
1.数据库设计规范
# 创建库和表 命名规范:小写字母和下划线,不要超过32个字符 命名要做到见名识意,禁止使用mysql保留关键字 # 临时表 临时库表必须以tmp为前缀并以日期为后缀 # 备份库 备份库,备份表必须以bak为前缀并以日期为后缀 # 所有存储相同类型的列名和列类型必须一致 CREATE TABLE customer_inf( customer_inf_id int unsigned auto_increment not null comment "自增", customer_name varchar(20) not null comment "用户真实姓名" ) ENGINE=INNODB COMMENT '用户信息表' ; # 所有表必须使用Innodb存储引擎(5.6以后的默认引擎) 支持事务,行级锁,更好的恢复性,高并发下性能更好 # 数据库和表的字符集统一使用UTF8 统一字符集可以避免由于字符集转换产生的乱码 mysql中UTF8字符集汉子占3个字节,ASCII码占用1个字节 # 所有的表和字段都需要添加注释 使用comment从句添加表和列的备注 从一开始就进行数据字典的维护 # 尽量控制单表数据量的大小,建议控制在500万以内 500万并不是mysql数据库的限制 修改表结构,备份,恢复都会有很大问题 可以用历史数据归档,分库分表等手段来控制数据量大小 mysql最多可以存储多少万数据呢? 这种限制取决于存储设置和文件系统 # 谨慎使用mysql分区表 分区表在物理上表现为多个文件,在逻辑上表现为一个表 谨慎选择分区键,跨分区查询效率可能更低 建议采用物理分表的方式管理大数据 # 尽量做到冷人数据分离,减小表的宽度 mysql限制最多存储4096列 减少磁盘IO,保证热数据的内存缓存命中率 利用更有效的利用率,避免读入无用的冷数据 经常一起使用的列放到一个表中 # 禁止在表中建立预留字段 预留字段的命名很难做到见名识义 预留字段无法确认存储的数据类型,所以无法选择合适的类型 对预留额字段类型的修改,会对表进行锁定(还不如直接创建一个新的字段) # 禁止在数据库中存储图片,文件等二进制数据(可以存它们的存储路径) # 禁止在线上做数据库压力测试 # 禁止从开发环境,测试环境直接连生产环境数据库 # 总结 所有表必须使用Innodb存储引擎 所有表及字段都要有备注信息,并使用UTF8字符集 要做到尽量控制单表大小,并且把冷热数据分离 禁止使用预留字段及在表中存储大的二进制数据
2.索引设计规范
# 索引对数据库的查询性能来说是非常重要的 不要滥用索引 # 限制每张表上的索引数量,建议单张表索引不超过5个 索引并不是越多越好!索引可以提高效率同样可以降低效率 索引可以增加查询效率,但同样也会降低插入和更新的效率 禁止给表中的每一列都建立单独的索引 # Innodb是按照那个索引的顺序来组织表的呢? 答案:主键 # 每个Innodb表必须有一个主键,如果没有主键的话,mysql会自己根据第一个索引来组织表 不适用更新频繁的列作为主键,不适用多列主键 不适用UUID,MD5,HASH,字符串列作为主键 主键建议选择适用自增ID值 # 常见索引列建议 SELECT、UPDATE、DELETE语句的WHERE从句中的列 包含在ORDER BY、GROUP BY、DISTINCT中的字段 多表JOIN的关联列 # 如何选择索引列的顺序 区分度最高的列放在联合索引的最左侧 尽量把字段长度小的列放在联合索引(复合索引)的最左侧 使用最频繁的列放到联合索引的左侧 # 避免建立冗余索引和重复索引 primary key(id) index(id) unique index(id) index(a,b,c) index(a,b) index(a) # 对于频繁的查询优先考虑使用覆盖索引 覆盖索引:就是包含了所有查询字段的索引 避免Innodb表进行索引的二次查找 可以把随机IO变为顺序为IO加快查询效率 # 尽量避免使用外键 不建议使用外键约束,但一定在表与表之间的关联键上建立索引 外键可用于保证数据的参照完整性,但建议在业务端实现 外键会影响父表和子表的写操作从而降低性能 # 总结 每个Innodb表都要有一个主键 限制表上索引的数量,避免建立重复和冗余索引 注意合理选择复合索引键值的顺序
3.数据库字段设计规范
# 优先选择符合存储需要的最小的数据类型 将字符串转化为数字类型存储,'255.255.255.255'占16个字节,4294967295占4个字节 INET_ATON('255.255.255.255') = 4294967295 INET_NTOA(4294967295) = '255.255.255.255' 对于非负型的数据来说,要优先使用无符号整型来存储 无符号相对于有符号可以多出一倍的存储空间 SIGNED INT -2147483647~2147483647 UNSIGNED INT 0~4294967295 VARCHAR(N)中的N代表的是字符数,而不是字节数 使用UTF8存储汉子Varchar(255)=765个字节 过大的长度会消耗更多的内存 # 避免使用TEXT、BLOB数据类型 TinyText、Text、MidumText、LongText 建议把BLOB或是TEXT列分离到单独的扩展表中 TEXT或BLOB类型只能使用前缀索引 # 避免使用ENUM数据类型 修改ENUM值需要使用ALTER语句 ENUM类型的ORDER BY操作效率低,需要额外操作 禁止使用数值作为ENUM的枚举值 # 尽可能把所有列定义为NOT NULL 索引NULL列需要额外的空间来保存,所以要占用更多的时间 进行比较和计算时要对NULL值做特别的处理 # 字符串存储日期型的数据(不正确的做法) 缺点1:无法用日期函数进行计算和比较 缺点2:用字符串存储日期要占用更多的空间 # 使用TIMESTAMP或DATETIME类型存储时间 TIMESTAMP 1970-01-01 00:00:01 ~ 2038-01-19 03:14:07 TIMESTAMP占用4个字节和INT相同,但比INT可读性搞 超出TIMESTAMP取值范围的使用DATETIME类型 # 同财务相关的金额类数据,必须使用decimal类型 1、非精准浮点: float、double 2、精准浮点 decimal Decimal类型为精准浮点数,在计算时不会丢失精度 占用空间由定义的宽度决定 可用于存储比bigint更大的整数数据 # 总结 选择符合存储要求的最小的数据类型 避免使用Blob或是Text类型及ENUM类型 每个字段尽可能具有NOT NULL属性 使用datetime或timestamp类型存储时间
4.数据库SQL开发规范
# 建议使用预编译语句进行数据库操作 mysql>PREPARE stmt1 ->FROM 'SELECT SQRT(POW(?,2) + POW(?,2)) AS hypotenuse'; mysql>SET @a = 3; mysql>SET @b = 4; mysql>EXECUTE stmt1 USING @a,@b; mysql>DEALLOCATE PREPARE stmt1; 只传参数,比传递SQL语句更高效 相同语句可以一次解析,多次使用,提高处理效率 # 避免数据类型的隐式转换 隐式转换会导致索引失效 # 合理利用存在索引,而不是盲目增加索引、 # 充分利用表上已经存在的索引 避免使用双%号的查询条件。如 a like '%123%' 一个SQL只能利用到复合索引中的一列进行范围查询(需要进行方位查询的列,放在复合索引的最右边) 使用left join 或 not exists 来优化not in操作(因为not in通常使索引失效) # 数据库设计时,应该要对以后扩展进行考虑 # 程序连接不同的数据库使用不同的账号,禁止跨库查询 为数据库迁移和分库分表留出余地 降低业务耦合度 避免权限过大而产生的安全风险 # 禁止使用SELECT * 必须使用 SELECT <字段列表> 查询 消耗更多的CPU和OO以及网络带宽资源 无法使用覆盖索引 可减少表结构变更带来的影响 # 禁止使用不含字段列表的INSERT语句 insert into t values('a','b','c'); # 错误 insert into t(c1,c2,c3) values('a','b','c'); # 正确 可减少表结构变更带来的影响 # 避免使用子查询,可以把子查询优化为join操作 子查询的结果集无法使用索引 子查询会产生临时表操作,如果子查询数据量大则严重影响效率 消耗过多的CPU及IO资源 # 避免使用JOIN关联太多的表 每Join一个表会多占用一部分内存(join_buffer_size) 会产生临时表操作,影响查询效率 MySQL最多允许关联61个表,建议不超过5个 # 减少同数据库的交互次数 数据库更适合处理批量操作 合并多个相同的操作到一起,可以提高处理效率 alter table t1 add column c1 int ,change column c2 c2 int... # 使用 in 代替 or in 的值不要超过500个 in 操作可以有效的利用索引 # 禁止使用 order by rand() 进行随机排序 会把表中所有符合条件的数据装载到内存中进行排序 会消耗大量的CPU和IO及内存资源 推荐在程序中获取一个随机值,然后从数据库中获取数据的方式 # WHERE从句中禁止对列进行函数转换和计算 对列进行函数转换或计算会导致无法使用索引 where date(createtime) = '20160901'; # 不好,索引失效 where createtime >= '20160901' and createtime < '20160902'; # 好,利用索引 # 在明显不会有重复值时使用UNION ALL 而不是UNION UNION会把所有数据放到临时表中后再进行去重操作 UNION ALL 不会再对结果集进行去重操作 # 拆分复杂的大SQL为多个小SQL MySQL一个SQL只能使用一个CPU进行计算 SQL拆分后可以通过并进行执行来提高处理效率 # 总结 建议使用预编译语句进行数据库操作 避免数据类型的隐式转换 禁止使用 select * 进行查询及没有字段列表的insert操作 优先利用表上已经存在的索引
5.数据库操作行为规范
# 超100万行的批量写操作,要分批多次进行操作 大批量操作可能会造成严重的主从延迟 binlog日志为row格式时会产生大量的日志 避免产生大事务操作 # 对大表数据结构的修改一定要谨慎,会造成严重的锁表操作。 # 尤其是生产环境,是不能忍受的 # 对于大表使用pt-online-schema-change修改表结构 避免大表修改产生的主从延迟 避免在对表字段进行修改时进行锁表 # 禁止为程序使用的账号赋予super权限 当达到最大连接数限制时,还允许1个有super权限的用户连接 super权限只能留给DBA处理问题的账号使用 # 对于程序连接数据库账号,遵循权限最小原则 程序使用数据库账号只能在一个DB下使用,不准跨库 程序使用的账号原则上不准有drop权限
6.mysql分区表
# 确认mysql服务器是否支持分区表 mysql>SHOW PLUGINS; # 主要看partition name status type library license partition ACTIVE STORAGE ENGINE NULL GPL # 在逻辑上为一个表,在物理上存储在多个文件中 CREATE TABLE customer_login( customer_id int unsigned AUTO_INCREMENT NOT NULL COMMENT '用户id', login_name varchar(20) NOT NULL COMMENT '用户登录名', password char(32) NOT NULL comment 'md5加密的密码', user_status tinyint NOT null default 1 comment '用户状态', modified_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRETN_TIMESTAMP COMMENT '最后修改时间', primary key pk_customerid(customer_id) # 指定主键 ) engine = innodb DEFAULT CHARSET=UTF8 comment = '用户登录表' PARTITION BY HASH(customer_id) # 以customer_id来进行分区 PARTITIONS 4;
6.1按HASH来分区
# HASH分区的特点 根据MOD(分区键,分区数)的值把数据行存储到表的不同分区中 数据可以平均的分布在各个分区中 HASH分区的键值必须是一个INT类型的值,或是通过函数可以转为INT类型
6.2按RANGE来分区
# RANGE分区特点 根据分区键值得范围把数据行存储到表的不同分区中 多个分区的范围要连续,但是不能重叠 默认情况下使用VALUES LESS THAN属性,即每个分区不包括指定的那个值 # 建立RANGE分区 CREATE TABLE customer_login_log( customer_id INT(10) UNSIGNED NOT NULL, login_time TIMESTAMP NOT NULL, login_ip INT(10) UNSIGNED NOT NULL, login_type TINYINT(4) NOT NULL ) ENGINE=INNODB PARITION BY RANGE(customer_id)( PARITION p0 VALUES LESS THAN(10000), # 0-9999 PARITION p1 VALUES LESS THAN(20000), # 10000-19999 PARITION p3 VALUES LESS THAN(30000), # 20000-29999 PARITION p4 VALUES LESS THAN MAXVALUE # >30000 ); # RANGE分区的使用场景 分区键为日期或是时间类型 所有查询中都包括分区键 定期按分区范围清理历史数据 # 按时间俩分区 CREATE TABLE customer_login_log( customer_id INT(10) UNSIGNED NOT NULL, login_time TIMESTAMP NOT NULL, login_ip INT(10) UNSIGNED NOT NULL, login_type TINYINT(4) NOT NULL ) ENGINE=INNODB PARITION BY RANGE(YEAR(login_time))( PARITION p0 VALUES LESS THAN(2015), # 2014-2015 PARITION p1 VALUES LESS THAN(2016), # 2015-2016 PARITION p3 VALUES LESS THAN(2017), # 2016-2017 );
6.3按LIST来分区
# LIST分区的特点 按分区键取值的列表进行分区 同范围分区一样,各分区的列表值不能重复 每一行数据必须能找到对应的分区列表,否则数据插入失败 # 建立LIST分区 CREATE TABLE customer_login_log( customer_id INT(10) UNSIGNED NOT NULL, login_time TIMESTAMP NOT NULL, login_ip INT(10) UNSIGNED NOT NULL, login_type TINYINT(4) NOT NULL ) ENGINE=INNODB PARTITION BY LIST(login_type)( PARTITION p0 VALUES IN (1,3,5,7,9), PARTITION p1 VALUES IN (2,4,6,8) );