记一次SQL优化

昨天(2022-7-22)上线了我的一个功能,测试环境数据量较小,问题不大,但是上生产之后,直接卡死了,然后就开始了这么一次SQL优化,这里记录一下。

不太方便透露公司的表结构,这里我自己建了几张表,模拟一下就可以了。

肯定有杠精要说表可以不这样设计了,但是事实现在系统就是这样设计的,如果想改动表设计,影响面就太大了(我们急着上线哦)。当然,本文的后面也会给出修改设计的方案,以达到更优解。

1. 创建表

进货单表:

CREATE TABLE purchase_order (
  id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键自增id',
  purchase_time varchar(255) DEFAULT NULL COMMENT '进货时间',
  purchase_pre_unit_price decimal(10,2) unsigned zerofill NOT NULL COMMENT '进货预订单价(元/kg)',
  purchase_weight decimal(10,2) unsigned zerofill NOT NULL COMMENT '进货重量(kg)',
  purchase_bill_no varchar(255) NOT NULL COMMENT '进货单号',
  PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=62181 DEFAULT CHARSET=utf8 COMMENT='进货单';

进货结算单表:

CREATE TABLE settlement_voucher (
  id int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  purchase_bill_no varchar(512) DEFAULT NULL COMMENT '进货单号',
  settlement_bill_no varchar(64) NOT NULL COMMENT '结算单号',
  unit_price decimal(10,2) unsigned zerofill NOT NULL COMMENT '实际结算单价(元/kg)',
  settlement_weight decimal(10,2) unsigned zerofill NOT NULL COMMENT '实际结算重量(kg)',
  cut_off_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '结算时间',
  PRIMARY KEY (id),
  KEY idx_settlement_bill_no (settlement_bill_no)
) ENGINE=InnoDB AUTO_INCREMENT=63288 DEFAULT CHARSET=utf8 COMMENT='进货结算单';

发票表:

CREATE TABLE invoice (
  id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  invoice_code varchar(255) NOT NULL COMMENT '发票代码',
  invoice_number varchar(255) NOT NULL COMMENT '发票号码',
  pay_amount decimal(10,2) DEFAULT NULL COMMENT '发票金额',
  PRIMARY KEY (id),
  KEY idx_invoice_number (invoice_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='发票表';

发票-结算单关联表:

CREATE TABLE settlement_invoice_relation (
  id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  invoice_code varchar(255) DEFAULT NULL COMMENT '发票代码',
  invoice_number varchar(255) DEFAULT NULL COMMENT '发票号码',
  settlement_bill_no varchar(64) DEFAULT NULL COMMENT '结算单号',
  PRIMARY KEY (id),
  KEY idx_settlement_bill_no (settlement_bill_no),
  KEY idx_invoice_number (invoice_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='发票-结算单关联表';

以上是我自己创建的几张表,先介绍一下这几张表的关系:

  • 进货单表(purchase_order)和进货结算单表(settlement_voucher)通过进货单号(purchase_bill_no)关联。这里值得注意的是:
  • 一个进货单可以对应多个进货结算单,通过 purchase_bill_no关联,如以下数据:
  • 一个进货结算单可以对应多个进货单,通过 purchase_bill_no关联, settlement_voucher表中的 purchase_bill_no字段存放多个进货单号,使用英文逗号隔开。如以下数据:
  • 发票表(invoice)和结算单表(settlement_voucher)有一个关联关系表(settlement_invoice_relation)
  • 发票表和关联关系表使用 invoice_codeinvoice_number关联
  • 结算单表和关联关系表使用 settlement_bill_no关联
  • 发票和结算单是多对多的关系

2. 需求

现在需要以进货结算单表(settlement_voucher)查询出一个列表:

  • 列表字段有【进货时间(多个使用英文逗号隔开)、进货平均单价、进货预定总金额,结算单号,结算平均单价,结算金额,结算时间,发票号码(多个使用英文逗号隔开),发票代码(多个使用英文逗号隔开)】
  • 查询条件有:进货时间(一个进货结算单对应多个进货单时,只要有一个进货单的时间在范围内,就查询到),结算时间,发票号码(一个结算单对应多个发票时,只要有一个发票能关联上,就查询到)
  • 根据结算时间排序

当然,实际当时的那个需求,列表字段比这多,查询条件也比这多……

3. 给表插入数据

先给货单表(purchase_order)和进货结算单表(settlement_voucher)各自插入10万条数据,我这里使用了存储过程:

begin
declare i int;
declare purchase_weight decimal(10,2);
declare unit_price decimal(10,2);
declare purchase_bill_no varchar(255);
declare settlement_bill_no varchar(255);
set i=0;
while i

调用存储过程生成数据:

call pre();

生成之后需要随机改几条数据,模拟一个进货单可以对应多个进货结算单,以及一个进货结算单可以对应多个进货单两种情况(这样数据更真实一点)。

一个进货单可以对应多个进货结算单的情况就不模拟了,这种情况其实对这次查询的影响并不大。

一个进货结算单可以对应多个进货单的情况:

记一次SQL优化

再创建一些发票数据和结算单-发票关联数据,需要体现多对多的关系:

insert into invoice(invoice_code,invoice_number,pay_amount)
VALUES
('111111','1111100','1000'),
('111112','1111101','1001'),
('111113','1111102','1002'),
('111114','1111103','1003'),
('111115','1111104','1004'),
('111116','1111105','1005'),
('111117','1111106','1006'),
('111118','1111107','1007'),
('111119','1111108','1008'),
('111110','1111109','1009');

INSERT into settlement_invoice_relation(invoice_code,invoice_number,settlement_bill_no)
VALUES
('111111','1111100','settlement-00000000'),
('111112','1111101','settlement-00000000'),
('111113','1111102','settlement-00000000'),
('111114','1111103','settlement-00000004'),
('111114','1111103','settlement-00000006'),
('111114','1111103','settlement-00000030'),
('111116','1111105','settlement-00000041'),
('111117','1111106','settlement-00000041'),
('111118','1111107','settlement-00000043');

4. 开始根据需求写SQL

优化第一步,当然是想让产品经理去掉一些查询条件,避免进货单表和进货结算表关联了,但是你懂的。。。。。。

这里就以进货时间为条件查询为例(因为主要就是进货单和进货结算单关联导致慢查询), 记得需求哦,就是一个进货结算单可能对应多个进货单,只要有其中一个进货单在时间范围内,就需要查询出这条进货结算单

还有:我上面创建的表中索引也模拟了当时优化之前的索引……

4.1 第一版

select
    GROUP_CONCAT(po.purchase_time) as 进货时间,
    AVG(IFNULL(po.purchase_pre_unit_price,0)) as 进货均价,
    t.settlement_bill_no as 结算单号,
    AVG(IFNULL(t.unit_price,0)) as 结算均价,
    any_value(t.cut_off_time) as 结算时间,
    any_value(invoice_tmp.invoice_code) as 发票代码,
    any_value(invoice_tmp.invoice_number) as 发票号码
from settlement_voucher t
left join purchase_order po on FIND_IN_SET(po.purchase_bill_no,t.purchase_bill_no)>0
left join (
    select sir.settlement_bill_no,
                 GROUP_CONCAT(i.invoice_number) invoice_number,
                 GROUP_CONCAT(i.invoice_code) invoice_code
    from settlement_invoice_relation sir, invoice i
    where sir.invoice_code = i.invoice_code and sir.invoice_number = i.invoice_number
    group by sir.settlement_bill_no
) invoice_tmp on invoice_tmp.settlement_bill_no = t.settlement_bill_no
where 1=1
-- and t.settlement_bill_no='settlement-00000000'
and EXISTS(select 1 from purchase_order po1 where FIND_IN_SET(po1.purchase_bill_no,t.purchase_bill_no)>0
    and po1.purchase_time >='2022-07-01 00:00:00'
)
and EXISTS(select 1 from purchase_order po1 where FIND_IN_SET(po1.purchase_bill_no,t.purchase_bill_no)>0
    and po1.purchase_time

第一版SQL当时在本地环境执行是用了5秒左右,此时就已经意识到问题了,这别说上生产了,就是在测试环境都得挂掉。

但是看看我在自己的垃圾服务器(双核4G)上跑这条SQL吧,是根本执行不出来的(虽然公司服务器好一些,但是生产环境确实卡死了):

记一次SQL优化

当时就还没没看执行计划,一眼看去,这个SQL中用到了 FIND_IN_SET,肯定是不会走索引的,建了索引也没用,也就是主要是进货单表(purchase_order)和进货结算单表 settlement_voucher关联会很慢,毕竟他们是多对多的关系,再加上这恶心的需求。所以现在想想该怎么才能不用 FIND_IN_SET

对,吃饭期间,突发奇想:我应该可以把进货结算单表拆成一个临时表,如果进货结算单表对应了5个进货单,我就把进货结算单拆成5条数据,这五条数据除了进货单号不一样,其他字段都 一样,这样就可以不用 FIND_IN_SET了。

说干就干,于是有了下面第二版SQL。

4.2 第二版

向把进货结算单表拆分成上面说的临时表,需要添加一个表:

CREATE TABLE incre_table (
  id int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用于分割进货结算单表';
-- 注意:这里一个进货结算单对应多少个进货单,这里就要依次插入多少条数据,我这里10条 就够用了
insert into incre_table(id) VALUES(1);
insert into incre_table(id) VALUES(2);
insert into incre_table(id) VALUES(3);
insert into incre_table(id) VALUES(4);
insert into incre_table(id) VALUES(5);
insert into incre_table(id) VALUES(6);
insert into incre_table(id) VALUES(7);
insert into incre_table(id) VALUES(8);
insert into incre_table(id) VALUES(9);
insert into incre_table(id) VALUES(10);

先来看看怎么把一条进货结算单数据拆分成多条:

select
    sv.cut_off_time,
    sv.settlement_bill_no,
    sv.unit_price,
    sv.settlement_weight,
    SUBSTRING_INDEX(SUBSTRING_INDEX(sv.purchase_bill_no,',',it.id),',',-1) purchase_bill_no
from settlement_voucher sv
RIGHT JOIN incre_table it on it.id

来解释一下这个骚操作:

  • 首先我创建了一个只有id的表 incre_table,插入了十条数据,并且这十条数据必须是1-10。
  • 然后我使用 settlement_voucher 右连接了 incre_table,并且只取 incre_table中id小于或等于进货单数量的数据。这样就控制了这条SQL应该查询多少条数据(就刚好是一个进货结算单对应的进货单条数)。
  • 然后使用 SUBSTRING_INDEX去一个一个拆分 settlement_voucher表中的进货单号

这套SQL执行的结果就是:

记一次SQL优化

综合起来,就写好了第二版SQL:

select
    GROUP_CONCAT(po.purchase_time) as 进货时间,
    AVG(IFNULL(po.purchase_pre_unit_price,0)) as 进货均价,
    t.settlement_bill_no as 结算单号,
    AVG(IFNULL(t.unit_price,0)) as 结算均价,
    any_value(t.cut_off_time) as 结算时间,
    any_value(invoice_tmp.invoice_code) as 发票代码,
    any_value(invoice_tmp.invoice_number) as 发票号码
from (
    select
        sv.cut_off_time,
        sv.settlement_bill_no,
        sv.unit_price,
        sv.settlement_weight,
        SUBSTRING_INDEX(SUBSTRING_INDEX(sv.purchase_bill_no,',',it.id),',',-1) purchase_bill_no
    from settlement_voucher sv
    RIGHT JOIN incre_table it on it.id='2022-07-01 00:00:00'
and po.purchase_time

测试查询数据结果肯定是没有问题的哦!!!

好的,到这里终于把所有用到 FIND_IN_SET的地方去掉了,这时看索引就有意义了!

看看执行计划吧:

记一次SQL优化

阿西巴,一堆的全表扫描,看看上面第二版SQL,发现进货表(purchase_order)的 purchase_bill_no字段是应该走索引的,按道理这个字段一般设计表的时候就应该已经加索引了,但是我以为只是我以为,它确实没加索引,好的,那就给它加上索引吧:

create index idx_purchase_bill_no on purchase_order(purchase_bill_no);

加完是这个索引后,再看看执行计划:

purchase_order表的 purchase_bill_no已经走了索引,但是 settlement_invoice_relation咋不走索引,它是有两个索引的。。。。。。

记一次SQL优化

再看看在我的垃圾服务器上执行,看能不能执行出来:

记一次SQL优化

好了,为了让 settlement_invoice_relation表的查询也走索引,开始下一轮的SQL优化

4.3 第三版

就不在下面去聚合获取 invoice_codeinvoice_number了,在上面来聚合,至于要以这两个字段作为查询条件,那可以把下面这条SQL再包一层,作为一个临时表再查询一遍,这里就不演示了

select
    GROUP_CONCAT(po.purchase_time) as 进货时间,
    AVG(IFNULL(po.purchase_pre_unit_price,0)) as 进货均价,
    t.settlement_bill_no as 结算单号,
    AVG(IFNULL(t.unit_price,0)) as 结算均价,
    any_value(t.cut_off_time) as 结算时间,
    GROUP_CONCAT(DISTINCT invoice_tmp.invoice_code) as 发票代码,
    GROUP_CONCAT(DISTINCT invoice_tmp.invoice_number) as 发票号码
from (
    select
        sv.cut_off_time,
        sv.settlement_bill_no,
        sv.unit_price,
        sv.settlement_weight,
        SUBSTRING_INDEX(SUBSTRING_INDEX(sv.purchase_bill_no,',',it.id),',',-1) purchase_bill_no
    from settlement_voucher sv
    RIGHT JOIN incre_table it on it.id='2022-07-01 00:00:00'
and po.purchase_time

再看看执行计划:

记一次SQL优化

这时,基本优化结束,再看看在我的垃圾服务器上跑出的结果:

记一次SQL优化

到这里,基本上生产上是可以在三秒以内查询出来了,本次SQL优化就到此结束了!!!

但是,其实还是可以继续优化的,但是设计到系统改的地方比较多了,影响面比较大,这里就说一下思路,暂时不能实践:

可以把进货单表 purchase_order和进货结算单表 settlement_voucher之间,建立一个中间表,实现多对多的关系,再加以索引,应该会更快,而且可以一劳永逸,以后这种关联都会比较方便了!

Original: https://www.cnblogs.com/fanchengmeng/p/16511121.html
Author: 下半夜的风
Title: 记一次SQL优化

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/571912/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Spring Boot + Mybatis 实现动态数据源

    动态数据源 在很多具体应用场景的时候,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务A要访问A数据库,业务B要访问B数…

    Java 2023年5月30日
    079
  • MySQL

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月9日
    084
  • Java获取Web服务器文件

    Java获取Web服务器文件 如果获取的是服务器上某个目录下的有关文件,就相对比较容易,可以设定死绝对目录,但是如果不能设定死绝对目录,也不确定web服务器的安装目录,可以考虑如下…

    Java 2023年6月7日
    073
  • 〖java实例〗利用线程检测变量

    public class Main {</p> <pre><code>static int i = 0; static int iAdd = i…

    Java 2023年6月7日
    066
  • ruoyi框架-将本地mybatis修改为mybatisplus

    1.修改根目录下pom.xml文件,注释mybatis依赖,引入mybatis-plus依赖。 <dependency> <groupId>com.baom…

    Java 2023年5月30日
    068
  • Java(8)数组

    数组 数组存储相同类型值的序列。 声明数组 数组是一种数据结构,用来存储同一类型值的集合。通过一个整型下标(index,或称索引)可以访问数组中的每一个值。例如,如果a是一个整型数…

    Java 2023年6月9日
    072
  • Git笔记

    Git 一个免费、开源的分布式版本控制系统,可以快速地处理从小型到大型的各种项目 集中式版本控制工具:SVN 分布式版本控制工具:Git Git常用命令 – git config …

    Java 2023年6月8日
    059
  • Java8日期时间(LocalDate、LocalTime、LocalDateTime)

    在看题的时候发现了Java8中新加入的日期时间类,觉得这个小哥写的不错,自己也跟着练习下。原文地址:https://blog.csdn.net/yy139926/article/d…

    Java 2023年6月6日
    085
  • 谈谈开发前编程的前置内容

    https://www.cnblogs.com/jingran/articles/15362137.html Original: https://www.cnblogs.com/j…

    Java 2023年6月9日
    080
  • 一文深入浅出理解国产开源木兰许可系列协议

    伴随着国际形式的变迁, 越来越多组织申明将遵循美国出口管制法律的约束,并且蔓延到了开源领域;关于国内开源生态如何自立,做到在逼不得已无法与国际接轨的情况下能够健康发展的措施也被提上…

    Java 2023年6月6日
    0118
  • Java使用正则表达式取网页中的一段内容(以取Js方法为例)

    关于正则表达式:表1.常用的元字符代码说明.匹配除换行符以外的任意字符\w 匹配字母或数字或下划线或汉字\s 匹配任意的空白符\d 匹配数字\b 匹配单词的开始或结束^匹配字符串的…

    Java 2023年5月29日
    066
  • java中FileInputStream,FileReader等的区别

    FileInputStream,FileReader C语言只需要一个File*就可以了,与C不同,java有一系列流类型,其数量超过60种。类库的设计者声称:”有足够…

    Java 2023年5月29日
    0108
  • 深入理解java泛型

    什么是Java泛型 泛型的使用 泛型类 泛型接口 泛型方法 泛型的底层实现机制 ArrayList源码解析 什么是泛型擦除 泛型的边界 ?:无界通配符 extends 上边界通配符…

    Java 2023年6月15日
    076
  • JDBC:加载数据库驱动、连接数据库(详细讲解)

    加载数据库驱动: 1)由于Java是一个纯面向对象语言,任何事物在其中都必须抽象成类或者类对象,数据库也不例外,JDBC同样也把数据库抽象成面向对象的结构; 2)JDBC将整个数据…

    Java 2023年6月5日
    0113
  • 线程从2022-06-23 18.21 等待到2022-06-24 11:00

    "task-scheduler-4" #412 prio=5 os_prio=0 tid=0x00007fc5c0017800 nid=0x55d4 in Ob…

    Java 2023年6月9日
    093
  • RenderX java的xml打印

    http://www.zdnet.com.cn/techupdate/apply/collaboration/story/0,3800030473,39347913,00.htm …

    Java 2023年5月29日
    096
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球