跳到主要内容

性能调优

查询性能优化是数据库管理的核心技能之一。本章将介绍如何分析执行计划、识别性能瓶颈,以及使用索引和查询重写等技术来提升 Neo4j 查询性能。

性能优化原则

在开始优化之前,理解以下核心原则非常重要:

1. 尽早过滤数据

查询应该尽可能早地减少数据量,避免在后续阶段处理不必要的行:

-- 不好的做法:先获取所有数据再过滤
MATCH (p:Person)
WHERE p.age > 25 AND p.city = '北京'
RETURN p

-- 好的做法:通过索引直接定位数据
MATCH (p:Person {city: '北京'})
WHERE p.age > 25
RETURN p

2. 只返回需要的数据

避免返回整个节点或关系,只选择必要的属性:

-- 不好的做法:返回整个节点
MATCH (p:Person)
RETURN p

-- 好的做法:只返回需要的属性
MATCH (p:Person)
RETURN p.name, p.age

3. 使用参数化查询

使用参数而不是字面值,可以让 Cypher 重用执行计划:

-- 不好的做法:硬编码值
MATCH (p:Person {name: '张三'})
RETURN p

-- 好的做法:使用参数
MATCH (p:Person {name: $name})
RETURN p

4. 限制可变长度路径的深度

无限制的可变长度路径可能导致遍历整个图:

-- 危险:可能遍历整个图
MATCH (a)-[:KNOWS*]-(b)
RETURN a, b

-- 安全:限制深度
MATCH (a)-[:KNOWS*1..5]-(b)
RETURN a, b

执行计划分析

Neo4j 提供了两个命令来分析查询:EXPLAINPROFILE

EXPLAIN:查看执行计划

EXPLAIN 只显示执行计划,不实际执行查询:

EXPLAIN MATCH (p:Person {name: '张三'})-[:FRIEND]->(friend)
RETURN friend.name

输出示例

+-------------------+----------------------------------------------------------+
| Operator | Details |
+-------------------+----------------------------------------------------------+
| +ProduceResults | friend.name |
| | +----------------------------------------------------------+
| +Projection | friend.name |
| | +----------------------------------------------------------+
| +Expand(All) | (p)-[r:FRIEND]->(friend) |
| | +----------------------------------------------------------+
| +NodeIndexSeek | p:Person(name = '张三') |
+-------------------+----------------------------------------------------------+

阅读执行计划的要点

  • 从下往上阅读
  • 底部是数据的起始点(叶子操作符)
  • 顶部是最终结果
  • Estimated Rows 列显示预估的行数

PROFILE:查看实际执行统计

PROFILE 实际执行查询并显示详细统计信息:

PROFILE MATCH (p:Person {name: '张三'})-[:FRIEND]->(friend)
RETURN friend.name

输出示例

+-------------------+------+---------+-----------+---------------+
| Operator | Id | Rows | DB Hits | Page Cache |
+-------------------+------+---------+-----------+---------------+
| +ProduceResults | 0 | 5 | 0 | 0 |
| | +------+---------+-----------+---------------+
| +Projection | 1 | 5 | 5 | 0 |
| | +------+---------+-----------+---------------+
| +Expand(All) | 2 | 5 | 11 | 2 |
| | +------+---------+-----------+---------------+
| +NodeIndexSeek | 3 | 1 | 2 | 1 |
+-------------------+------+---------+-----------+---------------+

Total database accesses: 18

关键指标

指标含义优化建议
Rows实际处理的行数越少越好
DB Hits数据库访问次数越少越好
Page Cache页缓存命中/未命中命中率高为佳
Time执行时间(毫秒)越短越好

常见操作符

操作符说明性能影响
NodeIndexSeek使用索引查找节点
NodeByLabelScan扫描标签下所有节点较慢
AllNodesScan扫描所有节点最慢
Expand(All)沿关系扩展取决于关系数量
Filter过滤数据取决于过滤条件
CartesianProduct笛卡尔积可能很慢
EagerAggregation聚合操作中等

识别性能问题

问题一:全表扫描

+-------------------+
| +AllNodesScan | ← 扫描所有节点,性能最差
+-------------------+

解决方案:添加索引或指定标签

-- 原始查询(全表扫描)
MATCH (n)
WHERE n.name = '张三'
RETURN n

-- 优化后(使用标签扫描)
MATCH (n:Person)
WHERE n.name = '张三'
RETURN n

-- 进一步优化(使用索引)
CREATE INDEX person_name FOR (p:Person) ON (p.name)

