性能建议

始终指定目标数据库

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

最佳实践

driver.executableQuery("<QUERY>")
    .withConfig(QueryConfig.builder().withDatabase("{neo4j-database-name}").build())
    .execute();
driver.session(SessionConfig.builder().withDatabase("{neo4j-database-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>")
        .withConfig(QueryConfig.builder().withDatabase("{neo4j-database-name}").build())
        .execute();
    // or session.executeRead/Write() calls
}

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

在集群中,将读取查询路由到任何读取器节点。您可以通过以下方式实现:

最佳实践

// 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"));
}

批量数据创建

当创建大量记录时,使用 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("neo4j").build())
    .execute();

不良实践

提交大量单个查询,每个值一个
for (int i=1; i<=10000; i++) {
driver.executableQuery("MERGE (: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

长期支持(Long Term Support)版本是保证支持多年的版本。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 数据库连接所需的详细信息。

© . All rights reserved.