MySQL性能优化,sql优化有哪些,数据库如何优化设计(一)

专栏前言:

本篇文章是整个专栏的第一篇文章,也是 针对 MySQL 数据库 进行性能优化的第一篇文章。

整个专栏包括以下几个部分:数据库优化接口性能优化Java底层数据性能调优中间件相关性能问题定位 以及 多线程高并发设计 等内容。

数据库优化 这部分内容大致包括以下方面:

1、SQL 优化 15 招
2、索引如何优化?
3、性能优化神器 -explain
4、count(*) 性能很差,如何优化?
5、limit 深分页问题,如何优化?
6、order by 如何优化性能?
7、一次 like 语句的优化
8、group by 如何优化性能?
9、这才是正确的批量 update 姿势!
10、数据库死锁,如何优化?
11、如何做分库分表?
12、千万级的大表,如何做性能优化?

下面开启第一篇文章:

一、数据库优化

1. SQL优化的15个方面

如果某个线上接口,出现了性能问题,需要做优化。那么首先想到的很有可能是优化 sql 语句,因为它的改造成本相对于代码来说也要小得多。

避免使用 select *

原因:

  • 查了很多数据,但是不用,白白浪费了数据库资源,比如:内存或者 cpu
  • 多查出来的数据,通过网络 IO 传输的过程中,也会增加数据传输的时间
  • 最重要的问题: select * 不会走 覆盖索引,会出现大量的 回表 操作,从而导致查询 sql 性能很低

优化:sql 语句查询时,只查需要用到的列,多余的列无需查出来:select name,age from user where id=1;

用 union all 代替 union

1、union 操作符用于合并两个或多个 select 语句的结果集。它可以从多个表中选择数据,并将结果集组合成一个结果集。使用 union 时,所有 select 语句中的列数和数据类型必须一致。

2、语法:

SELECT column1, column2, ... FROM table1 UNION SELECT column3, column4, ... FROM table2;

3、union 操作符默认去除重复的记录,如果需要保留所有重复记录,可以使用 union all 操作符。

注意几点:

列数必须相同:UNION本质上是按行将多个结果集纵向拼接,只有列数一致,才能确保每一行的数据在合并后有正确的列对应关系,如:

-- 正确示例,列数相同
SELECT id, name
FROM users
UNION
SELECT product_id, product_name
FROM products;

-- 错误示例,列数不同
SELECT id, name
FROM users
UNION
SELECT product_id, product_name, price
FROM products;

对应列数据类型兼容: 除了列数相同,对应列的数据类型也必须兼容(兼容:指的是数据类型要么完全相同,要么可以在 MySQL 中进行隐式类型转换)

列名以第一个select为准: UNION 结果集中的列名总是等于 UNION 中第一个 SELECT 语句中的列名。

4、特点:

① 结果集排序:UNION操作后的结果集可以使用ORDER BY子句进行排序。如果没有指定ORDER BY,则结果集的顺序是不确定的。需要注意的是,ORDER BY子句通常放在最后一个SELECT语句之后,它会对整个UNION操作后的结果集进行排序。

② 性能:UNION 操作在合并结果集时可能会有一定的性能开销,特别是在处理大量数据时。这是因为 UNION 默认会去除重复的行,这个去重过程需要额外的计算资源。如果使用 UNION ALL,由于不需要去除重复行,性能可能会更好一些。因此,在实际应用中,如果能够确定合并后的结果集中不会有重复行,或者重复行对业务没有影响,那么优先选择 UNION ALL 可以提高查询效率。

小表驱动大表

也就是说:用小表的数据集驱动大表的数据集。

举例:有 order 和 user 两张表,其中 order 表有 10000 条数据,而 user 表有100 条数据。这时如果想查:所有有效的用户下过的订单列表。

实现:可以使用 in 实现:

select * from order 
where user_id in (select id from user where status=1);

-- 也可以使用exists实现:
select * from order
where exists (select 1 from user where order.user_id = user.id and status=1)

这里选择 in ,为什么?

  • 如果 sql 语句中包含了 in 关键字,则它会优先执行 in 里面的子查询语句,然后再执行 in 外面的语句。如果 in 里面的数据量很少,作为条件查询速度更快。

  • 如果 sql 语句中包含了 exists 关键字,它优先执行 exists 左边的语句(即主查询语句)。然后把它作为条件,去跟右边的语句匹配。如果匹配上,则可以查询出数据。如果匹配不上,数据就被过滤掉了。

这个需求中,order 表有10000条数据,而 user 表有 100 条数据。order 表是大表,user 表是小表。如果 order表在左边,则用 in 关键字性能更好。总结一下:

  • in 适用于左边大表,右边小表。
  • exists 适用于左边小表,右边大表。

不管是用 in,还是 exists 关键字,其核心思想都是用小表驱动大表

select 1 是什么意思:

1、SELECT 1 在子查询(如 EXISTS 子句)中常用于提高查询效率。

  • select 1 只是用于测试是否存在满足条件的记录,并不会实际返回数据。
  • EXISTS (SELECT 1 FROM ...) 只关心是否有记录存在,不关心具体返回的值,因此 SELECT 1 只是一个占位符。
select * from orderwhere exists (select 1 from user where order.user_id = user.id and status=1)

比如上面的 sql 语句的执行流程为:

  • SELECT * FROM order 选取所有订单。

  • 对于 order 表的每一行,执行 EXISTS 子查询:

    • 子查询检查 user 表中是否存在 user.id = order.user_id and status = 1 的记录。
    • 如果有匹配记录,EXISTS 语句返回 TRUE,那么 order 记录会被选中;否则被过滤掉。

