需要有关 Neo4j model/Query performance/configurations 的建议以获得最佳性能

Require advice on Neo4j model/Query performance/configurations for optimum performance

我正在进行 GraphDB 实验,以测试我们是否可以将关系数据从 T-SQL 转移到 GraphDB(使用 Neo4j)。 我们希望处理大量数据,如果我们查询图形结构,这些数据会受益。目前,即使对于一些简单的 where 子句和聚合步骤,我们也看到了非常低的查询性能。由于 Neo4j 声称可以在数十亿个节点上工作,因此最好能获得一些关于如何实现更好性能的建议。这是我们尝试过的所有方法。

所以,让我描述一下数据: 我们在线拥有关于国家(地理)和产品(SKU)的客户数据 visited/purchased。每次客户访问该网站时,他的 views/purchases 都会作为唯一会话 ID 的一部分进行跟踪,该 ID 会在 30 分钟后更改。我们正在尝试通过计算不同的会话 ID 来准确计算一个人对网站的访问次数。

我们有大约 2600 万行与访问该网站的客户 visits/purchases 相关的数据。 SQL中的数据格式如下:

----------------------------------------------------------------------------
|    Date|   SessionId|   Geography|   SKU|   OrderId|    Revenue|   Units||
|--------|------------|------------|------|----------|-----------|--------||
|20160101|         111|         USA|     A|      null|          0|       0||
|20160101|         111|         USA|     B|         1|         50|       1||
|20160101|         222|          UK|     A|         2|         10|       1||
----------------------------------------------------------------------------

问题:我们需要准确计算客户访问网站的次数。访问计算为不同的会话 ID。

访问计算逻辑说明: 在上面的模型中,如果我们查看访问,其中一个人来到网站寻找名为 "A" 的 SKU,我们的答案将是 2。第一个视图在会话 111 中,第二个视图在会话 222 中。 同样,如果我们想知道一个人访问网站寻找 SKU "A" 或 "B" 的访问次数,那么答案也将是 2。这是因为在会话 111 中,两种产品被查看,但总访问量仅为 1 次。会话 111 中有 2 次产品浏览,但只有 1 次访问。所以从 222 算起其他访问,我们总共还有 2 次访问。

这是我们建立的模型: 我们有一个事实节点,数据中的每一行都有一个事实节点。 我们制作了不同的 Geography 和 Product 节点,分别为 400 和 4000。这些节点中的每一个都与多个事实有关系。 同样,我们有不同的日期节点。

我们也为会话 ID 和订单 ID 创建了不同的节点。这两者都指向事实。 所以基本上我们有具有以下属性的不同节点:

1) Geography  {Locale, Country}
2) SKU {SKU, ProductName}
3) Date {Date}
4) Sessions {SessionIds}
5) Orders {OrderIds}
6) Facts {Locale, Country, SKU, ProductName, Date, SessionIds, OrderIds}

关系模式基于匹配的 属性 值,看起来像:

(:Geography)-[:FactGeo]->(:Facts)
(:SKU)-[:FactSKU]->(:Facts]
(:Date)-[:FactDate]->(:Facts)
(:SessionId)-[:FactSessions]->(:Facts)
(:OrderId)-[:FactOrders]->(:Facts)

这是架构的快照:

正如你们中的一些人所说,缺少索引可能是导致问题的原因,但这里有我需要的所有索引,甚至更多。我假设添加我通常不查询的额外索引不会显着降低性能。

一共44M个节点,大部分是Facts和SessionId节点。 有 131M 关系。

如果我尝试查询识别属于大约 20 个国家和大约 20 种产品的人的不同访问,大约需要 44 秒才能得到答案。 它需要 SQL 大约 47 秒(没有索引)(当我在 Neo4j 中构建索引时)。 这并不是我希望通过使用 Neo4j 获得的特殊改进,因为我认为在 SQL 中构建索引会提供更好的性能。

我写的查询是这样的:

(geo: Geography)-[:FactGeo]->(fct: Facts)<-(sku: SKU)
WHERE geo.Country IN ["US", "India", "UK"...]
AND sku.SKU IN ["A","B","C".....]
MATCH (ssn: Sessions)-[:FactSessions]->(fct)
RETURN COUNT(DISTINCT ssn.SessionId);

当我使用 PROFILE 时,这会导致大约 69M 数据库命中:

Q1) 有没有办法改进这个模型以获得更好的查询性能? 例如,我可以通过删除 Session 节点并仅计算 Fact 节点上存在的 SessionId 来更改上述模型,如下面的查询所示:

(geo: Geography)-[:FactGeo]->(fct: Facts)<-(sku: SKU)
WHERE geo.Country IN ["US", "India", "UK"...]
AND sku.SKU IN ["A","B","C".....]
RETURN COUNT(DISTINCT fct.SessionId);

