空间重用

Neo4j 使用逻辑删除来移除数据库中的数据,以实现卓越的性能和可扩展性。逻辑删除意味着所有相关记录被标记为已删除,但它们所占用的空间并不会立即归还给操作系统。相反,这些空间随后会被用于创建数据的事务中。

将记录标记为已删除需要向 事务日志文件 写入记录更新命令,这与创建或更新数据时相同。因此,当删除大量数据时,会导致特定数据库的存储使用量增加,因为 Neo4j 需要将所有已删除节点、其属性及关系的相关记录写入事务日志。

请注意,当对大量节点执行 DETACH DELETE 时,这些删除操作在内存中事务状态和事务日志中所占用的空间可能会超出您的预期。

事务最终会从 事务日志文件 中被修剪掉,从而使日志的存储使用量恢复到预期水平。另一方面,存储文件(store files)在删除数据时不会缩小。已删除记录占用的空间会保留在存储文件中。在这些空间被重用之前,存储文件会呈现稀疏和碎片化状态,但这对性能的影响通常微乎其微。

ID 文件

Neo4j 使用 .id 文件来管理可重用的空间。这些文件包含各自存储文件中所有已删除记录的 ID 集合。记录的 ID 在存储文件中唯一标识该记录。例如,根据 存储格式 的不同,所有已删除节点的 ID 包含在 neostore.nodestore.db.idblock.x1.db.id 中。

这些 .id 文件作为与其交互的写事务的一部分进行维护。当写事务提交删除操作时,记录的 ID 会被缓冲在内存中。该缓冲区会追踪所有重叠且未完成的事务。当这些事务完成后,ID 便可供重用。

缓冲的 ID 会作为检查点(checkpointing)过程的一部分被刷新到 .id 文件中。同时,.id 文件的变更(ID 的添加和移除)可以从事务命令中推导出来。通过这种方式,恢复过程确保 .id 文件始终与其存储文件保持同步。同样的过程也确保了集群数据库能够实现精确且事务性的空间重用。

如果您想缩小数据库的大小,请不要删除 .id 文件。存储文件必须仅由 Neo4j 数据库和 neo4j-admin 工具进行修改。

块格式中大属性值的空间重用

从 Neo4j 2026.01 版本开始,当分配大小为 N 个单元的大数值时,如果没有可用的对应 ID,该记录将被拆分为多个较小的记录。这保证了存储在 block.big_values.db 存储文件中的大属性值能够重用空间,但在某些情况下会牺牲局部性。可接受的碎片化程度是动态的,取决于未使用记录的比例,例如:如果未使用空间 < 5%,则不会产生碎片;如果未使用比例稍高,则会接受一定程度的碎片化。如果未使用比例很高,记录可能会被进一步拆分。大多数写负载都能从这种改进的空间重用中受益,特别是那些具有“倾斜”分配大小的负载,例如分配大小随时间持续增长的情况。有关大属性值存储的更多详细信息,请参阅 块格式存储文件

回收未使用空间

您可以使用 neo4j-admin database copy 命令来创建数据库的碎片整理副本。copy 命令会创建一个全新的、独立的数据库。如果您想在集群中运行该数据库,则必须重新播种现有集群,或者使用该副本 播种 一个新集群。

示例 1. 使用 neo4j-admin database copy 进行数据库压缩的示例

以下是关于如何检查数据库存储使用情况以及如何回收空间的详细示例。

