目录
项目吐槽
其实,涉及部门层级关系的问题在很多情形下都会遇到,特别是针对toB的应用开发场景。
但奇葩的是,在我们的项目里头,项目经理在前期需求调研时,预估的用户部门最大数为1k,于是相关的开发同事就按照最大数1k*4=4k的目标进行了设计实现,而真正交付到用户生产环境时同步的数据是1w。
What?也就是说,即使之前已经按照最大预估数进行了4倍数放大设计,但是现在是10倍。于是,各种问题接踵而至。
导致该问题出现的原因主要有一下几点:
- 需求调研不严谨,我估计所谓最大子部门数为1k也只是产品经理拍脑袋想出来的,并没有真正与用户沟通过。
- 问题暴露得太晚,导致上线时才发现,加剧了项目问题的严重性和影响了项目投产进度。
- 设计实现考虑不周,原本就是大数据分析项目,却使用了不恰当的查询方式(查询子部门数据时通过传递子部门id列表使用in查询),遇到问题了必须推翻之前的实现。
这是一次非常惨痛的项目教训,虽然大家都在埋怨需求做得太扯淡,但是本着“我们是一个团队”的大和谐思想,还得见招拆招真正去解决问题。
遇到的问题
1.mysql函数group_contact()返回值限制
部门数据是存放在mysql中的,结构为:
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。
总结
- 在数据预估的时候要根据数据产生的速度进行适当的规划,且一定要经过严格的测试验证。
- 在大数据分析项目中,为了满足一定的查询性能,适当进行宽表设计是非常有必要的。
【参考】
http://www.cnblogs.com/kissdodog/p/3297894.html 逻辑数据库设计 - 单纯的树(递归关系数据)