Memgraph是一个内存中的图数据库,支持Cypher语言,并承诺以高速实现ACID事务。这是市场宣传的线。实际情况则更为复杂——也更有趣。经过深入文档的研究,这里总结了评估Memgraph或在生产环境中运行它时真正需要注意的内容。
存储模式:选择权衡
Memgraph有三种存储模式,你在这里的选择决定了后续的一切。
IN_MEMORY_TRANSACTIONAL 是默认模式,也是大多数人选择的原因。它提供了完整的ACID保证。使用WAL文件和定期快照来确保持久性。Delta对象记录每次更改,以便事务可以隔离并回滚。代价是每次更改都会创建一个Delta(每个56字节),这意味着批量导入会迅速消耗内存。
IN_MEMORY_ANALYTICAL 去掉了Delta机制。没有ACID保证。没有WAL。没有定期快照。你得到的是6倍于导入速度的提升和显著降低的内存消耗。多个事务可以同时写入同一个节点而不会发生冲突。但代价是:如果事务失败,更改不会被回滚。其他事务可以看到未提交的更改。持久性只能手动实现——你需要自己运行 CREATE SNAPSHOT。
这种模式适用于特定用例:你有一个稠密的图,包含超级节点,需要快速加载,并且在数据加载完成后会切换回事务模式。除非你真的不在乎一致性,否则不要在分析模式下运行生产查询。
ON_DISK_TRANSACTIONAL 使用RocksDB作为后端存储。数据存储在磁盘上,内存中缓存热点对象。它通过快照隔离支持ACID。性能比内存中模式差(显然),但可以容纳比内存更大的数据集。它仍然标记为实验性。如果你在考虑这种模式,你可能需要一个不同的数据库——或者你应该先与Memgraph团队交谈。
从一种内存模式切换到另一种模式很简单(STORAGE MODE IN_MEMORY_ANALYTICAL;)。从磁盘模式切换需要一个空数据库。请提前做好计划。
内存计算
Memgraph依赖于RAM。以下是事务模式下估计存储内存的公式:
StorageRAM = Vertices × 204B + Edges × 154B
这是一个粗略的估计。实际分解如下:一个顶点至少需要136字节(80字节的对象 + 56字节的Delta),一条边至少需要88字节(32字节的对象 + 56字节的Delta),再加上SkipList容器开销(每个对象约24字节,节点指针),以及属性。
属性部分很有趣。布尔值需要2字节。整数需要3到10字节,具体取决于数值大小。字符串需要至少4字节加上每个ASCII字符1字节。时间类型需要12到14字节。你可以通过查询中的 propertySize() 检查确切的属性大小。
Marvel漫画宇宙数据集(21,723个顶点,682,943条边)消耗大约117MB的存储内存。一个空的Memgraph实例仅存在就需要消耗约75MB的内存。计划时请考虑你的数据集大小的两倍左右的总RAM来应对查询执行开销。
三种回收内存的方法:
- 如果你的关系没有属性,可以禁用边上的属性(
--storage-properties-on-edges=false),这会完全消除边对象。 - 启用轻量级边(
--storage-light-edge=true),通过池分配而不是使用SkipList来节省每个边约24字节。 - 启用属性压缩(
--storage-property-store-compression-enabled=true),使用zlib。这会用CPU换取内存。压缩级别可以调整——low,mid(默认),high。
还有浮点数精度:--storage-floating-point-resolution-bits=32 将双精度浮点数存储为4字节浮点数而不是8字节双精度浮点数。每个浮点值节省4字节,但代价是精度降低。WAL和快照始终以完整的64位进行序列化,因此一旦写入,精度损失是永久的。
索引:复合索引的陷阱
Memgraph的索引基于并发SkipList——插入、删除和搜索的时间复杂度为O(log n)。它们不会自动创建(除非你启用 storage-automatic-label-index-creation-enabled)。
基本类型包括标签索引、标签属性索引、边类型索引和点索引。这些都很直接。但复合索引是一个陷阱。
对 :Person(name, age, occupation) 的复合索引遵循最左前缀规则。它可以高效地服务于仅过滤 name 的查询,name + age 的查询,或所有三个属性的查询。但它不能服务于仅过滤 age 或仅过滤 occupation 的查询。这与SQL数据库中的复合索引行为相同,但在图上下文中,查询模式更不可预测。
两条实用规则:
- 将高基数属性放在最前面。
- 如果你频繁查询单个属性,即使复合索引覆盖了它,也需要创建单独的单属性索引——复合索引可能不会表现得那么好。
降序索引(WITH CONFIG {"order": "DESC"})用于 ORDER BY ... DESC 查询,特别是在使用 LIMIT 时特别有价值——它们将排序后再截断转换为直接的O(N)索引读取。它们仅在 IN_MEMORY_TRANSACTIONAL 模式下有效。
在加载数据和创建索引后运行 ANALYZE GRAPH。它计算属性值分布,以便查询规划器可以根据平均组大小和卡方统计选择最优索引,而不仅仅是节点计数。只需运行一次,不要重复运行。
约束:三种类型,提交时检查
Memgraph有存在约束、唯一性约束和数据类型约束。
存在约束确保所有具有给定标签的节点上都有一个属性。唯一性约束确保标签-属性组合是唯一的——它们支持复合唯一性(多个属性)。数据类型约束确保属性持有特定类型(STRING、INTEGER等)。
所有约束在提交时检查,而不是在写入时检查。这是乐观的方法——多查询事务在最终提交前不会进行约束检查。如果违反约束,整个事务将回滚。这对于构建批量导入管道非常重要:一个坏行在一个包含10,000行的批次中会杀死整个批次。
唯一性约束不会创建索引。这是一个常见的误解。如果你需要同时进行唯一性验证和快速查找,你需要分别创建约束和索引。
事务:默认快照隔离
Memgraph默认使用快照隔离——你的事务看到数据库的一致快照,并在没有发生冲突更新的情况下提交。这防止了脏读、不可重复读和幻读。
你可以降低到 READ COMMITTED 或甚至 READ UNCOMMITTED(推荐使用后者时为只读)。在会话或事务级别更改隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
显式事务使用 BEGIN、COMMIT、ROLLBACK。适用于进行假设分析——修改一些数据,运行算法,然后回滚:
BEGIN;
MATCH (e:E)-[r:CONNECTED_TO]->(f:F) SET r.flow = 17;
MATCH (a:A), (f:F) CALL max_flow.get_flow(a, f, "flow") YIELD max_flow RETURN max_flow;
ROLLBACK;
使用 SHOW TRANSACTIONS 监控活跃事务。你会看到正在进行的快照的合成行(transaction_id = "snapshot")和垃圾收集的行(transaction_id = "gc")。这些不能被终止——它们是后台进程。gc 行上的 elapsed_ms 较大且 exclusive_lock: true 表示垃圾收集正在阻塞你的事务。
持久性:快照 + WAL,而不是单独的WAL
Memgraph使用两种持久性机制:定期快照和写前日志。关键规则:你不能单独使用WAL。如果WAL启用且快照间隔设置为零,Memgraph将拒绝启动。
启动时,Memgraph从最近的快照恢复,然后重放任何包含在该快照之后所做的更改的WAL文件。WAL文件在创建新快照后自动删除。
从v3.12开始,WAL文件具有每事务CRC32校验和。恢复期间检测到损坏会停止在损坏点的重放——损坏的数据从未到达数据库。快照尚未进行校验和。
恢复失败处理方式发生了显著变化:默认情况下,损坏的数据库会终止整个进程。设置 --storage-allow-recovery-failure=true 可以改变这一点——损坏的数据库会空闲且不可用,其他数据库继续运行,你可以使用 RECOVER SNAPSHOT 来修复。在HA集群中,损坏的副本会自动从主节点自我修复。
并行快照创建(--storage-parallel-snapshot-creation=true)可以显著加快大型数据集的快照创建速度。经验法则:总项目数 ≈ 4 × 线程数 × 批量大小。一旦饱和了磁盘I/O,增加线程数不会带来帮助。
触发器:基于事件的Cypher
触发器在CREATE、UPDATE或DELETE事件发生时执行openCypher语句。它们可以在COMMIT前或COMMIT后触发——即在事务中或异步。
预定义的变量是关键概念:createdVertices、updatedObjects、deletedEdges 等。每种事件类型暴露不同的变量。UPDATE事件特别详细——你可以获得 setVertexProperties、removedVertexProperties、setVertexLabels 以及混合一切的复合列表 updatedObjects。
触发器默认为 SECURITY DEFINER——它们以创建者的权限运行。如果需要针对触发用户的权限检查,可以切换为 SECURITY INVOKER。
实际用途:自动为节点添加时间戳,维护衍生数据,通过MAGE的 pagerank_online.update() 逐步更新图算法(如PageRank)。
存储访问:三种级别的并发
每个查询都需要一个存储访问器。共享访问允许并行读写。只读访问允许并行读取但阻止写入。唯一访问是独占的——用于索引/约束DDL、DROP GRAPH 和 RECOVER SNAPSHOT。
访问器类型在解析时自动推断。唯一需要关心的地方是显式事务。默认情况下它们获取写共享访问,这可能会无谓地阻塞其他事务。标记为只读的显式事务可以提高并发性。
最后总结
Memgraph快速、Cypher原生且默认模式下ACID合规。内存架构意味着你的数据集大小受限于RAM——内存公式是可预测的,但你需要考虑Delta开销、SkipList容器以及查询执行内存之外的原始存储。
存储模式选择是第一个决定,它决定了后续的一切。生产中使用事务模式。批量加载使用分析模式。仅在其他选项都用尽时才使用磁盘模式。
索引需要精心策略——复合索引的顺序很重要,ANALYZE GRAPH 不可或缺,降序索引是一个未充分利用的优化。约束在提交时检查,而不是在写入时检查。持久性需要快照和WAL——你不能单独使用其中之一。