本周SQL优化实战分享

Posted 我从二院来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了本周SQL优化实战分享相关的知识,希望对你有一定的参考价值。

分享一下本周SQL优化的两个场景。
如果能对读者有一定的启发,共同探讨,不胜荣幸。

版本信息:mysql,5.7.19
引擎: innodb


场景1


我们有一张常口表,里面的数据由各种数据源合并而来,所以人员可能有多个手机号其中还包括座机号。这点在这篇文章里也分享过。https://juejin.cn/post/7234355976458518586
现在人员详情页面需展示同手机号的人员列表,同手机号是包含,而非等同关系。

在人员列表里手机号页面有做展示,那么点击跳转人员详情的时候,是可以把手机号通过URL带过来的,但前端说参数过多,不好控制,所以只传递了人员ID参数。

所以后端查询的时候先得通过主键ID把手机号查出来。之所以不一次性通过join带出手机号再关联同手机号人员,是关联与被关联人员手机号都可能存在多个。


select * 还是select 指定字段


原通过主键查询手机号的SQL,是直接用的mybatis生成器自动生成的SQL。

<select id="selectPhoneByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
    phone
    from t_person_info
    where ID = #id,jdbcType=VARCHAR
  </select>

Base_Column_List可想而知是全部字段,类似于select *,这本身没什么,但其中有一部份字段长度在几百,全部加起来也算是个大字段,全部提取对效率还是有一定的影响,所以改为select phone 查询手机一个字段。

select
    phone
    from t_person_info
    where ID = #id,jdbcType=VARCHAR

更追求极致一点,可以添加一个idphone的覆盖索引,避免回表。

这一点的优化相对比较鸡肋,都在1-2ms之间看不出明显差别,但把limit放大的时候,还是能看出差距。

表数据70万左右。

select * form table limit 10000

select phone form table limit 10000

174ms vs 7ms

确实是聊胜于无。
但是到底是select * 还是select 指定字段,确实还是存在着一些争议。

一般情况下,表字段少,且不存在大字段,用select * 确实能减少许多麻烦,加减字段不用改sql,多个查询子功能可以共用等。
而且,页面查询多是分页,不太可能一下子查询10000条这种情况。

占用内存,不必要的IO,增加网络负担,拒绝覆盖索引,确实也是select *的问题。

我觉得需要根据具体情况,自行判断,没必要太过教条。


全文检索