MATCH (n:Person {name: '张三'})
RETURN n

问题二:笛卡尔积

+-------------------+
| +CartesianProduct | ← 笛卡尔积,可能产生大量中间结果
+-------------------+

解决方案:使用单一模式替代多个独立模式

-- 不好的做法:两个独立模式产生笛卡尔积
MATCH (a:Person), (b:Person)
WHERE a.city = b.city AND a <> b
RETURN a, b

-- 好的做法:使用单一模式
MATCH (a:Person)-[:LIVES_IN]->(city)<-[:LIVES_IN]-(b:Person)
WHERE a <> b
RETURN a, b

索引优化

索引是提升查询性能最重要的手段之一。

何时创建索引

为以下场景创建索引:

  • WHERE 子句中频繁使用的属性
  • MATCH 子句中的属性匹配
  • MERGE 操作的匹配属性

索引类型

索引类型适用场景示例
B-tree 索引精确匹配、范围查询CREATE INDEX FOR (p:Person) ON (p.age)
全文索引文本搜索CALL db.index.fulltext.createNodeIndex(...)
查找索引点查找CREATE LOOKUP INDEX FOR (n) ON EACH labels(n)
文本索引字符串前缀/包含查询CREATE TEXT INDEX FOR (p:Person) ON (p.name)

创建和管理索引

-- 创建 B-tree 索引
CREATE INDEX person_name IF NOT EXISTS
FOR (p:Person) ON (p.name)

-- 创建复合索引
CREATE INDEX person_city_age IF NOT EXISTS
FOR (p:Person) ON (p.city, p.age)

-- 查看所有索引
SHOW INDEXES

-- 删除索引
DROP INDEX person_name IF EXISTS

复合索引的使用规则

复合索引遵循最左前缀原则:

-- 创建复合索引
CREATE INDEX person_city_age FOR (p:Person) ON (p.city, p.age)

-- 能使用索引
MATCH (p:Person) WHERE p.city = '北京' RETURN p
MATCH (p:Person) WHERE p.city = '北京' AND p.age > 25 RETURN p

-- 不能使用索引(跳过了第一个字段)
MATCH (p:Person) WHERE p.age > 25 RETURN p

索引使用提示

使用 USING INDEX 强制使用特定索引(谨慎使用):

MATCH (p:Person)
USING INDEX p:Person(name)
WHERE p.name = '张三'
RETURN p

执行计划深度分析

理解执行计划的输出对于优化查询至关重要。本节详细介绍如何解读执行计划中的各项指标。

执行计划输出详解

让我们通过一个具体例子来分析:

PROFILE MATCH (p:Person {city: '北京'})-[:FRIEND]->(friend)
WHERE friend.age > 25
RETURN p.name, friend.name, friend.age

典型输出结构

+-------------------+------+---------+-----------+---------------+-----------+
| Operator | Id | Rows | DB Hits | Page Cache | Time (ms) |
+-------------------+------+---------+-----------+---------------+-----------+
| +ProduceResults | 0 | 150 | 0 | 0/0 | 0.12 |
| | +------+---------+-----------+---------------+-----------+
| +Projection | 1 | 150 | 150 | 0/0 | 0.45 |
| | +------+---------+-----------+---------------+-----------+
| +Filter | 2 | 150 | 500 | 2/0 | 1.23 |
| | +------+---------+-----------+---------------+-----------+
| +Expand(All) | 3 | 500 | 1501 | 5/2 | 3.56 |
| | +------+---------+-----------+---------------+-----------+
| +NodeIndexSeek | 4 | 1000 | 1001 | 10/5 | 2.34 |
+-------------------+------+---------+-----------+---------------+-----------+

Total database accesses: 3152
Total page cache hits: 17
Total page cache misses: 7

关键指标解读

Rows(行数)

表示每个操作符实际处理的行数。这个值越接近最终结果越好。如果某个中间操作的行数远大于最终结果,说明存在不必要的计算。

-- 问题示例:中间结果过大
-- 如果 Expand(All) 返回 500 行,但 Filter 后只剩 150 行
-- 说明有 350 行被浪费了

DB Hits(数据库访问次数)

这是最重要的性能指标之一,表示操作符访问存储层的次数。每次访问存储层都有开销,应该尽量减少。

操作类型典型 DB Hits说明
NodeIndexSeek1 + 结果行数索引查找
NodeByLabelScan标签下节点数全标签扫描
AllNodesScan全库节点数全表扫描
Expand(All)起始行数 + 关系数关系扩展
Filter过滤前行数过滤操作

