sql优化 减少数据库堆栈性能的12个小技巧

sql优化 减少数据库堆栈性能的12个小技巧

前言

显而易见,SQL运用广 ,但数据库堆栈性能问题也事故百出。

前段时间我们一个同事犯了一个大错,一个关联查询的sql没加分区过滤数据尽可能缩小数据范围,中间结果集很大,导致整个集群空间撑爆 ,间接导致集群内部的其他应用也崩溃,这都属于是严重的生产事故。

sql生产大致事件如下:

在这里插入图片描述

sql优化是一个大家都比较关注的热门话题。

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

此篇文章从15个方面,分享了sql优化的一些小技巧,希望对你有所帮助

那么,如何优化sql语句呢

展开来说

1.列裁剪

即避免使用select * ,可以只读取查询中所需要用到的列,而忽略其它列。

在实际业务场景中,可能我们真正需要使用的只有其中一两列。查了很多数据,但是不用,傻傻增加数据库堆栈性能,如:内存或者cpu。

此外,多查出来的数据,通过网络IO传输的过程中,也会增加数据传输的时间。

举个栗子:

select * from word_text;  【x错】

select word,100,num from word_text; 【√正确】

2.分区裁剪 提升group by的效率

栗子:

select userId ,userAccount from f_user group by userId where userId < 500; 【x错】

select userId ,userAccount from f_user where userId < 500  group by userId ; 【√正确】

我们有很多业务场景需要使用group by关键字,它主要的功能是去重和分组。

通常它会跟having一起配合使用,表示分组后再根据一定的条件过滤数据。

这种写法性能不好,它先把所有的订单根据用户id分组之后,再去过滤用户id大于等于200的用户。

分组是一个相对耗时的操作,为什么我们不先缩小数据的范围之后,再分组呢?

使用where条件在分组前,就把多余的数据过滤掉了,这样分组时效率就会更高一些。

其实这是一种思路,不仅限于group by的优化。我们的sql语句在做一些耗时的操作之前,应尽可能缩小数据范围,这样能提升sql整体的性能。

3.谓词下推
predicate
所谓的谓词下推就是在join之前的阶段提前对表进行过滤优化,使得最后参与join的表的数据量更小

谓词下推 --不扫描全表的表才能实现谓词下推,下面谓词是指name字段

    select 
    * 
    from
    (
    select * from table1
    ) t1
    join
    (
    select * from table2
    ) t2
    on 
    t1.id=t2.id
    where 
    t1.name='user1' and t2.name='user2';

    -- 谓词下推
    -- 针对内连接和全连接无论筛选条件写在哪里都不能实现谓词下推
    
    select 
    * 
    from
    (
    select * from table1 where name='user1'
    ) t1
    join
    (
    select * from table2 where name='user2'
    ) t2
    on 
    t1.id=t2.id

    -- 左连接谓词下推
    -- 如下能够实现谓词下推的例子,如下t1表是不能够实现谓词下推,会扫描全表(左表是全表),但是t2表是可以实现谓词下推,
    -- join的时候只扫描满足where条件的那部分数据
    select 
    * 
    from
    (
    select * from table1 where name='user1'
    ) t1
    left join
    (
    select * from table2 where name='user2'
    ) t2
    on 
    t1.id=t2.id;

    -- 右连接时谓词下推
    select 
    * 
    from
    (
    select * from table1 where name='user1'
    ) t1
    right join
    (
    select * from table2 where name='user2'
    ) t2
    on 

4.用union all代替union

sql语句使用union关键字后,可以获取排重后的数据。

而如果使用union all关键字,可以获取所有数据,包含重复的数据

反栗子:

