性能建议

始终指定目标数据库

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

良好实践

driver.executableQuery("<QUERY>")
    .withConfig(QueryConfig.builder().withDatabase("<DB NAME>").build())
    .execute();
driver.session(SessionConfig.builder().withDatabase("<DB NAME>").build());

不良实践

driver.executableQuery("<QUERY>")
    .execute();
driver.session();

了解事务的成本

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

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

优先考虑吞吐量而非数据完整性
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {
    for (int i=0; i<1000; i++) {
        session.run("<QUERY>");
    }
}
优先考虑数据完整性而非吞吐量
for (int i=0; i<1000; i++) {
    driver.executableQuery("<QUERY>").execute();
    // or session.executeRead/Write() calls
}

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

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

  • Driver.executableQuery()调用中使用.withRouting(RoutingControl.READ)方法

  • 使用Session.executeRead()而不是Session.executeWrite()(对于托管事务

  • 在创建新会话时使用.withRouting(RoutingControl.READ)方法(对于显式事务)。

良好实践

// import org.neo4j.driver.RoutingControl;

driver.executableQuery("MATCH (p:Person) RETURN p")
    .withConfig(QueryConfig.builder()
        .withDatabase("neo4j")
        .withRouting(RoutingControl.READ)
        .build())
    .execute();
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {
    session.executeRead(tx -> {
        var result = tx.run("MATCH (p:Person) RETURN p");
        return result.list();
    });
}

不良实践

// defaults to routing = writers
driver.executableQuery("MATCH (p:Person) RETURN p")
    .withConfig(QueryConfig.builder()
        .withDatabase("neo4j")
        .build())
    .execute();
// don't ask to write on a read-only operation
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {
    session.executeWrite(tx -> {
        var result = tx.run("MATCH (p:Person) RETURN p");
        return result.list();
    });
}

创建索引

为经常过滤的属性创建索引。例如,如果您经常根据name属性查找Person节点,则在Person.name上创建索引将很有帮助。您可以使用CREATE INDEX Cypher 子句为节点和关系创建索引。

在 Person.name 上创建索引
driver.executableQuery("CREATE INDEX person_name FOR (n:Person) ON (n.name)").execute();

有关更多信息,请参阅用于搜索性能的索引

分析查询

分析您的查询以查找可以提高性能的查询。您可以通过在查询前面加上PROFILE来分析查询。服务器输出可通过ResultSummary对象的.profile()方法获得。

var result = driver.executableQuery("PROFILE MATCH (p {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Alice"))
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute();
var queryPlan = result.summary().profile().arguments().get("string-representation");
System.out.println(queryPlan);

/*
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()方法获得。

var result = driver.executableQuery("EXPLAIN MATCH (p {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Alice"))
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute();
var queryPlan = result.summary().plan().arguments().get("string-representation");
System.out.println(queryPlan);

/*
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 → 标签表达式

良好实践

driver.executableQuery("MATCH (p:Person|Animal {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Alice"))
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute();
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {
    session.run("MATCH (p:Person|Animal {name: $name}) RETURN p", Map.of("name", "Alice"));
}

不良实践

driver.executableQuery("MATCH (p {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Alice"))
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute();
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {
    session.run("MATCH (p {name: $name}) RETURN p", Map.of("name", "Alice"));
}

批量数据创建

在使用WITHUNWINDCypher 子句创建大量记录时批量执行查询

良好实践

提交一个包含所有值的单个查询
// Generate a sequence of numbers
int start = 1;
int end = 10000;
List<Map> numbers = new ArrayList<>(end - start + 1);
for (int i=start; i<=end; i++) {
    numbers.add(Map.of("value", i));
}

driver.executableQuery("""
    UNWIND $numbers AS node
    CREATE (:Number {value: node.value})
    """)
    .withParameters(Map.of("numbers", numbers))
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute();

不良实践

提交许多单个查询,每个值一个
for (int i=1; i<=10000; i++) {
driver.executableQuery("CREATE (:Number {value: $value})")
    .withParameters(Map.of("value", i))
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute();
}
将大量数据首次导入到新数据库中最有效的方法是使用neo4j-admin database import命令。

使用查询参数

始终使用查询参数,而不是将值硬编码或连接到查询中。除了防止 Cypher 注入外,这还可以更好地利用数据库查询缓存。

良好实践

driver.executableQuery("MATCH (p:Person {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Alice"))
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute();
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {
    session.run("MATCH (p:Person {name: $name}) RETURN p", Map.of("name", "Alice"));
}

不良实践

driver.executableQuery("MATCH (p:Person {name: 'Alice'}) RETURN p")
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute();
// or
String name = "Alice";
driver.executableQuery("MATCH (p:Person {name: '" + name + "'}) RETURN p")
    .withConfig(QueryConfig.builder().withDatabase("neo4j").build())
    .execute();
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {
    session.run("MATCH (p:Person {name: 'Alice'}) RETURN p");
    // or
    String name = "Alice";
    session.run("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

Awesome Procedures On Cypher (APOC)是一个包含(许多)函数的库,这些函数本身无法用 Cypher 轻松表达。

Bolt

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

ACID

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

最终一致性

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

因果一致性

如果每个集群成员都以相同的顺序查看读写查询,则数据库是因果一致的。这比最终一致性更强。

NULL

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

事务

事务是一项工作单元,要么完全提交,要么在发生故障时回滚。例如银行转账:它涉及多个步骤,但这些步骤必须全部成功或被撤销,以避免从一个账户中扣款但未添加到另一个账户中。

背压

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

事务函数

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

驱动程序

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