Page Cache(页缓存)

  • Hits(命中):数据已在内存中,访问速度快
  • Misses(未命中):需要从磁盘读取,访问速度慢

高命中率表示查询使用了缓存的数据,性能更好。低命中率可能是因为:

  • 数据不在缓存中(冷启动)
  • 缓存大小不足
  • 数据分布分散

Time(执行时间)

每个操作符的实际执行时间(毫秒)。这是判断瓶颈的直接依据。

识别性能瓶颈

场景一:全表扫描

+-------------------+---------+-----------+
| Operator | Rows | DB Hits |
+-------------------+---------+-----------+
| +Filter | 10 | 1000000 | ← 问题在这里
| | +---------+-----------+
| +AllNodesScan | 1000000 | 1000001 | ← 扫描了所有节点
+-------------------+---------+-----------+

问题分析:查询扫描了 100 万个节点,但最终只需要 10 个。这是典型的全表扫描问题。

解决方案

-- 原始查询(全表扫描)
MATCH (p)
WHERE p.name = '张三'
RETURN p

-- 优化方案 1:添加标签
MATCH (p:Person)
WHERE p.name = '张三'
RETURN p

-- 优化方案 2:添加索引
CREATE INDEX person_name FOR (p:Person) ON (p.name)

MATCH (p:Person {name: '张三'})
RETURN p

场景二:笛卡尔积

+-------------------+---------+-----------+
| Operator | Rows | DB Hits |
+-------------------+---------+-----------+
| +Apply | 50000 | 75000 |
| | +---------+-----------+
| +CartesianProduct | 50000 | 0 | ← 问题在这里
| |\ +---------+-----------+
| | +NodeByLabelScan| 500 | 501 |
| | +---------+-----------+
| +NodeByLabelScan | 100 | 101 |
+-------------------+---------+-----------+

问题分析:两个独立的 MATCH 模式产生了 500 × 100 = 50000 行的笛卡尔积。

解决方案

-- 原始查询(产生笛卡尔积)
MATCH (a:Person), (b:Person)
WHERE a.city = b.city AND a <> b
RETURN a, b

-- 优化:使用单一模式
MATCH (a:Person)-[:LIVES_IN]->(city)<-[:LIVES_IN]-(b:Person)
WHERE a <> b
RETURN a, b

-- 或者使用更高效的关系遍历
MATCH (a:Person)
WHERE a.city = '北京'
MATCH (a)-[:FRIEND]-(b:Person)
WHERE b.city = a.city AND a <> b
RETURN a, b

场景三:过度扩展

+-------------------+---------+-----------+
| Operator | Rows | DB Hits |
+-------------------+---------+-----------+
| +Expand(All) | 500000 | 1500000 | ← 扩展了太多关系
| | +---------+-----------+
| +NodeIndexSeek | 1 | 2 |
+-------------------+---------+-----------+

问题分析:从一个节点扩展出 50 万个关系,这通常意味着遇到了"超级节点"问题。

解决方案

-- 问题:用户有 50 万粉丝
MATCH (u:User {id: 'celebrity'})-[:FOLLOWED_BY]->(follower)
RETURN follower

-- 方案 1:限制结果
MATCH (u:User {id: 'celebrity'})-[:FOLLOWED_BY]->(follower)
RETURN follower
LIMIT 100

-- 方案 2:分批处理
MATCH (u:User {id: 'celebrity'})-[:FOLLOWED_BY]->(follower)
WHERE follower.createdAt > date('2024-01-01')
RETURN follower

-- 方案 3:重构数据模型(将大关系拆分)
MATCH (u:User {id: 'celebrity'})-[:HAS_FOLLOWER_LIST]->(list:FollowList {year: 2024})
-[:CONTAINS]->(follower)
RETURN follower

使用 EXPLAIN 进行预分析

对于复杂查询,先使用 EXPLAIN 查看执行计划,避免执行代价高昂的查询:

-- 在执行前检查计划
EXPLAIN MATCH (a)-[*]-(b)
WHERE a.name = '起点'
RETURN b

-- 如果看到 "VariableLengthPath" 且没有深度限制,应该修改
EXPLAIN MATCH (a)-[*1..5]-(b) -- 添加深度限制
WHERE a.name = '起点'
RETURN b

查询优化工具

Neo4j 提供了内置的查询日志分析工具:

-- 启用查询日志
CALL dbms.setConfigValue('dbms.logs.query.enabled', 'true')

