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

    文章

    2904

    浏览量

    102994
  • SQL
    SQL
    +关注

    关注

    1

    文章

    738

    浏览量

    43462
  • 分析器
    +关注

    关注

    0

    文章

    90

    浏览量

    12410
  • MYSQL数据库
    +关注

    关注

    0

    文章

    95

    浏览量

    9277
收藏 人收藏

    评论

    相关推荐

    SQL语句生成器

    SQL语句生成器SQL数据库语句生成及分析器(支持表结构、索引、所有记录到SQL脚本)可用于数据数的备份和恢复!功能不用多说,试试就知道了
    发表于 06-12 16:15

    MaxCompute SQL原理解析及性能

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

    HBase性能概述

    HBase性能
    发表于 07-03 11:35

    flume读取文件延迟说明

    flume读取文件延迟
    发表于 07-17 16:38

    讨论一下如何创建、下载和运行脚本

    让我们简要讨论一下如何创建、下载和运行脚本
    发表于 05-11 06:31

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

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

    请问一下RV1109 buildroot是怎样增加PWM测试脚本

    请问一下RV1109 buildroot是怎样增加PWM测试脚本的?
    发表于 02-21 07:19

    基于全HDD aarch64服务器的Ceph性能实践总结

    如ISA-L也都在arm平台上进行了优化。- 对于SPDK,也是从软件层面在arm平台上进行了优化。4.3 操作系统从Linux内核来Ceph性能,这是
    发表于 07-05 14:26

    基于RT-Thread的功耗与测量实战

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

    KeenTune的算法之心——KeenOpt 算法框架 | 龙蜥技术

    文/KeenTune SIGKeenTune(轻豚)是款 AI 算法与专家知识库双轮驱动的操作系统全栈式智能优化产品,为主流的操作系统提供轻量化、跨平台的键式性能,让应用在智能
    发表于 10-28 10:36

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

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

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

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

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

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

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

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

    Oracle如何执行sql脚本文件

    Oracle是一种关系型数据库管理系统,可用于存储、查询和管理大量的数据。在Oracle中,可以通过执行SQL脚本文件来一次性地执行多个SQL语句或者批量处理数据。在下面的文章中,我将详细介绍
    的头像 发表于 12-06 10:51 2499次阅读