【总结】数据库优化策略
该文章为知识总结的文章,如果是初学者,建议先从专栏学习:数据库专栏
一、数据结构的优化
三大范式:
- 数据库设计尽量遵循三范式
- 根据实际情况进行取舍,有时可能会拿冗余换速度,减少表的关联查询,用空间换时间
- 对于需要经常联合查询的表,可以建立中间表以提高查询效率
- 对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表
数据库命令规范:
- 所有数据库对象名称必须使用小写字母并用下划线分割
- 数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符
- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)
列的选择:
- 字段类型优先级 整型 > date,time > char,varchar > blob
- 尽量不要有null,可以设置为不允许空,或者加一个默认值
- 数据大小够用就行
- 不要使用大数据字段,如TEXT,BLOB
- 财务数据使用decimal,不会丢失精度
主键的选择:
- 在 myisam 中,字符串索引会被压缩,用字符串做主键性能不如整型
- 用递增的值,不要用离散的值,离散值会导致文件在磁盘的位置有间隔,浪费空间且不易连续读取
二、服务器优化
优化服务器硬件
- 配置较大的内存
- 配置高速磁盘,比如 SSD
- 配置多核处理器, MySQL 是多线程的数据库,多处理器可以提高同时执行多个线程的能力
优化 MySQL 的参数
-
修改my.conf 或者 my.ini 配置文件
-
key_buffer_size:表示索引缓冲区大小,缓存数据,只对MyISAM表起作用,不是越大越好,大小取决于内存
-
innodb_buffer_pool_size:表示InnoDB类型表和索引的最大缓存,值越大查询速度越快
-
query_cache_size:表示查询缓冲区大小,MySQL8以上已经移除
-
max_connections:表示数据库的最大连接数,也不是越多越好,过多连接可能会导致僵死
三、线上定位
发现系统运行缓慢,如何定位和分析查询慢的 sql 语句 ?
- 开启 mysql 慢日志查询 定位查询较慢的 sql 语句 (200ms 500ms )
- 使用 EXPLAIN 关键字可以让你知道 MySQL 是如何处理你的 SQL 语句的。这可以帮你分析你的查询语句或是表结构的性能瓶颈。 EXPLAIN 的查询结果还会告诉你你的索引主键被如何利用的,你的数据表是如何被搜索和排序的……等等,等等
- 代码中可以使用 AOP 的操作 对每个持久层的 service 方法 打印执行时间,将所有执行时间较长的 sql 语句进行预警
- 查看sql语句是否出现了临时表,临时表可能会通过IO落地到磁盘上,要尽可能避免
四、查询语句的优化
1. sql 语句的时间花在哪儿?
- 等待数据的等待时间,以及执行SQL语句时间.
- 这两个时间并非孤立的, 如果单条语句执行的快了,对其他语句的锁定的也就少了
2. sql 语句的执行时间,又花在哪儿了?
- 查 ----> 沿着索引查,甚至全表扫描
- 取 ----> 查到行后,把数据取出来(sending data)
3. sql 语句的优化思路?
- 不查, 通过业务逻辑来计算,比如论坛的注册会员数,我们可以根据前 3 个月统计的每天注册数, 用程序来估算
- 少查, 尽量精准数据,少取行. 我们观察新闻网站,评论内容等,一般一次性取列表 10-30 条左右
- 必须要查,尽量走在索引上查询行
- 取时, 取尽量少的列
4. 如何定量分析查的多少行,和是否沿着索引查?
- 用 explain 来分析
5. 常见的查询优化
- SELECT 语句务必指明字段名称(避免直接使用 select * ),使用*时,分析器,会要分析出都有什么列,会消耗很多时间,同时执行的时候,会返回很多没有必要的列
- SQL 语句要避免造成索引失效的写法
- 当只需要一条数据的时候,使用 limit 1,这样就通过sql语句过滤,而避免了执行时的过滤
- 如果排序字段没有用到索引,就尽量少排序
- 如果限制条件中其他字段没有索引,尽量少用 or
- 避免在 where 子句中对字段进行 null 值判断
- 不建议使用%前缀模糊查询
- 避免在 where 子句中对字段进行表达式操作
- Join 优化 能用 inner join 就不用 left join right join,如必须使用 一定要已小表为驱动
- 可以使用连接查询(JOIN)代替子查询,连接查询时不需要建立临时表,其速度比子查询快
6. 缓存优化
- 为了提高查询速度,我们可以通过不同的方式去缓存我们的结果从而提高响应效,可以去打开查询缓存或者使用Redis
7. 读写分离
- 如果数据库的使用场景读的操作比较的时候,为了避免写的操作所造成的性能影响 可以采用读写分离的架构,读写分离,解决的是,数据库的写入,影响了查询的效率。读写分离的基本原理是让主数据库处理事务性增、改、删操作(INSERT、 UPDATE、 DELETE),而从数据库处理 SELECT 查询操作
8. mysql 的分库分表
-
数据量越来越大时,单体数据库无法满足要求,可以考虑分库分表
-
两种拆分方案:
- 垂直拆分:(分库)业务表太多? 将业务细化 不同的小业务专门用一个库来维护
- 水平拆分:(分表)单个表存的数据太多,装不下了? 将该表查分成多个
-
分库分表常用工具: MyCat
9. EXPLAIN使用
select_type,表示 SELECT 语句的类型,取值如下
- SIMPLE:表示简单查询,其中不包含连接查询和子查询
- PRIMARY:主键查血
- UNION:表示连接查询
- UNION RESULT :连接查询的结果
- SUBQUERY:子查询
type ,表示表的连接类型 ,取值如下:
- system:表仅有一行,这是 const 类型的特列,平时不会出现,这个也可以忽略不计
- const:数据表最多只有一个匹配行,因为只匹配一行数据,所以很快,常用于 PRIMARY KEY 或者 UNIQUE 索引的查询,
可理解为 const 是最优化的
- eq_ref :读取本表中和关联表表中的每行组合成的一行,索引是主键或唯一非 NULL 索引
- ref:查询条件索引既不是 UNIQUE 也不是 PRIMARY KEY 的情况
- ref_or_null:该联接类型如同 ref,但是添加了 MySQL 可以专门搜索包含 NULL 值的行。在解决子查询中经常使用该联接类型的优化。
上面这五种情况都是很理想的索引使用情况。
- index_merge:该联接类型表示使用了索引合并优化方法。在这种情况下, key 列包含了使用的索引的清单
- unique_subquery:是一个索引查找函数,可以完全替换子查询,效率更高。
- index_subquery:只检索给定范围的行,使用一个索引来选择行
- range:只检索给定范围的行,使用一个索引来选择行
- index:该联接类型与 ALL 相同,除了只有索引树被扫描。
- ALL:对于每个来自于先前的表的行组合,进行完整的表扫描,性能最差
**possible_keys,可能用到的索引 **
**key,最终用的索引 ,如果没有选择索引,键是 NULL **
五、索引及优化
注意避免冗余索引,比如说(name)和(name,price),(name)就是冗余索引
1. 索引的作用
当表中的数据量越来越大时,索引对于性能的影响愈发重要。索引优化应该是对查询性能优化最有效的手段了。
索引能够轻易将查询性能提高好几个数量级。有了索引相当于我们给数据库的数据加了目录一样,可以快速的找到数据,如果不适用索引则需要一点一点去查找数据。 简单来说提高数据查询的效率。
2. 索引的分类
- 普通索引 index :加速查找
- 唯一索引
- 主键索引: primary key :加速查找+约束(不为空且唯一)
- 唯一索引: unique:加速查找+约束 (唯一)
- 联合索引(组合索引)
3. 索引的优点(查的快)
- 可以通过建立唯一索引或者主键索引,保证数据库表中每一行数据的唯一性.
- 建立索引可以大大提高检索的数据,以及减少表的检索行数
- 在表连接的连接条件 可以加速表与表直接的相连
- 在分组和排序字句进行数据检索,可以减少查询时间中 分组 和 排序时所消耗的时间(数据库的记录会重新排序)
- 建立索引,在查询中使用索引 可以提高性能
4. 索引的缺点(增删改慢)
- 在创建索引和维护索引 会耗费时间,随着数据量的增加而增加
- 索引文件会占用物理空间,除了数据表需要占用物理空间之外,每一个索引还会占用一定的物理空间
- 当对表的数据进行 INSERT,UPDATE,DELETE 的时候,索引也要动态的维护,这样就会降低数据的维护速度,(建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快)
5. 哪些情况或字段适合加索引
- 在经常需要搜索的列上,可以加快索引的速度
- 主键列上可以确保列的唯一性
- 在表与表的而连接条件上加上索引,可以加快连接查询的速度
- 在经常需要排序(order by),分组(group by)和的 distinct 列上加索引 可以加快排序查询的时间
6. 哪些情况不适合创建索引
- 查询中很少使用到的列 不应该创建索引,如果建立了索引然而还会降低 mysql 的性能和增大了空间需求.
- 很少数据的列也不应该建立索引,比如 一个性别字段 0或者1,在查询中,结果集的数据占了表中数据行的比例比较大,mysql 需要扫描的行数很多,增加索引,并不能提高效率
- 定义为 text 和 image 和 bit 数据类型的列不应该增加索引,
- 当表的修改(UPDATE,INSERT,DELETE)操作远远大于检索(SELECT)操作时不应该创建索引,这两个操作是互斥的关系
7. 哪些情况会造成索引失效
- 如果条件中有 or,即使其中有条件带索引也不会使用(这也是为什么尽量少用 or 的原因)
- 索引字段的值不能有 null 值,有 null 值会使该列索引失效
- 对于多列索引,不是使用的第一部分,则不会使用索引(最左原则)
- like 查询以%开头
- 如果列类型是字符串,那一定要在条件中将数据使用单引号引用起来,否则不使用索引
- 在索引的列上使用表达式或者函数会使索引失效
8. 索引原理
-
MySQL 的基本存储结构是页(记录都存在页里边):
-
各个数据页可以组成一个双向链表
-
每个数据页中的记录又可以组成一个单向链表
- 每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录
- 以其他列(非主键)作为搜索条件:只能从最小记录开始依次遍历单链表中的每条记录。
所以说,如果我们写 select * from user where indexname = 'xxx’这样没有进行任何优化的 sql 语句,默认会这样做:
- 定位到记录所在的页:需要遍历双向链表,找到所在的页
- 从所在的页内中查找相应的记录:由于不是根据主键查询,只能遍历所在页的单链表了
很明显,在数据量很大的情况下这样查找会很慢!这样的时间复杂度为 O(n)
使用索引之后
- 其实就是将无序的数据变成有序(相对):
- 很明显的是:没有用索引我们是需要遍历双向链表来定位对应的页,现在通过 “目录” 就可以很快地定位到对应的页上了!(二分查找,时间复杂度近似为 O(logn))
- 其实底层结构就是 B+树, B+树作为树的一种实现,能够让我们很快地查找出对应的记录
面试题
有商品表, 有主键,goods_id, 栏目列 cat_id, 价格 price说:在价格列上已经加了索引,但按价格查询还是很慢,
问可能是什么原因,怎么解决?
答: 在实际场景中,一个电商网站的商品分类很多,直接在所有商品中,按价格查商品,是极少的,一般客户都来到分类下,然后再查.
改正: 去掉单独的 Price 列的索引, 加 (cat_id,price)复合索引再查询
索引优化策略
- 选择唯一性索引
- 唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。
- 为经常需要排序、分组和联合操作的字段建立索引:
- 为常作为查询条件的字段建立索引。
- 限制索引的数目: 越多的索引,会使更新表变得很浪费时间。
- 尽量使用数据量少的索引
- 如果索引的值很长,那么查询的速度会受到影响。
- 尽量使用前缀来索引
- 如果索引字段的值很长,最好使用值的前缀来索引。
- 删除不再使用或者很少使用的索引
- 最左前缀匹配原则,非常重要的原则。
- 尽量选择区分度高的列作为索引
- 区分度的公式是表示字段不重复的比例
- 索引列不能参与计算,保持列“干净”:带函数的查询不参与索引。
- 尽量的扩展索引,不要新建索引。
六、插入优化
InnoDB:
-
禁用唯一性检查,唯一性校验会降低插入记录的速度,可以在插入记录之前禁用唯一性检查,插入数据完成后再开启
-
禁用外键检查,插入数据之前执行禁止事务的自动提交
-
禁止自动提交,插入数据之前执行禁止事务的自动提交
-
批量插入数据,可以使用一条 INSERT 语句插入一条数据
七、大表优化
当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:
1. 限定数据的范围
务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;
2. 读/写分离
经典的数据库拆分方案,主库负责写,从库负责读;
3. 垂直分区
根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。
简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了
4. 水平分区
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。
水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。
5. 数据库分库策略
- 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
- 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。
6. 分库分表之后,id 主键如何处理?
因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要一个全局唯一的 id 来支持。
生成全局 id 有下面这几种方式:
- UUID:不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。
- 数据库自增 id : : 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
- 利用 redis 生成 id : 性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。
- Twitter的snowflake算法 、美团的Leaf分布式ID生成系统