-- 设置慢查询阈值(毫秒)
CALL dbms.setConfigValue('dbms.logs.query.threshold', '1000')

-- 查看最近的查询日志
CALL dbms.listQueries()
YIELD queryId, query, elapsedTime, cpuTime, pageHits, pageFaults
WHERE elapsedTime > 1000
RETURN queryId, query, elapsedTime, cpuTime, pageHits, pageFaults
ORDER BY elapsedTime DESC
LIMIT 10

查询重写优化

避免不必要的操作

-- 不好的做法:多次 MATCH 同一模式
MATCH (p:Person {name: '张三'})
MATCH (p)-[:FRIEND]->(f1)
MATCH (p)-[:COLLEAGUE]->(f2)
RETURN f1, f2

-- 好的做法:合并查询
MATCH (p:Person {name: '张三'})
OPTIONAL MATCH (p)-[:FRIEND]->(f1)
OPTIONAL MATCH (p)-[:COLLEAGUE]->(f2)
RETURN f1, f2

使用 WITH 优化查询

-- 使用 WITH 分割查询,减少中间结果
MATCH (p:Person)-[:FRIEND]->(friend)
WITH p, count(friend) AS friendCount
WHERE friendCount > 5
MATCH (p)-[:LIVES_IN]->(city)
RETURN p.name, city.name, friendCount

使用 EXISTS 替代返回结果

当只需要检查存在性时,使用 EXISTS 而不是返回实际数据:

-- 不好的做法:返回关系
MATCH (a:Person {name: '张三'})-[r:FRIEND]->(b:Person {name: '李四'})
RETURN r IS NOT NULL AS isFriend

-- 好的做法:使用 EXISTS
MATCH (a:Person {name: '张三'})
RETURN EXISTS((a)-[:FRIEND]->(:Person {name: '李四'})) AS isFriend

使用模式理解(Pattern Comprehension)

-- 传统写法
MATCH (p:Person {name: '张三'})
MATCH (p)-[:FRIEND]->(friend)
RETURN p.name, collect(friend.name) AS friends

-- 模式理解写法(更简洁高效)
MATCH (p:Person {name: '张三'})
RETURN p.name, [(p)-[:FRIEND]->(f) | f.name] AS friends

内存配置优化

堆内存配置

neo4j.conf 中配置堆内存:

# 初始堆大小
dbms.memory.heap.initial_size=4G

# 最大堆大小(建议不超过系统内存的 50%)
dbms.memory.heap.max_size=4G

页缓存配置

页缓存用于缓存图数据:

# 页缓存大小(建议设置为(可用内存 - 堆内存)的 50-70%)
dbms.memory.pagecache.size=4G

内存配置建议

数据规模堆内存页缓存
小型(< 100万节点)2-4 GB2-4 GB
中型(100万-1000万节点)4-8 GB4-8 GB
大型(> 1000万节点)8-16 GB8-16 GB

查询超时设置

为长时间运行的查询设置超时:

-- 设置查询超时(毫秒)
CALL dbms.setConfigValue('dbms.transaction.timeout', '30s')

-- 在查询中设置超时
CALL {
MATCH (n) RETURN n
} IN TRANSACTIONS
TIMEOUT 30 SECONDS

监控和诊断

查看运行中的查询

-- 查看当前运行的查询
CALL dbms.listQueries()
YIELD queryId, username, query, elapsedTime
RETURN queryId, username, query, elapsedTime
ORDER BY elapsedTime DESC

-- 终止长时间运行的查询
CALL dbms.killQuery('query-id')

查看数据库统计信息

-- 查看节点和关系数量
MATCH (n) RETURN count(n) AS nodeCount
MATCH ()-[r]->() RETURN count(r) AS relationshipCount

-- 查看标签统计
CALL db.stats.retrieve('GRAPH COUNTS')
YIELD data
RETURN data

使用 Neo4j 监控工具

Neo4j 提供了多种监控工具:

  • Neo4j Browser:查看查询计划和执行统计
  • Neo4j Ops Manager:企业级监控解决方案
  • JMX:Java 管理扩展,用于详细性能监控

性能优化清单

查询优化

  • 使用 EXPLAINPROFILE 分析查询
  • 确保查询从有索引的节点开始
  • 只返回必要的属性
  • 使用参数化查询
  • 限制可变长度路径的深度
  • 避免笛卡尔积

索引优化

  • 为常用查询属性创建索引
  • 为 MERGE 操作创建唯一约束
  • 使用复合索引优化多属性查询
  • 定期检查索引使用情况