这是因为大量的节点以及事实和会话之间的关系。因此,我似乎宁愿从将 SessionIds 作为 Facts 节点的 属性 中受益。

当我使用 PROFILE 时,这会导致大约 50M 数据库命中:

此外,有人可以帮助我理解随着节点具有的属性数量的增加,很难根据属性扫描节点的临界点吗?

Q2) 我的 Neo4j 配置是否有问题,因为它需要 44 秒?我有一个 114GB 内存用于 java 堆,但没有 SSD。我没有调整其他配置,想知道这些是否可能是这里的瓶颈,因为我被告知 Neo4j 可以 运行 在数十亿个节点上?

我机器的总内存: 140GB RAM 专用于 Java 堆: 114GB(据我回忆,当我从 64GB RAM 移动到 114GB 时,性能提升几乎可以忽略不计) 页面缓存大小: 4GB 大约 GraphDB 大小: 45GB 我使用的Neo4j版本:3.0.4企业版

Q3) 有没有更好的方法来制定性能更好的查询? 我尝试了以下查询:

(geo: Geography)-[:FactGeo]->(fct: Facts)
WHERE geo.Country IN ["US", "India", "UK"...]
MATCH (sku: SKU)-[:FactSKU]->(fct)
WHERE sku.SKU IN ["A","B","C".....]
RETURN COUNT(DISTINCT fct.SessionId);

但它提供了与第 1 季度略有改进的查询大致相同的性能并记录了相同数量的 DBhits。

当我使用 PROFILE 时,这会导致大约 5000 万次数据库命中,与第一季度中的查询完全相同:

Q4) 如果我将查询从 Q3 修改为如下所示,我没有看到改进,而是看到性能大幅下降:

MATCH (geo: Geography)
WHERE geo.Country IN ["US", "India", "UK"...]
WITH geo
MATCH (sku: SKU)
WHERE sku.SKU IN ["A","B","C".....]
WITH geo, sku
MATCH (geo)-[:FactGeo]->(fct: Facts)<-[:FactSKU]-(sku)
RETURN COUNT(DISTINCT fct.SessionId);

这似乎是在 400 个地理节点和 4000 个 sku 节点之间创建交叉连接,然后测试每个关系是否可能存在于这 1,600,000 个可能的关系组合之一之间。我理解正确吗?

我知道这些问题很长而且很长post。但是我已经不知疲倦地尝试了一个多星期来自己解决这些问题,并且我在这里分享了我的一些发现。希望社区能够指导我解决其中的一些问题。提前感谢您阅读 post!

EDIT-01: Tore、Inverse 和 Frank,非常感谢你们帮我解决问题,我希望我们能找出根本原因。

A) 我添加了更多细节,关于我的 PROFILE 结果以及我的 SCHEMA 和 Machine/Neo4j 配置统计信息。

B) 当我考虑@InverseFalcon 建议的模型时,请记住关系是更好的选择并限制关系数量的事实。

我正在稍微调整 Inverse 的模型,因为我认为我们可以稍微减少它。作为模特怎么样:

(:Session)-[:ON]->(:Date)
(:Session)-[:IN]->(:Geography)
(:Session)-[:Viewed]->(:SKU)
(:Session)-[:Bought]->(:SKU)

(:Session)-[:ON]->(:Date)
(:Session)-[:IN {SKU: "A", HasViewedOrBought: 1}]->(:Geography)

现在这两种模式各有优势。在第一个中,我将 SKU 维护为不同的节点,并在它们之间建立不同的关系以确定它是购买还是浏览。

在第二个中,我完全删除了将它们添加为关系的 SKU 节点。我知道这会导致很多关系,但关系的数量仍然很少,因为我们也丢弃了我们要删除的所有节点和 SKU 节点的关系。 我们将不得不通过比较 SKU 字符串来测试关系,这是一项密集型操作,也许可以通过仅保留会话和地理节点并删除日期节点并将日期 属性 添加到 SKU 关系来避免。如下:

(:Session)-[:ON]->(:Date)
(:Session)-[:IN {Date: {"2016-01-01"}, SKU: "A", HasViewedOrBought: 1}]->(:Geography)

但随后我将根据两个属性测试 Geography 和 SKU 节点之间的关系,这两个属性都是字符串。 (可以说日期可以转换为整数,但我仍然看到我们在替代模型之间有另一场对峙)

C) @Tore,感谢您解释并确认我对 Q4 的理解。但是,如果 GraphDB 进行这样的计算,它连接并比较每个关系与该连接,那么它实际上是否以 RDBMS 应有的相同方式工作? 它在利用图形遍历方面是无效的,它应该很容易地通过找到两组地理和产品节点之间的直接路径来完成。这对我来说似乎是一个糟糕的实现?

