是时候总结一下以前的SQL优化例子了😁哈哈哈哈,欢迎交流。
我总结的:SQL优化一般从几个方面考虑:
- 更简单的SQL:不要使用不恰当的SQL,同一个功能,每个人写的SQL语句可能都不一样,我们尽量选择更合适的SQL语句。比如子查询、连接查询、having、IN、窗口函数、WITH AS 等可以简化SQL。
- 无索引、索引失效的Query SQL:合理的表结构+合适的索引、更精确的查询范围,避免索引失效,可以提升查询SQL的时间。
- 锁等待、大事务的SQL:长事务意味着 可能会导致更多的redo日志生成、更大的undo日志、旧版本的ReadView的维护,锁等待时间可能会变长等等。
- 占用更小资源的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;
。。。持续更新。。。