图的思维

理解 LPG 模型

LPG带标签属性图,也就是Labeled Property Graph,它由四个核心要素组成:节点 (Node)关系 (Relationship)标签 (Label)属性 (Property)

LPG 模型之所以直观,是因为它完美契合人类对现实世界的认知方式。

  1. 节点 —— 实体

    • 定义:节点代表现实世界中的对象或实体,比如一个人、一辆车、一笔订单。

    • 特点:节点是独立的,拥有唯一的内部 ID。

    • 示例:Person(一个人)、:Movie(一部电影)。

  2. 关系 —— 连接

    • 定义:关系连接两个节点,表示它们之间的关联。

    • 特点:

      • 有向性:关系总是有方向的(从起始节点指向终止节点),例如 (:Person)-[:ACTED_IN]->(:Movie)
      • 类型化:关系必须有类型(如 ACTED_IN, FRIEND_OF),这相当于关系型数据库中的外键,但更具语义。
      • 可携带属性:这是 LPG 的一大亮点,关系不仅仅是线,它也可以像节点一样拥有属性(如“评分”、“时间”)。
  3. 标签 —— 分类

    • 定义:标签用于对节点进行分组或分类。

    • 作用

      • 语义定义:告诉数据库这个节点是什么(如 Person)。
      • 性能优化:Neo4j 会利用标签来建立索引和约束,加速查询。
    • 多标签:一个节点可以拥有多个标签,例如一个节点可以同时是 :Person:Employee

  4. 属性 —— 细节

    • 定义:属性是存储在节点或关系上的键值对(Key-Value)。

    • 示例name: "Tom Hanks", age: 65, released: 1999

在图数据领域,主要有两种模型:LPGRDF (Resource Description Framework)

特性 LPG (Neo4j 采用) RDF (语义网标准)
数据模型 属性图:节点、关系、属性 三元组:主体、谓词、客体 (S-P-O)
直观性 高:像白板画图,符合直觉 低:需要理解 URI 和本体论
关系处理 关系即一等公民:关系可以有属性 关系即谓词:关系通常只是连接符,难以携带复杂数据
查询语言 Cypher / Gremlin SPARQL
适用场景 社交网络、推荐系统、实时风控 语义推理、数据集成、跨域知识共享

Neo4j 之所以快,是因为它的底层存储是原生 LPG 图,在 Neo4j 中,LPG 的关系在物理存储上就是指针。这和在关系型数据库中,表与表之间的连接是在查询时通过计算完成的有很大区别

  • 这意味着,无论数据量有多大, traversing(遍历)一条关系的成本是恒定的(O(1)),就像顺藤摸瓜一样,不需要全表扫描。

随着大模型(LLM)的兴起,LPG 模型也在进化,现在的 Neo4j 不仅仅是存图,还在向多模态发展。

  1. 向量属性

    现在的 LPG 节点和关系不仅可以存字符串和数字,还可以存向量 (Vector)

    • 这意味着你可以把非结构化数据(如文本、图片)转化为向量,存储在 LPG 的属性中。
  2. 混合检索

    结合 LPG 的结构化查询能力和向量相似度搜索,你可以实现强大的 GraphRAG (图检索增强生成)

    • 场景:用户问“张三在哪个项目里和李四合作过?”

    • LPG 的作用:利用 Cypher 进行多跳查询 (张三)-[:WORKS_ON]->(项目)<-[:WORKS_ON]-(李四),精准找到结构化的路径。

    • 向量的作用:如果问题很模糊,先通过向量搜索找到相关实体,再通过 LPG 的关系网络进行推理。

白板建模原则与去范式化

这部分是学习如何把思路从 RDBMS 到 Graph 进行一个迁移,反正这篇文章的后面,也会进行一个真实的业务去了解如何进行图存储思维的搭建,真实的体验图数据库的也实际应用。

在关系型数据库中,我们为了避免数据冗余,通常会将数据拆分成无数张小表,这是范式化;而在 Neo4j 中,为了让查询跑得更快、逻辑更直观,我们倾向于将数据捏在一起,这就是反范式化。

Neo4j 官方一直推崇“白板友好”的理念。这意味着,你在白板上画出的业务草图,应该能直接映射到数据库结构中,中间不需要经过复杂的表结构转换。

当你面对一个业务需求时,按照以下步骤在白板上画图,就能直接得到 Neo4j 的模型:

  1. 名词即节点
    • 业务中的核心实体(如:用户、商品、订单、电影、演员)就是节点
    • 原则:如果一个对象是独立的、有自己属性的,它通常是一个节点。
  2. 动词即关系
    • 实体之间的动作或联系(如:购买、主演、属于、认识)就是关系
    • 原则:关系必须是有意义的,且通常用动词或动词短语命名(如 PURCHASED, ACTED_IN)。
  3. 形容词即属性
    • 描述节点或关系的细节数据(如:用户的年龄、订单的时间、购买的数量)就是属性

