一、故事是这样开始的

在自己团队开发的一个内部工程中,有一个页面在切换业务线tab时,会出现如下图的报错:

查看一张表的索引_数据库

其中undefined元素是页面渲染时需要的字段。

二、问题的追根溯源

当数据加载时间很长,在页面开始渲染时,还没有加载到数据就会导致这样的报错。所以问题在于数据加载超时。
在浏览器或者使用postman等工具去查看不同tab在调用接口时的耗时

查看一张表的索引_sql_02

有的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工具

查看一张表的索引_查看一张表的索引_03

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 分类

查看一张表的索引_sql_04

 

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、优缺点

优点:查询性能提升,不用遍历
缺点:

  1. 降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。
  2. 建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快。