强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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: 'zhangsan@example.com', -- 唯一标识属性
  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_TOREPORTS_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 练习

  1. 为"在线教育平台"设计图模型,包括学生、课程、教师、章节、评价等实体。
  2. 为你的建模创建完整的约束(唯一性 + 存在性)。
  3. 识别以下场景中的超级节点风险并提出改进方案:一个有 500 万用户的电商系统中,热门商品被数万人收藏。

7.11 扩展阅读