性能调优
查询性能优化是数据库管理的核心技能之一。本章将介绍如何分析执行计划、识别性能瓶颈,以及使用索引和查询重写等技术来提升 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 提供了两个命令来分析查询:EXPLAIN 和 PROFILE。
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 | 说明 |
|---|---|---|
| NodeIndexSeek | 1 + 结果行数 | 索引查找 |
| 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 GB | 2-4 GB |
| 中型(100万-1000万节点) | 4-8 GB | 4-8 GB |
| 大型(> 1000万节点) | 8-16 GB | 8-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 管理扩展,用于详细性能监控
性能优化清单
查询优化
- 使用
EXPLAIN或PROFILE分析查询 - 确保查询从有索引的节点开始
- 只返回必要的属性
- 使用参数化查询
- 限制可变长度路径的深度
- 避免笛卡尔积
索引优化
- 为常用查询属性创建索引
- 为 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 查询性能优化的核心内容:
- 优化原则:尽早过滤、只返回需要的数据、使用参数
- 执行计划分析:使用
EXPLAIN和PROFILE识别问题 - 索引优化:为常用查询属性创建索引
- 查询重写:避免全表扫描和笛卡尔积
- 内存配置:合理设置堆内存和页缓存
- 监控诊断:使用 Neo4j 提供的工具监控性能
性能优化是一个持续的过程,需要根据实际数据和使用场景不断调整。建议定期使用 PROFILE 分析关键查询的性能,并根据执行计划进行优化。
参考资源
下一步
掌握了性能优化技巧后,可以查看 知识速查表 快速回顾所有重要概念和语法。