假设你要设计一个电商系统,在白板上你会画出:

  • 节点:客户 (Customer)、订单 (Order)、商品 (Product)。
  • 关系:客户 下达 (PLACED) 订单,订单 包含 (CONTAINS) 商品。

在 Neo4j 中,这直接对应:

1
(:Customer)-[:PLACED]->(:Order)-[:CONTAINS]->(:Product)

重点:你不需要像 SQL 那样创建一张 Order_Items 中间表来连接订单和商品。在 Neo4j 中,CONTAINS 关系本身就充当了这张中间表,而且它可以携带属性

在 SQL 中,我们遵循第三范式(3NF),力求数据不冗余。但在 Neo4j 中,适度的冗余会让性能很好

因为图数据库的查询通常是沿着关系跳跃的。虽然遍历关系很快,但如果每次查询都需要跳很多层,性能依然会受影响。

通过去范式化,我们将一些常用的数据搬运到离查询入口更近的地方,减少跳跃次数。

  1. 关系属性化

    在 SQL 中,多对多关系需要一张中间表。在 Neo4j 中,这张表变成了关系,你可以直接把原本属于中间表的字段放在关系上。

    1
    (:Order)-[:CONTAINS {quantity: 2, price: 199.0}]->(:Product)
  2. 预计算与缓存

    对于复杂的统计结果,不要每次都实时计算,而是作为属性存储在节点上。

    范式化做法场景下,需要遍历该用户的所有 Order -> 遍历所有 CONTAINS 关系 -> 累加金额。这在数据量大时非常慢。反范式化场景下,在 Customer 节点上增加一个 total_spent 属性就可以了

    1
    (:Customer {name: "Alice", total_spent: 5000.0})
  3. 扁平化

    如果某些关联查询非常频繁,可以考虑建立捷径

    • 业务中会经常查询某用户购买了某类目的商品。

    • 原始路径User -> Order -> Product -> Category(3跳)。

    • 优化路径:直接在 UserCategory 之间建立 BOUGHT_CATEGORY 关系(1跳)。

      1
      (:User)-[:BOUGHT_CATEGORY]->(:Category)

最后,Neo4j 是 Schema-Free(无模式)的,这意味着你可以随时给节点加属性,非常灵活。但这也是一把双刃剑。

  • 陷阱:因为太灵活,开发人员可能会随意创建属性名,比如一会儿叫 user_name,一会儿叫 name,或者数据类型不统一,一会儿是字符串 “100”,一会儿是数字 100。这会导致后期数据治理极其困难,查询也变得复杂。
  • 建议:虽然数据库不强制 Schema,但应用层必须约束 Schema。或者使用 Neo4j 的 约束,强制规定关键节点必须有某些属性,以此来模拟强模式管理,保证数据质量。

Cypher 查询语句

了解 Cypher

Cypher 是 Neo4j 的声明式图查询语言。

它由 Neo4j 工程师于 2011 年创建,旨在作为图数据库中 SQL 的等效语言,但是 Cypher 的目的是允许用户通过高效且富有表现力的查询来揭示此前未知的数据连接和集群,从而充分发挥其属性图数据库的潜力

Cypher 提供了一种匹配模式和关系的视觉化方式。

它依赖于以下 ASCII 艺术风格的语法

1
(nodes)-[:CONNECT_TO]→(otherNodes)

圆括号用于表示圆形的节点,而 -[:ARROWS]→ 则用于表示关系。编写查询实际上就像在图中的数据上绘制模式。换句话说,节点及其关系等实体被直观地构建到查询中。这使得 Cypher 成为一种在阅读和编写方面都非常直观的语言。

Cypher 和 SQL 确实有着深厚的渊源,它们都是声明式查询语言,即你只需要告诉数据库“想要什么”,而不需要告诉它“怎么做”,因此共享了许多如 WHEREORDER BYLIMIT 等关键字。

对于 Cypher,它通过模式匹配、,通过 ASCII 艺术风格的语法“画”出数据结构,以 MATCH 开头,RETURN 结尾,先找模式,再返回,关系是显式的物理指针,多层查询只需延长箭头,直观且高效。模式灵活(Schema-on-Read),可随时添加新属性和关系。

而对于 SQL,它基于代数运算,通过 JOIN 将表拼凑在一起,以 SELECT 开头,直接定义的就是结果,关系是隐式的,通过 ID 匹配,多层查询需多次 JOIN,代码冗长。模式严格(Schema-on-Write),修改表结构较繁琐。

