8.SQL优化、分库分表、读写分离

发布时间 2023-09-19 15:06:58作者: 壹索007
1.MySQL高性能优化规范建议总结
(1)数据库命名规范:
·  所有数据库对象名称必须使用小写字母并用下划线分割
·  所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)
·  数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符
·  临时库表必须以 tmp_ 为前缀并以日期为后缀,备份表必须以 bak_ 为前缀并以日期 (时间戳) 为后缀
·  所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)
 
(2)数据库基本设计规范:
所有表必须使用 InnoDB 存储引擎
  没有特殊要求(即 InnoDB 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 Myisam,5.6 以后默认的为 InnoDB)。
  InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。
数据库和表的字符集统一使用 UTF8
  统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效,如果数据库中有存储emoji表情的需要,字符集需采用utf8mb4字符集。
所有表和字段都需要添加注释
  使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护
尽量控制单表数据量的大小,建议控制在 500 万以内
  500万并不是MySQL数据库的限制,过大会造成修改表结构,备份,恢复有很大问题。
  可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。
谨慎使用 MySQL 分区表
  分区表在物理上表现为多个文件,在逻辑上表现为一个表;
  谨慎选择分区键,跨分区查询效率可能更低;
  建议采用物理分表的方式管理大数据。
经常一起使用的列放到一个表中:避免更多的关联操作。
禁止在表中建立预留字段
禁止在数据库中存储文件(比如图片)这类大的二进制数据
  在数据库中存储文件会严重影响数据库性能,消耗过多存储空间。
  文件(比如图片)这类大的二进制数据通常存储于文件服务器,数据库只存储文件地址信息。
不要被数据库范式所束缚
禁止在线上做数据库压力测试
禁止从开发环境,测试环境直接连接生产环境数据库
 
(3)数据库字段设计规范
优先选择符合存储需要的最小的数据类型
  a.某些字符串可以转换成数字类型存储比如可以将 IP 地址转换成整型数据。
  b.对于非负型的数据 (如自增 ID,整型 IP,年龄) 来说,要优先使用无符号整型来存储。
  c.小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型。
避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据
  1. 建议把 BLOB 或是 TEXT 列分离到单独的扩展表中。
  2. TEXT 或 BLOB 类型只能使用前缀索引
避免使用 ENUM 类型
  修改 ENUM 值需要使用 ALTER 语句;
  ENUM 类型的 ORDER BY 操作效率低,需要额外操作;
  ENUM 数据类型存在一些限制比如建议不要使用数值作为 ENUM 的枚举值。
尽可能把所有列定义为 NOT NULL
  索引 NULL 列需要额外的空间来保存,所以要占用更多的空间;
  进行比较和计算时要对 NULL 值做特别的处理。
使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间
  用字符串存储日期型的数据(不正确的做法)
    缺点 1:无法用日期函数进行计算和比较
    缺点 2:用字符串存储日期要占用更多的空间
同财务相关的金额类数据必须使用 decimal 类型
  非精准浮点 :float,double
  精准浮点 :decimal在计算时不会丢失精度,占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据.
    decimal需要额外的空间和计算开销,尽量只在需要对数据进行精确计算时才使用decimal。
单表不要包含过多字段
  如果一个表包含过多字段的话,可以考虑将其分解成多个表,必要时增加中间表进行关联。
 
(4)索引设计规范
限制每张表上的索引数量,建议单张表索引不超过 5 个
  索引并不是越多越好!索引可以提高效率同样可以降低效率。
  索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。
  因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
# 禁止使用全文索引
  全文索引不适用于 OLTP 场景。 联机事务处理
# 禁止给表中的每一列都建立单独的索引
  5.6 版本之前,一个 sql 只能使用到一个表中的一个索引,5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。
每个 InnoDB 表必须有个主键
  InnoDB 是一种索引组织表:数据的存储的逻辑顺序和索引的顺序是相同的。每个表都可以有多个索引,但是表的存储顺序只能有一种。
