一、故事是这样开始的
在自己团队开发的一个内部工程中,有一个页面在切换业务线tab时,会出现如下图的报错:
其中undefined元素是页面渲染时需要的字段。
二、问题的追根溯源
当数据加载时间很长,在页面开始渲染时,还没有加载到数据就会导致这样的报错。所以问题在于数据加载超时。
在浏览器或者使用postman等工具去查看不同tab在调用接口时的耗时
有的tab是毫秒量级,有的tab是7秒左右的耗时。两个业务线的实现均一致,所以问题定位到mapper中查询语句。
在代码中加上查询前后的时间打印进行确认。
System.out.println("begin = " + System.currentTimeMillis());
bugPerMap = toolBugDataDao.getOverviewBugPer(appPlanDemand,productMap.get(key));
System.out.println("end = " + System.currentTimeMillis());
好了,是这儿的问题。
mapper中的查询语句是这样的:
<select id="getOverviewBugPer" parameterType="com.beibei.test.tdbank.appplandemand.AppPlanDemand" resultType="java.util.Map">
select (case when bugPer is null then '0%' else bugPer end) as bugPer,(case when androidBugPer is null then '0%'
else androidBugPer end) as androidBugPer ,(case when iosBugPer is null then '0%' else iosBugPer end) as
iosBugPer ,(case when h5BugPer is null then '0%' else h5BugPer end) as h5BugPer,(case when serverBugPer is null
then '0%' else serverBugPer end) as serverBugPer from(
select concat(cast(sum(bug_num)* 100/sum(case_num)as decimal(18,2)) ,'%') as 'bugPer',
concat(cast(sum(android_bug_num)* 100/sum(android_case_num)as decimal(18,2)) ,'%') as 'androidBugPer',
concat(cast(round(sum(ios_bug_num)* 100/sum(ios_case_num),4)as decimal(18,2)) ,'%') as
'iosBugPer',concat(cast(sum(H5_bug_num)* 100/sum(H5_case_num)as decimal(18,2)) ,'%') as 'h5BugPer',
concat(cast(sum(server_bug_num)* 100/sum(server_case_num)as decimal(18,2)) ,'%') as 'serverBugPer' from (
select * FROM `tool_bugdata` where
version in ( select demand_name from `appplan_demand` where id != 0
and demand_statue = 1
<if test="appPlanDemand.begin!=null and appPlanDemand.end!=null">
AND gmt_plan_test >= #{appPlanDemand.begin} AND gmt_plan_test <=#{appPlanDemand.end}
</if>
<if test="product != 'all'">
AND product = #{product}
</if>)
union all
select * FROM `tool_bugdata` where
version in (select type_version from app_schedule where app_version_id in (select app_version_id from
`appplan_demand` where id != 0 and demand_statue = 1
<if test="appPlanDemand.begin!=null and appPlanDemand.end!=null">
AND gmt_plan_test >= #{appPlanDemand.begin} AND gmt_plan_test <= #{appPlanDemand.end}
</if>
<if test="product != 'all'">
and product = #{product}
</if>
) )) as v) as a;
</select>
这样的设计虽然很牛逼,但是代码不好理解并不利于维护,不建议这样写哦!!! 术业有专攻,选择找DBA大佬帮忙分析并优化一下。
三、优化方案
其实也可以通过在前端页面加一个加载中状态优化,但这个遇到问题的我们应该有多种备选方案,所以和我一起探索一下sql的水吧
1、加一个索引
可以利用这个语句查看索引
show index from #表名#
在你的工具中就可以看到对应表的索引,例如navicat工具
2、优化查询语句
<select id="getOverviewBugPer" parameterType="com.beibei.test.tdbank.appplandemand.AppPlanDemand" resultType="java.util.Map">
select if (concat(cast(sum(bug_num) * 100 / sum(case_num) as decimal(18, 2)),'%') is null,'0%',concat(cast(sum(bug_num) * 100 / sum(case_num) as decimal(18, 2)),'%')) as 'bugPer',
if (concat(cast(sum(android_bug_num) * 100 / sum(android_case_num) as decimal(18, 2)),'%') is null, '0%',concat(cast(sum(android_bug_num) * 100 / sum(android_case_num) as decimal(18, 2)),'%')) as 'androidBugPer',
if (concat(cast(round(sum(ios_bug_num) * 100 / sum(ios_case_num), 4) as decimal(18, 2)),'%') is null,'0%',concat(cast(round(sum(ios_bug_num) * 100 / sum(ios_case_num), 4) as decimal(18, 2)),'%')) as 'iosBugPer',
if (concat(cast(sum(H5_bug_num) * 100 / sum(H5_case_num) as decimal(18, 2)),'%') is null,'0%',concat(cast(sum(H5_bug_num) * 100 / sum(H5_case_num) as decimal(18, 2)),'%')) as 'h5BugPer',
if (concat(cast(sum(server_bug_num) * 100 / sum(server_case_num) as decimal(18, 2)),'%') is null,'0%',concat(cast(sum(server_bug_num) * 100 / sum(server_case_num) as decimal(18, 2)),'%')) as 'serverBugPer'
from(
select bug_num,case_num,android_bug_num,android_case_num,ios_bug_num,ios_case_num,H5_bug_num,H5_case_num,server_bug_num,server_case_num FROM `tool_bugdata` t
join( select distinct demand_name from `appplan_demand` where id != 0 and demand_statue = 1
<if test="appPlanDemand.begin!=null and appPlanDemand.end!=null">
and gmt_plan_test >= #{appPlanDemand.begin} AND gmt_plan_test <= #{appPlanDemand.end}
</if>) t5 on t.version=t5.demand_name
<where>
<if test="product!='all' ">
and product=#{product}
</if>
</where>
union all select bug_num,case_num,android_bug_num,android_case_num,ios_bug_num,ios_case_num,H5_bug_num,H5_case_num,server_bug_num,server_case_num FROM `tool_bugdata` t1
join ( select distinct type_version from app_schedule t4
join (select distinct app_version_id from `appplan_demand` where id != 0 and demand_statue = 1
<if test="appPlanDemand.begin!=null and appPlanDemand.end!=null">
and gmt_plan_test >= #{appPlanDemand.begin} AND gmt_plan_test <= #{appPlanDemand.end}
</if> )t3 using(app_version_id) )t2 on t1.version=t2.type_version
<where>
<if test="product!='all' ">
and product=#{product}
</if>
</where>
)as v;
</select>
实现中用到了left join、union、distinct等操作符,了解这些操作符的实现就不难理解这个查询咯。可以参照https://www.w3school.com.cn/sql/sql_union.asp
优化后数据是这样的(特别秀~!!!)
原逻辑 | tab1:XXXms;tab2:7s秒 |
加索引后 | tab1:XXXms;tab2:XXXms量级 |
索引+查询优化 | tab1:XXms;tab2:XXms |
四、索引的知识点来了
1、什么是索引?
索引(Index)是帮助MySQL高效获取数据的数据结构,通俗来讲索引就好比书本的目录,加快数据库的查询速度。
PS:太简短,想知道原理:饶了我吧,本文不拓展了
2、什么时候需要索引?
一般来说,在WHERE和JOIN中出现的列需要建立索引,但也不完全如此,因为MySQL只对<,<=,=,>,>=,BETWEEN,IN,以及某些时候的LIKE才会使用索引
(说明:某些时候的LIKE才会使用索引:在以通配符%和_开头作查询时,MySQL不会使用索引。举例如下:
//下句会使用索引:
SELECT * FROM mytable WHERE username like'admin%'
//下句就不会使用索引:
SELECT * FROM mytable WHEREt Name like'%admin'
3、索引有几类?什么场景使用什么索引?
3.1 分类
3.2 什么场景使用什么索引?
看这个表的查询用法,哪种最多,就可以考虑通过索引优化哦。
例如:最常用的商品表item,一般都是
select * from item where id=3290509 //主键索引就可以
select * from item where product_id=439028 //唯一索引就可以
4、索引使用简单教程
假设有一个表testers
创建方式1.我们可以在表创建时创建索引
CREATE TABLE tester(
ID INT NOT NULL,
username VARCHAR(16) NOT NULL,
city VARCHAR(50) NOT NULL,
age INT NOT NULL );
CREATE TABLE tester(
ID INT NOT NULL,
username VARCHAR(16) NOT NULL,
city VARCHAR(50) NOT NULL,
age INT NOT NULL ,
//1、主键索引,一般是在创建表时会同步创建主键索引 PRIMARY KEY(ID))
//or 2、唯一索引:索引列的值必须唯一,但允许有空值 UNIQUE [索引名] (索引列(length))
//例如:UNIQUE indexForUsername (username(10))//这里为啥是10?见下方的短索引
//or 3、普通索引:无限制条件 INDEX [索引名] (索引列(length)) );
创建方式2.创建索引
//唯一索引
CREATE UNIQUE INDEX indexName ON mytable(username(length))
//普通索引 CREATE INDEX indexName ON mytable(username(length))
//组合索引 CREATE INDEX [组合索引名](组合索引中涉及的列)
//组合索引示例:CREATE INDEX name_city_age(name(10),city,age);
//length要依赖列的类型,char varchar类型可以不设置;列的顺序很重要
//建立这样的组合索引,其实是相当于分别建立了下面三组组合索引:
//usernname,city,age
//usernname,city
//usernname
//MySQL组合索引“最左前缀”,简单的理解就是只从最左面的开始组合。并不是只要包含这三列的查询都会用到该组合索引
//例如:SELECT * FROM mytable WHREE username="admin" AND city="郑州" ,可以用到索引
//例如:SELECT * FROM mytable WHREE age=20 AND city="郑州" ,用不到索引
创建方式3.修改表结构
ALTER 表名 ADD UNIQUE [索引名] ON (列名(length)) //不同类型的索引同理类推
5、不是所有查询都会用到索引
◆索引不会包含有NULL值的列
只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。
◆使用短索引
对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的列,如果在前10个或20个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。
◆索引列排序
MySQL查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。
◆like语句操作
一般情况下不鼓励使用like操作
◆不要在列上进行运算
//在列上进行运算:
select * from users where YEAR(date)<2007;
//将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成 select * from users where date<‘2007-01-01’;
6、优缺点
优点:查询性能提升,不用遍历
缺点:
- 降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。
- 建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快。