一、業務背景:三類指標與四種狀態
指標類型 | 定義規則 | 依賴關系 |
---|---|---|
原子指標 | 單表聚合(SELECT + WHERE + GROUP) | 無 |
派生指標 | 在原子/派生指標上加 WHERE、改 GROUP | 依賴 1~N 個父指標 |
復合指標 | 多個原子/派生指標做加減運算 | 依賴 1~N 個父指標 |
狀態 | 說明 |
---|---|
已保存 | 草稿,可反復修改 |
已發布 | 對外可見,禁止修改 |
已落庫 | 數據已固化到 Hive/ClickHouse,禁止修改 |
約束
指標不能重復(通過唯一鍵
(name, version)
保證)。已發布/已落庫 的指標不允許任何變更;一旦變更,必須級聯同步所有下游。
二、存儲模型:全量 vs 依賴 ID 的抉擇
方案 | 優點 | 缺點 | 結論 |
---|---|---|---|
全量 SQL 保存 | 查詢快,無級聯問題 | 冗余高、父級改動無法同步 | ? |
只存依賴 ID | 無冗余、天然同步 | 查詢需遞歸或額外緩存 | ? |
最終采用“只存依賴 ID + 運行時動態拼裝 SQL”的方案。
三、表結構:指標 & 血緣兩張核心表
1)指標主表?t_indicator
CREATE TABLE t_indicator (id BIGSERIAL PRIMARY KEY,name TEXT NOT NULL,version INT NOT NULL,type VARCHAR(16) CHECK (type IN ('ATOMIC','DERIVED','COMPOSITE')),status VARCHAR(16) CHECK (status IN ('SAVED','PUBLISHED','LOADED')),definition JSONB, -- 原子:SQL片段;派生/復合:依賴列表+運算created_at TIMESTAMPTZ DEFAULT now(),UNIQUE(name, version)
);
2)血緣關系表?t_indicator_dependency
CREATE TABLE t_indicator_dependency (id BIGSERIAL PRIMARY KEY,indicator_id BIGINT NOT NULL REFERENCES t_indicator(id),parent_id BIGINT NOT NULL REFERENCES t_indicator(id),depth INT NOT NULL, -- 當前節點到父節點的深度UNIQUE(indicator_id, parent_id)
);-- 常用索引
CREATE INDEX idx_dep_parent ON t_indicator_dependency(parent_id);
CREATE INDEX idx_dep_child ON t_indicator_dependency(indicator_id);
四、多級依賴查詢:四種實戰方案
方案 A:PostgreSQL 遞歸 CTE(開發階段首選)
-- 查詢節點 100 的所有下游(包括多級)
WITH RECURSIVE down AS (SELECT indicator_id, 1 AS lvlFROM t_indicator_dependencyWHERE parent_id = 100UNION ALLSELECT d.indicator_id, lvl + 1FROM t_indicator_dependency dJOIN down ON d.parent_id = down.indicator_id
)
SELECT * FROM down;
如何快速查詢全線依賴鏈(A ← B ← C ← D ← E)
🔍 場景1:查某個指標的所有上游依賴(如A依賴了誰)
-- 查詢A的所有上游依賴(包括多級)
WITH RECURSIVE upstream AS (SELECT parent_id, 1 AS levelFROM indicator_dependencyWHERE indicator_id = ? -- A的IDUNION ALLSELECT d.parent_id, u.level + 1FROM indicator_dependency dJOIN upstream u ON d.indicator_id = u.parent_id
)
SELECT * FROM upstream;
🔍 場景2:查某個指標的所有下游影響(如E被誰依賴)
-- 查詢E的所有下游影響(包括多級)
WITH RECURSIVE downstream AS (SELECT indicator_id, 1 AS levelFROM indicator_dependencyWHERE parent_id = ? -- E的IDUNION ALLSELECT d.indicator_id, dw.level + 1FROM indicator_dependency dJOIN downstream dw ON d.parent_id = dw.indicator_id
)
SELECT * FROM downstream;
方案 B:封裝成函數,一行調用
CREATE OR REPLACE FUNCTION f_get_rel(node BIGINT,dir INT DEFAULT 1, -- 1 下游;-1 上游;0 雙向max_depth INT DEFAULT 10
)
RETURNS TABLE(direction INT, depth INT, node_id BIGINT) AS $$
BEGINIF dir = 1 OR dir = 0 THENRETURN QUERYWITH RECURSIVE down AS (SELECT 1, 1, indicator_idFROM t_indicator_dependency WHERE parent_id = nodeUNION ALLSELECT 1, d.depth + 1, t.indicator_idFROM t_indicator_dependency tJOIN down d ON t.parent_id = d.node_idWHERE d.depth < max_depth) SELECT * FROM down;END IF;IF dir = -1 OR dir = 0 THENRETURN QUERYWITH RECURSIVE up AS (SELECT -1, 1, parent_idFROM t_indicator_dependency WHERE indicator_id = nodeUNION ALLSELECT -1, u.depth + 1, t.parent_idFROM t_indicator_dependency tJOIN up u ON t.indicator_id = u.node_idWHERE u.depth < max_depth) SELECT * FROM up;END IF;
END;
$$ LANGUAGE plpgsql;
使用示例
SELECT * FROM f_get_rel(100, 1, 5); -- 下游5層
SELECT * FROM f_get_rel(100, -1, 5); -- 上游5層
方案 C:圖數據庫 Neo4j(可視化 & 超深層級)
MATCH p = (a:Indicator {id: 100})<-[:DEPENDS_ON*]-(b)
RETURN nodes(p) AS chain;
方案 D:閉包表(寫入重、查詢快)
ancestor_id | descendant_id | depth |
---|---|---|
100 | 101 | 1 |
100 | 102 | 2 |
… | … | … |
每次變更指標需批量刷新閉包表,適合“讀多寫極少”系統。
? 推薦實踐
中小型系統:優先使用
WITH RECURSIVE
,簡潔夠用。中大型系統:考慮 閉包表 + 緩存,或引入 圖數據庫 做血緣分析。
實時性要求高:用 預構建路徑表 或 緩存方案。
? 總結對比表
方案 | 查詢性能 | 寫入成本 | 架構復雜度 | 是否支持實時變更 | 適用場景 |
---|---|---|---|---|---|
RECURSIVE | 中等 | 低 | 低 | ? | 層級淺、變更頻繁 |
預構建路徑表 | 高 | 高 | 中 | ? | 讀多寫少、層級深 |
圖數據庫 | 高 | 中 | 高 | ? | 復雜血緣、可視化 |
閉包表 | 高 | 高 | 中 | ? | 層級深、變更少 |
緩存+異步刷新 | 極高 | 中 | 中 | ?(延遲) | 查詢頻繁、變更低頻 |
五、狀態級聯校驗:防止“已發布”被改動
1)觸發器:禁止直接 UPDATE 已發布/已落庫指標
CREATE OR REPLACE FUNCTION f_block_if_published()
RETURNS TRIGGER AS $$
BEGINIF EXISTS (SELECT 1 FROM t_indicator WHERE id = NEW.idAND status IN ('PUBLISHED','LOADED')) THENRAISE EXCEPTION 'Cannot modify published/loaded indicator %', NEW.id;END IF;RETURN NEW;
END;
$$ LANGUAGE plpgsql;CREATE TRIGGER trg_block_update
BEFORE UPDATE ON t_indicator
FOR EACH ROW EXECUTE FUNCTION f_block_if_published();
2)級聯校驗:新增派生/復合指標時檢查所有父級狀態
public void validateParents(List<Long> parentIds) {String sql = """SELECT id FROM t_indicatorWHERE id = ANY(?) AND status NOT IN ('PUBLISHED','LOADED')""";List<Long> bad = jdbcTemplate.queryForList(sql, Long.class, parentIds);if (!bad.isEmpty()) {throw new BizException("存在未發布父級: " + bad);}
}
六、線上性能優化三板斧
緩存:將
f_get_rel()
結果寫入 Redis,Key=rel:{id}:{dir}:{depth}
,TTL=5 min。預編譯:對常用 SQL 模板(原子、派生、復合)提前生成 PreparedStatement。
異步刷新:指標發布后,通過 MQ 觸發“血緣刷新”任務,預熱緩存。
七、總結
維度 | 實踐要點 |
---|---|
存儲 | 只存依賴 ID,避免全量冗余 |
查詢 | 函數封裝遞歸,必要時圖數據庫 |
狀態 | 觸發器 + 服務層雙重鎖 |
性能 | 緩存 + 預編譯 + 異步刷新 |
可視化 | Neo4j / ECharts 桑基圖 |
八、提問:在面對可能存在多級依賴的場景下,針對新建的派生指標,復合指標,指標的定義是保存成全量數據【把父級關系維護一份全量再加上自己的定義整合在一起保存】合適還是只保存父級之間的依賴關系【例如父級id】?
? 推薦方案:只保存依賴關系(父級ID)
1.?避免數據冗余與一致性問題
如果每個派生或復合指標都保存全量定義(包括父級SQL、維度、過濾條件等),一旦父級發生變更(如下線、修訂),子級將無法自動同步,導致邏輯漂移。
只保存ID可確保子級始終引用父級的最新定義,通過運行時動態拼裝SQL或邏輯,天然支持依賴鏈同步。
2.?滿足“已發布/已落庫不可改”的強約束
全量保存模式下,若父級已發布,子級無法感知父級的“凍結”狀態,可能導致子級基于過期定義構建,違反業務規則。
依賴ID模式下,子級創建時必須校驗父級狀態(如只允許引用已發布父級),系統可在元數據層面強制阻斷非法引用。
3.?支持多級依賴的靈活擴展
復合指標可能依賴多個派生指標,派生指標又依賴原子指標,依賴鏈可能非常深。
全量保存會導致存儲膨脹(如一個復合指標需冗余存儲整個依賴樹的定義),而ID模式只需記錄有向無環圖(DAG)的邊關系,存儲輕量且易于維護。
4.?版本控制與回滾更簡單
依賴ID模式下,每個指標定義可獨立版本化(如使用Git-style的版本號),子級通過ID+版本號鎖定父級快照,支持精確回滾。
全量保存模式下,任何父級字段的微調都會級聯影響所有子級版本,回滾復雜度呈指數級增長。
?? 需注意的配套設計
運行時解析性能:依賴ID模式需在查詢時動態拼裝SQL/邏輯,可能引入延遲。可通過預編譯緩存(如Flink SQL的物化視圖、Presto的查詢緩存)優化。
循環依賴檢測:需在建模時通過DAG校驗禁止環形引用(如A→B→A)。
父級下線影響:若父級被下線,需級聯校驗所有子級狀態(如提示“依賴指標已下線,請重新編輯”)。
📌 結論
在強依賴、強狀態控制、多級衍生的場景下,“只保存依賴關系(父級ID)”是更可持續的設計,配合版本化、狀態校驗和緩存機制,可兼顧靈活性、一致性與性能。
?循環檢測
如果 indicator_dependency
里沒有循環引用(即不存在 A→B→A 這種環),查詢會正常結束;
如果可能成環,建議加上防環條件,例如
WITH RECURSIVE upstream AS (SELECT parent_id, 1 AS level,ARRAY[indicator_id] AS path -- 記錄已走過的節點FROM indicator_dependencyWHERE indicator_id = ? -- A的IDUNION ALLSELECT d.parent_id, u.level + 1,u.path || d.indicator_idFROM indicator_dependency dJOIN upstream u ON d.indicator_id = u.parent_idWHERE NOT d.parent_id = ANY(u.path) -- 避免環
)
SELECT * FROM upstream;
返回列完整性
目前只返回了parent_id
和level
,如果后續還要用到indicator_id
或其它字段,最好在 CTE 里一并攜帶。
生產環境若存在成環風險,務必加防環條件。