Cypher查询语言

Cypher是Neo4j的声明式图查询语言,它专为图数据而设计。在SQL中,关联数据需要通过外键和JOIN操作临时拼凑出关联关系;而在Cypher中,关系(Relationship)是一等公民,它直接存储在数据库中,查询时我们直接匹配图结构,引擎便会沿着关系指针进行高效遍历,这种差异在查询深度关联时尤其明显。下面我们将系统性地梳理Cypher基本语法,以便快速上手编写图查询语句。

基本语法

节点和关系表示

Cypher的查询由模式组成,模式用于描述图的形状,它最基础的两个构件是节点关系

节点用一对圆括号表示,里面可以跟上变量、标签和属性。

()                      // 匿名节点
(p)                     // 节点变量p
(:Person)               // 标签为Person的节点
(p:Person {name: 'Alice'})  // 带标签和属性的节点,赋值给变量p

关系用方括号和短横线表示,方向用><表明。

-->                     // 有向关系,未指定类型
-[r]->                  // 关系变量r
-[:FRIEND]->            // 类型为FRIEND的关系
-[r:FRIEND {since: 2020}]-> // 带属性的关系,赋值给变量r

模式将这些构件组合在一起,形成类似ASCII图形的表达式,比如“Alice 认识 Bob”我们采用如下表达。

(:Person {name: 'Alice'})-[:KNOWS]->(:Person {name: 'Bob'})

CREATE 创建节点和关系

CREATE语句用于创建节点或关系,语法上我们只需要把图模式写出来即可,下面是一些例子。

创建一个节点。

CREATE (n:Person {name: 'Alice', age: 30})

创建多个节点。

CREATE (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})

创建关系需要先有节点,下面的写法会同时创建两个节点和它们之间的关系。

CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})

CREATE是无条件创建的,即使要创建的内容已经存在它也不会去匹配,而是永远都直接创建一个新的,如果你需要“不存在则创建存在则匹配”的语义,应该使用MERGE

MATCH 查询节点和关系

MATCH是Cypher中最核心的查询子句,它的作用是在图中寻找符合指定模式的数据,下面是一些例子。

查询所有Person节点。

MATCH (n:Person)
RETURN n

按属性查询。

MATCH (n:Person {name: 'Alice'})
RETURN n

查询两个节点之间的关系。

MATCH (a:Person)-[:KNOWS]->(b:Person)
RETURN a.name, b.name

查询多跳关系。

MATCH (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)
RETURN c.name

MATCH找不到匹配结果时不会报错而是返回空结果集,这和SQL中SELECT查不到数据时返回空行集合的行为是一致的。

RETURN 返回结果

RETURN决定了查询返回哪些内容,它支持返回整个节点、关系,也支持只返回特定属性,下面是一些例子。

返回整个节点对象。

MATCH (n:Person)
RETURN n

返回节点的具体属性。

MATCH (n:Person)
RETURN n.name, n.age

使用AS给返回列起别名。

MATCH (n:Person)
RETURN n.name AS name, n.age AS age

WHERE 条件过滤

WHERE用于在MATCH的结果上进行条件过滤,下面是一些例子。

基本比较。

MATCH (n:Person)
WHERE n.age > 25
RETURN n.name, n.age

多条件组合,支持ANDORNOT

MATCH (n:Person)
WHERE n.age >= 18 AND n.age <= 40
RETURN n.name

字符串操作包括STARTS WITHENDS WITHCONTAINS、正则匹配等。

MATCH (n:Person)
WHERE n.name STARTS WITH 'Al'
RETURN n.name
MATCH (n:Person)
WHERE n.name =~ 'Al.*'
RETURN n.name

IN操作符用于判断值是否在列表中。

MATCH (n:Person)
WHERE n.name IN ['Alice', 'Bob', 'Charlie']
RETURN n.name

空值判断,Cypher中写法为IS NULLIS NOT NULL

MATCH (n:Person)
WHERE n.email IS NOT NULL
RETURN n.name, n.email

这里有一个细节值得注意,Cypher中其实也可以把简单的属性过滤写在MATCH的节点模式里,如MATCH (n:Person {name: 'Alice'}),这和MATCH (n:Person) WHERE n.name = 'Alice'是完全等价的,但复杂条件(如比较、范围、字符串匹配、组合逻辑)必须放在WHERE子句中。

SET 修改属性

SET用于修改节点或关系的属性,通常配合MATCH使用,下面是一些例子。

修改单个属性。

MATCH (n:Person {name: 'Alice'})
SET n.age = 31

同时修改多个属性。

MATCH (n:Person {name: 'Alice'})
SET n.age = 31, n.email = 'alice@example.com'

前面写法表达的语义是对属性的独立赋值操作,Cypher还支持属性合并操作,下面例子使用+=,虽然最终效果与前面直接赋值相同,但它表达n的属性与{age: 31, email: 'alice@example.com'}这个Map合并,相关的属性会被更新,未指定的属性不会被修改。

MATCH (n:Person {name: 'Alice'})
SET n += {age: 31, email: 'alice@example.com'}

不过这里注意如果出现类似age: null的情况,Neo4j会删除该属性。