tl,dr:您几乎可以肯定缺少 属性 上的索引。仔细检查架构中 geo.Countrysku:SKUSessions.SessionIds.

上的索引

Q1) 只有一个临界点:如果您在查询中 完全 引用任何未索引的属性,您的查询将大大减慢速度。未索引的属性用于数据存储,而不是查询,除非你能认真地缩小首先检查的节点数。

Q2) 您的瓶颈几乎可以肯定是缺少索引。仔细检查您的 :schema 以确保您在查询中访问的 every 属性 在 any 标签上已编入索引.

Q3) 这两个查询几乎完全等同,是的。除非您需要非常敏感的单个结果,否则与查询的其余部分相比,从一个或另一个中获得的任何效率提升都可以忽略不计。

Q4) 你的理解基本正确。此查询为每个匹配项 Geo 创建一行,然后为每个匹配项 Sku 创建一个查询,即使 之间没有潜在联系他们俩。然后它会过滤掉所有不存在连接的行。匹配包括早期查询中的中间 Fact 的模式可确保永远不会创建或跟踪这些行。

编辑:请参阅下面的 InverseFalcon,以更好地了解如何在图表中对数据建模以及它是否值得首先这样做。

编辑 2:在看到您的完整查询后,我认为 Frank 和 Inverse 提出的观点更加相关。如果您需要频繁访问单个 Geo 的最新数据,那么图形查询可能会更高效,但您正在提取涉及图形很大一部分的报告,并且您的数据很重结构化,因此很难击败常规关系数据库。

在我看来,您正在尝试同时进行图形建模和 RDBMS 建模,并且至少在您的查询中添加了一个额外的遍历步骤。

虽然我不能说这会带来重大的性能改进,但我会考虑删除您的 :Fact 节点,因为它们包含已在您的图表中捕获的冗余信息。 (假设会话 ID 从未被重复使用)

这只是在没有 :Fact 作为将它们捆绑在一起的中心节点的情况下连接节点的问题。会话和订单可能会成为您的主要节点。

因此您的节点之间的关系可能如下所示:

(:Session)-[:From]->(:Geography)
(:Session)-[:Visited]->(:Product)
(:Session)-[:On]->(:Date)
(:Session)-[:Ordered]->(:Order)
(:Order)-[:Of]->(:Product)

我们假设由于会话时间 window 足够小,我们可以将会话日期计算为与该会话的订单或访问日期相同。如果我们需要更具体的东西,我们可以在 :Order 和 :Date 之间添加关系,并将日期 属性 添加到 :Visited 关系(假设我们不想添加 :Visit 节点作为会话和产品之间的中介)。

这会将您的查询更改为:

MATCH (geo:Geography)<-[:From]-(ssn:Session)-[:Ordered]->(:Order)-[:Of]->(sku:Product)
WHERE geo.Country IN ["US", "India", "UK"...]
AND sku.SKU IN ["A","B","C".....]
RETURN COUNT(DISTINCT ssn);

我假设 :Sessions 是唯一的,具有唯一的 SessionId 属性,因此不需要获取不同的 属性 本身,只需使用节点即可。

正如 Tore 指出的那样,索引和唯一约束在这里至关重要,尤其是对于数据集的大小。 Geography.Country、Session.SessionID、Product.SKU 和 Order.OrderId 应该都有唯一的约束。

使用 PROFILE 查看您的查询可能 运行 出现问题的地方。

综上所述,与 RDBMS 相比,您的用例可能不会有显着改进,因为这种数据在关系数据库中建模和查询都很好。您的数据是否有任何问题,您无法在当前数据库中获取或无法快速获取?

编辑

为了响应您的编辑,在您的 PROFILE 中展开(显示更多详细信息)操作也很有帮助,这样您不仅可以看到操作和数据库命中,还可以看到查询的哪个方面的操作担忧。

根据我们在扩展这些操作时发现的情况,我们很可能会看到提高查询性能的机会,因为我猜购买了相关产品的人数之间存在巨大差异,以及一个国家/地区的总会话数。

我们可以改进的一个可能领域是建议在查询中使用哪个索引,因为从产品遍历到购买它的用户的会话,然后到与会话相关的国家/地区,应该更高效而不是尝试匹配给定国家/地区所有用户的会话。

重要的是要注意,当您查询较小的数据子图而不是整个数据集或数据集的大块时,Neo4j 的优势会大放异彩。您在示例查询中查看的子图仍然很大,查看整个国家/地区用户的购买历史记录。这类查询最好使用 RDBM 完成,在这种规模下,您将进行数百万次图形遍历,这并非微不足道……要找到 Geography 和 Product 节点之间的联系,它仍然必须执行这些遍历并使用集合操作来过滤只有那些连接。不过,我想,当询问有关这种规模的数据(许多不同国家/地区的用户购买的产品)的查询时,这更像是一种分析操作,而不是实时为用户提供服务的操作,所以我想知道性能是否问题对于这类查询至关重要。

