一、业务背景:三类指标与四种状态
指标类型 | 定义规则 | 依赖关系 |
---|---|---|
原子指标 | 单表聚合(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 里一并携带。
生产环境若存在成环风险,务必加防环条件。