性能建议

始终指定目标数据库

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

最佳实践

driver.executableQuery("<QUERY>")
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
driver.session(SessionConfig.builder().withDatabase("<database-name>").build());

不良实践

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

了解事务的成本

当通过 .executableQuery().executeRead/Write() 提交查询时,驱动程序会将它们包装在 事务 中。这种行为确保了无论在事务执行期间发生什么(断电、软件崩溃等),数据库最终都处于一致的状态。作为进一步的鲁棒性层,驱动程序还会以指数退避的方式重试失败的事务。

围绕查询创建安全的执行上下文会产生一定的开销,虽然很小,但随着事务数量的增加会累积。当每个查询都作为单独的事务发送时,如果一个事务失败并需要回滚,所有其他事务都不会受到影响。就故障而言,这是最安全的执行模式,但由于事务开销随查询数量线性增加,这也是最慢的模式。

每个查询作为一个单独的事务(低吞吐量)
for (int i=0; i<1000; i++) {
    driver.executableQuery("<QUERY>")
        .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
        .execute();
    // or session.executeRead/Write() calls
}

一种性能更高的做法是将所有查询组合到一个事务中。这样,整个事务与其他事务隔离,但事务中的单个查询并不相互隔离,其中一个查询失败会导致所有查询回滚。

将查询组合为一个事务(高吞吐量)
try (var session = driver.session(SessionConfig.builder().withDatabase("<database-name>").build())) {
    session.executeWriteWithoutResult(tx -> {
        for (int i=0; i<1000; i++) {
            tx.run("<QUERY>");
        }
    });
}

一种更快的方法是跳过 .executeRead/Write(),直接在会话上调用 .run()。这些查询作为自动提交事务运行,并且仍然与其他并发查询隔离,但如果其中任何一个失败,它们将不会被重试。使用此方法,您用一部分鲁棒性换取了更高的吞吐量,因为查询会以服务器所能处理的最快速度发送过去。客户端规模的一个上限由连接池的大小决定:每次调用 .run() 都会借用一个连接,因此并行工作的数量受可用连接数的限制。

查询作为自动提交事务(最高吞吐量)
try (var session = driver.session(SessionConfig.builder().withDatabase("<database-name>").build())) {
    for (int i=0; i<1000; i++) {
        session.run("<QUERY>");
    }
}

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

在集群中,将读取查询路由到任何读取器节点。您可以执行以下操作:

  • 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("<database-name>")
        .withRouting(RoutingControl.READ)
        .build())
    .execute();
try (var session = driver.session(SessionConfig.builder().withDatabase("<database-name>").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("<database-name>")
        .build())
    .execute();
// don't ask to write on a read-only operation
try (var session = driver.session(SessionConfig.builder().withDatabase("<database-name>").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 queries)

分析您的查询 (Profile) 以定位可以优化性能的查询。您可以通过在查询前加上 PROFILE 来分析它们。服务器输出可通过 ResultSummary 对象的 .profile() 方法获取。

var result = driver.executableQuery("PROFILE MATCH (p {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Alice"))
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").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
*/

如果某些查询太慢,以至于您无法在合理的时间内运行它们,您可以将它们的前缀从 PROFILE 改为 EXPLAIN。这将返回服务器运行查询时会使用的执行计划,而无需实际执行它。服务器输出可通过 ResultSummary 对象的 .plan() 方法获取。

var result = driver.executableQuery("EXPLAIN MATCH (p {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Alice"))
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").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: ?
*/

指定节点标签

指定节点标签(node labels)。这使得查询规划器可以更高效地工作,并利用可用的索引。要了解如何组合标签,请参阅 Cypher → 标签表达式

最佳实践

driver.executableQuery("MATCH (p:Person|Animal {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Alice"))
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
try (var session = driver.session(SessionConfig.builder().withDatabase("<database-name>").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("<database-name>").build())
    .execute();
try (var session = driver.session(SessionConfig.builder().withDatabase("<database-name>").build())) {
    session.run("MATCH (p {name: $name}) RETURN p", Map.of("name", "Alice"));
}

批量创建数据

使用 WITHUNWIND Cypher 子句,在创建大量记录时对查询进行批处理

最佳实践

提交包含所有值的单个查询
// 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
    MERGE (:Number {value: node.value})
    """)
    .withParameters(Map.of("numbers", numbers))
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();

不良实践

提交许多单个查询,每个值对应一个查询
for (int i=1; i<=10000; i++) {
driver.executableQuery("MERGE (:Number {value: $value})")
    .withParameters(Map.of("value", i))
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").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("<database-name>").build())
    .execute();
try (var session = driver.session(SessionConfig.builder().withDatabase("<database-name>").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("<database-name>").build())
    .execute();
// or
String name = "Alice";
driver.executableQuery("MATCH (p:Person {name: '" + name + "'}) RETURN p")
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
try (var session = driver.session(SessionConfig.builder().withDatabase("<database-name>").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——这实际上将数据库查询的数量减半了。

过滤通知

过滤服务器应引发的通知的类别和/或严重级别。

切换到 Netty 本地传输 (Native Transports)

Netty 本地传输 添加了特定于某个平台的特性,产生的垃圾回收更少,并且与基于 NIO 的传输相比,通常能提高性能。

您需要在包管理器中提供有效的本地依赖项。驱动程序所依赖的 Netty 版本为 4.2,您为传输依赖项指定的 Netty 版本必须与此版本兼容。

使用 Maven 的本地传输依赖示例
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport-native-io_uring</artifactId>
    <version>${netty-version}</version>
    <classifier>linux-x86_64</classifier>
</dependency>

术语表

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

事务

事务是一个工作单元,要么被提交,要么在失败时被回滚。例如银行转账:它涉及多个步骤,但它们必须全部成功或全部撤销,以避免钱从一个账户扣除却未存入另一个账户的情况。

背压

背压是对数据流的抵抗力。它确保客户端不会被过快发送的数据压垮,从而超出其处理能力。

书签

书签是代表数据库某种状态的标记。通过将一个或多个书签与查询一起传递,服务器将确保在所表示的状态建立之前,该查询不会被执行。

事务函数

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

驱动程序 (Driver)

Driver 对象保存了与 Neo4j 数据库建立连接所需的详细信息。