让我们使用 Cypher Shell 命令行工具添加 10 万个节点,然后查看它们占用了多少存储空间。

  1. 在运行中的 Neo4j 独立实例中,使用您的凭据登录到 Cypher Shell 命令行工具。

    bin/cypher-shell -u neo4j -p <password>
    Connected to Neo4j at neo4j://:7687 as user neo4j.
    Type :help for a list of available commands or :exit to exit the shell.
    Note that Cypher queries must end with a semicolon.
  2. 使用以下命令向 neo4j 数据库添加 10 万个节点

    neo4j@neo4j> foreach (x in range (1,100000) | create (n:testnode1 {id:x}));
    0 rows available after 1071 ms, consumed after another 0 ms
    Added 100000 nodes, Set 100000 properties, Added 100000 labels
  3. 检查分配的 ID 范围

    neo4j@neo4j> MATCH (n:testnode1) RETURN ID(n) as ID order by ID limit 5;
    +----+
    | ID |
    +----+
    | 0  |
    | 1  |
    | 2  |
    | 3  |
    | 4  |
    +----+
    
    5 rows available after 171 ms, consumed after another 84 ms
  4. 运行 call db.checkpoint() 过程以强制执行检查点。

    neo4j@neo4j> call db.checkpoint();
    +-----------------------------------+
    | success | message                 |
    +-----------------------------------+
    | TRUE    | "Checkpoint completed." |
    +-----------------------------------+
    
    1 row available after 18 ms, consumed after another 407 ms
  5. 在 Neo4j Browser 中,运行 :sysinfo 来检查 neo4j 的总存储大小。

    报告的存储大小输出为 791.92 KiB,ID 分配:节点 ID 100000,属性 ID 100000。

  6. 删除上述创建的节点。

    neo4j@neo4j> Match (n) detach delete n;
  7. 再次运行 call db.checkpoint() 过程。

    neo4j@neo4j> call db.checkpoint();
    +-----------------------------------+
    | success | message                 |
    +-----------------------------------+
    | TRUE    | "Checkpoint completed." |
    +-----------------------------------+
    
    1 row available after 18 ms, consumed after another 407 ms
  8. 在 Neo4j Browser 中,运行 :sysinfo 来检查 neo4j 的总存储大小。

    报告的存储大小输出为 31.01 MiB,ID 分配:节点 ID 100000,属性 ID 100000。

    默认情况下,检查点会将页面缓存(pagecache)中任何已缓存的更新刷新到存储文件中。因此,分配的 ID 保持不变,且尽管删除了数据,存储大小仍会增加或保持不变(如果实例重启)。在生产数据库中,由于频繁执行大量的加载/删除操作,结果会导致存储文件占用大量未使用的空间。