Cypher 语法

Cypher 的核心哲学是 所见即所得

在 Cypher 中,我们用类似 ASCII 艺术的方式来表示图结构。

  • 节点 (Node):用圆括号表示,像这样 ( )
  • 关系 (Relationship):用方括号和箭头表示,像这样 -[]->
  • 属性 (Property):用大括号表示键值对,像这样 {key: 'value'}
1
(:Person {name: 'Alice'}) -[:KNOWS]-> (:Person {name: 'Bob'})

上述例子表述的就是找到一个标签为 Person 且名字是 Alice 的节点,它通过 KNOWS 关系指向另一个名字是 BobPerson 节点。

Cypher 查询

Cypher 的查询结构非常清晰,通常遵循以下顺序:

MATCH:查找模式

这是查询的起点,用来描述你要在图中找什么结构。

  • 查找所有节点

    1
    MATCH (n) RETURN n
  • 查找特定标签的节点

    1
    MATCH (m:Movie) RETURN m
    • 当然,标签可以多匹配

      1
      2
      3
      // 如果一个节点既是“人”又是“员工”,你可以同时指定。
      MATCH (p:Person:Employee)
      RETURN p.name
    • 当然,关系也可以多匹配,可以用 |(或)符号。

      1
      2
      MATCH (p:Person)-[:FRIEND|:FAMILY]->(relative)
      RETURN relative.name
    • 路径变量: 你可以给整条路径(节点+关系+节点)起个名字,方便后续直接返回整条链路。

      1
      2
      MATCH p = (a:Person)-[:KNOWS]->(b:Person)
      RETURN p
  • 查找特定属性的节点

    1
    MATCH (p:Person {name: 'Tom Hanks'}) RETURN p
    • 直接在括号里写属性是简写,效果等同于 WHERE
  • 可选匹配:

    有时候你想找某样东西,但如果找不到也不想让查询失败(类似于 SQL 的 LEFT JOIN),那么可以使用 OPTIONAL MATCH

    1
    2
    3
    // 查找电影及其导演,如果有的话
    OPTIONAL MATCH (m:Movie {title: 'Unknown Movie'})<-[:DIRECTED]-(d:Person)
    RETURN m.title, d.name
    • 如果这部电影没有导演,d.name 会返回 NULL,但电影信息依然会显示。这个就是可选匹配

WHERE :过滤条件

如果你需要更复杂的过滤,比如比较大小、模糊搜索,就用 WHERE

  • 比较运算

    1
    2
    3
    MATCH (m:Movie)
    WHERE m.released > 2000
    RETURN m.title, m.released
    • 除了基础的 =, <, >,你还可以组合条件。一样,也是使用逻辑运算符:AND, OR, NOT

      1
      2
      3
      MATCH (p:Person)
      WHERE p.age > 30 AND p.city = 'London'
      RETURN p.name
  • 模糊查询

    1
    2
    3
    MATCH (p:Person)
    WHERE p.name CONTAINS 'Tom'
    RETURN p
    • 字符串的匹配

      处理文本是 WHERE 的强项,特别是模糊查询。

      • 前缀/后缀/包含

        1
        2
        3
        4
        5
        MATCH (p:Person)
        WHERE p.name STARTS WITH 'A' -- 以 A 开头
        AND p.name ENDS WITH 'e' -- 以 e 结尾
        AND p.name CONTAINS 'li' -- 包含 li
        RETURN p.name
      • 正则表达式: 如果你需要复杂的匹配规则,可以使用 =~输入正则表达谁

        1
        2
        3
        MATCH (p:Person)
        WHERE p.email =~ '.*@gmail\\.com' -- 匹配所有 Gmail 邮箱
        RETURN p.name, p.email
  • 空值判断

    1
    2
    3
    MATCH (p:Person)
    WHERE p.age IS NULL
    RETURN p
  • 列表与集合操作

    Cypher 对列表的支持非常友好。

    • IN 操作符: 检查某个值是否在一个列表中。

      1
      2
      3
      MATCH (p:Person)
      WHERE p.name IN ['Tom Hanks', 'Brad Pitt', 'Johnny Deep']
      RETURN p.name
    • 检查列表属性: 假设一个人有多个技能,存在一个列表属性 skills 中。

      1
      2
      3
      MATCH (p:Person)
      WHERE 'Java' IN p.skills
      RETURN p.name
  • 属性存在性检查

    有时候你只关心某个属性是否存在,而不关心它的值。

    • IS NULL / IS NOT NULL

      1
      2
      3
      MATCH (p:Person)
      WHERE p.phone IS NOT NULL
      RETURN p.name
  • 过滤关系属性

    WHERE 不仅可以过滤节点,还可以过滤关系

    1
    2
    3
    MATCH (a:Person)-[r:KNOWS]->(b:Person)
    WHERE r.since > 2010
    RETURN a.name, b.name, r.since