拿到手机号以后,根据手机号去查询关联人员。
因为是包含关系,所以同事一开始用的是like模糊匹配。

 select p.id, p.id as pid,p.name,p.idcard,p.phone,count( w.EVENT_NO ) AS count 
      from t_person_info p 
      left join t_other w on w.pid = p.ID
        where
            <foreach collection="phones" item="phone" separator="or" open="(" close=")">
                p.phone like concat("%",#phone,"%")
            </foreach>
         and p.id != #id
        group by p.id

这里的!=有可能会导致索引失效,这时候可以在sql去掉,然后在代码中过滤掉当前人员。

因为where条件中有 p.id != #id,执行计划倒是从从ALL上升到了range。 耗时1.5秒。

将phone加上全文索引。 where 条件改为

match(p.phone) against (#phones IN boolean MODE) and p.id != #id

每个手机号需要全匹配,所以这里使用布尔模式,
因为手机号有多个,需要做到or,
又因为涉及到座机号,其中带的-可能会被mysql识别为逻辑运算符。

具体参照我写的这篇文章 https://juejin.cn/post/7234355976458518586


布尔模式的逻辑运算符


  1. +
    select * from t_user where match(phone) AGAINST(\'a +b\' in boolean mode)
    其中 + 会被识别成逻辑运算符,而不是将a +b作为一个整体,以下同理。
    \'a +b\' 指\'a\'和\'b\'必须同时出现才满足搜索条件。
  2. -
    select * from t_user where match(phone) AGAINST(\'0797 -12345\' in boolean mode)
    0797 -123450797必须包含,但不包含12345才能满足搜索条件。
    以下查询排除了包含0797-12345的记录。

    注意-前后空格 0797 -12345才表示包含0797 同时不包含12345.
    0797-12345等于0797 - 12345,它并不等于0797 -12345
    有图为证:

  3. > <
    提高/降低该条匹配数据的权重值。不管使用>还是 <,其权重值均大于没使用其中任何一个的。
    select * from t_user where match(phone) AGAINST(\'0797(>94649 <12345)\' in boolean mode)
    表示匹配0797,同时包含94649的列往前排,包含12345的往后排
    select * from t_user where match(phone) AGAINST(\'a > b\' in NATURAL LANGUAGE mode)
  4. ()
    相当于表达式分组,参考上一个例子。
  5. *
    通配符,只能在字符串后面使用
  6. "
    完全匹配,被双引号包起来的单词必须整个被匹配。
    select * from t_user where match(phone) AGAINST(\'"0797-1789"\' in boolean mode)
    "0797-1789"中不可再分。其它包含0797-1234等记录就不再匹配。
  7. 空格表示 or

这里使用6,7来解决上述的两种问题。
如下SQL,与以下4个手机号其中一个全区配的人员都将被筛选出来。

#phone参数应为"135****6" "136****9" "1387****2" "0791-123"格式 。

耗时从1.5秒降到了2毫秒。


场景2

还是常口表,列表查询。


排序


每个用户呢会关联一些事件,无需理会什么是事件,反正这张表中的每条记录与事件表形成一对多的关联关系。
事件实时进入。然后再用户列表展示的时候需要根据关联的事件数来进行排序。

实时join关联事件表,耗时4.9秒。
sql执行计划 extra为 Using temporary; Using filesort 产生了临时表和IO文件排序。当然快不起来。

这还是在没有查询条件,以及没有深度分页的情况下。

那么很明显,需要在用户表建一个冗余字段,保存用户所关联的事件数,再对这个字段建立索引。

但这会牺牲一定的实时性。
以及需要定时任务去统计用户的关联事件数。

然后需要跟产品沟通,因为我们的产品是2B的,还需要跟客户进行沟通。

结合我们的业务场景,经过我们的努力沟通,客户认为牺牲适当的实时性,换来页面的响应效率,是值得的。

然后耗时降到了3毫秒。

一旦 where having order by 里的字段是通过max,min,count等计算出来的虚拟字段,那么肯定会产生 Using temporary; Using filesort 临时表和IO文件排序。
要想办法消灭,不管从业务还是技术上。

适当的建立冗余字段,或者宽表。

但阿里巴巴java开发手册,禁止3张表以上的关联,毕竟只是比较理想的状态。

幸福的公司都是 相似 的;不幸的公司我看也有相似不幸。
不外乎难搞的产品,多变的客户,睿(s)智(13)的老板。


深度分页


上面小节同样的sql,首页查询只需耗时2ms,但是到了700000以后,耗时达到了2.6秒。

这就是著名的mysql深度分页的问题。
通过执行计划,可以明显的看出,mysql会将前 700015条数据取出来,然后丢掉前700000条,只取后15条数据。
前面读取的700000条数据是不必要耗时操作。

解决深度分页的方式有几种。 看具体情况,没有通用的办法。


利用覆盖索引


或者叫利用不回表。
这里为了便利,用主键索引id来演示,innodb下,主键索引为聚簇索引,本身就是回表啦,相当于普通索引省掉了回表操作。

如此查询只需200毫秒左右。

但是,这里不合适把需要展示的字段全部建成一个覆盖索引。


利用覆盖索引延迟关联


先通过覆盖索引把id拿到,再把这15条数据去关联一次拿到其它字段不就好了吗?

select p.id ,p.name,p.idcard,p.phone
from t_person_info p
inner join (select id from t_person_info order by EVENTCOUNT desc limit 700000,15) p2 on p.id = p2.id

如此同样只需要200毫秒左右。


其它方式


其它方式,通过记录上次的位置,通过子查询,都只适用于id为自增主键的情况。

不适用我的这个业务场景。

类似于 这样的SQL

select id ,name,idcard,phone,EVENTCOUNT from t_person_info where id <=(select id from t_person_info order by EVENTCOUNT limit 700000, 1) limit 15;

由于历史友商等原因,我们的数据ID有部份是UUID,它是不连续的,且人员关联事件数EVENTCOUNT也不连续,大量的人员集中在某一个数量上,这都使得此种方式不可取。


分页插件


在做列表展示时肯定需要分页,分页就需要查询总数。
分页插件pagehelper默认会生成一个查询总数的方法。

假如mapper查询方法为selectList(),那么查询总数的方法名为selectList_COUNT()。
对应的SQL为SELECT count(0) FROM 原sql

在一些比较比较简单的SQL的时候,分页的SQL还是会进行重写,比较去掉多余的select字段,不必要的排序等。

但当SQL比较复杂的时候,那就是直接在原SQL上包一层select count(0)。

这个时候我们就可以自已去实现这个selectList_COUNT()这个方法,让它执行效率更高的自定义SQL.


完。

那些好用数据库连接池JVM调优实战开源项目分享


本周是2021年的第15周,看看本周代码实验室都做了哪些实验,本周工作比较忙,分享的干货不是特别多,主要讲了两个好用的数据库连接池、PS+PO的JVM调优实战入门,还有带来一个后端开发脚手架的项目分享,马上去片~


那些好用数据库连接池、JVM调优实战、开源项目分享


文末有福利分享噢~

那些好用的数据库连接池

数据库连接池协同原理:


那些好用数据库连接池、JVM调优实战、开源项目分享


连接池基本的思想是在系统初始化的时候,将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象。使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用。而连接的建立、断开都由连接池自身来管理。同时,还可以通过设置连接池的参数来控制连接池中的初始连接数、连接的上下限数以及每个连接的最大使用次数、最大空闲时间等等。也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。


Java数据库连接池有哪些?


在Java中开源的数据库连接池有以下几种 :


C3P0:是一个开放源代码的JDBC连接池,它在lib目录中与Hibernate [2] 一起发布,包括了实现jdbc3和jdbc2扩展规范说明的Connection 和Statement 池的DataSources 对象。


Proxool:是一个Java SQL Driver驱动程序,提供了对选择的其它类型的驱动程序的连接池封装。可以非常简单的移植到现存的代码中,完全可配置,快速、成熟、健壮。可以透明地为现存的JDBC驱动程序增加连接池功能。


DBCP:DBCP是一个依赖Jakartacommons-pool对象池机制的数据库连接池。DBCP可以直接的在应用程序中使用。


DDConnectionBroker:是一个简单、轻量级的数据库连接池。


DBPool:是一个高效、易配置的数据库连接池。它除了支持连接池应有的功能之外,还包括了一个对象池,使用户能够开发一个满足自己需求的数据库连接池。


XAPool:是一个XA数据库连接池。它实现了javax.sql.XADataSource并提供了连接池工具。


Primrose:是一个Java开发的数据库连接池。当前支持的容器包括Tomcat4&5、Resin3与JBoss3。它同样也有一个独立的版本,可以在应用程序中使用而不必运行在容器中。Primrose通过一个WEB接口来控制SQL处理的追踪、配置,以及动态池管理。在重负荷的情况下可进行连接请求队列处理。


SmartPool:是一个连接池组件,它模仿应用服务器对象池的特性。SmartPool能够解决一些临界问题如连接泄漏(connection leaks)、连接阻塞、打开的JDBC对象(如Statements、PreparedStatements)等。SmartPool的特性包括:

  • 支持多个pool

  • 自动关闭相关联的JDBC对象

  • 在所设定time-outs之后察觉连接泄漏

  • 追踪连接使用情况

  • 强制启用最近最少用到的连接

  • 把SmartPool“包装”成现存的一个pool

MiniConnectionPoolManager:是一个轻量级JDBC数据库连接池。它只需要Java1.5(或更高)并且没有依赖第三方包。


BoneCP:是一个快速、开源的数据库连接池。帮用户管理数据连接,让应用程序能更快速地访问数据库。比C3P0/DBCP连接池速度快25倍。


Druid:Druid不仅是一个数据库连接池,还包含一个ProxyDriver、一系列内置的JDBC组件库、一个SQL Parser。

支持所有JDBC兼容的数据库,包括Oracle、MySql、Derby、Postgresql、SQL Server、H2等。


Druid针对Oracle和MySql做了特别优化,比如:

    • Oracle的PS Cache内存占用优化

    • MySql的ping检测优化

Druid提供了MySql、Oracle、Postgresql、SQL-92的SQL的完整支持,这是一个手写的高性能SQL Parser,支持Visitor模式,使得分析SQL的抽象语法树很方便。

简单SQL语句用时10微秒以内,复杂SQL用时30微秒。


通过Druid提供的SQL Parser可以在JDBC层拦截SQL做相应处理,比如说分库分表、审计等。Druid防御SQL注入攻击的WallFilter,就是通过Druid的SQL Parser分析语义实现的。


HikariCPHikariCP是由日本程序员开源的一个数据库连接池组件,代码非常轻量,并且速度非常的快。根据官方提供的数据,在i7,开启32个线程32个连接的情况下,进行随机数据库读写操作,HikariCP的速度是现在常用的C3P0数据库连接池的数百倍。在SpringBoot2.0中,官方也是推荐使用HikariCP。


本周代码实验室详细介绍了目前最主流使用的两款数据库连接池HikariCP和Druid,Hikari为何如此快?Druid为监控而生的连接池,欢迎围观

PS+PO调优实战

JVM性能优化到底在优化什么?


JVM性能优化就是指的是通过内存的合理分配,以及合理的设置参数,避免jvm频繁的发生垃圾回收,减少系统停顿时间等。


JVM在年轻代发生的是YGC使用的是复制回收算法,一般使用的垃圾收集器是ParNew+CMS,复制回收算法效率较高,并且大部分对象都会在年轻代被回收掉(GcRoot跟路径扫描判断对象是否可回收),耗时较短。


JVM在老年代会发生full Gc 使用的是标记整理法,垃圾回收时分为初始标记,并发标记,并发清理,内存整理等阶段,耗时较长一般能有YGC耗时的10倍左右,并且老年代的对象大部分都是长期存活的对象,执行过Full GC之后回收掉无用对象剩余的空间可能还不够年轻代将要晋升老年代的对象空间,那么可能就会频繁发生Full GC了。


所以总结下来JVM性能优化的本质就是:通过年轻代&老年代内存合理的分配,尽量让对象都在年轻代时被回收,尽量减少对象或者没有对象进入老年代,尽量做到长时间或者零Full GC。


PS+PO调优相关参数设置欢迎围观

开源项目分享

今天开始做第一期的开源项目分享,首先带来的第一款是TIMO管理系统,基于SpringBoot2.0 + Spring Data Jpa + Thymeleaf 的后台脚手架。


基本情况


基于SpringBoot2.0 + Spring Data Jpa + Thymeleaf + Shiro 开发的后台管理系统,采用分模块的方式便于开发和维护,支持前后台模块分别部署,目前支持的功能有:权限管理、部门管理、字典管理、日志记录、文件上传、代码生成等,为快速开发后台系统而生的脚手架!


技术选型

  • 后端技术:SpringBoot + Spring Data Jpa + Thymeleaf + Shiro + Jwt + EhCache

  • 前端技术:Layui + Jquery + zTree + Font-awesome


功能列表

  • 用户管理:用于管理后台系统的用户,可进行增删改查等操作。

  • 角色管理:分配权限的最小单元,通过角色给用户分配权限。

  • 菜单管理:用于配置系统菜单,同时也作为权限资源。

  • 部门管理:通过不同的部门来管理和区分用户。

  • 字典管理:对一些需要转换的数据进行统一管理,如:男、女等。

  • 行为日志:用于记录用户对系统的操作,同时监视系统运行时发生的错误。

  • 文件上传:内置了文件上传接口,方便开发者使用文件上传功能。

  • 代码生成:可以帮助开发者快速开发项目,减少不必要的重复操作,花更多精力注重业务实现。

  • 表单构建:通过拖拽的方式快速构建一个表单模块。

  • 数据接口:根据业务代码自动生成相关的api接口文档


项目文档



社区活跃度


Gitee源码Star数2.3k,Fork数1.1k,还是比较高的,Gitee指数综合得分是57,代码最近一次更新是28天前,从数据上看社区活跃程度还是比较高的。


实验室管理员评测


技术栈评分:技术栈使用的目前较为主流的开发技术,后端技术像SpringBoot、JPA、JWT认证技术等等,前端技术Layui等没有用目前企业中流行的Vue框架开发,稍微欠缺。综合打分60,技术栈评分达到及格线。


实现功能评分:基础的通用功能齐全,像用户、角色、菜单、字典、文件上传等等这些都是一些比较实用的功能,综合打分70。







以上就是本周代码实验室的所有干货汇总,感谢阅读~

我是Seven,一个不懈努力的程序猿,希望本文能对你有所裨益

往期文章


喜欢就点个"在看"呗^_^

以上是关于本周SQL优化实战分享的主要内容,如果未能解决你的问题,请参考以下文章

SQL调优实战-常见性能调优案例介绍(第二期)

Hive性能调优实战 分享

实战篇:MySQL优化系列--SQL优化实战

sql优化-项目实战

Flutter项目实战教程分享基础使用性能优化每日积累

实战Java秒杀系统方案优化 高性能高并发实战