位元組跳動資料質量動態探查及相關前端實現

語言: CN / TW / HK

更多技術交流、求職機會、試用福利,歡迎關注位元組跳動資料平臺微信公眾號,回覆【1】進入官方交流群

 

需求背景

 

資料探查上線之前,資料驗證都是通過寫 SQL 方式進行查詢的,從編寫 SQL,到解析執行出結果,不僅時間長,還會反覆消耗計算資源,探查上線後,只需要一次探查,就可以得到整張表的探查報告,但後續我們還發現了一些問題,主要有三點:

 

  1. 無法看到探查的資料明細以及關聯的行詳情,無法對資料進行預處理操作。

  2. 探查還是需要資源排程,等待時長平均分鐘級。

  3. 與質量監控沒有打通,探查資料的後續走向不明確。

 

針對這些問題,我們進一步開發了動態探查需求,解決的問題如下:

 

  1. 基於大資料預覽的探查,支援對資料進行函式級別的預處理。

  2. 探查結果秒級更新,實時響應。

  3. 與資料監控打通,探索 SQL 的生成模式。

 

圖片

本文主要介紹動態探查的應用場景和相關的技術實現。

 

應用場景

 

探查主要應用在元資料管理,資料研發,數倉的開發以及資料治理,可為對資料質量有需求的場景提供資料質量的發現和識別能力。目標使用者除了研發同學,也包含不是以 SQL 研發為主的群體,比如演算法建模和資料探勘等領域。

 

探查可以有效的打通三個閉環:

 

  1. 元資料管理 -> 探查 -> 資料預覽探查(庫表的質量報告)

  2. 資料監控 <-> 資料探查

  3. 動態探查 -> SQL -> 資料開發 -> 除錯 -> 探查報告(質量分析)

 

圖片

 

名詞解釋

 

  • 全量探查:基於庫表的全量探查,後端引擎執行,展示探查後列的統計分佈結果。

  • 動態探查:基於抽樣的部分資料探查,展示欄位明細,可以使用操作對資料進行預處理,並實時動態的展示統計分佈結果。資料獲取後的過程都由前端執行。

 

兩者的對比示意圖

 

圖片

 

圖片

 

技術實現

除了資料的抽樣部分在後端做,其他的都是前端實現的。包括大資料展示,探查計算,卡片聯動,操作棧互動,以及未來要做的函式編輯器以及 SQL 生成。

 

技術架構

 

圖片

  • 抽樣能力:對資料進行基於質量分佈特徵的抽取。目前做的是隨機抽樣,後續嘗試基於特徵來抽樣。

  • 資料展現:大容量的資料載體,支援對資料處理的實時展現。前端目前是基於虛擬滾動 Table 做的,後續打算遷移到 canvas table 上。

  • 前端探查:實時探查,視覺化展現資料分佈,突出質量指標。資料處理能力:函式處理能力(GroupBy..)

  • 操作棧:需要對資料操作進行管理和回溯基於 immutable 和操作流實現操作棧。

  • 編輯器:提供完整函式的功能,需要:詞法解析,智慧提醒,語法高亮。基於編輯器實現函式的功能,antlr4 實現詞法解析,配合 monaco editor 實現一些智慧提醒和語法高亮。

  • 生成 SQL:將視覺化的互動式操作轉換成可執行的 SQL。

目前 sql generator 有以下幾種方式:

  • 基於鏈式呼叫生成

  • 基於標籤模板生成

  • 基於 AST(抽象語法樹)去做

 

關鍵技術及實現

大資料渲染

由於動態探查場景下前端需要支援最大 5000 條資料的展示和互動,所以在渲染這塊存在比較大的壓力,主要集中在探查卡片和資料預覽兩個部分。

 

探查卡片包含了特定列的部分關鍵資訊彙總,比如 0 值、Null 值、列舉值等,如下圖紅框部分:

 

圖片

圖片探查卡片部分由於存在較多定製化內容,所以採用了虛擬列表方案進行渲染,支援收起狀態和展開狀態:

 

圖片

圖片資料預覽部分展示的是探查的全部資料集合,可以快速檢視原始資料的詳細內容,由於內容同質化比較高,所以資料預覽採用的是基於團隊內部維護的 canvas 版本 Table 方案進行渲染,如下圖紅框部分:

 

圖片