RETURN :返回结果

指定你要看什么数据。你可以返回节点、关系,或者具体的属性。

  • 返回特定属性

    通常我们不需要返回整个节点对象,只需要其中的几个字段属性

    1
    2
    MATCH (p:Person)
    RETURN p.name, p.birthYear
    • 如果你匹配了多个变量,想一次性返回它们包含的所有内容,可以使用 *

      1
      2
      MATCH (p:Person)-[:KNOWS]->(friend)
      RETURN *
    • 和 SQL 一样,如果属性名很长,或者你想让结果列名更好看,可以使用 AS 起别名。

      1
      2
      MATCH (m:Movie)
      RETURN m.title AS MovieName, m.released AS Year
  • 返回完整的节点或关系

    如果你直接返回变量名,Neo4j 会返回该元素的完整 JSON 结构(包括所有标签和属性)。

    1
    2
    MATCH (p:Person {name: 'Tom Hanks'})
    RETURN p
  • 返回关系: 你可以给关系起个变量名(比如 r),然后直接返回它。

    1
    2
    3
    MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
    WHERE p.name = 'Tom Hanks'
    RETURN r
  • 返回路径: 你可以把整条路径(节点+关系+节点)赋值给一个变量,这在查找最短路径或全路径时非常有用。

    1
    2
    3
    MATCH p = (p:Person)-[*1..3]->(m:Movie)
    WHERE p.name = 'Kevin Bacon'
    RETURN p

对于返回结果的处理与修饰

  • 排序与分页

    对于排序,一样支持升序 (ASC) 和降序 (DESC)。

    对于分页也类似,LIMIT限制返回的总行数。SKIP跳过前 N 行。

    1
    2
    3
    4
    MATCH (m:Movie)
    RETURN m.title
    ORDER BY m.released DESC -- 按年份降序
    SKIP 10 LIMIT 5 -- 跳过前10个,取5个(分页)
  • 去重

    一样,依旧 DISTINCT

    1
    2
    3
    MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
    WHERE m.title CONTAINS 'Star Wars'
    RETURN DISTINCT p.name

当然,上述内容完全可以进行组合,在这里就不演示了

Cypher 多跳查询

Cypher 的多跳查询,也称为可变长度路径查询。它解决了关系型数据库中处理递归查询中数据关系如何组织的痛苦的问题。简单来说,多跳查询允许你告诉数据库:“帮我找到从 A 点出发,经过 N 步关系能到达的所有 B 点”,而不用像 SQL 那样写无数个 JOIN

多跳查询的语法结构非常直观,核心在于关系部分的 * 号。

1
-[:RELATIONSHIP_TYPE*最小跳数..最大跳数]->
  • *:表示这是一个可变长度的路径。
  • 最小跳数(可选):路径的最短长度。默认为 1。
  • ..:范围分隔符。
  • 最大跳数(可选):路径的最长长度。
  • ->:关系的方向。

那么,就存在这么多的写法

语法示例 含义 说明
[*1..5] 1 到 5 跳 包含 1, 2, 3, 4, 5 跳的所有路径。
[*3] 恰好 3 跳 只查找距离正好是 3 的节点。
[*..5] 0 到 5 跳 包含起始节点本身(0跳)以及 1 到 5 跳的节点。
[*5..] 5 跳及以上 至少 5 跳,没有上限(慎用,可能导致性能问题)。
[*] 任意跳数 只要有路径连通就行,没有长度限制(极度慎用)。

对于一个简单的示例:查找 Tom Hanks 的 3 度人脉

1
2
MATCH (p:Person {name: 'Tom Hanks'})-[:KNOWS*1..3]->(friend)
RETURN friend.name
  • *1..3 表示沿着 KNOWS 关系走 1 步、2 步或 3 步能到达的人。

获取路径详情:

有时候你不仅想知道“是谁”,还想知道是怎么连过去的,也就是中间经过了谁。你可以把整条路径赋值给一个变量。

1
2
MATCH path = (p:Person {name: 'Tom Hanks'})-[:KNOWS*2]->(fof)
RETURN path, fof.name

path 变量包含了从 Tom 到目标节点中间经过的所有节点和关系。在 Neo4j 浏览器中,这通常会直接可视化为一条链路图。