InnoDB 是按照主键索引的顺序来组织表的
  不要使用更新频繁的列作为主键,不适用多列主键(相当于联合索引)
  不要使用 UUID,MD5,HASH,字符串列作为主键(无法保证数据的顺序增长)
  主键建议使用自增 ID 值
常见索引列建议
  出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列
  包含在 ORDER BY、GROUP BY、DISTINCT 中的字段
  不要将符合1和2中的字段都建立一个索引, 通常将1、2中的字段建立联合索引效果更好
  多表 join的关联列
如何选择索引列的顺序
建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数)
  尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好)
  使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)
避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)
  重复索引示例:primary key(id)、index(id)、unique index(id)
  冗余索引示例:index(a,b,c)、index(a,b)、index(a)
对于频繁的查询优先考虑使用覆盖索引
  覆盖索引:就是包含了所有查询字段 (where,select,order by,group by 包含的字段) 的索引
覆盖索引的好处
  避免 InnoDB 表进行索引的二次查询
  可以把随机 IO 变成顺序 IO 加快查询效率
索引 SET 规范
  尽量避免使用外键约束
  不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引
  外键可用于保证数据的参照完整性,但建议在业务端实现,外键会影响父表和子表的写操作从而降低性能。
 
(5)数据库SQL开发规范
优化对性能影响较大的SQL语句
  要找到最需要优化的 SQL 语句。要么是使用最频繁的语句,要么是优化后提高最明显的语句,可以通过查询 MySQL 的慢查询日志来发现需要进行优化的 SQL 语句;
# 充分利用表上已经存在的索引
  避免使用双%号的查询条件。如:a like '%123%',(如果无前置%,只有后置%,是可以用到列上的索引的)
  一个 SQL 只能利用到复合索引中的一列进行范围查询。如:有 a,b,c 列的联合索引,在查询条件中有 a 列的范围查询,则在 b,c 列上的索引将不会被用到。
  在定义联合索引时,如果 a 列要用到范围查找的话,就要把 a 列放到联合索引的右侧,使用 left join 或 not exists 来优化 not in 操作,因为 not in 也通常会使用索引失效。
# 禁止使用 SELECT * 必须使用 SELECT <字段列表> 查询
  SELECT * 消耗更多的 CPU 和 IO 以网络带宽资源
  SELECT * 无法使用覆盖索引
  SELECT <字段列表> 可减少表结构变更带来的影响
# 禁止使用不含字段列表c1,c2,c3的 INSERT 语句
  应使用:insert into t(c1,c2,c3) values ('a','b','c');
建议使用预编译语句进行数据库操作
避免数据类型的隐式转换
  隐式转换会导致索引失效如:
  select name,phone from customer where id = '111';
避免使用子查询,可以把子查询优化为 join 操作
  通常子查询在 in子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。
子查询性能差的原因子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。由于子查询会产生大量的临时表也没有索引,所以会消耗过多的CPU和IO资源,产生大量的慢查询。
 
避免使用 JOIN 关联太多的表
  对于 MySQL 来说,是存在关联缓存的,缓存的大小可以由 join_buffer_size 参数进行设置。
  在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。
  如果程序中大量的使用了多表关联的操作,同时 join_buffer_size 设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
  同时对于关联操作来说,会产生临时表操作,影响查询效率MySQL 最多允许关联 61 个表,建议不超过 5 个。
  减少同数据库的交互次数
  数据库更适合处理批量操作,合并多个相同的操作到一起,可以提高处理效率。
# 对应同一列进行 or 判断时,使用 in 代替 or
  in 的值不要超过 500 个,in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。
# 禁止使用 order by rand() 进行随机排序
  order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值,如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。
  推荐在程序中获取一个随机值,然后从数据库中获取数据的方式。
