图数据模型基础:节点、边与属性

图数据库入门:图论基础概念、属性图模型、标签与类型、与关系型数据库的建模对比。理解为什么"关系即一等公民"让图数据库在关联查询中碾压 SQL。

导语:为什么关系型数据库处理关联查询很慢

想象这样一个场景:在一个社交网络中,找到"用户 A 的朋友的朋友中喜欢摄影的人"。

在关系型数据库中,需要执行多次 JOIN:

-- 查找用户 A 的二级好友的兴趣
SELECT DISTINCT u3.name
FROM users u1
JOIN friendships f1 ON u1.id = f1.user_id
JOIN users u2 ON f1.friend_id = u2.id
JOIN friendships f2 ON u2.id = f2.user_id
JOIN users u3 ON f2.friend_id = u3.id
JOIN interests i ON u3.id = i.user_id
WHERE u1.name = 'Alice'
  AND u3.id != u1.id
  AND i.name = 'Photography';

随着深度增加,JOIN 数量线性增长,查询时间呈指数级恶化。在 6 层深度时,关系型数据库可能需要数分钟甚至更久。

图数据库的思路完全不同:它不是把"关系"存储在独立的关联表中,而是让关系本身成为数据模型的一等公民,与节点平起平坐。

一句话总结:图数据库的核心理念是"关系即数据"——用节点表示实体、用边表示关系,查询时沿着关系自然遍历,而非反复 JOIN。


1. 图的基本概念

1.1 数学图论基础

图(Graph)是由**顶点(Vertex/Node)和边(Edge)**组成的数学结构:

G = (V, E)

V = {Alice, Bob, Carol, David}     ← 顶点集合
E = {(Alice, Bob), (Bob, Carol), (Alice, David)}  ← 边集合

图的分类:

类型特征应用场景
无向图边没有方向无向社交网络(互为好友)
有向图边有方向关注关系、引用关系、资金流向
加权图边带有权重路由距离、交易成本、亲密度
多重图两个节点间有多条边同一用户多次购买同一商品
属性图节点和边都有属性现代图数据库的主要模型

1.2 属性图模型(Property Graph Model)

现实世界的数据不是抽象的数学节点——每个实体都有属性。属性图模型是现代图数据库(Neo4j、Amazon Neptune、JanusGraph)采用的标准:

┌─────────────────────┐         ┌─────────────────────┐
│      (节点)          │         │      (节点)          │
│   ┌───────────┐     │         │   ┌───────────┐     │
│   │   Alice   │     │         │   │    Bob    │     │
│   └───────────┘     │         │   └───────────┘     │
│   标签: Person      │         │   标签: Person      │
│   ├─ name: "Alice"  │         │   ├─ name: "Bob"    │
│   ├─ age: 28        │    ────────►  ├─ age: 32      │
│   ├─ city: "Beijing"│  KNOWS    │   └─ city: "Shanghai"│
│   └─ role: "Engineer"         │        标签: Person  │
│                     │         │  关系: KNOWS         │
│                     │         │  ├─ since: 2019     │
│                     │         │  └─ mutual: true    │
└─────────────────────┘         └─────────────────────┘

属性图的四大核心元素:

元素说明类比
节点(Node/Vertex)代表实体,如人、商品、地点SQL 中的一行
关系(Relationship/Edge)连接两个节点的有向边,本身也是数据SQL 中的外键关联
标签(Label)节点的类型分类SQL 中的表名
属性(Property)键值对,附加在节点或关系上SQL 中的列

一句话总结:属性图模型 = 带标签的节点 + 带类型的有向关系 + 键值属性,这三者的组合可以表达几乎任何现实世界的关联数据。


2. 图数据库 vs 关系型数据库

2.1 建模方式对比

假设建模一个"用户购买商品"的场景:

关系型数据库:

CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    age INT
);

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    price DECIMAL(10,2)
);

CREATE TABLE purchases (
    user_id INT REFERENCES users(id),
    product_id INT REFERENCES products(id),
    quantity INT,
    purchase_date DATE,
    PRIMARY KEY (user_id, product_id, purchase_date)
);

图数据库:

(:Person {id: 1, name: "Alice", age: 28})
  -[:BOUGHT {quantity: 2, date: "2024-01-15"}]->
(:Product {id: 101, name: "iPhone 15", price: 7999.00})

2.2 查询性能对比

查询:找到"购买过 Alice 买过的任何商品的其他用户"

关系型数据库:

SELECT DISTINCT u2.name
FROM users u
JOIN purchases p1 ON u.id = p1.user_id
JOIN purchases p2 ON p1.product_id = p2.product_id
JOIN users u2 ON p2.user_id = u2.id
WHERE u.name = 'Alice' AND u2.id != u.id;
-- 需要 3 次 JOIN,涉及 3 个索引查找 + 大量中间结果去重