2、为什么用 SELECT 1 而不是 SELECT *

  • 性能优化:EXISTS 只关心是否有记录匹配,不需要实际查询列的值,所以 SELECT 1 只返回一个常量,比 SELECT * 更高效。
  • 避免不必要的字段读取:SELECT * 可能会读取所有列,而 SELECT 1 只是返回一个常量 1,减少数据库负载。

3、可能的优化方案:用 join 代替 exists:如果 user_iduser 表和 order 表上有索引JOIN 可能更高效。

方法适用情况优势可能的缺点
EXISTSstatus = 1 数据较少子查询一旦找到匹配行就停止,适合较大数据集可能比 JOIN
JOIN需要获取 user 表额外信息更易读,性能好,索引优化后可能更快可能引入重复数据,需 DISTINCT

✅ 适合 status = 1 数据量较少的情况。
✅ 适合 order 表较大,但 user 表较小的情况。
❌ 若 user.status = 1 占比较高,考虑用 JOIN 以优化性能。

批量操作

如果你有一批数据经过业务处理之后,需要插入数据库,该怎么办?

// 反例:在循环中逐条插入数据
for(Order order: orderList){   
	orderMapper.insert(order);
}

// 原因:多次请求数据库

// 正例:
orderMapper.insertBatch(orderList);

// 注意:不建议一次批量操作太多的数据,如果数据太多数据库响应也会很慢。批量操作需要把握一个度,建议每批数据尽量控制在500以内。如果数据多于500,则分多批次处理。
多用 limit

需要查询某些数据中的第一条,比如:查询某个用户下的第一个订单,想看看他第一次的首单时间。

// 反例:
select id, create_date from order where user_id=123 
    order by create_date asc;
List list = orderMapper.getOrderList();
Order order = list.get(0);

// 根据用户id查询订单,按下单时间排序,先查出该用户所有的订单数据,得到一个订单集合。然后在代码中,获取第一个元素的数据,即首单的数据,就能获取首单时间。
// 正例:
select id, create_date from order where user_id=123 
    order by create_date asc limit 1;

使用limit 1,只返回该用户下单时间最小的那一条数据即可。

此外:在删除或者修改数据时,为了防止误操作,导致删除或修改了不相干的数据,也可以在 sql 语句最后加上limit。例如:

update order set status=0 where id>=100 and id<200 limit 100;

这样即使误操作,比如把 id 搞错了,也不会对太多的数据造成影响。

in 中值不能太多

对于 批量查询 功能的接口,通常会使用 in 关键字过滤出数据。比如:想通过指定的一些id,批量查询出用户信息。sql 语句如下:

select id,name from category where id in (1,2,3...100000000);

如果我们不做任何限制,该查询语句一次性可能会查询出非常多的数据,很容易导致接口超时。这时该怎么办呢?

1、可以在 sql 中对 in 中的数据做 limit 限制:

select id,name from category
where id in (1,2,3...100) limit 500;

2、不过更多的是要在业务代码中加限制,伪代码如下:

public List getCategory(List ids) {   
  if(CollectionUtils.isEmpty(ids)) {      
     return null;   
  }   
  if(ids.size() > 500) {      
     throw new BusinessException("一次最多允许查询500条记录")   
  }   
  return mapper.getCategoryList(ids);
}

3、还有一个方案就是:如果 ids 超过 500 条记录,可以分批用多线程去查询数据。每批只查 500 条记录,最后把查询到的数据汇总到一起返回。

不过这只是一个临时方案,不适合于 ids 实在太多的场景。因为 ids 太多,即使能快速查出数据,但如果返回的数据量太大了,网络传输也是非常消耗性能的,接口性能始终好不到哪里去。

增量查询

有时候,我们需要通过远程接口查询数据,然后同步到另外一个数据库。

反例: 全量查询: select * from user;

如果直接获取所有的数据,然后同步过去。这样虽说非常方便,但是带来了一个非常大的问题,就是如果数据很多的话,查询性能会非常差。这时该怎么办呢?

正例: 增量查询:按主键 id 和时间 create_time 进行增量查询

select * from user where id > #{lastId} and create_time >= #{lastCreateTime} limit 100;

每次只同步一批数据,这一批数据只有 100 条记录。每次同步完成之后,保存这 100 条数据中最大的 id 和时间,给同步下一批数据的时候用。通过这种增量查询的方式,能够提升单次查询的效率。

① 如何保存 lastIdlastCreateTime

方式 1:数据库记录

  • 在目标数据库中,创建一个 同步进度表,保存 lastIdlastCreateTime
  • 每次同步完成后,更新这两个值,供下一次查询使用。

方式 2:缓存存储

  • 使用 Redis 或本地文件 记录 lastIdlastCreateTime,避免频繁查询数据库。

② 如何确保增量同步不漏数据

  • 边界条件处理
    • create_time >= #{lastCreateTime}:使用 >= 而不是 >,确保不会遗漏刚好等于 lastCreateTime 的数据。
    • 但是如果数据量大,可能出现重复数据(因为 create_time 相同),需要使用 id > #{lastId} 进一步保证唯一性。
  • 索引优化
    • 确保 id 是主键或有索引,提升 id > #{lastId} 的查询性能。
    • create_time 上添加索引,避免 where create_time >= #{lastCreateTime} 变成全表扫描
对比项全量查询增量查询
查询方式select * from user;``select * from user where id > #{lastId} and create_time >= #{lastCreateTime} limit 100;
数据量查询所有数据仅查询 新增 或 修改 的数据
性能影响高,影响数据库性能低,数据库压力小
查询效率(利用索引优化)
数据一致性可能存在数据延迟数据较新,更新及时
容错性同步失败需要重新拉取全部数据可断点续传,减少重复查询

后续会更新更多干货文章,记得订阅专栏,点赞收藏!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小学鸡!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值