配置优化

  • 根据数据规模调整堆内存
  • 配置合适的页缓存大小
  • 设置查询超时
  • 启用查询日志用于分析

常见性能问题案例

案例一:好友推荐查询优化

原始查询

MATCH (u:User {id: 'u001'})-[:FRIEND]-(friend)-[:FRIEND]-(potential)
WHERE NOT (u)-[:FRIEND]-(potential) AND u <> potential
RETURN potential, count(friend) AS commonFriends
ORDER BY commonFriends DESC
LIMIT 10

问题:没有限制路径深度,可能遍历大量节点

优化后

MATCH (u:User {id: 'u001'})-[:FRIEND]-(friend)
WITH u, friend
MATCH (friend)-[:FRIEND]-(potential)
WHERE NOT (u)-[:FRIEND]-(potential) AND u <> potential
WITH potential, count(DISTINCT friend) AS commonFriends
ORDER BY commonFriends DESC
LIMIT 10
RETURN potential, commonFriends

案例二:分页查询优化

原始查询

MATCH (p:Person)
RETURN p
ORDER BY p.name
SKIP 10000
LIMIT 10

问题:大 OFFSET 会导致扫描大量数据

优化后(使用游标分页):

MATCH (p:Person)
WHERE p.name > $lastSeenName
RETURN p
ORDER BY p.name
LIMIT 10

案例三:多层关系查询优化

场景:查询某人 3 度以内的人脉网络

原始查询

MATCH (p:Person {id: 'u001'})-[:FRIEND*1..3]-(connection)
RETURN DISTINCT connection

问题分析

  • 无限制地遍历所有路径
  • 即使已经找到目标,仍会继续遍历
  • 产生大量重复的中间结果

优化后

-- 方案 1:使用路径模式,明确区分每层
MATCH (p:Person {id: 'u001'})
OPTIONAL MATCH (p)-[:FRIEND]->(l1:Person)
OPTIONAL MATCH (l1)-[:FRIEND]->(l2:Person)
OPTIONAL MATCH (l2)-[:FRIEND]->(l3:Person)
WITH p, l1, l2, l3
UNWIND [l1, l2, l3] AS connection
WHERE connection IS NOT NULL
RETURN DISTINCT connection

-- 方案 2:使用子查询控制遍历
MATCH (p:Person {id: 'u001'})
CALL {
WITH p
MATCH (p)-[:FRIEND*1..3]-(connection)
RETURN DISTINCT connection
LIMIT 1000 -- 限制结果数量
}
RETURN connection

-- 方案 3:分批处理,适合大规模网络
MATCH (p:Person {id: 'u001'})-[:FRIEND]->(friend)
WITH p, collect(DISTINCT friend) AS level1
UNWIND level1 AS f1
MATCH (f1)-[:FRIEND]->(f2)
WHERE NOT f2 IN level1 AND f2 <> p
WITH p, level1, collect(DISTINCT f2) AS level2
UNWIND level2 AS f3
MATCH (f3)-[:FRIEND]->(f4)
WHERE NOT f4 IN level1 AND NOT f4 IN level2 AND f4 <> p
RETURN DISTINCT f4 AS level3Connection
LIMIT 100

案例四:聚合查询优化

场景:统计每个城市的用户数量和平均年龄

原始查询

MATCH (p:Person)
RETURN p.city AS city, count(p) AS count, avg(p.age) AS avgAge
ORDER BY count DESC

问题分析

  • 需要扫描所有 Person 节点
  • 如果 Person 节点很多,性能较差

优化方案

-- 方案 1:如果有城市索引,从城市开始
MATCH (c:City)<-[:LIVES_IN]-(p:Person)
RETURN c.name AS city, count(p) AS count, avg(p.age) AS avgAge
ORDER BY count DESC

-- 方案 2:使用存储的聚合数据(适合实时性要求不高的场景)
-- 预先计算并存储统计数据
MATCH (c:City)
SET c.population = size((c)<-[:LIVES_IN]-()),
c.avgAge = avg([(c)<-[:LIVES_IN]-(p) | p.age])

-- 查询时直接读取
MATCH (c:City)
RETURN c.name AS city, c.population AS count, c.avgAge AS avgAge
ORDER BY count DESC

案例五:复杂条件查询优化

场景:查找同时满足多个条件的用户

原始查询

MATCH (u:User)
WHERE u.age > 25
AND u.city IN ['北京', '上海', '广州']
AND u.status = 'active'
AND ANY(interest IN u.interests WHERE interest IN ['科技', '音乐'])
RETURN u