多跳查询中,前面提到的过滤和匹配什么的,依旧可以组合使用,例如假如我们过滤路径中的节点,

1
2
3
MATCH (p:Person {name: 'Tom Hanks'})-[:KNOWS*2]->(fof)
WHERE fof.age > 30
RETURN fof.name

这里的 WHERE 过滤的是最终节点 fof。如果你想过滤中间节点,通常需要配合 ALLNONE 等列表谓词函数

但是,这种多条查询,你整多了,很容易变成对图上数据的求笛卡尔积的情况,例如,在社交网络中,随着跳数增加,可达节点的数量通常呈指数级增长。

  • 1跳:可能有 500 个朋友。
  • 2跳:可能有 50,000 个朋友的朋友。
  • 3跳:可能覆盖全网用户。

所以说,多条查询始终设置上限:尽量不要使用 [*][*5..] 这种没有上限的写法。

对于环路问题,如果图中存在环路(A认识B,B认识C,C认识A),查询可能会在圈里打转。

虽然 Cypher 的默认行为在可变长度路径中通常会寻找简单路径(Simple Path),即同一个节点在一条路径中不重复出现。但为了保险起见,理解业务数据是否存在密集环路很重要。

Cypher WITH 子句

在复杂的查询中,我们经常需要在查询中途返回一些结果作为下一步的输入。这时要用 WITH。它就像是 Unix 管道 |

对于这样的场景,例如,先找到 2000 年后的电影,统计每部电影的演员数量,然后只返回演员超过 10 人的电影标题。

1
2
3
4
5
6
7
8
9
10
11
// 第一步:匹配
MATCH (m:Movie)<-[:ACTED_IN]-(p:Person)
WHERE m.released > 2000

// 第二步:中途“返回”并聚合,过滤出数量大于10的
WITH m, count(p) AS actorCount
WHERE actorCount > 10

// 第三步:最终返回
RETURN m.title, actorCount
ORDER BY actorCount DESC

Cypher 聚合和统计

Cypher 和 SQL 一样,支持强大的聚合函数。当你使用聚合函数时,Cypher 会自动根据 RETURN非聚合的列进行分组(Group By)。

在 SQL 中,你需要显式地写出 GROUP BY 子句。而在 Cypher 中,这个规则被简化了

什么情况,大约就是当你使用聚合函数时,Cypher 会自动将 RETURN 子句中所有 非聚合 的项作为分组依据(即 GROUP BY 的键)。

例如,在查询 RETURN p.name, count(friend) 中:

  • count(friend) 是一个聚合函数。
  • p.name 是一个非聚合项。
  • 因此,Cypher 会自动按照 p.name 进行分组,然后计算每个名字对应的好友数量。

常用聚合函数

  1. count() - 计数

    count() 是最常用的函数,但它有两种行为,理解其区别非常重要。

    • count(*): 统计行数。它会计算匹配到的所有结果行的数量,包括包含 NULL 值的行。
    • count(变量或属性): 统计非空值的数量。它会忽略值为 NULL 的项。
    1
    2
    3
    MATCH (u:User)
    RETURN u.city, count(*) AS userCount
    // 按 u.city 分组,统计每个城市有多少个用户节点
  2. collect() - 收集列表

    这是 Cypher 中一个非常强大且独特的函数。它将多行数据收集到一个列表(List)中,非常适合将一对多的关系扁平化展示。

    1
    2
    3
    // 收集每个客户购买的所有产品
    MATCH (c:Customer)-[:BUYS]->(p:Product)
    RETURN c.name AS customer, collect(p.name) AS productsBought
  3. 数值统计函数

    这些函数用于对数值类型的属性进行计算,会自动忽略 NULL 值。

    • sum(): 求和。
    • avg(): 求平均值。
    • min(): 求最小值。
    • max(): 求最大值。
    1
    2
    3
    // 计算每个部门的平均薪资和最高薪资
    MATCH (e:Employee)-[:WORKS_IN]->(d:Department)
    RETURN d.name AS department, avg(e.salary) AS avgSalary, max(e.salary) AS maxSalary

除了基础函数,Cypher 还提供了一些更高级的统计函数,用于更深入的数据分析。

  • stdev(): 计算样本标准差。
  • stdevp(): 计算总体标准差。
  • percentileCont(): 计算百分位数(连续)。
  • percentileDisc(): 计算百分位数(离散)。

这就是用到的时候再了解的内容了

Cypher 关系查询

这是 Cypher 最强大的地方。你可以像画画一样,把关系连起来

关系的方向