要回收该未使用的空间,您可以使用 neo4j-admin database copy 命令来创建数据库的碎片整理副本。在运行该命令之前,请使用 system 数据库并停止 neo4j 数据库。

  1. 调用 neo4j-admin database copy 命令来创建 neo4j 数据库的副本。

    bin/neo4j-admin database copy neo4j neo4jcopy1 --compact-node-store --verbose
    Starting to copy store, output will be saved to: $neo4j_home/logs/neo4j-admin-copy-2020-11-04.11.30.57.log
    2020-10-23 11:40:00.749+0000 INFO [StoreCopy] ### Copy Data ###
    2020-10-23 11:40:00.750+0000 INFO [StoreCopy] Source: $neo4j_home/data/databases/neo4j (page cache 8m) (page cache 8m)
    2020-10-23 11:40:00.750+0000 INFO [StoreCopy] Target: $neo4j_home/data/databases/neo4jcopy1 (page cache 8m)
    2020-10-23 11:40:00.750+0000 INFO [StoreCopy] Empty database created, will start importing readable data from the source.
    2020-10-23 11:40:02.397+0000 INFO [o.n.i.b.ImportLogic] Import starting
    Nodes, started 2020-11-04 11:31:00.088+0000
    [*Nodes:?? 7.969MiB---------------------------------------------------------------------------] 100K ∆ 100K
    Done in 632ms
    Prepare node index, started 2020-11-04 11:31:00.735+0000
    [*DETECT:7.969MiB-----------------------------------------------------------------------------]    0 ∆    0
    Done in 79ms
    Relationships, started 2020-11-04 11:31:00.819+0000
    [*Relationships:?? 7.969MiB-------------------------------------------------------------------]    0 ∆    0
    Done in 37ms
    Node Degrees, started 2020-11-04 11:31:01.162+0000
    [*>:??----------------------------------------------------------------------------------------]    0 ∆    0
    Done in 12ms
    Relationship --> Relationship 1/1, started 2020-11-04 11:31:01.207+0000
    [*>:??----------------------------------------------------------------------------------------]    0 ∆    0
    Done in 0ms
    RelationshipGroup 1/1, started 2020-11-04 11:31:01.232+0000
    [*>:??----------------------------------------------------------------------------------------]    0 ∆    0
    Done in 10ms
    Node --> Relationship, started 2020-11-04 11:31:01.245+0000
    [*>:??----------------------------------------------------------------------------------------]    0 ∆    0
    Done in 10ms
    Relationship <-- Relationship 1/1, started 2020-11-04 11:31:01.287+0000
    [*>:??----------------------------------------------------------------------------------------]    0 ∆    0
    Done in 0ms
    Count groups, started 2020-11-04 11:31:01.549+0000
    [*>:??----------------------------------------------------------------------------------------]    0 ∆    0
    Done in 0ms
    Node --> Group, started 2020-11-04 11:31:01.579+0000
    [*>:??----------------------------------------------------------------------------------------]    0 ∆    0
    Done in 1ms
    Node counts and label index build, started 2020-11-04 11:31:01.986+0000
    [*>:??----------------------------------------------------------------------------------------]    0 ∆    0
    Done in 11ms
    Relationship counts, started 2020-11-04 11:31:02.034+0000
    [*>:??----------------------------------------------------------------------------------------]    0 ∆    0
    Done in 0ms
    
    IMPORT DONE in 3s 345ms.
    Imported:
      0 nodes
      0 relationships
      0 properties
    Peak memory usage: 7.969MiB
    2020-11-04 11:31:02.835+0000 INFO [o.n.i.b.ImportLogic] Import completed successfully, took 3s 345ms. Imported:
      0 nodes
      0 relationships
      0 properties
    2020-11-04 11:31:03.330+0000 INFO [StoreCopy] Import summary: Copying of 100704 records took 5 seconds (20140 rec/s). Unused Records 100704 (100%) Removed Records 0 (0%)
    2020-11-04 11:31:03.330+0000 INFO [StoreCopy] ### Extracting schema ###
    2020-11-04 11:31:03.330+0000 INFO [StoreCopy] Trying to extract schema...
    2020-11-04 11:31:03.338+0000 INFO [StoreCopy] ... found 0 schema definitions.

    该示例产生了一个紧凑且一致的存储(任何不一致的节点、属性、关系都不会复制到新创建的存储中)。

  2. 使用 system 数据库并创建 neo4jcopy1 数据库。

    neo4j@system> create database neo4jcopy1;
    0 rows available after 60 ms, consumed after another 0 ms
  3. 验证 neo4jcopy1 数据库是否已联机。

    neo4j@system> show databases;
    +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | name         | type       | aliases | access       | address          | role      | writer | requestedStatus | currentStatus | statusMessage | default | home  | constituents |
    +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | "neo4j"      | "standard" | []      | "read-write" | "localhost:7687" | "primary" | TRUE   | "offline"       | "offline"     | ""            | TRUE    | TRUE  | []           |
    | "neo4jcopy1" | "standard" | []      | "read-write" | "localhost:7687" | "primary" | TRUE   | "online"        | "online"      | ""            | FALSE   | FALSE | []           |
    | "system"     | "system"   | []      | "read-write" | "localhost:7687" | "primary" | TRUE   | "online"        | "online"      | ""            | FALSE   | FALSE | []           |
    +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    
    3 rows available after 2 ms, consumed after another 1 ms
  4. 在 Neo4j Browser 中,运行 :sysinfo 来检查 neo4jcopy1 的总存储大小。

    压缩后报告的存储大小输出为 800.68 KiB,ID 分配:节点 ID 0,属性 ID 0。