0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

品鉴一下祖传SQL脚本调优方法

冬至子 来源:Kida的技术小屋 作者:kida tech 2023-05-19 10:50 次阅读

看到这个题目你敢相信自己的眼睛吗?居然有人敢动祖传代码?没错,那个人就是我,而且这次不仅要动而且要调优(心中一万个无奈,实在是没办法)。不过这次调优其实也挺经典的,于是整理了一下发出来给各位品鉴一下,希望对各位有用。

本次调优的难点:

  1. 本次脚本太过雍长,不知道之前那位高人几乎将所有业务逻辑都写到SQL里面了;
  2. 据了解本次脚本已经经过3位高人之手调整过3次,只不过一直没有调好。后来得知脚本在“登录”和“非登录”时会出现两个分支处理,这是不恰当使用Mybatis动态脚本特性出来的锅;

首先,先看看再“非登录”状态下接口的响应时间,如下图:

图片

如上图所示接口在“非登录”状态下耗时1.76秒。 需要说明一下的是,图片显示的是7.83秒是整个事务操作的响应结果(里面存在大量的实时统计与运算,当时并没有针对运算和代码逻辑的优化...其实说白了也不敢优化,因此整个事务耗时比较长),图片上说的接口与本次文章中说的接口并不是同一个接口,而有问题的接口经排查耗时为1.76秒,因此本文中的图片是为了直观看出性能结果截取的并不是对应接口真实的执行时间(其实就是一句“懒”,不想写log展示数据库执行时间了......)

言归正传,当登录后再查询时性能急剧下降,如下图:

图片

问了最后一位修改的高人得知,他已经在Java层面优化过了,若不重构的情况下已经没有可以继续优化的地方了。所以这次调优主要将集中精力优化SQL查询,先看看登录后的查询语句。执行的SQL脚本如下:

SELECT *
FROM
    (SELECT 
        p.procurement_id,
            p.display_type,
            p.publish_type,
            p.valid_time,
            p.pay_type,
            p.cust_id,
            p.add_user,
            t.trade_name,
            p.add_time,
            p.oper_user,
            p.oper_time,
            p.platform_audit_status,
            p.platform_back_reason,
            p.platform_audit_user,
            p.platform_audit_time,
            p.status,
            p.procurement_title,
            p.alive_flag,
            c.is_gsp,
            c.is_gmp,
            c.customer_service_user,
            IFNULL(IF(p.display_type = 2, sui.CONTACT_NAME, fc.CONTACT_NAME), '暂无') AS CONTACT_NAME,
            IFNULL(IF(p.display_type = 2, sui.CELLPHONE, fc.cell_phone), '暂无') AS cellphone,
            IF(fc.SEX = 1, '先生', '女士') AS sex,
            IF(INSTR(GROUP_CONCAT(t.TRADE_PUBLISH_STATE), '0') > 0, 0, 1) AS TRADE_PUBLISH_STATE,
            IF(p.display_type = 2, '*******', c.CUST_NAME) AS CUST_NAME,
            SUM(IF((SELECT 
                    COUNT(0)
                FROM
                    spot_procurement_details spd
                WHERE
                    FIND_IN_SET(spd.trade_name_id, '35,65,124,1145,1168,255,288,81,')
                        AND spd.procurement_detail_id = pd.procurement_detail_id) > 0, 1, 0)) AS flag,
            pn.status AS inviteStatus,
            pn.invitation_id,
            pn.send_time,
            (SELECT IF(p.valid_time >= DATE_FORMAT(NOW(), '%Y-%m-%d'), 1, 2)) AS info_status,
            IF(p.status = 1, 1, IF(p.status = 6, 1.5, 2)) AS proc_status,
            p.top_type AS topType,
            p.top_time AS topTime
    FROM spot_procurement p
    LEFT JOIN spot_procurement_invitation pn ON pn.procurement_id = p.procurement_id
    LEFT JOIN spot_procurement_details pd ON pd.procurement_id = p.procurement_id
    LEFT JOIN spot_trade_name t ON t.trade_name_id = pd.trade_name_id
    LEFT JOIN spot_frequent_contacts fc ON p.cust_id = fc.CUST_ID AND fc.ALIVE_FLAG = 1 AND fc.IS_FREQUENT = 1
    LEFT JOIN spot_company c ON c.cust_id = p.cust_id
    LEFT JOIN spot_user_info sui ON c.CUSTOMER_SERVICE_USER = sui.USER_ID
    WHERE p.platform_audit_status = 1 AND p.alive_flag = 1 AND p.status >= 1
            AND (pd.is_split IS NULL OR pd.is_split != 'Y')
            AND (pn.receive_cust_id = '100000000000365' OR p.publish_type = 2)
            AND p.top_Type IN (1 , '3')
    GROUP BY p.procurement_id UNION (SELECT 
        p.procurement_id,
            p.display_type,
            p.publish_type,
            p.valid_time,
            p.pay_type,
            p.cust_id,
            p.add_user,
            t.trade_name,
            p.add_time,
            p.oper_user,
            p.oper_time,
            p.platform_audit_status,
            p.platform_back_reason,
            p.platform_audit_user,
            p.platform_audit_time,
            p.status,
            p.procurement_title,
            p.alive_flag,
            c.is_gsp,
            c.is_gmp,
            c.customer_service_user,
            IFNULL(IF(p.display_type = 2, sui.CONTACT_NAME, fc.CONTACT_NAME), '暂无') AS CONTACT_NAME,
            IFNULL(IF(p.display_type = 2, sui.CELLPHONE, fc.cell_phone), '暂无') AS cellphone,
            IF(fc.SEX = 1, '先生', '女士') AS sex,
            IF(INSTR(GROUP_CONCAT(t.TRADE_PUBLISH_STATE), '0') > 0, 0, 1) AS TRADE_PUBLISH_STATE,
            IF(p.display_type = 2, '*******', c.CUST_NAME) AS CUST_NAME,
            SUM(IF((SELECT COUNT(0)
                FROM spot_procurement_details spd
                WHERE FIND_IN_SET(spd.trade_name_id, '35,65,124,1145,1168,255,288,81,')
                        AND spd.procurement_detail_id = pd.procurement_detail_id) > 0, 1, 0)) AS flag,
            pn.status AS inviteStatus,
            pn.invitation_id,
            pn.send_time,
            (SELECT IF(p.valid_time >= DATE_FORMAT(NOW(), '%Y-%m-%d'), 1, 2)) AS info_status,
            IF(p.status = 1, 1, IF(p.status = 6, 1.5, 2)) AS proc_status,
            p.top_type AS topType,
            p.top_time AS topTime
    FROM spot_procurement p
    LEFT JOIN spot_procurement_invitation pn ON pn.procurement_id = p.procurement_id
    LEFT JOIN spot_procurement_details pd ON pd.procurement_id = p.procurement_id
    LEFT JOIN spot_trade_name t ON t.trade_name_id = pd.trade_name_id
    LEFT JOIN spot_frequent_contacts fc ON p.cust_id = fc.CUST_ID AND fc.ALIVE_FLAG = 1 AND fc.IS_FREQUENT = 1
    LEFT JOIN spot_company c ON c.cust_id = p.cust_id
    LEFT JOIN spot_user_info sui ON c.CUSTOMER_SERVICE_USER = sui.USER_ID
    WHERE
        p.platform_audit_status = 1 AND p.alive_flag = 1 AND p.status >= 1
            AND (pd.is_split IS NULL OR pd.is_split != 'Y')
            AND (pn.receive_cust_id = '100000000000365' OR p.publish_type = 2)
    GROUP BY p.procurement_id)) sss