图数据库(Cypher):

MATCH (alice:Person {name: 'Alice'})-[:BOUGHT]->(product)<-[:BOUGHT]-(other)
RETURN DISTINCT other.name;
-- 从 Alice 节点出发,沿 BOUGHT 关系自然遍历到商品,再反向遍历到买家

性能对比(1000 万节点/1 亿关系):

查询深度SQL 响应时间图数据库响应时间
1 跳(直接好友)~10ms~1ms
2 跳(朋友的朋友)~100ms~2ms
3 跳(朋友的朋友的朋友)~2s~5ms
4 跳~30s+~10ms
6 跳超时/不可行~50ms

为什么图数据库在深层查询上快这么多?

  • 指针遍历:图数据库的边是物理指针,从一个节点直接跳转到关联节点,时间复杂度 O(1)
  • 免 JOIN:不需要笛卡尔积式的中间结果集
  • 局部性:相关数据在存储上相邻,CPU 缓存命中率高

一句话总结:图数据库的优势在"关联查询的深度"上呈指数级放大——3 跳以内 SQL 尚可应付,超过 3 跳后图数据库通常是唯一可行的选择。


3. 图建模最佳实践

3.1 标签设计原则

// ✅ 好的标签设计:按实体类型分类
(:Person {name: "Alice"})
(:Product {name: "iPhone"})
(:Company {name: "Apple"})
(:City {name: "Beijing"})

// ❌ 差的标签设计:把类型属性化
(:Entity {type: "person", name: "Alice"})
(:Entity {type: "product", name: "iPhone"})
-- 这样会丧失索引优势和查询便利性

3.2 关系方向设计

// 关注关系(有向)
(:Person)-[:FOLLOWS]->(:Person)

// 好友关系(双向,两个方向各一条或一条无向)
(:Person)-[:FRIENDS_WITH {since: 2015}]->(:Person)

// 购买关系(从用户指向商品)
(:Person)-[:BOUGHT {quantity: 2, date: "2024-01-15"}]->(:Product)

3.3 属性放置决策

一个关键设计问题:信息应该作为节点属性还是独立节点?

// 方案 A:城市作为属性
(:Person {name: "Alice", city: "Beijing"})

// 方案 B:城市作为独立节点
(:Person {name: "Alice"})-[:LIVES_IN]->(:City {name: "Beijing"})
方案适用场景
属性化城市只是描述性信息,不参与关系遍历
节点化需要查询"住在同一城市的其他用户"、城市之间的层级关系

一句话总结:建模的核心原则是"如果会沿着它查询,就做成节点或关系;如果只会读取它,就做成属性"。


4. 常见图模式

4.1 社交网络

(:Person)-[:FOLLOWS]->(:Person)
(:Person)-[:FRIENDS_WITH]->(:Person)
(:Person)-[:POSTED]->(:Post)
(:Post)-[:HAS_TAG]->(:Tag)
(:Person)-[:LIKED]->(:Post)

4.2 电商推荐

(:User)-[:VIEWED {timestamp: ...}]->(:Product)
(:User)-[:ADDED_TO_CART {timestamp: ...}]->(:Product)
(:User)-[:PURCHASED {timestamp: ..., amount: ...}]->(:Product)
(:Product)-[:BELONGS_TO]->(:Category)
(:Product)-[:SIMILAR_TO {score: 0.87}]->(:Product)

4.3 知识图谱

(:Entity {name: "爱因斯坦"})-[:BORN_IN]->(:Place {name: "德国"})
(:Entity {name: "爱因斯坦"})-[:DEVELOPED]->(:Theory {name: "相对论"})
(:Theory {name: "相对论"})-[:INFLUENCES]->(:Field {name: "天体物理学"})

一句话总结:图模式(Graph Pattern)是图数据库查询的核心表达方式——用 ASCII 艺术描述"从哪到哪"的遍历路径。


5. 总结

图数据模型的核心认知:

  1. 关系是一等公民:不是外键,不是关联表,而是直接存储的、带属性的、可遍历的数据
  2. 查询即遍历:图查询语言(如 Cypher)本质上是声明式的路径描述
  3. 性能来自指针:边的物理指针让相邻节点的访问是 O(1),而非 B+ 树的 log N
  4. 建模决定一切:节点 vs 属性、关系方向、标签粒度,这些选择在查询时会产生数量级差异

后续专题将在此基础上深入:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「graphdb」更多文章

  1. 查询缓存与物化视图
  2. 图数据测试策略与回归验证
  3. 图数据库并发控制与批量更新