方向很重要,因为当你明确知道数据的存储方向时,使用单向箭头。这通常性能最好,因为数据库只需要沿着一个方向遍历。

  • -->:箭头向右,表示从左边节点指向右边节点。
  • <--:箭头向左。
  • --:没有箭头,表示不关心方向(双向)。

例如,查找 minori 制作的 galgame

1
MATCH (m:maker {name: 'minori'})-[:MACKED]->(g:Galgame)

而关系不仅有方向,还有类型(Type)和属性。

多类型匹配

如果你想查找 minori 制作的游戏 或者 发行的游戏(假设这是两种不同的关系),可以使用 | (或) 语法。

1
2
MATCH (m:maker {name: 'minori'})-[:MAKED|:PUBLISHED]->(g:Galgame)
RETURN g.title, type(g)

获取关系本身

有时候你不仅想知道结果,还想知道它们之间是“怎么连的”。你可以给关系起个变量名。

1
2
MATCH (m:maker {name: 'minori'})-[r]->(g:Galgame)
RETURN g.title, type(r)

关系的属性

在图数据库中,关系不仅仅是线,它还可以像节点一样拥有属性。比如在 Galgame 数据库中,玩家和游戏的游玩关系上,通常会有通关日期或自己给游戏的评分。

1
2
3
MATCH (m:maker {name: 'minori'})-[:MAKED]->(g:Galgame)
WHERE g.score > 9.0
RETURN g.title

如何查找玩家在 2023 年玩过的 minori 游戏呢?双向排布关系试一下

1
2
3
MATCH (p:Player {name: 'Alice'})-[r:PLAYED]->(g:Galgame)<-[:MAKED]-(m:maker {name: 'minori'})
WHERE r.play_date >= 2023
RETURN g.title, r.play_date

复杂路径,像链条一样连接

你可以将多个关系串联起来,形成一条路径。这是 SQL 中多表 JOIN 的直观替代品。但是几乎没有人会这样写

1
2
3
MATCH (m:maker {name: 'minori'})-[:MAKED]->(g:Galgame)-[:HAS_TAG]->(t:Tag)<-[:HAS_TAG]-(other_g:Galgame)<-[:MAKED]-(other_m:maker)
WHERE m <> other_m
RETURN DISTINCT other_m.name

Cypher 创建

CREATE

CREATE 是最直接的创建指令

  • 创建单个节点

    虽然可以创建一个没有任何属性的空节点,但通常我们会同时指定标签和属性。

    1
    2
    CREATE (n:Person {name: 'Alice', age: 30})
    RETURN n
  • 创建多个节点

    你可以一次性创建多个不相关的节点。

    1
    2
    CREATE (a:Person {name: 'Bob'}), (b:Person {name: 'Charlie'})
    RETURN a, b
  • 创建关系

    在图数据库中,创建关系的同时,往往也会创建节点

    1
    2
    CREATE (a:Person {name: 'Alice'})-[:KNOWS {since: 2023}]->(b:Person {name: 'Bob'})
    RETURN a, b

MATCH + CREATE

基于现有数据创建就是 MATCH + CREATE,很多时候,我们不需要创建新节点,而是要给已经存在的节点建立关系。这时需要先用 MATCH 找到它们。

假设 Alice 和 Charlie 已经存在于数据库中,我们要建立关系:

1
2
3
MATCH (a:Person {name: 'Alice'}), (c:Person {name: 'Charlie'})
CREATE (a)-[:KNOWS]->(c)
RETURN a, c

MERGE

接下来说说防重创建

MERGEMATCHCREATE 的结合体。它的意思是:先试着匹配一下,如果找到了就用现有的;如果没找到,就创建一个新的。一般就是 SQL 的防重复创建的 NOT EXIST

1
2
MERGE (p:Person {name: 'Alice'})
RETURN p

如果数据库里已经有 Alice,它就返回那个 Alice;如果没有,它就新建一个 Alice

MERGE 的高级技巧:ON CREATE / ON MATCH

我们可以根据 MERGE 的结果(是创建了还是匹配了)来执行不同的操作,比如设置时间戳。

1
2
3
4
MERGE (n:Person {name: 'Alice'})
ON CREATE SET n.created = timestamp() -- 如果是新建的,设置创建时间
ON MATCH SET n.lastLogin = timestamp() -- 如果是已有的,更新最后登录时间
RETURN n

批量创建:UNWIND

如果你有一堆数据(比如一个列表或从 CSV 导入的数据)要创建,不要写循环,要用 UNWIND。它的作用是将一个列表展开成多行数据。

1
2
3
UNWIND ['Alice', 'Bob', 'Charlie'] AS name
CREATE (n:Person {name: name})
RETURN n

Cypher 更新

可以使用 SET 来更新与添加节点的属性,标签,关系等

