性能建议

始终指定目标数据库

在所有查询中指定目标数据库,可以通过 ExecuteQuery() 中的 ExecuteQueryWithDatabase() 配置回调,或者在 创建新会话时使用 DatabaseName 配置参数。如果未提供数据库,驱动程序必须向服务器发送额外请求以确定默认数据库是什么。对于单个查询,开销很小,但对于数百个查询则变得显著。

良好实践

result, err := neo4j.ExecuteQuery(ctx, driver, "<QUERY>", nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
session := driver.NewSession(ctx, neo4j.SessionConfig{
    DatabaseName: "neo4j",
})

不良实践

result, err := neo4j.ExecuteQuery(ctx, driver, "<QUERY>", nil,
    neo4j.EagerResultTransformer)
session := driver.NewSession(ctx, neo4j.SessionConfig{})

注意事务的开销

通过 ExecuteQuery().ExecuteRead/Write() 提交查询时,服务器会自动将其包装成一个 事务。这种行为确保数据库始终处于一致状态,无论事务执行期间发生什么(断电、软件崩溃等)。

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

优先考虑吞吐量而非数据完整性
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
for i := 0; i < 10000; i++ {
    session.Run(ctx, "<QUERY>", nil)
}
优先考虑数据完整性而非吞吐量
for i := 0; i < 10000; i++ {
    neo4j.ExecuteQuery(ctx, driver, "<QUERY>", nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
    // or session.executeRead/Write() calls
}

不要一次性获取大量结果集

提交可能产生大量记录的查询时,不要一次性检索所有记录。Neo4j 服务器可以分批检索记录,并在记录可用时将其流式传输给驱动程序。惰性加载结果可以分散网络流量和内存使用。

为了方便起见,.ExecuteQuery() 总是立即检索所有结果记录(这也是 EagerResultEager 的含义)。要惰性加载结果,您必须使用 .ExecuteRead/Write()(或以其他形式手动处理 事务),并且不要在结果上调用 .Collect(ctx);而是迭代它。

示例 1. 立即加载与惰性加载的比较
立即加载 惰性加载
  • 服务器必须从存储中读取所有 250 条记录,然后才能将第一条记录发送给驱动程序(即客户端接收第一条记录需要更多时间)。

  • 在任何记录可供应用程序使用之前,驱动程序必须接收所有 250 条记录。

  • 客户端必须在内存中保存所有 250 条记录。

  • 服务器读取第一条记录并将其发送给驱动程序。

  • 一旦第一条记录传输完成,应用程序就可以处理记录。

  • 剩余记录的等待时间和资源消耗将推迟到应用程序请求更多记录时。

  • 服务器的获取时间可用于客户端处理。

  • 资源消耗受驱动程序获取大小的限制。

立即加载与惰性加载的时间和内存比较
package main

import (
    "context"
    "time"
    "fmt"
    "github.com/neo4j/neo4j-go-driver/v5/neo4j"
)

// Returns 250 records, each with properties
// - `output` (an expensive computation, to slow down retrieval)
// - `dummyData` (a list of 10000 ints, about 8 KB).
var slowQuery = `
UNWIND range(1, 250) AS s
RETURN reduce(s=s, x in range(1,1000000) | s + sin(toFloat(x))+cos(toFloat(x))) AS output,
range(1, 10000) AS dummyData
`
// Delay for each processed record
var sleepTime = "0.5s"

func main() {
    ctx := context.Background()
    dbUri := "{neo4j-database-uri}"
    dbUser := "{neo4j-username}"
    dbPassword := "{neo4j-password}"
    driver, err := neo4j.NewDriverWithContext(
        dbUri,
        neo4j.BasicAuth(dbUser, dbPassword, ""))
    if err != nil {
        panic(err)
    }
    defer driver.Close(ctx)

    err = driver.VerifyConnectivity(ctx)
    if err != nil {
        panic(err)
    }

    log("LAZY LOADING (executeRead)")
    lazyLoading(ctx, driver)

    log("EAGER LOADING (executeQuery)")
    eagerLoading(ctx, driver)
}

func lazyLoading(ctx context.Context, driver neo4j.DriverWithContext) {
    defer timer("lazyLoading")()

    sleepTimeParsed, err := time.ParseDuration(sleepTime)
    if err != nil {
        panic(err)
    }

    session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
    defer session.Close(ctx)
    session.ExecuteRead(ctx,
        func(tx neo4j.ManagedTransaction) (any, error) {
            log("Submit query")
            result, err := tx.Run(ctx, slowQuery, nil)
            if err != nil {
                return nil, err
            }
            for result.Next(ctx) != false {
                record := result.Record()
                output, _ := record.Get("output")
                log(fmt.Sprintf("Processing record %v", output))
                time.Sleep(sleepTimeParsed)  // proxy for some expensive operation
            }
            return nil, nil
        })
}

func eagerLoading(ctx context.Context, driver neo4j.DriverWithContext) {
    defer timer("eagerLoading")()

    log("Submit query")
    result, err := neo4j.ExecuteQuery(ctx, driver,
        slowQuery,
        nil,
        neo4j.EagerResultTransformer,
        neo4j.ExecuteQueryWithDatabase("neo4j"))
    if err != nil {
        panic(err)
    }

    sleepTimeParsed, err := time.ParseDuration(sleepTime)
    if err != nil {
        panic(err)
    }

    // Loop through results and do something with them
    for _, record := range result.Records {
        output, _ := record.Get("output")
        log(fmt.Sprintf("Processing record %v", output))
        time.Sleep(sleepTimeParsed)  // proxy for some expensive operation
    }
}

func log(msg string) {
    fmt.Println("[", time.Now().Unix(), "] ", msg)
}

func timer(name string) func() {
    start := time.Now()
    return func() {
        fmt.Printf("-- %s took %v --\n\n", name, time.Since(start))
    }
}
输出
[ 1718802595 ]  LAZY LOADING (executeRead)
[ 1718802595 ]  Submit query
[ 1718802595 ]  Processing record 0.5309371354666308  (1)
[ 1718802595 ]  Processing record 1.5309371354662915
[ 1718802596 ]  Processing record 2.5309371354663197
...
[ 1718802720 ]  Processing record 249.53093713547042
-- lazyLoading took 2m5.467064085s --

[ 1718802720 ]  EAGER LOADING (executeQuery)
[ 1718802720 ]  Submit query
[ 1718802744 ]  Processing record 0.5309371354666308  (2)
[ 1718802744 ]  Processing record 1.5309371354662915
[ 1718802745 ]  Processing record 2.5309371354663197
...
[ 1718802869 ]  Processing record 249.53093713547042
-- eagerLoading took 2m29.113482541s --  (3)
1 采用惰性加载,第一条记录可以更快地可用。
2 采用立即加载,第一条记录在结果被消费后(即服务器检索所有 250 条记录后)才可用。
3 立即加载的总运行时间更长,因为客户端会等待直到收到最后一条记录,而惰性加载时,客户端可以在服务器获取下一条记录的同时处理记录。使用惰性加载,客户端还可以在满足某些条件后停止请求记录(通过在 Result 对象上调用 .consume())以节省时间和资源。

驱动程序的 获取大小(fetch size)影响惰性加载的行为。它指示服务器流式传输与获取大小相等数量的记录,然后等待客户端处理完成后再检索和发送更多记录。

获取大小通常可以限制客户端的内存消耗,特别是对于记录大小方差较小的结果。如果单个记录非常大,驱动程序仍然需要为整个对象分配空间,因此即使获取大小很小,内存使用量也可能很大。

另一方面,获取大小并不总是能限制服务器端的内存消耗:这取决于查询。例如,带有 ORDER BY 子句的查询需要将整个结果集加载到内存中进行排序,然后才能将记录流式传输到客户端。

获取大小越小,客户端和服务器之间需要交换的消息越多。特别是当服务器延迟较高时,较小的获取大小可能会降低性能。

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

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

良好实践

result, err := neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p:Person) RETURN p", nil, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"),
    neo4j.ExecuteQueryWithReadersRouting())
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
result, err := session.ExecuteRead(ctx,
    func(tx neo4j.ManagedTransaction) (any, error) {
        return tx.Run(ctx, "MATCH (p:Person) RETURN p", nil)
    })