问题分析

  • 多个条件可能无法有效利用索引
  • ANY 条件难以优化

优化方案

-- 方案 1:将兴趣建模为节点,利用图遍历
MATCH (u:User)-[:INTERESTED_IN]->(interest:Interest)
WHERE interest.name IN ['科技', '音乐']
AND u.age > 25
AND u.city IN ['北京', '上海', '广州']
AND u.status = 'active'
RETURN DISTINCT u

-- 方案 2:创建复合索引(Neo4j 4.0+)
CREATE INDEX user_city_status FOR (u:User) ON (u.city, u.status)

-- 方案 3:使用全文索引处理兴趣搜索
CALL db.index.fulltext.createNodeIndex(
'userInterests',
['User'],
['interests']
)

CALL db.index.fulltext.queryNodes('userInterests', '科技 OR 音乐')
YIELD node AS u
WHERE u.age > 25
AND u.city IN ['北京', '上海', '广州']
AND u.status = 'active'
RETURN u

案例六:批量更新优化

场景:批量更新用户的积分

原始查询

// 在应用层循环执行多次
MATCH (u:User {id: $userId})
SET u.points = u.points + $points

问题分析

  • 每次更新都是单独的事务
  • 网络开销大
  • 无法利用批量优化

优化方案

-- 方案 1:使用 UNWIND 批量处理
UNWIND $updates AS update
MATCH (u:User {id: update.userId})
SET u.points = u.points + update.points

-- 方案 2:使用 CALL IN TRANSACTIONS 处理大批量
UNWIND $updates AS update
CALL {
WITH update
MATCH (u:User {id: update.userId})
SET u.points = u.points + update.points
} IN TRANSACTIONS OF 1000 ROWS

-- 方案 3:使用临时节点批量处理
CREATE (batch:UpdateBatch)
SET batch.updates = $updates
WITH batch
UNWIND batch.updates AS update
MATCH (u:User {id: update.userId})
SET u.points = u.points + update.points
WITH batch
DETACH DELETE batch

案例七:路径查询优化

场景:查找两个用户之间的最短路径

原始查询

MATCH path = shortestPath(
(a:User {id: 'u001'})-[*]-(b:User {id: 'u999'})
)
RETURN path

问题分析

  • 没有深度限制可能遍历整个图
  • 大图上性能极差

优化方案

-- 方案 1:添加深度限制
MATCH path = shortestPath(
(a:User {id: 'u001'})-[*1..10]-(b:User {id: 'u999'})
)
RETURN path, length(path) AS depth

-- 方案 2:使用 GDS 库的最短路径算法
MATCH (a:User {id: 'u001'}), (b:User {id: 'u999'})
CALL gds.shortestPath.dijkstra.stream('userGraph', {
sourceNode: a,
targetNode: b,
relationshipWeightProperty: 'weight'
})
YIELD nodeIds, costs
RETURN [id IN nodeIds | gds.util.asNode(id).name] AS path, costs[-1] AS totalCost

-- 方案 3:双向搜索(需要自定义逻辑)
// 先从起点搜索 3 层
MATCH (a:User {id: 'u001'})-[*1..3]-(mid)
WITH a, collect(DISTINCT mid) AS aSide
// 再从终点搜索 3 层
MATCH (b:User {id: 'u999'})-[*1..3]-(mid2)
WITH aSide, b, collect(DISTINCT mid2) AS bSide
// 找交集
WITH [n IN aSide WHERE n IN bSide] AS common
UNWIND common AS midPoint
MATCH path = shortestPath(
(a:User {id: 'u001'})-[*1..3]-(midPoint)-[*1..3]-(b:User {id: 'u999'})
)
RETURN path
LIMIT 1

小结

本章介绍了 Neo4j 查询性能优化的核心内容:

  1. 优化原则:尽早过滤、只返回需要的数据、使用参数
  2. 执行计划分析:使用 EXPLAINPROFILE 识别问题
  3. 索引优化:为常用查询属性创建索引
  4. 查询重写:避免全表扫描和笛卡尔积
  5. 内存配置:合理设置堆内存和页缓存
  6. 监控诊断:使用 Neo4j 提供的工具监控性能

性能优化是一个持续的过程,需要根据实际数据和使用场景不断调整。建议定期使用 PROFILE 分析关键查询的性能,并根据执行计划进行优化。

参考资源

下一步

掌握了性能优化技巧后,可以查看 知识速查表 快速回顾所有重要概念和语法。