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
多条件组合,支持AND、OR、NOT。
MATCH (n:Person)
WHERE n.age >= 18 AND n.age <= 40
RETURN n.name
字符串操作包括STARTS WITH、ENDS WITH、CONTAINS、正则匹配等。
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 NULL和IS 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 IGNORE加UPDATE组合,但更加灵活。下面例子中表达“如果name为Alice的Person不存在则创建,存在则匹配”。
MERGE (n:Person {name: 'Alice'})
RETURN n
MERGE支持配合ON CREATE SET和ON MATCH SET来区分两种情况下的操作。下面例子表达“如果Alice不存在,创建时设置createdAt和age;如果已经存在,只更新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创建关系之前,两端的节点通常应该先用MATCH或MERGE确保存在,不然会凭空造出一堆重复节点,具体写法如下。
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
上面的查询如果没有WITH,WHERE就没有地方过滤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的写法几乎可以直接对着业务描述逐句翻译,图模式匹配的优势在这里得到了很好的体现。