性能建议
始终指定目标数据库
在所有查询中指定目标数据库,请使用 database 参数,无论是在调用 Driver.executeQuery() 时,还是在创建新的 会话 (sessions) 时。如果不提供数据库,驱动程序必须向服务器发送额外的请求以确定默认数据库。对于单个查询,这种开销微乎其微,但如果进行数百次查询,开销就会变得显著。
最佳实践
await driver.executeQuery('<QUERY>', {}, {database: '<database-name>'})
driver.session({database: '<database-name>'})
不良实践
await driver.executeQuery('<QUERY>')
driver.session()
了解事务的成本
当通过 .executeQuery() 或 .executeRead/Write() 提交查询时,驱动程序会将它们包装在一个 事务 中。这种行为确保数据库始终处于一致状态,无论事务执行期间发生什么(停电、软件崩溃等)。作为进一步的稳健性层,驱动程序还会以指数退避算法重试失败的事务。
围绕查询创建安全的执行上下文会产生一定的开销,虽然很小,但随着事务数量的增加会累积。当每个查询都作为单独的事务发送时,如果一个事务失败并需要回滚,所有其他事务都不会受到影响。就故障而言,这是最安全的执行模式,但由于事务开销随查询数量线性增加,这也是最慢的模式。
for(let i=0; i<1000; i++) {
await driver.executeQuery("<QUERY>", {}, {database: '<database-name>'})
// or session.executeRead/Write() calls
}
一种性能更高的做法是将所有查询组合到一个事务中。这样,整个事务与其他事务隔离,但事务中的单个查询并不相互隔离,其中一个查询失败会导致所有查询回滚。
let session = driver.session({ database: '<database-name>' })
await session.executeRead(async tx => {
for(let i=0; i<1000; i++) {
await tx.run('<QUERY>')
}
})
一种更快的做法是跳过 .executeRead/Write() 并直接在会话上调用 .run()。查询将作为自动提交事务运行,它们仍然与其他并发查询隔离,但如果其中任何一个失败,它们将不会被重试。使用此方法,您是用一定的稳健性换取了更高的吞吐量,因为查询会以服务器所能处理的最快速度发送过去。客户端规模的一个上限由连接池大小决定:每次调用 .run() 都会借用一个连接,因此并行工作的数量受限于可用连接的数量。
let session = driver.session({database: '<database-name>'})
for(let i=0; i<1000; i++) {
await session.run("<QUERY>")
}
session.close()
将读取查询路由到集群读取器
在集群中,将读取查询路由到任何读取器节点。您可以执行以下操作:
-
在
Driver.executeQuery()调用中将routing: READ设置为配置 -
使用
Session.executeRead()而不是Session.executeWrite()(适用于托管事务) -
在创建新会话时设置
AccessMode: neo4j.AccessModeRead(适用于显式事务)。
最佳实践
await driver.executeQuery(
'MATCH (p:Person) RETURN p.name',
{},
{
routing: 'READ', // short for neo4j.routing.READ
database: '<database-name>'
}
)
let session = driver.session({ database: '<database-name>' })
await session.executeRead(async tx => {
return await tx.run('MATCH (p:Person) RETURN p.name', {})
})
不良实践
await driver.executeQuery(
'MATCH (p:Person) RETURN p.name',
{},
{
database: '<database-name>'
}
)
// defaults to routing = writers
let session = driver.session({ database: '<database-name>' })
await session.executeRead(async tx => {
return await tx.run('MATCH (p:Person) RETURN p.name', {})
})
// don't ask to write on a read-only operation
创建索引
为经常过滤的属性创建索引。例如,如果您经常通过 name 属性查找 Person 节点,则为 Person.name 创建索引是有利的。您可以使用 Cypher 的 CREATE INDEX 函数为节点和关系创建索引。
// Create an index on Person.name
await driver.executeQuery('CREATE INDEX personName FOR (n:Person) ON (n.name)')
有关更多信息,请参阅 搜索性能索引。
分析查询 (Profile queries)
分析您的查询 以定位可以提高性能的查询。您可以通过在查询前加上 PROFILE 来分析查询。服务器的输出可在 ResultSummary 对象的 profile 属性中找到。
const result = await driver.executeQuery('PROFILE MATCH (p {name: $name}) RETURN p', { name: 'Alice' })
console.log(result.summary.profile.arguments['string-representation'])
/*
Planner COST
Runtime PIPELINED
Runtime version 5.0
Batch size 128
+-----------------+----------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+
| Operator | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline |
+-----------------+----------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+
| +ProduceResults | p | 1 | 1 | 3 | | | | |
| | +----------------+----------------+------+---------+----------------+ | | |
| +Filter | p.name = $name | 1 | 1 | 4 | | | | |
| | +----------------+----------------+------+---------+----------------+ | | |
| +AllNodesScan | p | 10 | 4 | 5 | 120 | 9160/0 | 108.923 | Fused in Pipeline 0 |
+-----------------+----------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+
Total database accesses: 12, total allocated memory: 184
*/
如果某些查询速度太慢,以至于无法在合理的时间内运行,您可以改用 EXPLAIN 而不是 PROFILE。这将返回服务器运行查询所使用的计划,而无需实际执行它。服务器的输出可在 ResultSummary 对象的 plan 属性中找到。
const result = await driver.executeQuery('EXPLAIN MATCH (p {name: $name}) RETURN p', { name: 'Alice' })
console.log(result.summary.plan.arguments['string-representation'])
/*
Planner COST
Runtime PIPELINED
Runtime version 5.0
Batch size 128
+-----------------+----------------+----------------+---------------------+
| Operator | Details | Estimated Rows | Pipeline |
+-----------------+----------------+----------------+---------------------+
| +ProduceResults | p | 1 | |
| | +----------------+----------------+ |
| +Filter | p.name = $name | 1 | |
| | +----------------+----------------+ |
| +AllNodesScan | p | 10 | Fused in Pipeline 0 |
+-----------------+----------------+----------------+---------------------+
Total database accesses: ?
*/
指定节点标签
在所有查询中指定节点标签。这使得查询规划器能够更高效地工作,并利用现有的索引。要了解如何组合标签,请参阅 Cypher → 标签表达式。
最佳实践
await driver.executeQuery(
'MATCH (p:Person|Animal {name: $name}) RETURN p',
{ name: 'Alice' }
)
let session = driver.session({database: '<database-name>'})
await session.run(
'MATCH (p:Person|Animal {name: $name}) RETURN p',
{ name: 'Alice' }
)
不良实践
await driver.executeQuery(
'MATCH (p {name: $name}) RETURN p',
{ name: 'Alice' }
)
let session = driver.session({database: '<database-name>'})
await session.run(
'MATCH (p {name: $name}) RETURN p',
{ name: 'Alice' }
)
批量创建数据
最佳实践
numbers = []
for(let i=0; i<10000; i++) {
numbers.push({value: Math.random()})
}
await driver.executeQuery(`
UNWIND $numbers AS node
MERGE (:Number {value: node.value})
`, { numbers: numbers }
)
不良实践
for(let i=0; i<10000; i++) {
await driver.executeQuery(
'MERGE (:Number {value: $value})',
{ value: Math.random() }
)
}
将大量数据首次导入新数据库的最有效方式是使用 neo4j-admin database import 命令。 |
使用查询参数
使用 查询参数,而不是将值硬编码或连接到查询中。这使得数据库可以利用查询缓存。
最佳实践
await driver.executeQuery(
'MATCH (p:Person {name: $name}) RETURN p',
{ name: 'Alice' } // query parameters
)
let session = driver.session({database: '<database-name>'})
await session.run(
'MATCH (p:Person {name: $name}) RETURN p',
{ name: 'Alice' } // query parameters
)
不良实践
await driver.executeQuery('MATCH (p:Person {name: "Alice"}) RETURN p')
let name = "Alice"
await driver.executeQuery('MATCH (p:Person {name: "' + name + '"}) RETURN p')
let session = driver.session({database: '<database-name>'})
await session.run(
'MATCH (p:Person {name: "Alice"}) RETURN p',
// or 'MATCH (p:Person {name: ' + name + '}) RETURN p'
{}
)
并发
使用 异步查询。如果您在应用程序中并行化复杂且耗时的查询,这对性能的影响可能会更大,但如果您运行许多简单查询,则影响不大。
过滤通知
过滤服务器应引发的通知的类别和/或严重级别。
术语表
- LTS (长期支持版)
-
长期支持 (Long Term Support) 版本是保证在若干年内得到支持的版本。Neo4j 4.4 和 5.26 是 LTS 版本。
- Aura
-
Aura 是 Neo4j 的全托管云服务。它提供免费和付费计划。
- Cypher
-
Cypher 是 Neo4j 的图查询语言,允许您从数据库中检索数据。它就像 SQL,但专用于图数据库。
- APOC
-
Awesome Procedures On Cypher (APOC) 是一个包含(许多)函数的库,这些函数在 Cypher 本身中难以轻松实现。
- Bolt
-
Bolt 是用于 Neo4j 实例和驱动程序之间交互的协议。默认监听 7687 端口。
- ACID
-
原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation)、持久性 (Durability) (ACID) 是保证数据库事务可靠处理的属性。符合 ACID 的 DBMS 确保即使发生故障,数据库中的数据也能保持准确和一致。
- 最终一致性
-
如果一个数据库能保证所有集群成员在某个时间点都存储了数据的最新版本,则该数据库具有最终一致性。
- 因果一致性
-
如果读写查询被集群中的每个成员以相同的顺序看到,则数据库具有因果一致性。这比最终一致性更强。
- NULL
-
空标记不是一种类型,而是缺失值的占位符。更多信息,请参阅 Cypher → 使用
null。 - 事务
-
事务是一个工作单元,要么被提交,要么在失败时被回滚。例如银行转账:它涉及多个步骤,但它们必须全部成功或全部撤销,以避免钱从一个账户扣除却未存入另一个账户的情况。
- 背压
-
背压是对数据流的抵抗力。它确保客户端不会被过快发送的数据压垮,从而超出其处理能力。
- 书签
-
书签是代表数据库某种状态的标记。通过将一个或多个书签与查询一起传递,服务器将确保在所表示的状态建立之前,该查询不会被执行。
- 事务函数
-
事务函数是由
executeRead或executeWrite调用执行的回调。如果发生服务器故障,驱动程序会自动重新执行该回调。 - 驱动程序 (Driver)
-
Driver对象保存了与 Neo4j 数据库建立连接所需的详细信息。