背景

查询字段其实比较多,我选择聚焦在瓶颈点上,让我们开始吧

功能背景简介:

​ 我们在一个进入数据中心的入口设置了一台记录人员进出的机器,由保卫员操作记录人员进出(通过换取通关卡的方式,在换取通关卡时,记录进入时间,在归还通关卡时,记录离开时间),业务方需要知道某段时间内在数据中心内的人数、次数 、具体进入的人、进入的人的进入时长等。

按照功能背景,我们建立了一张表

CREATE TABLE "USER"."ENTER_HISTORY"
{
    "ID" NUMBER,
    "LOGIN_TIME" VARCHAR2(128),
    "LOGOUT_TIME" VARCHAR2(128),
    "USER_ID" VARCHAR2(128),
    PRIMARY KEY ("ID")
};
COMMENT ON COLUMN "USER"."ENTER_HISTORY"."ID" IS "id";
COMMENT ON COLUMN "USER"."ENTER_HISTORY"."LOGIN_TIME" IS "进入时间";
COMMENT ON COLUMN "USER"."ENTER_HISTORY"."LOGOUT_TIME" IS "离开时间";
COMMENT ON COLUMN "USER"."ENTER_HISTORY"."USER_ID" IS "用户标识";

我们以2021-12-01 00:00:00到2022-01-01 00:00:00的时间为例,统计在此时间在数据中心的人数(进出、停留的人数)

SELECT
    SUM(DISTINCT EH.USER_ID)
FROM USER.ENTER_HISTORY EH
WHERE
    NOT (
        EH.LOGOUT_TIME<'2021-12-01 00:00:00'
        OR
        EH.LOGIN_TIME>'2022-01-01 00:00:00'
    )

SQL语句写成这样的原因如下图,排除红色的两块区域,剩下的就都是符合条件的记录


业务问题1:

​ 某一天,业务反馈,保卫员没有将每张卡都执行归还操作,即没有将离开时间给记录,导致业务在查询人数时一直有些记录干扰,业务提出,将没有离开时间的,记为(进入时间+1天/或当前时间--视哪个时间比较近),考虑到此数据是同步机器数据,如果在我们这里直接修改,会导致数据不一致,因此考虑在查询时下功夫,考虑到查询逻辑,在离开时间不存在时,将离开时间统一为(进入时间+1天)进行计算即可

改写的SQL语句如下

SELECT
    SUM(DISTINCT EH.USER_ID)
FROM USER.ENTER_HISTORY EH
WHERE
    NOT (
        NVL(EH.LOGOUT_TIME,TO_CHAR(
            TO_DATE(EH.LOGIN_TIME,'yyyy-mm-dd hh24:mi:ss')+1
            ,'yyyy-mm-dd hh24:mi:ss')))
        <'2021-12-01 00:00:00')
        OR
        EH.LOGIN_TIME>'2022-01-01 00:00:00'
    )

业务问题2

改写语句后,起初因为功能刚上线没有多少数据,因此查询效率被忽略

过了几个月,业务反馈查询时等待时间过长,叫我们看看能不能弄快点

于是我又看起了这条语句,未建立索引


步骤1:建索引

按照此语句进行查询,在DBEAVER上看执行计划发现并没有走索引,因为我们还没创建索引

以我们使用到的SQL,涉及到两个查询字段,且是同一级,我们可以只用一个字段建立索引,或者是建立两个字段的联合索引,这里采取建立联合索引,建立索引语句如下

CREATE INDEX IDX_EH_LOGINOUT_TIME ON USER.ENTER_HISTORY(LOGIN_TIME,LOGOUT_TIME)

建立索引后,在DBEAVER上查看执行计划,

发现还是没走索引

此时开始分析没走索引的原因

1、查询字段使用了函数

2、优化器觉得使用全表比使用索引更快


步骤2:改写SQL

这里有个关于ORACLE的NOT语句的一个小知识

我们的条件

   NOT (
        NVL(EH.LOGOUT_TIME,TO_CHAR(
            TO_DATE(EH.LOGIN_TIME,'yyyy-mm-dd hh24:mi:ss')+1
            ,'yyyy-mm-dd hh24:mi:ss')))
        <'2021-12-01 00:00:00')
        OR
        EH.LOGIN_TIME>'2022-01-01 00:00:00'
    )