# WHERE 从句中禁止对列进行函数转换和计算
  对列进行函数转换或计算时会导致无法使用索引
在明显不会有重复值时使用 UNION ALL 而不是 UNION
  UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作
UNION ALL 不会再对结果集进行去重操作
拆分复杂的大 SQL 为多个小 SQL
  大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL
  MySQL 中,一个 SQL 只能使用一个 CPU 进行计算
  SQL 拆分后可以通过并行执行来提高处理效率
程序连接不同的数据库使用不同的账号,禁止跨库查询
  为数据库迁移和分库分表留出余地
  降低业务耦合度
  避免权限过大而产生的安全风险
 
(6)数据库操作行为规范
超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作
  大批量操作可能会造成严重的主从延迟
  binlog 日志为 row 格式时会产生大量的日志
  避免产生大事务操作
对于大表使用 pt-online-schema-change 修改表结构
  避免大表修改产生的主从延迟
  避免在对表字段进行修改时进行锁表
  对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。
禁止为程序使用的账号赋予 super 权限
  当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接
  super 权限只能留给 DBA(数据管理员)处理问题的账号使用
# 对于程序连接数据库账号,遵循权限最小原则
  程序使用数据库账号只能在一个 DB 下使用,不准跨库
  程序使用的账号原则上不准有 drop 权限
————————————————————————————
能用MySQL直接存储文件(比如图片)嘛?
  可以直接存储文件对应的二进制数据。建议不要在数据库中存储文件,会严重影响数据库性能,消耗过多存储空间。
  可以选择使用云服务厂商提供的开箱即用的文件存储服务,成熟稳定,价格也比较低。
  也可以选择自建文件存储服务,实现起来也不难,基于 FastDFS、MinIO(推荐) 等开源项目就可以实现分布式文件服务。
  数据库只存储文件地址信息,文件由文件存储服务负责存储。
MySQL 如何存储 IP 地址?
  可以将 IP 地址转换成整型数据存储,性能更好,占用空间也更小。
  MySQL 提供了两个方法来处理 ip 地址:
  INET_ATON() : 把ip转为无符号整型 (4-8 位)   address to number
  INET_NTOA() :把整型的ip转为地址
  插入数据前,先用 INET_ATON() 把 ip 地址转为整型,显示数据时,使用 INET_NTOA() 把整型的 ip 地址转为地址显示即可。
有哪些常见的SQL优化手段?

如何分析SQL性能:
我们可以使用 EXPLAIN 命令来分析 SQL 的执行计划 。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。

 

2.读写分离和分库分表了解吗?

(1)读写分离:读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。
一般情况下,我们都会选择一主多从,也就是一台数据库负责,其他的数据库负责。主库和从库之间会进行数据同步,以保证从库中数据的准确性。
 
(2)如何实现读写分离?
·  部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。
·  保证主数据库和从数据库之间的数据是实时同步的,这个过程就是我们常说的主从复制
·  系统将写请求交给主数据库处理,读请求交给从数据库处理。
落实到项目本身
代理方式:我们可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。
  提供类似功能的中间件有 MySQL Router(官方)、Atlas(基于 MySQL Proxy)、Maxscale、MyCat。
组件方式:可以通过引入第三方组件来帮助我们读写请求。推荐使用 sharding-jdbc ,直接引入 jar 包即可使用,非常方便。同时,也节省了很多运维的成本。
 
(3)主从复制的原理:
MySQL binlog(binary log 即二进制日志文件) 主要记录了MySQL数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。因此,我们根据主库的MySQL binlog日志就能够将主库的数据同步到从库中。
详细过程:
·  主库将数据库中数据的变化写入到 binlog
·  从库连接主库
·  从库会创建一个 I/O 线程向主库请求更新的 binlog
·  主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收
·  从库的 I/O 线程将接收的 binlog 写入到 relay log 中。
·  从库的 SQL 线程读取 relay log 同步数据本地(也就是再执行一遍 SQL )。
 