使用=能直接替换所有属性,这也意味着它会删除未指定的属性。

// 
MATCH (n:Person {name: 'Alice'})
SET n = {name: 'Alice', age: 31}

SET也可以用于给节点添加标签,写法例子如下。

MATCH (n:Person {name: 'Alice'})
SET n:VIP

REMOVE 移除属性或标签

REMOVE用于删除节点的属性或标签,下面是两个例子。

移除属性。

MATCH (n:Person {name: 'Alice'})
REMOVE n.email

移除标签。

MATCH (n:Person {name: 'Alice'})
REMOVE n:VIP

DELETE 删除节点和关系

DELETE用于删除节点或关系,使用前必须先MATCH到目标,下面例子删除了关系。

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

删除节点时,Neo4j要求节点上不能有任何关系存在,否则会报错。如果要删除节点及其所有关系,需要使用DETACH DELETE

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

MERGE 查找或创建

MERGE是Cypher中比较特殊的一个子句,它的语义是“如果存在则匹配,不存在则创建”,有点类似SQL中的INSERT OR IGNOREUPDATE组合,但更加灵活。下面例子中表达“如果name为Alice的Person不存在则创建,存在则匹配”。

MERGE (n:Person {name: 'Alice'})
RETURN n

MERGE支持配合ON CREATE SETON MATCH SET来区分两种情况下的操作。下面例子表达“如果Alice不存在,创建时设置createdAtage;如果已经存在,只更新updatedAt”。

MERGE (n:Person {name: 'Alice'})
ON CREATE SET n.createdAt = timestamp(), n.age = 30
ON MATCH SET n.updatedAt = timestamp()
RETURN n

MERGE同样可以用于关系。但这里要注意,我们不能直接写MERGE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'}),如果你这样做会发现MERGE又重新创建了一份Alice和Bob!使用MERGE创建关系时,它会尝试精确匹配整个模式(包括两端节点和关系类型),因此在用MERGE创建关系之前,两端的节点通常应该先用MATCHMERGE确保存在,不然会凭空造出一堆重复节点,具体写法如下。

MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
MERGE (a)-[:KNOWS]->(b)

聚合与统计

Cypher内置了一组聚合函数,用法和SQL中的聚合函数类似,下面是一些常用的例子。

统计数量。

MATCH (n:Person)
RETURN count(n)

统计非空属性数量。

MATCH (n:Person)
RETURN count(n.email)

求和、均值、最大值、最小值。

MATCH (n:Person)
RETURN sum(n.age), avg(n.age), max(n.age), min(n.age)

将属性值收集到列表。

MATCH (n:Person)
RETURN collect(n.name)

聚合通常配合分组使用,Cypher中分组是隐式的,RETURN子句中非聚合的列会自动成为分组依据。下面例子中,我们按城市统计Person数量。

MATCH (n:Person)
RETURN n.city, count(n) AS total
ORDER BY total DESC

DISTINCT用于去重,它可以和count组合使用。下面例子中,我们统计认识Alice的不重复的人数。

MATCH (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person)
RETURN count(DISTINCT b) AS friendCount

排序与分页

ORDER BY

ORDER BY用于对结果排序,支持升序(ASC,默认)和降序(DESC)。

MATCH (n:Person)
RETURN n.name, n.age
ORDER BY n.age DESC

Cypher还支持多列排序。

MATCH (n:Person)
RETURN n.name, n.city, n.age
ORDER BY n.city ASC, n.age DESC

SKIP和LIMIT

LIMIT限制返回记录数,SKIP跳过指定数量的记录,两者组合可以实现分页。下面例子实现只返回前5条。

MATCH (n:Person)
RETURN n.name
ORDER BY n.name
LIMIT 5

下面例子中,我们跳过前10条,取接下来的5条(即第3页,每页5条)。

MATCH (n:Person)
RETURN n.name
ORDER BY n.name
SKIP 10 LIMIT 5

路径查询

路径查询是Cypher中最强大的能力,它能让我们用极简单的语法表达在图中寻路的需求。

变长路径

前面我们看到的模式都是固定跳数的,当我们不知道两个节点之间有几跳时,可以用*表示变长路径。下面例子我们匹配1到3跳的KNOWS关系。

MATCH (a:Person {name: 'Alice'})-[:KNOWS*1..3]->(b:Person)
RETURN b.name

下面我们匹配任意跳数(慎用,在大图上可能性能较差)。

MATCH (a:Person {name: 'Alice'})-[:KNOWS*]->(b:Person)
RETURN b.name

最短路径

shortestPath()是Neo4j的内置函数,用于查找两个节点之间的最短路径。

MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Charlie'})
MATCH p = shortestPath((a)-[:KNOWS*]-(b))
RETURN p

注意shortestPath()内部的关系不指定方向,即使用-而不是->,这样可以双向搜索找到实际最短路径。返回的p是一个路径对象,包含了路径上所有的节点和关系。

最短路径可能有多条(等长),如果需要所有最短路径,可以使用allShortestPaths()

MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Charlie'})
MATCH p = allShortestPaths((a)-[:KNOWS*]-(b))
RETURN p