WHERE sss.TRADE_PUBLISH_STATE = 1
ORDER BY sss.info_status ASC , sss.add_time DESC
LIMIT 0 , 10

这浅浅的107行脚本...通过拆解分析,发现脚本可以通过UNION关键字拆解成两部分,在此之前先在客户端直接运行看看执行效率,如下图:

图片

分页返回10条数据,总耗时为2.29秒。

之后将嵌套查询的内部脚本拆解成两部分,每部分都通过explain分析执行结果,先看第一部分,如下图: 图片

从上图中可以看出,除pn和pd两表的连接出现异常外,其他表的连接都比较正常,最起码它们都能够走到索引了(key和key_len说明了索引的名称和索引长度)。之后就看看pn和pd对应的Extra列提示什么,返回的内容是“Range checked for each record (index map: 0x2)”。

“Range checked for each record”在以前其他调优分享里也说过,当前表的连接字段虽然有一个possibile_key的字段,但是MySQL的执行分析器在执行期间由于“某种”原因没有使用到该索引(从上图也看到了,虽然pn,pd两表都有possibile_key但是key和key_len都是null的,证明他们都没有走索引)因此出现了Range checked的提示,表示连接中的每一条记录都需要进行检查。因此这个报错也是MySQL里面最慢的错误提示之一。

既然没有走索引那就要看看为什么没有走索引。pn、pd表的连接如下所示:

FROM spot_procurement p
LEFT JOIN spot_procurement_invitation pn ON pn.procurement_id = p.procurement_id
LEFT JOIN spot_procurement_details pd ON pd.procurement_id = p.procurement_id

