多级部门查询性能问题解决方案

查询慢

目录

项目吐槽

其实,涉及部门层级关系的问题在不少情形下都会遇到,特别是针对toB的应用开发场景。
但奇葩的是,在咱们的项目里头,项目经理在前期需求调研时,预估的用户部门最大数为1k,因而相关的开发同事就按照最大数1k*4=4k的目标进行了设计实现,而真正交付到用户生产环境时同步的数据是1w。
What?也就是说,即便以前已经按照最大预估数进行了4倍数放大设计,可是如今是10倍。因而,各类问题接踵而至。
致使该问题出现的缘由主要有一下几点:html

  1. 需求调研不严谨,我估计所谓最大子部门数为1k也只是产品经理拍脑壳想出来的,并无真正与用户沟经过。
  2. 问题暴露得太晚,致使上线时才发现,加重了项目问题的严重性和影响了项目投产进度。
  3. 设计实现考虑不周,本来就是大数据分析项目,却使用了不恰当的查询方式(查询子部门数据时经过传递子部门id列表使用in查询),遇到问题了必须推翻以前的实现。

这是一次很是惨痛的项目教训,虽然你们都在埋怨需求作得太扯淡,可是本着“咱们是一个团队”的大和谐思想,还得见招拆招真正去解决问题。mysql

遇到的问题

1.mysql函数group_contact()返回值限制

部门数据是存放在mysql中的,结构为:sql

