SQL优化-简单例子

63 阅读4分钟

是时候总结一下以前的SQL优化例子了😁哈哈哈哈,欢迎交流。

工作中经常用的SQL例子

我总结的:SQL优化一般从几个方面考虑:

  1. 更简单的SQL:不要使用不恰当的SQL,同一个功能,每个人写的SQL语句可能都不一样,我们尽量选择更合适的SQL语句。比如子查询、连接查询、having、IN、窗口函数、WITH AS 等可以简化SQL。
  2. 无索引、索引失效的Query SQL:合理的表结构+合适的索引、更精确的查询范围,避免索引失效,可以提升查询SQL的时间。
  3. 锁等待、大事务的SQL:长事务意味着 可能会导致更多的redo日志生成、更大的undo日志、旧版本的ReadView的维护,锁等待时间可能会变长等等。
  4. 占用更小资源的SQL:比如优化掉临时表、避免锁全表、降低死锁等。

建议尽量不要使用长事务。长事务会存在很老的read-view,长事务还会占用锁资源 。

查找长事务>5s的SQL:

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>5

如果SQL优化还是慢,那就试试数据库调优,调整数据库参数等(这超出SQL优化的范畴了吧,调优这块经验不足😅)

使用大砍刀(分库分表、读写分离),将大表垂直切分为多张表,比如user表拆分为user_account,user_info,或者水平切分为user_0,...user_7。

注意:SQL含有伪代码

1、投影(只取所需要的字段)

号称最简单的SQL优化-投影,就是只返回需要的字段数据。

比如我想查询groupID和成员ID list,就可以直接查询group_id,user_id,减少查询返回的数据。

//实体的投影:group_member表字段很多,当我只需要查询 group的ID和group的成员ID时
@Data
public class GroupRelId implements Serializable {
    Long id;
    String userIds;
}
@Select(value = "SELECT group_id as id, GROUP_CONCAT(user_id) AS user_ids FROM ch_group_member WHERE group_id is not null GROUP BY group_id")
List<GroupRelId> findRelIdAll();
2、多个SQL 优化为单个SQL(适合级联查询的数据)

有时为了减少数据库的访问次数,会将几个简单的SQL 改成一条SQL去执行。 可能有人会说不要连表查询,阿里规约上禁止连表,我觉得应该根据业务需求去选择,而不是用金科玉律去束缚自己。

比如:查询群组Group的详情,group和member虽然是两个实体对象,但是他们的关系不仅仅是组合,而是聚合关系。而且一个group的members不会太大,顶多1000多个。

多次SQL查询,再组装数据:group.setMembers(members)

-- 查询 group
select * from tb_group where id=#{groupId};
-- 查询 member list
select * from tb_group_member where group_id=#{groupId};

优化为 直接级联查询。

 SELECT g.*,
        m.id AS m_id,
        m.group_id AS m_group_id,
        m.user_id AS m_user_id,
        m.alias_name AS m_alias_name,
        m.nickname AS m_nickname,
        m.avatar_url AS m_avatar_url,
        m.gender AS m_gender
        FROM tb_group g
        LEFT JOIN tb_group_member m ON g.id = m.group_id
        WHERE g.group_id=#{groupId} )
3、复杂SQL 拆分为 简单SQL(将一条业务复杂的SQL操作拆分为几条简单的SQL)

有时为了降低业务的复杂程度,会将一条复杂的SQL拆分为几条简单的SQL去执行。

比如现在要分页查询商品goods+分类名+品牌名+销售属性list(因为前端页面要展示这些数据):

SELECT g.*, c.name AS cate_name, b.name AS brand_name, sa.* 
FROM 
    (SELECT * FROM tb_goods WHERE is_sale = TRUE LIMIT ${size} OFFSET ${offset}) g  --商品主表
LEFT JOIN tb_category c --商品分类表
    ON g.category_id = c.id  
LEFT JOIN tb_brand b --商品品牌表
    ON g.brand_id = b.id  
LEFT JOIN rel_goods_saleattr rs  --关联表
    ON g.id = rs.goods_id 
LEFT JOIN tb_sale_attribute sa  --商品销售属性表(一对多)
    ON rs.attr_id = sa.id;

拆分为多条SQL执行

step1——查询商品主表分页; step2——查询商品的销售属性list; 再组合并返回前端渲染

-- 查询商品主表分页
SELECT * FROM tb_goods WHERE is_sale = TRUE LIMIT ${size} OFFSET ${offset};

-- 查询销售属性list(但是这样会在分页内,循环查询SQL)
SELECT * FROM tb_sale_attribute sa --商品销售属性表(一对多)
INNER JOIN rel_goods_saleattr rs   --关联表
ON rs.attr_id = sa.id
WHERE rs.goods_id=#{goodsId} 

-- (优化为批量查询消息属性list,业务代码组装为map,再去goods.setSaleAttrs(map[goodsId]))
SELECT * FROM tb_sale_attribute sa --商品销售属性表(一对多)
INNER JOIN rel_goods_saleattr rs   --关联表
ON rs.attr_id = sa.id
WHERE rs.goods_id IN #{goodsIds} -- goodsIds为每次分页后的id list

进一步优化:由于品牌名称、分类名称几乎不会更改。避免每次连表查询。

可以冗余brandName、categoryName字段到goods表中(缺点是名称更改时,需要刷数据)。 也可以缓存到Redis中: id2brandName、id2categoryName(名称更改时,需更新缓存)。

4、使用窗口函数

比如要查询 每个分类下销量前100的商品

SELECT g1.*, cate.name AS category_name 
FROM serve_goods g1 
INNER JOIN tb_category cate --关联分类表获取分类名
ON g1.cate_id = cate.id 
WHERE g1.is_sale = true
AND
    ( SELECT COUNT(*) FROM serve_goods g2 
        WHERE g2.cate_id = g1.cate_id 
        AND 
        (g2.sale_count > g1.sale_count OR (g2.sale_count=g1.sale_count AND g2.id < g1.id) )
    ) < #{limitSize} 
ORDER BY g1.cate_id, g1.sale_count DESC;

优化后

-- 根据 category 分组且排序

WITH SortedGoods AS (
    SELECT sg.*, cate.name AS category_name,
        row_number() OVER (PARTITION BY sg.cate_id ORDER BY sg.sales_count DESC) as rn
    FROM serve_goods sg
    INNER JOIN tb_category cate --关联分类表获取分类名
        ON sg.cate_id = cate.id 
    WHERE sg.is_sale = true 
)
SELECT * FROM SortedGoods WHERE rn<=#{limitSize} ORDER BY cate_id,rn;

。。。持续更新。。。