(select userId ,userAccount from f_user where userId=1 ) 
union 
((select userId ,userAccount from f_user where userId=2);

排重的过程需要遍历、排序和比较,它更耗时,更消耗cpu资源。

所以如果能用union all的时候,尽量不用union。

正栗子:

(select userId ,userAccount from f_user where userId=1 ) 
union all
((select userId ,userAccount from f_user where userId=2);

除非是有些特殊的场景,比如union all之后,结果集中出现了重复数据,而业务场景中是不允许产生重复数据的,这时可以使用union。

5.小表驱动大表 in和exists用法

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

假如有f_post(帖子表)和f_user_relationship_thumb(用户关系表)两张表,其中f_post表有10000条数据,而f_user_relationship_thumb表有100条数据。

这时如果想查一下,仅查我的好友帖子列表。

可以使用in关键字实现:**(备注:**以下是从用户关系表中获得我的好友,再通过好友userAccount关联帖子表中的userAccount获得好友帖子数据)

 select * FROM f_post where userAccount
        in (
            SELECT userAccount FROM f_user_relationship_thumb
            WHERE friendsId = #{userId} AND reviewStatus = 1
            UNION
            SELECT friendsUserAccount as userAccount FROM f_user_relationship_thumb
            WHERE userId = #{userId} AND reviewStatus = 1
            LIMIT 500
        )

也可以使用exists关键字实现:

 select * FROM f_post where userAccount
        exists (
            SELECT userAccount FROM f_user_relationship_thumb
            WHERE friendsId = #{userId} AND reviewStatus = 1
            UNION
            SELECT friendsUserAccount as userAccount FROM f_user_relationship_thumb
            WHERE userId = #{userId} AND reviewStatus = 1
            LIMIT 500
        )

前面提到的这种业务场景,使用in关键字去实现业务需求,更加合适。

为什么呢?

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

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

这个需求中,其中f_post表有10000条数据,而f_user_relationship_thumb表有100条数据。f_post表是大表,f_user_relationship_thumb表是小表。如果f_post表在左边,则用in关键字性能更好。

结论:

  • 其实in、exists在算法复杂度Log N层面来讲,大差不差
  • in 适用于左边大表,右边小表。
  • exists 适用于左边小表,右边大表。

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

真实栗子如图:

在这里插入图片描述

6.批量操作 减少对数据库的操作次数

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

栗子:批量上传图片

反栗子:

for(photoNames photoName: list){
   postMapper.insert(photoName):
}

在循环中逐条插入数据。

insert into f_post(id,photoName) 
values(001,'/assets/userPost/photoName1');
...

该操作需要多次请求数据库,才能完成这批数据的插入。

但众所周知,我们在代码中,每次远程请求数据库,是会消耗一定性能的。而如果我们的代码需要请求多次数据库,才能完成本次业务功能,势必会消耗更多的性能。

那么如何优化呢?

正例√:

postMapper.insertBatch(list):sql

提供一个批量插入数据的方法。

insert into f_post(id,photoName) 
values(001,'/assets/userPost/photoName1'),(001,'/assets/userPost/photoName2'),(001,'/assets/userPost/photoName3');

这时只需要远程请求一次数据库,sql性能会得到提升,数据量越多,提升越大。

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

7.巧用limit

举个栗子,有时候,我们需要查询某些数据中的第一条,比如:查询某个用户下的第一个帖子,想看看他第一次的发帖时间。

反栗子:

select id, createTime 
 from f_post
where userAccount='500佰' 
order by createTime asc;

根据用户userAccount查询帖子,按帖子创建时间排序,先查出该用户所有的帖子数据,得到一个帖子集合。然后在代码中,获取第一个元素的数据,即第一个帖子的数据,就能获取第一次发帖时间。

List<Post> list = postMapper.getPostList();
Post post = list.get(0);

虽说这种做法在功能上没有问题,但它的效率非常不高,需要先查询出所有的数据,有点浪费资源。

那么,如何优化呢?

正栗子:

select id, createTime 
 from f_post
where userAccount='500佰' 
order by createTime asc
limit 1;

使用limit 1,只返回该用户第一次发帖的那一条数据即可。

8. in中值太多

简单说,对于批量查询接口,我们通常会使用in关键字过滤出数据。比如:想通过指定的一些id,批量查询出用户信息。

sql语句如下:

select * FROM f_post where userId in ( 1,2,3...9999999);

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

这时该怎么办呢?

select * FROM f_post where userId in (1,2,3...100) limit 500;

可以在sql中对数据用limit做限制。

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

public List<Post> getPost(List<String> uids) {
   if(CollectionUtils.isEmpty(uids)) {
      return null;
   }
   if(uids.size() > 500) {
      throw new BusinessException("每次最多允许查询500条记录")
   }
   return mapper.getPostList(uids);
}

还有一个方案就是:如果uids超过500条记录,可以分批用多线程去查询数据。每批只查500条记录,最后把查询到的数据汇总到一起返回。不过这只是一个临时方案,不适合于uids实在太多的场景。因为uids太多,即使能快速查出数据,但如果返回的数据量太大了,网络传输也是非常消耗性能的,要注意这一点。

9.高效的分页查询

简单说,有时候,列表页在查询数据时,为了避免一次性返回过多的数据影响接口性能,我们一般会对查询接口做分页处理。

分页一般用的limit关键字:

select userId,userName,userAvatar 
from f_user limit 10,20;

如果表中数据量少,用limit关键字做分页,没啥问题。但如果表中数据量很多,用它就会出现性能问题。

比如现在分页参数变成了:

select userId,userName,userAvatar 
from f_user limit 1000000,20;

这样会查到1000020条数据,然后丢弃前面的1000000条,只查后面的20条数据,这个是消浪费据库资源和增加堆栈性能。

那么,这种海量数据该怎么分页呢?

所谓高效分页,优化sql:

select userId,userName,userAvatar 
from f_user where userId > 1000000 limit 20;

先找到上次分页最大的id,然后利用id上的索引查询。不过该方案,要求id是连续的,并且有序的。

还能使用between优化分页。

select userId,userName,userAvatar 
from f_user where userId between 1000000 and 1000020;

需要注意的是between要在唯一索引上分页,不然会出现每页大小不一致的问题。

10.用连接查询代替子查询

为啥?原因在哪?

原因是执行子查询时,需要创建临时表,查询完毕后,需要再删除这些临时表,有一些额外的性能消耗。

子查询的栗子如下:

如果需要从两张以上的表中查询出数据的话,一般有两种实现方式:子查询连接查询

 select * FROM f_post where userAccount
        in (
            SELECT userAccount FROM f_user_relationship_thumb
            WHERE friendsId = #{userId} AND reviewStatus = 1
            UNION
            SELECT friendsUserAccount as userAccount FROM f_user_relationship_thumb
            WHERE userId = #{userId} AND reviewStatus = 1
            LIMIT 500
        )

**执行逻辑:**子查询语句可以通过in关键字实现,一个查询语句的条件落在另一个select语句的查询结果中。程序先运行在嵌套在最内层的语句,再运行外层的语句。

如果涉及的表数量不多的话,子查询语句的优点是简单,结构化。

这时可以改成连接查询减小临时额外的性能消耗。具体栗子如下:

 select * FROM f_post where userAccount
        JOIN (
            SELECT userAccount FROM f_user_relationship_thumb
            WHERE friendsId = #{userId} AND reviewStatus = 1
            UNION
            SELECT friendsUserAccount as userAccount as remarks FROM f_user_relationship_thumb
            WHERE userId = #{userId} AND reviewStatus = 1
            LIMIT 500
        ) fr ON pm.userAccount = fr.userAccount

11.连续join的表不宜超过3个

根据阿里巴巴开发者手册的规定,join表的数量不应该超过3个。

连续join多表
select a.name,b.name.c.name,d.name,e.name,f.name
from a 
inner join b on a.id = b.a_id
inner join c on c.b_id = b.id
inner join d on d.c_id = c.id
inner join e on e.d_id = d.id
inner join f on f.e_id = e.id
inner join g on g.f_id = f.id

**原因:**如果join太多,在选择索引的时候会非常复杂,很容易选错索引。

解决办法:如果实现业务场景中需要查询出另外几张表中的数据,可以在a、b、c表中冗余专门的字段

12. join时要注意

涉及到多张表联合查询的时候,一般会使用join关键字。

而join使用最多的是left join和inner join。

  • left join:求两个表的交集外加左表剩下的数据 。
  • inner join:求两个表交集的数据。
  • 另外不管是left join、inner join还是其他jion操作,切记一定不要使用多个连续的jion 这样会极大的增加查询数据库堆栈。

如果两张表使用inner join关联,会自动选择两张表中的小表,去驱动大表,所以性能上不会有太大的问题。

如果两张表使用left join关联,会默认用left join关键字左边的表,去驱动它右边的表。如果左边的表数据很多时,就会出现性能问题。

特别提醒:在用left join关联查询时,左边要用小表,右边可以用大表。如果能用inner join的地方,尽量少用left join。

阅读到此,您已经超越99%的技术boy

最后:

如果这篇sql优化文章对您有所帮助,或者有所启发的话,给作者一个小关,您的支持是的精神粮食。

🥇个人主页:500佰 欢迎留言

评论 84
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值