路径上的节点和关系

拿到路径对象p之后,我们可以用内置函数提取路径上的信息。

MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Charlie'})
MATCH p = shortestPath((a)-[:KNOWS*]-(b))
RETURN nodes(p) AS pathNodes,
       relationships(p) AS pathRelationships,
       length(p) AS pathLength
  • nodes(p):返回路径上所有节点的列表
  • relationships(p):返回路径上所有关系的列表
  • length(p):返回路径的跳数

常用内置函数

Cypher内置了丰富的函数,这里整理一些常用函数。

字符串函数

RETURN toUpper('hello')          // HELLO
RETURN toLower('HELLO')          // hello
RETURN trim('  hello  ')         // hello
RETURN substring('hello', 1, 3)  // ell
RETURN size('hello')             // 5
RETURN replace('hello', 'l', 'r') // herro
RETURN split('a,b,c', ',')       // ['a', 'b', 'c']

数值函数

RETURN abs(-5)      // 5
RETURN ceil(1.2)    // 2.0
RETURN floor(1.9)   // 1.0
RETURN round(1.5)   // 2.0
RETURN rand()       // 0到1之间的随机数

列表函数

RETURN size([1, 2, 3])           // 3
RETURN head([1, 2, 3])           // 1(第一个元素)
RETURN tail([1, 2, 3])           // [2, 3](除第一个外的其余元素)
RETURN last([1, 2, 3])           // 3(最后一个元素)
RETURN reverse([1, 2, 3])        // [3, 2, 1]
RETURN [x IN [1,2,3] WHERE x > 1] // [2, 3](列表推导式)

类型转换函数

RETURN toInteger('42')    // 42
RETURN toFloat('3.14')    // 3.14
RETURN toString(42)       // '42'
RETURN toBoolean('true')  // true

存在性检查

检查属性是否存在。

MATCH (n:Person)
WHERE n.email IS NOT NULL
RETURN n

WHERE EXISTS也可以用于检查模式是否存在。

WHERE EXISTS {
  MATCH (n)-[:KNOWS]->(:Person)
}

WITH 子查询

WITH是Cypher中非常重要但初学者容易忽略的子句。它的作用是将前一阶段的查询结果传递给下一阶段,同时可以做过滤、聚合和重命名,相当于一种“管道”或“子查询”的概念。下面例子中,我们先统计每个人的朋友数量,再筛选出朋友数大于3的人。

MATCH (n:Person)-[:KNOWS]->(friend:Person)
WITH n, count(friend) AS friendCount
WHERE friendCount > 3
RETURN n.name, friendCount
ORDER BY friendCount DESC

上面的查询如果没有WITHWHERE就没有地方过滤friendCount,因为count()是聚合函数,结果在RETURN之前是不可见的。

WITH也常用于在查询中间引入新的变量或限制结果集规模,下面例子中,我们先找到年龄最大的5个人,再查询他们各自认识的人。

MATCH (n:Person)
WITH n
ORDER BY n.age DESC
LIMIT 5
MATCH (n)-[:KNOWS]->(friend:Person)
RETURN n.name, friend.name

OPTIONAL MATCH 左外连接

OPTIONAL MATCH类似SQL中的LEFT JOIN,即使右侧没有匹配的数据,左侧的结果也会被保留,未匹配的部分以null填充。下面例子中,我们查询所有Person以及他们发表的文章(如果有)。

MATCH (n:Person)
OPTIONAL MATCH (n)-[:WROTE]->(post:Post)
RETURN n.name, post.title

如果某个Person没有发表过文章,post.title会是null,但这个Person仍然会出现在结果中,这和直接使用MATCH的行为完全不同。如果用MATCH,没有文章的Person会被直接过滤掉。

UNION 合并查询结果

UNION用于合并两个查询的结果,但会自动去重;UNION ALL合并结果但保留重复行。合并时两个查询的返回列名必须相同。

MATCH (n:Person {city: 'Beijing'})
RETURN n.name AS name
UNION
MATCH (n:Person {city: 'Shanghai'})
RETURN n.name AS name

一个综合示例

学完上面这些语法之后,我们来看个较为复杂的综合查询。例子中,在一个社交网络中,我们查询与Alice共同认识至少2位朋友的人,且这些人自己的朋友数量超过5,并按共同朋友数降序返回前10名。

MATCH (alice:Person {name: 'Alice'})-[:KNOWS]->(common:Person)<-[:KNOWS]-(other:Person)
WHERE other <> alice
WITH other, count(common) AS commonFriendCount
WHERE commonFriendCount >= 2
MATCH (other)-[:KNOWS]->(anyFriend:Person)
WITH other, commonFriendCount, count(anyFriend) AS totalFriendCount
WHERE totalFriendCount > 5
RETURN other.name AS name,
       commonFriendCount,
       totalFriendCount
ORDER BY commonFriendCount DESC
LIMIT 10

这段查询用SQL写出来至少需要两层子查询加两次JOIN,而Cypher的写法几乎可以直接对着业务描述逐句翻译,图模式匹配的优势在这里得到了很好的体现。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。