例如,创建节点后,我们经常需要修改或添加属性。这通常与 MATCHCREATE 配合使用。

1
2
3
MATCH (n:Person {name: 'Alice'})
SET n.age = 31, n.city = 'New York'
RETURN n

标签也可以动态添加。

1
2
3
MATCH (n:Person {name: 'Alice'})
SET n:Developer
RETURN n

关系也是可以更新的。逻辑和更新节点一模一样。

1
2
3
4
// 找到 Alice 买 Laptop 的那条购买记录(关系),把状态改为“已发货”,并记录发货日期。
MATCH (u:User {name: 'Alice'})-[r:BOUGHT]->(p:Product {name: 'Laptop'})
SET r.status = 'Shipped', r.shipDate = date()
RETURN r

使用 += 进行增量更新,+=可以增量更新属性或者标签。如果你使用 =,就是进行完全替换了

  • 属性增量更新 当你想添加或更新多个属性,但又不想影响节点上其他现有属性时,+= 非常有用。它会合并你提供的属性 Map 和节点原有的属性。

    1
    2
    3
    MATCH (p:Person {name: 'Alice'})
    SET p += {age: 32, country: 'USA'}
    RETURN p
  • 添加标签 使用 += 可以为节点添加一个或多个新标签,同时保留其原有的所有标签。

    1
    2
    3
    MATCH (p:Person {name: 'Alice'})
    SET p += :Developer:Manager
    RETURN labels(p)

SET 不仅可以赋值,还可以进行数学运算和批量更新。

  • 数学运算更新

    你可以直接对现有的数值属性进行加减乘除,而不需要先查出来再算回去。

    1
    2
    3
    MATCH (p:Product {name: 'iPhone 15'})
    SET p.stock = p.stock - 1, p.price = p.price * 0.95
    RETURN p
  • 批量更新,配合 UNWIND,这是处理大量数据更新时最高效的方法。

    1
    2
    3
    4
    5
    6
    7
    UNWIND [
    {name: 'Alice', age: 31},
    {name: 'Bob', age: 28}
    ] AS row
    MATCH (p:Person {name: row.name})
    SET p.age = row.age
    RETURN p

如果你想一次性重置一个节点的所有属性(删除旧属性,只保留新设置的),可以使用直接赋值。

1
2
3
MATCH (p:Person {name: 'Alice'})
SET p = {name: 'Alice', age: 30, job: 'Engineer'}
RETURN p

和 MySQL 一样,在更新数据时,Cypher 会自动对涉及的节点加锁,以保证数据一致性。

Cypher 删除

删除属性和标签

如果你想删除某个属性,或者把值设为 null,应该使用 REMOVE 而不是 SET x = nullREMOVE 是专门用来“做减法”的。

1
2
3
MATCH (p:Person {name: 'Alice'})
REMOVE p.age
RETURN p

删除标签也类似

1
2
3
MATCH (p:Person {name: 'Alice'})
REMOVE p:Developer
RETURN labels(p)

而且可以同时删除属性和标签。

1
2
3
MATCH (p:Person {name: 'Alice'})
REMOVE p.age, p:Developer
RETURN p

删除关系

Cypher 不允许直接删除还有关系的节点,所以说,删除关系是用的最多的

创建的反义词是删除。但是,删除比较简单

1
2
MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})
DELETE r

删除节点

如上所述,删除节点必须先断连

1
2
MATCH (n:Person {name: 'Alice'})
DETACH DELETE n

Cypher 的约束

在 Cypher 中,约束的作用与在 SQL 中完全一致:它们都是用来强制保证数据库中数据的一致性、完整性和唯一性的规则。

Cypher 的约束,是基于标签和属性的,本质上是无模式的(Schema-less)。SQL 的约束是写时模式,在写入数据前必须先定义好表结构,而读时模式可以灵活写入数据,通过约束来逐步规范。

创建约束基本语法结构如下:

1
2
3
1CREATE CONSTRAINT constraint_name
2FOR (n:Label)
3REQUIRE n.property IS UNIQUE | IS NODE KEY | IS NOT NULL

当你需要移除一个约束时,使用 DROP 命令:

1
1DROP CONSTRAINT unique_person_name

你也可以使用 DROP CONSTRAINT IF EXISTS ... 来避免因约束不存在而报错。

你可以随时查询数据库中现有的所有约束:

1
1SHOW CONSTRAINTS