卡片聯動

由於卡片和資料預覽列的寬度差異較大,並且上下兩部分滑動是獨立的,造成在選擇檢視某個具體列的時候,上下對齊位置會比較麻煩,為了解決這個問題,這塊增加了自動定位功能,演示效果如下:

 

圖片

這部分需要解決的問題有兩個:卡片中間點座標計算和自動定位邏輯。

 

圖片

中間點座標計算邏輯如下:

// 計算卡片中點座標 index是卡片序號,adsorbSider表示是否吸邊
getCardCenter(index: number, adsorbSider?: boolean) {
    ...
    // 獲取卡片資訊
    const cardBox: IBaseBox = this.cardList[index];
    // 獲取列資訊
    const colBox: IBaseBox = this.colList[index];
    const clientWidth = getClientWidth();
    if(adsorbSider) {
      // 吸邊處理
      if(cardBox.offset < this.cardScroll) {
        return cardBox.offset;
      }
      if(cardBox.offset + cardBox.width - this.cardScroll > clientWidth) {
        return cardBox.offset + cardBox.width - clientWidth;
      }
      return this.cardScroll;
    }
    return getTargetPosition(colBox, this.tableScroll, cardBox);
}

// 獲取滾動目標位置
// originBox: 滾動起始物件
// originScroll: 滾動起始左側scroll
// targetBox: 滾動結束物件
const getTargetPosition = (originBox: IBaseBox, originScroll: number, targetBox: IBaseBox) => {
  const clientWidth = getClientWidth();
  if(!originBox || !targetBox) return 0;

  let offsetLeftSider = Math.max(originBox?.offset - originScroll, 0);
  if(offsetLeftSider + targetBox.width >= clientWidth) {
    if(targetBox.offset + targetBox.width > clientWidth) {
      // 此處容易出現吸邊
      return targetBox.offset + targetBox.width - clientWidth;
    } else {
      return 0;
    }
  }
  const scroll = targetBox?.offset - offsetLeftSider + (targetBox.width - originBox.width) / 2;
  return Math.max(
    Math.min(targetBox.offset, scroll),
    0
  );
}

獲取到中點座標後,自動定位需要符合如下規則:

 

  1. 選中卡片後,表格要自動滾動定位到下方居中對齊,無法滿足對齊標準的,儘量靠近選中卡片位置。

  2. 選中表格列後,卡片要自動滾動定位到上方居中對齊,無法滿足對齊標準的,儘量靠近選中表格位置。

  3. 搜尋選中列後,卡片和表格要自動滿足上面兩個規則,並滾動到可視區域內。

 

規則中有幾種邊界情況,參考下圖:

 

圖片

居中對齊是對於卡片和列寬在 scroll 距離允許情況下的理想對齊方式,貼邊對齊是針對卡片在起始和結束位置 scroll 不足以滿足居中對齊要求時候的對齊方式,除此之外還有一種是卡片的寬度遠大於列寬,並且不是起始或者結束位置的時候所採取的對齊方式,如下如卡片 B 因為無法滾動,卡片 A 的寬度又佔據了底部第二列的一部分,所以此時卡片 B 只能高亮和底部的列進行對齊。

 

圖片

操作棧

動態探查支援了對於探查結果的基礎分析能力,比如列刪除、過濾、排序等,如下圖紅框部分:

 

圖片

圖片使用者對於探查結果的每一次操作都會被記作一次操作,多次操作串聯起來形成操作棧,可以自由的修改或者刪減操作棧裡的操作,並實時檢視最新結果,以過濾操作演示效果如下:

 

圖片

圖片操作棧部分需要處理的問題主要有以下幾點:

 

  1. 如何管理多種操作進行序列計算

這裡把所有操作都抽象成了Input + Logic = Ouput的結構,Input 是輸入引數,此處可以是指某一列的資料、上一步操作的結果或者其他計算值,Logic 是操作的具體邏輯,負責根據 Input 轉換生成 Output,Output 可以作為最終結果進行渲染,也可以再次進入下一環節參與計算,拿列刪除操作舉個栗子,下面是大體程式碼實現:

class ColDelOpt {
  run = (params: IOptEngineMetaInfo) => {
    // 操作Input部分
    const {
      columns = [],
      dataSourceMap = {}
    } = params;
    const {
      fields = []
    } = this.params;

    // 操作Logic部分
    const nextColumns = columns.filter((item) => !fields.includes(item.name));

    // 操作的Output
    return {
      columns: nextColumns,
      dataSourceMap
    }
  }
}

 