CREATE TABLE `organization` (
  `org_id` bigint(10) unsigned NOT NULL,
  `org_name` varchar(128) DEFAULT NULL,
  `parent_org_id` bigint(10) DEFAULT NULL,
  `status` varchar(4) DEFAULT NULL,
  `sync_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `update_time` timestamp NULL DEFAULT NULL,
  `cutecomm_dep_id` varchar(50) DEFAULT NULL,
  `company_id` bigint(20) DEFAULT NULL,
  `org_code` varchar(30) DEFAULT NULL COMMENT '机构代码',
  `parent_org_code` varchar(30) DEFAULT NULL COMMENT '上级机构代码',
  PRIMARY KEY (`org_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

使用自定义函数查询指定部门下的子部门列表:数据库

DELIMITER $$ 
CREATE FUNCTION `getChildList` (parentId BIGINT, companyid BIGINT) RETURNS VARCHAR (10000) CHARSET utf8
DETERMINISTIC
BEGIN
    DECLARE
        parent VARCHAR (10000);
    DECLARE
        child VARCHAR (10000);
    SET parent = '$';
    SET child = cast(parentId as CHAR) ;

    WHILE child IS NOT NULL DO
        SET parent = concat(parent, ',', child);
        SELECT group_concat(org_id) INTO child FROM organization
         WHERE company_id = companyid and FIND_IN_SET(parent_org_id, child) > 0;
    END WHILE;
    RETURN parent;
END $$

查询全部部门结构目录树:缓存

SELECT * FROM organization WHERE FIND_IN_SET(org_id, getChildList(0, 100000)) and company_id = 100000 ORDER BY parent_org_id, org_id

以前都是使用1k的数据进行验证的,因为group_contact函数返回值默认大小为1204字节,并无遇到这个限制。
如今数据量是1w,是测试数据的10倍,因而首先遇到的第一个问题就是mysql函数的限制。致使某些实际在MySQL中存在的数据,经过上述SQL语句却查询不出来。
因而修改该变量值为:max_group_contact=1024000。
同时修改自定义函数中的变量值类型从VARCHAR修改成LongText(在函数中先查询子部门id列表,使用逗号分隔,当数据量很大时varchar类型存放不了),以下:数据库设计

DELIMITER $$ 
CREATE FUNCTION `getChildList` (parentId BIGINT, companyid BIGINT) RETURNS LongText CHARSET utf8
DETERMINISTIC
BEGIN
    DECLARE
        parent LongText;
    DECLARE
        child LongText;
    SET parent = '$';
    SET child = cast(parentId as CHAR) ;

    WHILE child IS NOT NULL DO
        SET parent = concat(parent, ',', child);
        SELECT group_concat(org_id) INTO child FROM organization
         WHERE company_id = companyid and FIND_IN_SET(parent_org_id, child) > 0;
    END WHILE;
    RETURN parent;
END $$

OK,到这里就能够正常查出数据了。可是!效率低的吓人:25s!
实际上,对于查询部门目录树这个需求而言,因为全表数据才1w,直接全表查询效率都比使用上述函数递归方式查询高太多了。
改用全表查询,响应时间为:1s !!! 不知道以前写这部分代码得同事连这个最基本的事实都没注意到。函数

2. Impala使用in语句存在限制

于此同时,由于有另一部分数据是存放在hive表中,经过impala进行查询。奇怪的是居然使用是in查询,查询的in条件就是部门id,当查询根部门下的全部子部门数据时,须要传递1w+个部门id到in中。性能

select max(totalnum) from (select count(distinct did) as totalnum, dt from mocdb_day_activity_device_all t where t.companyid = ? AND t.dt between ? AND ?  AND t.groupid in (?,?,?,...,?) group by dt ) a

中间的省略号表示有不少参数,至于多少个就看想象力了!
观察一下,这么一个SQL语句出现眼前,光长度就够吓人的了,就别奢望着它的查询性能了,用这样一个语句进行查询就是灾难的开始。
并且impala最大只能支持到9999个in参数,因而到这里。基本上你们就炸开锅了,完全要崩溃的节奏。怎么办?项目存在这么大的缺陷,同时还要及时交付,这不是在开国际玩笑嘛。
因为impala对于当前的查询方式存在限制,因此要绕开限制(其实即使impala不存在限制,想一想在一个in中传递1w+部门id进行匹配查询,性能也不可能好到那里去)。测试

解决方案

优化MySQL函数递归调用方案

既然是数据分析项目,不须要处理事务,为了达到必定的查询性能,应该进行适当的冗余设计。
针对使用MySQL函数进行递归查询不合理的问题,直接修改成使用全表查询的方式解决,再结合缓存解决性能问题。大数据

将Impala的in查询转换为等值查询

针对在Impala中使用in查询不合理的问题和限制,因而从新作以下宽表方案设计:
为了避免在Impala中使用in查询,须要作冗余字段设计,针对多级部门这个场景,在每条记录中都记录员工或设备的直接部门与其全部祖先部门的信息,这样能够直接在Impala中将in查询改成“=”等值查询。

首先,自定义函数查找全部祖先部门id列表(以前是查找全部子部门id列表,数据量级相差很大),用逗号分隔:

drop function `getOrgParentStr`;
delimiter $$
create function getOrgParentStr(orgId bigint) returns varchar(1024) 
begin
    declare tmpId bigint default 0;
    declare parentStr varchar(1024) default '';
    declare tmpParent bigint default -1;
    set tmpId = orgId;
    WHILE tmpId > 0 do
        set tmpParent = (select parent from orgnization where id = tmpId);
        set tmpId = tmpParent;
        IF ISNULL(parentStr) || LENGTH(trim(parentStr)) < 1 THEN 
            set parentStr = CAST(tmpParent as CHAR(50));
        ELSE 
            set parentStr = CONCAT_WS(',',parentStr,CAST(tmpParent as CHAR(50)));
        END IF;
    end WHILE;
    return parentStr;
end $$

其次,修改部门表设计,添加冗余字段:

CREATE TABLE `organization` (
  `org_id` bigint(10) unsigned NOT NULL,
  `org_name` varchar(128) DEFAULT NULL,
  `parent_org_id` bigint(10) DEFAULT NULL,
  `status` varchar(4) DEFAULT NULL,
  `sync_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `update_time` timestamp NULL DEFAULT NULL,
  `cutecomm_dep_id` varchar(50) DEFAULT NULL,
  `company_id` bigint(20) DEFAULT NULL,
  `org_code` varchar(30) DEFAULT NULL COMMENT '机构代码',
  `parent_org_code` varchar(30) DEFAULT NULL COMMENT '上级机构代码',
  `depth` int default 0 COMMENT '部门所在层级深度值',
  `level0` bigint(20) DEFAULT -1,
  `level1` bigint(20) DEFAULT -1,
  `level2` bigint(20) DEFAULT -1,
  `level3` bigint(20) DEFAULT -1,
  `level4` bigint(20) DEFAULT -1,
  `level5` bigint(20) DEFAULT -1,
  `level6` bigint(20) DEFAULT -1,
  `level7` bigint(20) DEFAULT -1,
  `level8` bigint(20) DEFAULT -1,
  `level9` bigint(20) DEFAULT -1,
  PRIMARY KEY (`org_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

在对部门表作冗余字段设计时须要遵照以下约定:

(1) 最多支持10级部门,新增冗余字段存储父部门信息,如: level0,level1,...,level9;

(2) 冗余字段存储的父部门含义规约,level0存储最顶级根部门id,level1存储次级部门id,以此类推,level9存储当前部门的直接父部门id

(3) 新增部门层级深度字段depth,存储当前部门所在层级深度值(从0开始),例如:

-x
     -xx
      -xxx
       -xxxx

在上述部门树结构中,“xxxx”部门的层级深度值为:3。

(4) 导入数据时对于父部门信息不正确的记录丢弃。

通过验证,数据量在2kw时,使用原来impala的in查询方式响应时间在15s,使用宽表方式响应时间为4s;当数据量在5kw时,impala的in查询响应时间为50s,宽表方式响应时间为5s。

总结

  1. 在数据预估的时候要根据数据产生的速度进行适当的规划,且必定要通过严格的测试验证。
  2. 在大数据分析项目中,为了知足必定的查询性能,适当进行宽表设计是很是有必要的。

【参考】
http://www.cnblogs.com/kissdodog/p/3297894.html 逻辑数据库设计 - 单纯的树(递归关系数据)

相关文章
相关标签/搜索