Cypher 的约束主要可以分为以下几类:

  1. 唯一性约束

    UNIQUE,和 SQL 一样,是最常见的一种约束,确保某个标签下的特定属性值不重复。

    1
    2
    3
    4
    // 创建约束
    CREATE CONSTRAINT unique_person_name
    FOR (p:Person)
    REQUIRE p.name IS UNIQUE
  2. 节点键约束

    这是 Cypher 中最接近 SQL 主键(PRIMARY KEY)的概念,类似于由多列组成的复合主键,但它更灵活。它确保一组属性的组合是唯一的,并且这些属性都不能为空(NOT NULL)。

    1
    2
    3
    4
    // 创建约束
    CREATE CONSTRAINT person_key
    FOR (p:Person)
    REQUIRE (p.firstName, p.lastName) IS NODE KEY
  3. 存在性约束

    这种约束确保某个标签的所有节点都必须包含指定的属性。

    1
    2
    3
    4
    // 创建约束
    CREATE CONSTRAINT person_id_exists
    FOR (p:Person)
    REQUIRE p.id IS NOT NULL
  4. 关系约束

    它允许你对关系本身施加约束,例如确保某种类型的关系必须拥有某个属性。就是让某种关系必须有这样的属性

    1
    2
    3
    4
    // 创建关系属性存在性约束
    CREATE CONSTRAINT relationship_likes_day_exists
    FOR ()-[r:LIKED]-()
    REQUIRE r.day IS NOT NULL

Cypher 索引

和 SQL 中的索引作用一样,都是用来加速查询的,而且 Neo4j 默认会为你创建一些基础索引

在图数据库中,查询通常分为两步

  1. 定位入口点:找到起始节点
  2. 遍历关系:从起点出发,沿着关系找到她的朋友。

索引的作用仅限于第一步。所以说,索引的作用是在定位起点的时候避免扫描数据库中的几百万个节点。

Neo4j 提供了多种索引类型,针对不同的数据场景。

  1. 范围索引

    这是默认的索引类型。适用于精确匹配=)、范围查询>, <)和前缀搜索STARTS WITH)。数据结构基于 B-Tree。

    1
    CREATE RANGE INDEX FOR (p:Person) ON (p.age)
  2. 文本索引

    专门用于字符串的快速查找。适用于 STARTS WITH 和精确匹配。不支持范围查询

    1
    CREATE TEXT INDEX FOR (p:Person) ON (p.email)
  3. 全文索引

    用于模糊搜索和复杂的文本分析。支持 CONTAINS(包含)、ENDS WITH(结尾)以及分词搜索。

    1
    CREATE FULLTEXT INDEX movieTitleIndex FOR (m:Movie) ON EACH [m.title]
  4. 点索引

    一般是专门用于地理空间数据

    1
    CREATE POINT INDEX FOR (l:Location) ON (l.coordinates)
  5. 查找索引

    这是系统默认存在的索引,用于快速查找节点标签(如 :Person)或关系类型。通常不需要手动创建,也不要删除它,否则全图扫描会变慢。

创建单属性索引

1
1CREATE INDEX person_name_index FOR (p:Person) ON (p.name)
  • Person 标签的 name 属性创建索引。

创建复合索引 (Composite Index)

如果一个查询经常同时过滤多个属性(例如:查找“姓张”且“年龄30岁”的人),复合索引效率极高。

1
1CREATE INDEX person_composite_index FOR (p:Person) ON (p.lastName, p.age)
  • 同样需要注意最左前缀原则:
    • 查询 WHERE p.lastName = 'Zhang' -> 会使用索引
    • 查询 WHERE p.lastName = 'Zhang' AND p.age = 30 -> 会使用索引
    • 查询 WHERE p.age = 30 -> 不会使用索引(因为它跳过了第一个属性)。

创建索引后,Cypher 的查询优化器通常会自动检测并使用它。你不需要在查询中显式指定索引

验证索引是否生效

和 MySQL 一样,使用 EXPLAINPROFILE 关键字查看查询计划。

1
1EXPLAIN MATCH (p:Person) WHERE p.name = 'Alice' RETURN p

注意不要让索引失效

查看所有索引

1
1SHOW INDEXES

这会列出数据库中所有的索引名称、类型、状态(ONLINE/OFFLINE)和关联的属性。

删除索引

如果索引不再需要,或者建错了,可以删除。

1
1DROP INDEX person_name_index

Cypher 速查表

操作 关键字 示例
查找 MATCH MATCH (n:Movie)
过滤 WHERE WHERE n.year > 2000
返回 RETURN RETURN n.title
创建 CREATE CREATE (n:Movie {title: 'New Movie'})
更新 SET SET n.rating = 5.0
合并 MERGE MERGE (n:Movie {title: 'New Movie'})
删除 DELETE DETACH DELETE n
关系 -[]-> (a)-[:LOVES]->(b)