性能建议

始终指定目标数据库

在所有查询中指定目标数据库,使用database参数,在Driver.executeQuery()调用中以及创建新的会话时。如果未提供数据库,驱动程序必须向服务器发送额外的请求以确定默认数据库是什么。对于单个查询,开销很小,但在数百个查询中会变得很大。

良好实践

await driver.executeQuery('<QUERY>', {}, {database: '<DB NAME>'})
driver.session({database: '<DB NAME>'})

不良实践

await driver.executeQuery('<QUERY>')
driver.session()

了解事务的成本

当通过.executeQuery()或通过.executeRead/Qrite()提交查询时,服务器会自动将它们包装到一个事务中。此行为确保数据库始终处于一致状态,无论事务执行过程中发生什么情况(断电、软件崩溃等)。

在多个查询周围创建安全执行上下文会产生在驱动程序只是向服务器发送查询并希望它们能够通过时不存在的开销。开销很小,但随着查询数量的增加而累积。因此,如果您的用例更重视吞吐量而不是数据完整性,则可以通过在单个(自动提交)事务中运行所有查询来提取更多性能。您可以通过创建一个会话并使用session.run()运行任意数量的查询来做到这一点。

优先考虑吞吐量而非数据完整性
let session = driver.session({database: '<DB NAME>'})
for(let i=0; i<1000; i++) {
    await session.run("<QUERY>")
}
session.close()
优先考虑数据完整性而非吞吐量
for(let i=0; i<1000; i++) {
    await driver.executeQuery("<QUERY>", {}, {database: '<DB NAME>'})
    // or session.executeRead/Write() calls
}

将读取查询路由到集群读取器

在集群中,将读取查询路由到辅助节点。您可以通过以下方式执行此操作:

良好实践

await driver.executeQuery(
  'MATCH (p:Person) RETURN p.name',
  {},
  {
    routing: 'READ',  // short for neo4j.routing.READ
    database: 'neo4j'
  }
)
let session = driver.session({ database: 'neo4j' })
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: 'neo4j'
  }
)
// defaults to routing = writers
let session = driver.session({ database: 'neo4j' })
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上创建索引将很有帮助。您可以使用CREATE INDEX Cypher 函数为节点和关系创建索引。

// Create an index on Person.name
await driver.executeQuery('CREATE INDEX personName FOR (n:Person) ON (n.name)')

有关更多信息,请参阅索引以提高搜索性能

分析查询

分析您的查询以找到可以提高性能的查询。您可以通过在查询前面加上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: '<DB 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: '<DB NAME>'})
await session.run(
  'MATCH (p {name: $name}) RETURN p',
  { name: 'Alice' }
)

批量数据创建

在创建大量记录时批量查询,使用WITHUNWIND Cypher 子句。

良好实践

numbers = []
for(let i=0; i<10000; i++) {
  numbers.push({value: Math.random()})
}
await driver.executeQuery(`
    WITH $numbers AS batch
    UNWIND batch AS node
    MERGE (n: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: '<DB 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: '<DB NAME>'})
await session.run(
  'MATCH (p:Person {name: "Alice"}) RETURN p',
  // or 'MATCH (p:Person {name: ' + name + '}) RETURN p'
  {}
)

并发

使用异步查询。如果您在应用程序中并行化复杂且耗时的查询,这可能会对性能产生更大的影响,但如果您运行许多简单的查询,则影响不大。

仅在需要时使用MERGE进行创建

Cypher 子句MERGE对于数据创建非常方便,因为它允许在给定模式的完全克隆存在时避免重复数据。但是,它需要数据库运行两个查询:它首先需要MATCH模式,然后才能CREATE它(如果需要)。

如果您已经知道要插入的数据是新的,请避免使用MERGE并直接使用CREATE代替——这实际上将数据库查询的数量减少了一半。

过滤通知

词汇表

LTS

长期支持版本是保证支持若干年的版本。Neo4j 4.4 是 LTS,Neo4j 5 也将具有 LTS 版本。

Aura

Aura 是 Neo4j 的完全托管云服务。它提供免费和付费计划。

Cypher

Cypher 是 Neo4j 的图查询语言,允许您从数据库中检索数据。它类似于 SQL,但适用于图。

APOC

Cypher 上的强大过程 (APOC) 是一个(许多)函数库,这些函数本身不容易用 Cypher 表示。

Bolt

Bolt 是 Neo4j 实例和驱动程序之间交互使用的协议。默认情况下,它侦听端口 7687。

ACID

原子性、一致性、隔离性、持久性 (ACID) 是保证数据库事务可靠处理的属性。符合 ACID 的 DBMS 确保数据库中的数据在发生故障时仍然准确且一致。

最终一致性

如果数据库保证所有集群成员将在某个时间点存储数据的最新版本,则该数据库最终一致。

因果一致性

如果读取和写入查询按相同的顺序由集群的每个成员看到,则数据库因果一致。这比最终一致性更强。

NULL

空标记不是类型,而是值不存在的占位符。有关更多信息,请参阅Cypher → 使用null

事务

事务是工作单元,要么全部提交,要么在失败时回滚。一个例子是银行转账:它涉及多个步骤,但它们必须全部成功或被撤销,以避免从一个账户中扣除资金但未添加到另一个账户中。

背压

背压是反对数据流动的力。它确保客户端不会被超出其处理能力的数据所淹没。

事务函数

事务函数是executeReadexecuteWrite调用执行的回调。如果服务器发生故障,驱动程序会自动重新执行回调。

驱动程序

一个 Driver 对象包含建立与 Neo4j 数据库连接所需的详细信息。