实际上会被改写成

        NVL(EH.LOGOUT_TIME,TO_CHAR(
            TO_DATE(EH.LOGIN_TIME,'yyyy-mm-dd hh24:mi:ss')+1
            ,'yyyy-mm-dd hh24:mi:ss')))
        >='2021-12-01 00:00:00')
        AND
        EH.LOGIN_TIME<='2022-01-01 00:00:00'

所以可以排除NOT的影响

对于查询列使用到NVL和TO_DATE,我采取判空和将计算改到数值侧的方式,更改后如下

        (
        (EH.LOGOUT_TIME IS NOT NULL
            AND
            EH.LOGOUT_TIME >= '2021-12-01 00:00:00')
        OR
        (EH.LOGOUT_TIME IS NULL
            AND
            EH.LOGIN_TIME >= TO_DATE('2021-12-01 00:00:00','yyyy-mm-dd hh24:mi:ss')+1
            ,'yyyy-mm-dd hh24:mi:ss')))
        )
        AND
        EH.LOGIN_TIME<='2022-01-01 00:00:00'

改写后再次查看执行计划,发现还是不走索引

步骤3:开启ORACLE强制索引

我们判断可能是因为建立的字段是文本值,ORACLE在建立执行计划的时候,优化器不选择此索引。此时由于我们觉得走索引效率可能会更高,因此我们强制让SQL走索引,语句如下

SELECT /*+index(EH IDX_EH_LOGINOUT_TIME)*/
    SUM(DISTINCT EH.USER_ID)
FROM USER.ENTER_HISTORY EH
WHERE
     (EH.LOGOUT_TIME IS NOT NULL
               AND
               EH.LOGOUT_TIME >= '2021-12-01 00:00:00')
     OR
     (EH.LOGOUT_TIME IS NULL
            AND
            EH.LOGIN_TIME >= TO_DATE('2021-12-01 00:00:00','yyyy-mm-dd hh24:mi:ss')+1
            ,'yyyy-mm-dd hh24:mi:ss')))
     )
     AND
     EH.LOGIN_TIME<='2022-01-01 00:00:00'

此时查看执行计划,发现已经走了索引

其实我们最好是在建立字段的时候考虑字段的功能,设置合适的格式,例如在这里用字符串存储时间是不合适的,但是由于我在做这个功能的时候这个表就已经在了,所以没去做改动。

步骤4:尝试开启ORACLE并行模式

但是,由于实际的查询范围,在一些查询时间跨度比较大的时候,实际上相当于全表查询,索引的作用并不大,例如这个功能是从6月份上线,如果查询6月份到现在的数据,就相当于全表查询。团队的DBA给出建议,说开启ORACLE的并行模式。但我在实际使用后比较性能发现差别不大。修改后SQL如下

SELECT /*+parallel(EH,4)*/
    SUM(DISTINCT EH.USER_ID)
FROM USER.ENTER_HISTORY EH
WHERE
     (EH.LOGOUT_TIME IS NOT NULL
               AND
               EH.LOGOUT_TIME >= '2021-12-01 00:00:00')
     OR
     (EH.LOGOUT_TIME IS NULL
            AND
            EH.LOGIN_TIME >= TO_DATE('2021-12-01 00:00:00','yyyy-mm-dd hh24:mi:ss')+1
            ,'yyyy-mm-dd hh24:mi:ss')))
     )
     AND
     EH.LOGIN_TIME<='2022-01-01 00:00:00'

总结

实际上在真实环境上,是直接查出所有数据,在程序内进行统计,以下是改造前后的大致性能数据

性能比较,表内总数5w3,查询七月份到明年一月份的时间,约3.8w条数据,

优化前:耗时约4s

优化后:耗时约4s

总的来说,这是一次不成功的优化案例,但其中的思路、做法和做法希望值得大家参考


如果你对本文章有建议或疑问,欢迎在下面进行留言,一起交流

我是Vapire,一个普通的全栈开发。

以开发的角度看问题,用开发的方式学知识。

03-05 23:37