可以看到 ColDelOpt 內部有一個 run 方法,該方法支援傳入一個包含了列資訊 columns 和資料集 dataSourceMap 的 params 物件,此處 params 即被抽象的外部輸入引數 Input,run 方法內部的邏輯部分即被抽象的 Logic 部分,最後方法返回值包含了最新的 columns 和 dataSourceMap,即為 Output 部分。基於這種結構,使用者所有的操作都可以被初始化成不同的 Opt 例項,由操作引擎統一呼叫例項的 run 方法,並傳入所需的引數,最終得到計算結果。

 

  1. 某個操作被修改後如何進行二次計算

操作棧的計算是由計算引擎來完成的,引擎負責根據外部事件,來自動執行現有操作的資料處理工作,引擎執行流程和大體程式碼如下:

 

圖片

 

// 操作引擎
class OptEngine {

  // 操作列表
  private optList: IOptEngineItem[] = [];

  // 原始資料
  private metaData: IOptEngineMetaInfo = {
    columns: [],
    dataSourceMap: {},
  };

  // 執行運算元
  optRun = () => {
    let {
      columns = [],
      dataSourceMap = {}
    } = this.metaData;

    if(!this.optList.length) return {
      columns,
      dataSourceMap
    };

    for(let index = 0; index < this.optList.length; index++) {
      // 讀取操作運算元
      const optItem = this.optList[index];
      let startTime = performance.now();

      try {
        // 執行運算元計算
        const result = optItem.run({
          columns,
          dataSourceMap
        });

        // 更新運算元結果
        columns = result.columns || [];
        dataSourceMap = result.dataSourceMap || {};
      } catch(e) {
        // 報錯後直接直接返回
        return {
          columns,
          dataSourceMap,
          // 裝填報錯資訊
          errorInfo: {
            key: optItem.key || '',
            message: e.message
          }
        }
      }
    }

    return {
      columns,
      dataSourceMap,
    }
  }

  autoRun = (
    metaInfo: IOptEngineMetaInfo,
    optList: IOptItem[],
    callback: (params: IAutoRunResult) => void
  ) => {
    // 裝填資料
    this.setupMetaData(metaInfo);
    // 裝填操作棧
    this.setupOptList(optList.map((item) => {
      // 行過濾
      if(item.type === OPT_TYPE.FILTER) {
        return new FilterOpt({
          key: item.key,
          params: item.params
        })
      }
      // 其餘型別操作
      ...
      // 預設原值返回
      return new IdentityOpt({
        key: item.key,
      })
    }));

    // 執行操作計算
    const result = this.optRun();

    // 返回資料
    return {
      // 計算列
      columns: result.columns,
      // 執行結果
      dataSource: Object.entries(result.dataSourceMap).map(([key, value]) => ({
        field: key,
        value
      })),
      // 操作棧執行異常資訊
      errorInfo: result.errorInfo
    };
  }
}

 

應用實踐

以一個小例子來演示下動態探查的使用。前端開發過程中,有一個真實的場景,我們為了排查一個豎屏顯示器的 bug(1080*1920),想找到關聯的使用者,看其分佈情況,就可以很方便的用動態探查去尋找。

 

圖片

後續計劃

關注動態探查的操作豐富性以及之後的資料走向,比如離線資料匯出,和生成 SQL 等,技術方向上主要放在以下幾個方面:

  • 更多的探查型別和圖表支援動態探查目前支援空值,列舉值,零值,資料統計等基礎的探查功能,未來會計劃支援包括 map,json,time,sql 語句等型別的識別和探查。同時提供更豐富的圖表支援。

  • 操作棧的編輯器體驗動態探查目前還是以類 Excel 的操作為主,未來主要提供編輯器級別的操作體驗,可以提供 HSQL 支援的大部分函式,包括支援多表 join 功能。

  • 操作流程的 SQL 生成動態探查目前的 SQL 能力還未建設完成,會在未來結合編輯器級別的操作,並支援多表,配合詞法解析功能,提供更精準的生成 SQL 能力。

立即跳轉 火山引擎大資料研發治理套件官網瞭解詳情!