不良实践

result, err := neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p:Person) RETURN p", nil, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
    // defaults to routing = writers
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
result, err := session.ExecuteWrite(ctx,  // don't ask to write on a read-only operation
    func(tx neo4j.ManagedTransaction) (any, error) {
        return tx.Run(ctx, "MATCH (p:Person) RETURN p", nil)
    })

创建索引

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

// Create an index on Person.name
neo4j.ExecuteQuery(ctx, driver,
    "CREATE INDEX personName FOR (n:Person) ON (n.name)",
    nil, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))

更多信息,请参阅索引以提升搜索性能

剖析查询

剖析您的查询以找出可以改进性能的查询。您可以通过在查询前加上 PROFILE 来剖析查询。服务器输出可通过 ResultSummary 对象的 .Profile() 方法获取。

result, _ := neo4j.ExecuteQuery(ctx, driver,
    "PROFILE MATCH (p {name: $name}) RETURN p",
    map[string]any{
        "name": "Alice",
    },
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
fmt.Println(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() 方法获取。

result, _ := neo4j.ExecuteQuery(ctx, driver,
    "EXPLAIN MATCH (p {name: $name}) RETURN p",
    map[string]any{
        "name": "Alice",
    },
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
fmt.Println(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 → 标签表达式

良好实践

result, err := neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p:Person|Animal {name: $name}) RETURN p",
    map[string]any{
        "name": "Alice",
    }, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
result, err := session.Run(ctx,
    "MATCH (p:Person|Animal {name: $name}) RETURN p",
    map[string]any{
        "name": "Alice",
    })

不良实践

result, err := neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p {name: $name}) RETURN p",
    map[string]any{
        "name": "Alice",
    }, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
result, err := session.Run(ctx,
    "MATCH (p {name: $name}) RETURN p",
    map[string]any{
        "name": "Alice",
    })

批量数据创建

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

良好实践

numbers := make([]int, 10000)
for i := range numbers { numbers[i] = i }
neo4j.ExecuteQuery(ctx, driver, `
    UNWIND $numbers AS node
    MERGE (:Number {value: node.value})
    `, map[string]any{
        "numbers": numbers,
    }, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))

不良实践

for i := 0; i < 10000; i++ {
    neo4j.ExecuteQuery(ctx, driver,
        "MERGE (:Number {value: $value})",
        map[string]any{
            "value": i,
        }, neo4j.EagerResultTransformer,
        neo4j.ExecuteQueryWithDatabase("neo4j"))
}
将大量数据首次导入新数据库的最有效方式是使用 neo4j-admin database import 命令。

使用查询参数

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

良好实践

result, err := neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p:Person {name: $name}) RETURN p",
    map[string]any{
        "name": "Alice",
    }, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
session.Run(ctx, "MATCH (p:Person {name: $name}) RETURN p", map[string]any{
    "name": "Alice",
})

不良实践

result, err := neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p:Person {name: 'Alice'}) RETURN p",
    // or "MATCH (p:Person {name: '" + name + "'}) RETURN p"
    nil, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
session.Run(ctx, "MATCH (p:Person {name: $name}) RETURN p", nil)
           // 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

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

最终一致性

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

因果一致性

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

NULL

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

事务

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

反压

反压是阻止数据流动的力。它确保客户端不会因数据传输速度过快而超负荷。

事务函数

事务函数是由 ExecuteReadExecuteWrite 调用执行的回调。在服务器故障的情况下,驱动程序会自动重新执行该回调。

DriverWithContext

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

© . All rights reserved.