随着查询的子图缩小,您会开始看到性能改进。如果您的查询范围缩小了所查询的国家/地区,您可能会开始看到这一点。

如果您询问的是单个用户的购买历史记录,那就更好了,因为您查询的子图对于用户来说是本地的。但是随后您已经在 RDBM 中进行了完美的建模,因为您需要的所有数据行都在一个 table 中。

请记住,Neo4j 的优势在于进行遍历与连接,但在您当前的 RDBM 数据模型中,您没有进行任何连接,您需要的一切都在索引行中。在我看来,您的用例是您计划使用的查询跨越巨大的子图,并且数据模型在图中实际上比在 RDBM 中更复杂,而且您从中获得的收益并不多您提供的查询的复杂性。

当您考虑图形数据库时,真正推动您做出决定的是您计划对其进行的查询,以及通常您可能会对该数据中的关系提出哪些问题,以及这些问题是否很难在您当前的数据库中回答。在我看来,如果您的示例查询代表您计划定期进行的查询,那么您当前的数据库可以很好地处理这个问题。如果您要问的问题在当前解决方案中更难回答,并且更基于关系(例如,根据其他购买或查看这些产品的用户购买的产品向用户推荐产品),图形数据库解决方案将更有意义,并且可以用于实时查询,或者定期缓存和更新查询结果。

性能改进编辑

在我看来,既然您拥有这些 :Facts 节点,您实际上就不必进行多次遍历。但这与 RDBMS 完全一样,因此对于这类查询,RDBMS 的性能更好。

MATCH (sku: SKU)-[:FactSKU]->(fct: Facts)
WHERE sku.SKU IN ["A","B","C".....]
AND fct.Country IN ["US", "India", "UK"...]
RETURN COUNT(DISTINCT fct.SessionId)

在此查询中(假设 sku.SKU 是唯一的或已编入索引),您将仅使用图表来优化与产品相关的查找 :Facts(因为您直接取回所有相关的 :Facts 而不是根据产品筛选)。到那时,由于 Country 字段已经出现在 :Fact 对象上,您已经拥有了需要过滤的所有内容,所以就在那里做吧。

为了好玩,您可能想在此处将其与纯关系查询进行比较:

MATCH (fct: Facts)
WHERE fct.SKU IN ["A","B","C".....]
AND fct.Country IN ["US", "India", "UK"...]
RETURN COUNT(DISTINCT fct.SessionId)

Tore 和 InverseFalcon 的答案已经包含了很多优点,但我会再添加几点以供考虑。

Q1

遍历节点和关系很便宜,但不遍历它们更便宜,尤其是在重复的时候! Fact 节点复制的信息似乎确实属于 Session 节点:以 Geography 为例,感觉每个会话应该只有一个(除非知道用户切换区域设置很重要)。

Q2

114 GB 对于 JVM 来说是一个很大的堆。此配置是某些测量的结果,还是只是在问题上投入更多内存?巨大的堆有几个缺点:

除非您看到请求消耗大量堆,否则您应该限制堆,为 page cache.

留出更多内存

Neo4j 可以容纳数十亿个节点和关系,但是一个查询涉及的越多,所需的时间就越长。如果您只接触一个有限的局部子图,Neo4j 可以比 RDBMS 更高效,即使它是一个非常大的子图的一部分,因为一旦找到起始节点,遍历到其他节点的关系只需要追逐指针(如例如,通常是 presented) instead of using indices (index-free adjacency). See this SO question


编辑 1

关于内存,我真的会尝试不同的设置,JVM 的堆更少,但页面缓存更大,考虑到数据库的大小:不是 114 GB / 4 GB,也许是 64 GB / 48国标.

关于模型,考虑到数量,我不认为将所有内容移动到关系中的属性会有帮助,恰恰相反:对于节点,您只需执行一次查找,然后查找关系,而对于属性,您必须比较每个关系的属性。实际上,通过将系统性 属性 比较替换为节点查找,然后是简单的 id 比较,我在之前项目的图形遍历中获得了很多性能。当您之前在功能上只有一个关系时,您还与 Geography 创建了多个关系,这可能会改变一些与 Geography 相关的查询的行为方式(或者甚至是编写方式)。

关于第 4 季度,数据库确实按照您的指示进行操作:使用 WITH,您创建 "barriers",这会在该点强制调整数据的形状。由于您要求引擎创建 GeographySKU 节点的笛卡尔积,这就是它所做的,即使它只是用于尝试查找相关的 Facts。 Cypher 主要是声明式的,但仍有一些方法可以塑造计算的发生方式,在性能方面具有不同的结果:

  • WITH
  • USING INDEX
  • any(...) 对比 size(filter(...)) > 0
  • 等等