多級部門查詢性能問題解決方案

查詢慢

目錄

項目吐槽

其實,涉及部門層級關係的問題在不少情形下都會遇到,特別是針對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 邏輯數據庫設計 - 單純的樹(遞歸關係數據)

相關文章
相關標籤/搜索