AgensGraph 完全指南 / 第 07 章:图数据建模
第 07 章:图数据建模
7.1 图建模方法论
7.1.1 建模思维转变
从关系型建模到图建模,需要进行思维转换:
关系型建模思维:
1. 识别实体 → 设计表
2. 识别关系 → 外键
3. 规范化 → 拆表消除冗余
4. 反规范化 → 按需合并表
图建模思维:
1. 识别实体 → 设计顶点(Vertex Label)
2. 识别关系 → 设计边(Edge Label)
3. 识别属性 → 分配到顶点和边上
4. 设计遍历路径 → 优化高频查询路径
7.1.2 建模四步法
| 步骤 | 活动 | 输出 |
|---|
| 1. 需求分析 | 识别业务实体和关系 | 实体-关系列表 |
| 2. 概念建模 | 绘制图模型草图 | 顶点/边/属性定义 |
| 3. 逻辑建模 | 细化标签和属性类型 | 逻辑图 Schema |
| 4. 物理建模 | 创建索引、优化存储 | 可执行的 DDL |
7.2 顶点设计
7.2.1 标签设计原则
-- ❌ 错误:过于细粒度的标签
CREATE (n:Person:Employee:Manager:FullTime:BeijingOffice {});
-- ✅ 正确:使用核心分类标签
CREATE (n:Person:Employee {name: '张三', role: 'manager', office: '北京'});
-- 标签设计原则:
-- 1. 标签表示"是什么",属性表示"属于什么分类"
-- 2. 一般不超过 3 个标签
-- 3. 标签应有业务含义
7.2.2 标签命名规范
| 规范 | 推荐 | 不推荐 |
|---|
| 使用单数名词 | :Person | :Persons |
| 使用 PascalCase | :OrderItem | :order_item |
| 表示核心身份 | :Person:Employee | :Male:Age30:Beijing |
| 简洁有意义 | :Company | :OrganizationEntity |
7.2.3 属性设计
-- 良好的属性设计
CREATE (:Person {
name: '张三', -- 必填属性
age: 30, -- 基本属性
email: '[email protected]', -- 唯一标识属性
skills: ['Java', 'Python'], -- 数组属性
address: { -- 嵌套属性
city: '北京',
district: '海淀'
},
created: datetime(), -- 时间戳
version: 1 -- 版本号(乐观锁)
});
属性设计原则:
| 原则 | 说明 | 示例 |
|---|
| 原子性 | 属性值不可再分 | first_name, last_name 而非 full_name |
| 可查询性 | 高频过滤的字段作为属性 | status, category |
| 适度冗余 | 高频访问的属性可冗余存储 | 顶点上冗余 name |
| 类型一致性 | 同名属性保持类型一致 | age 始终为 Integer |
| 避免大对象 | 不存储二进制/大文本 | 使用文件系统或对象存储 |
7.3 边设计
7.3.1 边的粒度
-- ❌ 过细:每种关系一种类型
(a)-[:KNOWS]->(b)
(a)-[:FRIENDS_WITH]->(b)
(a)-[:FOLLOWS]->(b)
(a)-[:COLLEAGUE_OF]->(b)
-- ✅ 合理:使用属性区分关系细节
(a)-[:RELATED_TO {type: 'friend', since: 2020}]->(b)
(a)-[:RELATED_TO {type: 'colleague', since: 2021}]->(b)
-- ✅ 最佳:有意义的关系类型 + 关键属性
(a)-[:KNOWS {context: '大学', since: 2018}]->(b)
(a)-[:WORKS_WITH {project: 'GraphDB', since: 2022}]->(b)
7.3.2 边的命名规范
| 规范 | 推荐 | 说明 |
|---|
| 使用动词或动词短语 | :KNOWS, :WORKS_AT | 语义清晰 |
| 使用大写蛇形 | :REPORTS_TO | REPORTS_TO |
| 方向一致 | 从主体到客体 | (Person)-[:WORKS_AT]->(Company) |
| 避免歧义 | :MANAGES | 而非 :HAS |
7.3.3 何时将关系提升为顶点
当关系本身需要被关联时,将其"提升"(Reification)为顶点:
场景:一次购买包含多个产品
❌ 简单边方案:
(Customer)-[:BOUGHT {quantity: 2, date: '2024-01-01'}]->(Product)
✅ 中间顶点方案:
(Customer)-[:PLACED]->(Order)-[:CONTAINS]->(Product)
(Order)-[:SHIPPED_TO]->(Address)
(Order {date: datetime(), total: 99.99})
-- 创建订单中间顶点
MATCH (c:Customer {id: 'C001'}), (p:Product {id: 'P001'})
CREATE (c)-[:PLACED]->(o:Order {
order_id: 'O001',
date: datetime(),
status: 'pending'
})-[:CONTAINS {quantity: 2, unit_price: 49.99}]->(p);
7.4 经典图模型模式
7.4.1 社交网络模型
社交网络图模型:
(Person)-[:KNOWS {since, context}]->(Person)
(Person)-[:FOLLOWS]->(Person)
(Person)-[:MEMBER_OF]->(Group)
(Person)-[:POSTED]->(Post)
(Post)-[:TAGGED_WITH]->(Tag)
(Person)-[:LIKED]->(Post)
(Person)-[:COMMENTED_ON]->(Post)
-- 社交网络 Schema
CREATE (:Person {id, name, email, bio, joined});
CREATE (:Group {id, name, description, created});
CREATE (:Post {id, content, created, likes_count});
CREATE (:Tag {name});
-- 关系类型
(:Person)-[:KNOWS {since, context}]->(:Person)
(:Person)-[:FOLLOWS]->(:Person)
(:Person)-[:MEMBER_OF {role, since}]->(:Group)
(:Person)-[:POSTED]->(:Post)
(:Post)-[:TAGGED_WITH]->(:Tag)
(:Person)-[:LIKED {at}]->(:Post)
7.4.2 企业组织模型
企业组织图:
(Employee)-[:REPORTS_TO]->(Employee)
(Employee)-[:BELONGS_TO]->(Department)
(Department)-[:PART_OF]->(Department)
(Employee)-[:HAS_SKILL]->(Skill)
(Employee)-[:WORKS_ON]->(Project)
(Project)-[:DELIVERS]->(Product)
-- 企业组织 Schema
CREATE (:Employee {id, name, title, level, salary, hire_date});
CREATE (:Department {id, name, budget, location});
CREATE (:Project {id, name, start_date, end_date, status});
CREATE (:Skill {name, category, level});
-- 关系
(:Employee)-[:REPORTS_TO {since}]->(:Employee)
(:Employee)-[:BELONGS_TO {role, since}]->(:Department)
(:Department)-[:PART_OF]->(:Department)
(:Employee)-[:HAS_SKILL {proficiency, years}]->(:Skill)
(:Employee)-[:WORKS_ON {role, allocation}]->(:Project)
7.4.3 知识图谱模型
知识图谱:
(Entity)-[:IS_A]->(Type)
(Entity)-[:RELATED_TO {relation}]->(Entity)
(Entity)-[:HAS_PROPERTY]->(PropertyValue)
(Concept)-[:SUBCLASS_OF]->(Concept)
(Document)-[:MENTIONS]->(Entity)
-- 知识图谱通用 Schema
CREATE (:Entity {id, name, description, source});
CREATE (:Type {name, namespace});
CREATE (:Document {id, title, content, url});
CREATE (:Concept {name, definition});
(:Entity)-[:IS_A]->(:Type)
(:Entity)-[:RELATED_TO {relation, weight, source}]->(:Entity)
(:Concept)-[:SUBCLASS_OF]->(:Concept)
(:Document)-[:MENTIONS {position, frequency}]->(:Entity)
7.4.4 金融风控模型
金融交易图:
(Account)-[:TRANSACTS_TO {amount, time, type}]->(Account)
(Account)-[:OWNED_BY]->(Person)
(Person)-[:IDENTIFIED_BY]->(IDDocument)
(Person)-[:ASSOCIATED_WITH {risk_score}]->(Person)
(Account)-[:FLAGGED_BY]->(Alert)
7.5 Schema 管理
7.5.1 AgensGraph 的 Schema 特性
AgensGraph 采用灵活 Schema(Schema-optional)模式:
| 特性 | 说明 |
|---|
| 标签无需预定义 | 可在创建顶点时直接使用新标签 |
| 属性无需预定义 | 可随时添加新属性 |
| 同标签异构 | 同一标签的顶点可有不同的属性集合 |
| 约束可选 | 可选择性地创建唯一性约束和存在性约束 |
7.5.2 唯一性约束
-- 为 Person.name 创建唯一性约束
CREATE CONSTRAINT person_name_unique
ON (p:Person)
ASSERT p.name IS UNIQUE;
-- 为多个属性创建复合唯一约束
CREATE CONSTRAINT person_email_unique
ON (p:Person)
ASSERT (p.email) IS UNIQUE;
-- 查看所有约束
-- (在 psql 中执行)
\d person
-- 删除约束
DROP CONSTRAINT person_name_unique;
7.5.3 存在性约束
-- 确保 Person 必须有 name 属性
CREATE CONSTRAINT person_name_exists
ON (p:Person)
ASSERT EXISTS(p.name);
-- 确保 KNOWS 关系必须有 since 属性
CREATE CONSTRAINT knows_since_exists
ON ()-[r:KNOWS]-()
ASSERT EXISTS(r.since);
7.5.4 约束一览
| 约束类型 | 语法 | 作用 |
|---|
| 唯一性约束 | ASSERT p.prop IS UNIQUE | 属性值在标签内唯一 |
| 存在性约束 | ASSERT EXISTS(p.prop) | 顶点/边必须有该属性 |
| 节点键约束 | ASSERT (p.prop1, p.prop2) IS NODE KEY | 复合唯一 + 存在 |
7.6 模式演化
7.6.1 添加新标签
-- 给现有顶点添加标签
MATCH (p:Person)
WHERE p.role = 'manager'
SET p:Manager
RETURN count(p) AS managers_labeled;
7.6.2 添加新属性
-- 批量添加属性
MATCH (p:Person)
WHERE NOT EXISTS(p.created_at)
SET p.created_at = datetime()
RETURN count(p) AS updated;
-- 修改属性名
MATCH (p:Person)
WHERE EXISTS(p.old_name)
SET p.new_name = p.old_name
REMOVE p.old_name;
7.6.3 数据迁移脚本模板
-- 迁移脚本: v1 → v2
-- 1. 添加新属性
MATCH (p:Person)
SET p.schema_version = 2;
-- 2. 数据转换
MATCH (p:Person)
WHERE p.full_name IS NOT NULL
SET p.first_name = split(p.full_name, ' ')[0],
p.last_name = split(p.full_name, ' ')[1];
-- 3. 验证
MATCH (p:Person)
WHERE p.schema_version = 2 AND p.first_name IS NULL
RETURN count(p) AS unmigrated;
-- 期望结果: 0
7.7 业务场景:电商平台图建模
7.7.1 需求分析
| 实体 | 属性 | 关系 |
|---|
| 用户 (User) | name, email, age, city | 购买、浏览、收藏、评价 |
| 商品 (Product) | name, price, category, brand | 属于分类、相似商品 |
| 订单 (Order) | order_id, date, total, status | 包含商品、由用户创建 |
| 分类 (Category) | name, level | 父子分类关系 |
| 评论 (Review) | content, rating, date | 用户评价商品 |
7.7.2 图模型定义
-- 创建 Schema
-- 顶点
CREATE (:User {user_id, name, email, age, city, registered});
CREATE (:Product {product_id, name, price, category, brand, stock});
CREATE (:Order {order_id, date, total, status, payment_method});
CREATE (:Category {category_id, name, level});
CREATE (:Review {review_id, content, rating, date});
-- 边
(:User)-[:PLACED {at}]->(:Order)
(:Order)-[:CONTAINS {quantity, unit_price}]->(:Product)
(:User)-[:VIEWED {at, duration_sec}]->(:Product)
(:User)-[:FAVORITED {at}]->(:Product)
(:User)-[:WROTE]->(:Review)-[:ABOUT]->(:Product)
(:Product)-[:BELONGS_TO]->(:Category)
(:Category)-[:SUBCATEGORY_OF]->(:Category)
(:Product)-[:SIMILAR_TO {score}]->(:Product)
7.7.3 典型查询
-- 协同过滤推荐:买了此商品的人还买了什么
MATCH (p:Product {name: 'iPhone 15'})<-[:CONTAINS]-(o:Order)-[:PLACED]->(u:User)
MATCH (u)-[:PLACED]->(o2:Order)-[:CONTAINS]->(recommended:Product)
WHERE recommended <> p
RETURN recommended.name, count(*) AS frequency
ORDER BY frequency DESC
LIMIT 10;
-- 用户行为分析
MATCH (u:User {user_id: 'U001'})
OPTIONAL MATCH (u)-[v:VIEWED]->(p:Product)
OPTIONAL MATCH (u)-[f:FAVORITED]->(fp:Product)
OPTIONAL MATCH (u)-[:PLACED]->(o:Order)-[:CONTAINS]->(op:Product)
RETURN u.name,
count(DISTINCT p) AS viewed_products,
count(DISTINCT fp) AS favorited_products,
count(DISTINCT o) AS orders,
count(DISTINCT op) AS purchased_products;
7.8 建模反模式
| 反模式 | 问题 | 改进方案 |
|---|
| 巨型超级节点 | 某节点有数百万条边 | 边按时间分区、使用属性过滤 |
| 过度规范化 | 关系拆分过细 | 合并为有意义的关系类型 |
| 属性滥用 | 将关系类型编码为属性值 | 使用独立的关系类型 |
| 无索引设计 | 高频查询属性无索引 | 创建相应索引 |
| 深度标签嵌套 | :A:B:C:D:E | 限制在 2-3 个标签内 |
| 大属性存储 | 在属性中存储二进制/长文本 | 使用外部存储 + URL 引用 |
7.8.1 超级节点问题详解
超级节点示例: 某明星在社交网络中有 1000 万粉丝
(明星)-[:FOLLOWS]-(粉丝1)
(明星)-[:FOLLOWS]-(粉丝2)
...
(明星)-[:FOLLOWS]-(粉丝1000万)
问题:
- 遍历所有粉丝时性能极差
- 每次查询都需要处理数百万条边
解决方案:
1. 边分区: (明星)-[:FOLLOWS_2024]->(粉丝)
2. 属性过滤: -[:FOLLOWS {tier: 'vip'}]->
3. 分层: (明星)-[:HAS_FAN_GROUP]->(Group)-[:MEMBER]->(粉丝)
7.9 本章小结
| 要点 | 说明 |
|---|
| 顶点设计 | 使用核心标签,属性保持原子性 |
| 边设计 | 有意义的类型名,方向一致 |
| Reification | 当关系本身需要关联时,提升为顶点 |
| 约束管理 | 唯一性约束 + 存在性约束保证数据质量 |
| 模式演化 | 增量式迁移,版本控制 |
| 反模式 | 避免超级节点、过度规范化、属性滥用 |
7.10 练习
- 为"在线教育平台"设计图模型,包括学生、课程、教师、章节、评价等实体。
- 为你的建模创建完整的约束(唯一性 + 存在性)。
- 识别以下场景中的超级节点风险并提出改进方案:一个有 500 万用户的电商系统中,热门商品被数万人收藏。
7.11 扩展阅读