(4)分库分表:
读写分离主要应对的是数据库读并发,没有解决数据库存储问题。
  如果MySQL一张表的数据量过大怎么办?换言之,我们该如何解决MySQL的存储压力? 答案之一就是分库分表。
分库:就是将数据库中的数据分散到不同的数据库上,可以垂直分库,也可以水平分库。
垂直分库就是把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库。
  举个例子:说你将数据库中的用户表、订单表和商品表分别单独拆分为用户数据库、订单数据库和商品数据库。
水平分库把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。
  举个例子:订单表数据量太大,你对订单表进行了水平切分(水平分表),然后将切分后的 2 张订单表分别放在两个不同的数据库。
分表:就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。
垂直分表:是对数据表列的拆分,把一张列比较多的表拆分为多张表。
  举个例子:我们可以将用户信息表中的一些列单独抽出来作为一个表。
水平分表:是对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题。(水平拆分只能解决单表数据量大的问题,为了提升性能,我们通常会选择将拆分后的多张表放在不同的数据库中。也就是说,水平分表通常和水平分库同时出现)
 
什么情况下需要分库分表?
遇到下面几种场景可以考虑分库分表:
  单表的数据达到千万级别以上,数据库读写速度比较缓慢。
  数据库中的数据占用的空间越来越大,备份时间越来越长。
  应用的并发量太大。
 
常见的分片算法有哪些?
分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。
哈希分片 :求指定 key(比如 id) 的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。
范围分片 :按照特性的范围区间(比如时间区间、ID区间)来分配数据,比如 将 id 为 1~299999 的记录分到第一个库, 300000~599999 的分到第二个库。范围分片适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。
地理位置分片 :很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。
融合算法 :灵活组合多种分片算法,比如将哈希分片和范围分片组合。
 
分库分表会带来什么问题呢? 同一个数据库中的表分布在了不同的数据库中
·  join 操作 : 同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。
·  事务问题 :同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。
·  分布式 id :分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 id 了。
引入分库分表之后,一般需要 DBA 的参与,同时还需要更多的数据库服务器,这些都属于成本。
 
分库分表有没有什么比较推荐的方案?
  ShardingSphere 项目(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar)是当当捐入 Apache 的,目前主要由京东数科的一些巨佬维护。除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。
 
分库分表后,数据怎么迁移呢?
  比较简单同时也是非常常用的方案就是停机迁移,写个脚本老库的数据写到新库中。比如你在凌晨 2 点,系统使用的人数非常少的时候,挂一个公告说系统要维护升级预计 1 小时。然后,你写一个脚本将老库的数据都同步到新库中。
  如果你不想停机迁移数据的话,也可以考虑双写方案。双写方案是针对那种不能停机迁移的场景,实现起来要稍微麻烦一些。具体原理是这样的:
  我们对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。
  在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。
  重复上一步的操作,直到老库和新库的数据一致为止。
  想要在项目中实施双写还是比较麻烦的,很容易会出现问题。我们可以借助上面提到的数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。
 
总结
1)读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。
2)读写分离基于主从复制,MySQL 主从复制是依赖于 binlog 。
3)分库 就是将数据库中的数据分散到不同的数据库上。分表 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。
4)引入分库分表之后,需要系统解决事务、分布式 id、无法 join 操作问题。
5)ShardingSphere 绝对可以说是当前分库分表的首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。

 

3.MySQL自增主键一定是连续的么? 

不!自增值不连续的4个场景:
·自增初始值和自增步长设置不为 1
·唯一键冲突
·事务回滚为了提高性能(假设 MySQL 在事务回滚的时候会把自增值改回去,会出现主键冲突)
·批量插入(如 insert...select 语句):一次申请多个id

 

4.MySQL时间类型数据存储建议

根据实际场景
数值型时间戳效率又高还各种兼容,但是很多人又觉得它表现的不够直观。