3. 图数据模型设计:节点、边、属性的建模优化

模型设计是性能的起点。代码写得再漂亮,跑在糟糕的模型上,查询一样慢到让你崩溃。

我记得刚接触药物图谱那会儿,一股脑把所有属性挂到节点上,觉得这样最“完整”。结果查询一跑,几秒都出不来。后来我花了大量时间重新设计模型,性能直接提升了两个数量级。今天就跟大家聊聊,节点、边、属性到底该怎么设计才高效。

3.1 节点建模:别让一个节点包罗万象

很多人刚开始设计图模型时,喜欢用一个「药物」节点把化学式、分子量、靶点、副作用全挂上去。这其实是在用图数据库的关系型思维。

我建议反过来想:把每个实体拆成独立的节点类型。

不好的设计好的设计
药物节点(包含所有属性)
- 化学式
- 分子量
- 靶点(列表)
- 副作用(列表)
- 生产厂商
药物节点 → 靶点节点 → 蛋白质节点
药物节点 → 副作用节点 → 症状节点
药物节点 → 厂商节点

为什么要拆?因为图数据库的核心优势是 遍历。你把数据揉在一个节点里,查询时只能全量扫描;拆开后通过边跳转,索引能精准定位。

实用技巧:用一个「类型」标签(label)区分不同层级。比如在药物图谱中,我习惯给所有节点打上层级标记:L1_药物、L2_靶点、L3_基因。这样查询时可以直接按标签过滤,避免走到不相关的节点上。

我遇到过最典型的场景:一个药物节点挂了上百个靶点ID,查询「这个药的所有靶点」需要把整个节点取出来再解析。改成靶点独立节点后,查询变成了简单的边遍历,性能提升了 8 倍。

3.2 边建模:方向、类型和权重

边是图的灵魂。设计边的核心原则是 方向有意义、类型够明确、权重用得巧。

3.2.1 方向的合理性

有向边和无向边的选择影响巨大。我在一个项目中,把「药物-靶点」关系设计成了无向边,结果查询「哪些靶点被这个药物抑制」和「哪个药物作用于这个靶点」用同一套路径扫描,性能浪费了一半。

我更推荐的做法:

// 方向明确,语义清晰
药物 -[抑制]-> 靶点
靶点 -[激活]-> 基因
药物 -[副作用]-> 症状

3.2.2 边类型不要太多

有经验的工程师会控制边类型数量。我个人的经验是:一个图谱里边类型最好控制在 15 种以内。超过这个数,查询优化器很难找到最优路径。

推荐边类型典型查询
靶向、激活、抑制找药物直接靶点
副作用找药物的不良反应
相互作用找药物组合风险
代谢酶找代谢通路

3.2.3 权重信息放在边上

亲和力、结合强度这类数值,天然属于边的属性。把它们放在节点上,会增加遍历时的计算量。

// 正确姿势
药物(节点) -[靶向 {affinity: 0.85}]-> 靶点(节点)

// 错误姿势
药物(节点 {target_affinity: 0.85}) -[靶向]-> 靶点(节点)

我曾经踩过的坑:把药物与靶点的最大亲和力作为属性挂在药物节点上。结果查询时要过滤所有药物,再比较属性值,慢得要命。后来把亲和力放在边上,查询变成了「从药物出发,沿靶向边走,过滤 affinity > 0.9」,跑得快多了。

3.3 属性建模:不索引的就别挂节点上

属性 索引 是性能的分水岭。我说句实在话:不参与查询过滤的属性,不要浪费索引资源。

核心原则:只索引会被频繁用作查询条件的属性。比如药物名称、靶点ID、症状名称。而摘要、描述、参考文献这些,写进节点就好,不用建索引。

怎么判断一个属性要不要建索引?你想想看,以下场景哪个最频繁:

  1. 「找某个药物的副作用」→ 药物名称需要索引
  2. 「找某基因相关的所有药物」→ 基因ID需要索引
  3. 「查看某个药物的化学式描述」→ 化学式很少作为查询条件,不建索引

3.3.1 复合索引实战

药物图谱里最常见的查询就是「某类疾病的关联药物」。这时候单独建药物名称索引或疾病名称索引都不够快。我试过复合索引:

// Neo4j 示例
CREATE INDEX drug_disease_index FOR (n:Disease) ON (n.name, n.type)

这样匹配「肿瘤」且类型为「恶性」的疾病时,两步过滤变成一步到位。

3.3.2 属性值类型要统一

别把字符串和数字混在同一个属性里。我在项目中见过药物节点的「剂量」属性,有的存 "10mg",有的存 "10"。查询时按数字范围过滤,字符串字段直接跳过索引。

// 统一类型
药物节点 {
  dose_mg: 10.0,   // 浮点型
  dose_text: "10mg tablet"  // 描述型,不参与过滤
}

3.4 核心结构

下面这张图,是我在做药物图谱时经常用的模型结构。它把节点按层级拆分,边负责传递关系,属性挂载在合适的位置:

药物图谱建模优化核心结构 药物 属性: 名称, 结构式 靶点 属性: ID, 名称 副作用 属性: 名称, 严重度 基因 属性: symbol, 位置 靶向 {亲和力} 导致 {发生率} 编码 要点说明 1. 药物、靶点、副作用、基因独立节点 2. 边携带权重属性(亲和力、发病率) 3. 每个节点只挂载会被索引的属性

看到没?每个节点只存它的 核心标识属性,关系数据(亲和力、发生率)放在边上。这样无论是「从药物出发查靶点」还是「从靶点回溯药物」,路径都短而直。

避坑指南:别用属性去替代边。比如用药物节点的「target_list」字段存多个靶点ID。这样做看似省了边的创建,但查询时你没法利用图的遍历优势。我曾为此重构过整个图谱,花了三天,换来的是稳定 10 倍以上的查询提升。

3.5 小结

模型设计没有完美的模板,只有适合场景的方案。我个人总结了三条经验:

  • 节点精减:每个节点只存它最核心的、会被作为查询入口的属性
  • 边带权:数量型、计算型的属性放在边上
  • 索引优先:凡是要作为过滤条件的属性,建索引;描述型属性不建

做到这三点,你的药物图谱基本不会跑偏。接下来要做的就是对照自己的业务,一步一优化。嗯,这里有个关键点:模型设计要在业务理解完成后就做,别等代码写了一半才想起来改模型——到时候改起来可难受了。