其实两个表都是p这张表的右连接,而且都是通过procurement_id字段进行连接的,procurement_id字段是p这张表的主键,而pn、pd两张表procurement_id字段是他们的数据外键,本应该是不存在问题的。但是通过对比p、pn、pd这三张表得知,p表中procurement_id字段是bigint的数据类型,而pn、pd表中procurement_id数据类型是varchar类型,因此explain中不走索引的原因极有可能是因为数据类型不一致导致的**(又是数据类型不一致导致的性能问题)** 。

因为字段数据类型不一致,所以在on的时候需要将外表中的字段先隐式转型成内表字段对应的数据类型后再做关联,在这个过程中其实跟下面的语句是等价的:

FROM spot_procurement p
LEFT JOIN spot_procurement_invitation pn ON CAST(pn.procurement_id AS UNSIGNED integer) = p.procurement_id
LEFT JOIN spot_procurement_details pd ON CAST(pd.procurement_id AS UNSIGNED integer) = p.procurement_id

在这里看出了其他问题,pn、pd作为外联表放在=的前面,而外表字段又要使用CAST函数对字段进行类型转换,因此该字段不走索引。

因此,在不改变原有逻辑的情况下修改成如下:

SELECT 
        p.procurement_id,
            p.display_type,
            p.publish_type,
            p.valid_time,
            p.pay_type,
            p.cust_id,
            p.add_user,
            t.trade_name,
            p.add_time,
            p.oper_user,
            p.oper_time,
            p.platform_audit_status,
            p.platform_back_reason,
            p.platform_audit_user,
            p.platform_audit_time,
            p.status,
            p.procurement_title,
            p.alive_flag,
            c.is_gsp,
            c.is_gmp,
            c.customer_service_user,
            IFNULL(IF(p.display_type = 2, sui.CONTACT_NAME, fc.CONTACT_NAME), '暂无') AS CONTACT_NAME,
            IFNULL(IF(p.display_type = 2, sui.CELLPHONE, fc.cell_phone), '暂无') AS cellphone,
            IF(fc.SEX = 1, '先生', '女士') AS sex,
            IF(INSTR(GROUP_CONCAT(t.TRADE_PUBLISH_STATE), '0') > 0, 0, 1) AS TRADE_PUBLISH_STATE,
            IF(p.display_type = 2, '*******', c.CUST_NAME) AS CUST_NAME,
            SUM(IF((SELECT COUNT(0)
                FROM spot_procurement_details spd
                WHERE FIND_IN_SET(spd.trade_name_id, '35,65,124,1145,1168,255,288,81')
                        AND spd.procurement_detail_id = pd.procurement_detail_id) > 0, 1, 0)) AS flag,
            pn.status AS inviteStatus,
            pn.invitation_id,
            pn.send_time,
            (SELECT IF(p.valid_time >= DATE_FORMAT(NOW(), '%Y-%m-%d'), 1, 2)) AS info_status,
            IF(p.status = 1, 1, IF(p.status = 6, 1.5, 2)) AS proc_status,
            p.top_type AS topType,
            p.top_time AS topTime
    FROM spot_procurement p
    LEFT JOIN 
    (select a.receive_cust_id,a.status,a.invitation_id,a.send_time, CAST(a.procurement_id AS UNSIGNED integer) as procurement_id from spot_procurement_invitation a) pn ON pn.procurement_id = p.procurement_id
    LEFT JOIN 
    (select b.procurement_detail_id,CAST(b.procurement_id AS UNSIGNED integer) as procurement_id,b.trade_name_id,b.is_split from spot_procurement_details b ) pd ON pd.procurement_id = p.procurement_id
    LEFT JOIN spot_trade_name t ON t.trade_name_id = pd.trade_name_id
    LEFT JOIN spot_frequent_contacts fc ON p.cust_id = fc.CUST_ID AND fc.ALIVE_FLAG = 1 AND fc.IS_FREQUENT = 1
    LEFT JOIN spot_company c ON c.cust_id = p.cust_id
    LEFT JOIN spot_user_info sui ON c.CUSTOMER_SERVICE_USER = sui.USER_ID
    WHERE p.platform_audit_status = 1 AND (pd.is_split IS NULL OR pd.is_split != 'Y')
            AND p.alive_flag = 1 AND p.status >= 1
            AND (pn.receive_cust_id = '100000000000365' OR p.publish_type = 2)
            AND p.top_Type IN (1 , '3')
    GROUP BY p.procurement_id

这里先将需要转类型的字段做显式转换,然后再做join连接,通过explain后得出执行计划如下:

图片

在外联的时候使用了auto_key1带代替了原来的null了,而a和b两个表由于只是转义用因此是全表扫描的。但是留意Extra列中已经不存在Range checked的提示了。

接下来再看看第二部分的语句,经过对比与第一部分的语句基本相似,因此可以使用同样的优化手段进行sql的优化,优化后的整体explain执行计划如下图:

图片

如上图所示暂时没有发现其他特殊的情况,接下来就直接运行看看查询效果,如下图:

图片

在修改了sql之后再去验证一下接口的加载速度,如下图:

图片

在账号登录的状态下接口从5.42秒提升到0.82秒,执行效率提升了81.5%。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2966

    浏览量

    104704
  • SQL
    SQL
    +关注

    关注

    1

    文章

    762

    浏览量

    44117
  • 分析器
    +关注

    关注

    0

    文章

    92

    浏览量

    12492
  • MYSQL数据库
    +关注

    关注

    0

    文章

    96

    浏览量

    9389
收藏 人收藏

    评论

    相关推荐

    史上最全性能总结

    在说什么是性能之前,我们先来说一下,计算机的体系结构。
    的头像 发表于 05-13 08:57 6312次阅读
    史上最全性能<b class='flag-5'>调</b><b class='flag-5'>优</b>总结

    MaxCompute SQL原理解析及性能

    摘要: 分享内容 介绍了ODPS SQL的基于mapreduce是如何实现的及些使用小技巧,回顾了mapreduce各个阶段可能产生的问题及相应的处理方法,同时介绍了些应对数据倾斜
    发表于 02-05 11:35

    功耗时经常用到的几个方法

    前言不清楚当前产品的整机功耗,就不清楚怎么获取产品的整机及各个模块的功耗数据,需要测量正确的功耗测量方法,快速的了解整机的功耗分布,为功耗
    发表于 12-21 06:31

    紫金桥软件SQL语句变量拼接的使用方法

    许多用户在使用紫金桥软件构建控制系统的同时也会与关系型数据库进行数据交互,在使用关系库的过程中必然会用到大量的SQL脚本,而SQL脚本中的where语句常常需要由变量组成,那么如何在
    发表于 10-12 14:24 3次下载
    紫金桥软件<b class='flag-5'>SQL</b>语句变量拼接的使用<b class='flag-5'>方法</b>

    机器学习如何数据库

    。吞吐量方面,Postgres 在 OtterTune 的配置比 DBA 和脚本要高 12%,比 RDS 要高 32%。
    发表于 11-07 13:50 1137次阅读
    机器学习如何<b class='flag-5'>调</b><b class='flag-5'>优</b>数据库

    如何对电机进行的好处是什么?

    如何自动对电机进行
    的头像 发表于 08-22 00:03 3130次阅读

    Linux用电功耗的笔记分享

    整理些Linux用电功耗的笔记,分享给小伙伴,关于用电个人觉得
    的头像 发表于 06-23 15:19 4092次阅读

    起聊聊系统上线时SQL脚本的9大坑

    即使之前在测试环境,已经执行过SQL脚本了。但是有时候,在系统上线时,在生产环境执行相同的SQL脚本,还是有可能出现些问题。
    的头像 发表于 03-07 09:08 449次阅读

    系统上线时SQL脚本的9大坑

    有些小公司,SQL脚本是开发自己执行的,有很大的风险。 有些大厂,有专业的DBA把关,但DBA也不是万能的,还是有可能会让些错误的SQL脚本
    的头像 发表于 03-24 14:25 486次阅读

    系统上线时SQL脚本的9大坑

    即使之前在测试环境,已经执行过SQL脚本了。但是有时候,在系统上线时,在生产环境执行相同的SQL脚本,还是有可能出现些问题。 有些小
    的头像 发表于 04-24 17:10 554次阅读

    javajvm有几种方法

    JVM是Java应用程序性能优化过程中的重要步骤,它通过针对JVM进行优化来提高应用程序的性能和可靠性。JVM可以根据具体的场景和需求,采用不同的
    的头像 发表于 12-05 11:11 2110次阅读

    jvm主要是哪里

    ,栈内存存储方法调用和局部变量,非堆内存用于存储加载的类信息以及些静态变量等。 1.1 堆内存 堆内存是JVM中最主要的内存区域,常见的
    的头像 发表于 12-05 11:37 1558次阅读

    Oracle如何执行sql脚本文件

    Oracle是种关系型数据库管理系统,可用于存储、查询和管理大量的数据。在Oracle中,可以通过执行SQL脚本文件来次性地执行多个SQL
    的头像 发表于 12-06 10:51 6675次阅读

    鸿蒙开发实战:【性能组件】

    性能组件包含系统和应用框架,旨在为开发者提供套性能
    的头像 发表于 03-13 15:12 435次阅读
    鸿蒙开发实战:【性能<b class='flag-5'>调</b><b class='flag-5'>优</b>组件】

    大数据从业者必知必会的Hive SQL技巧

    不尽人意。本文针对Hive SQL的性能优化进行深入研究,提出了系列可行的方案,并给出了相应的优化案例和优化前后的SQL代码。通过合理
    的头像 发表于 09-24 13:30 238次阅读