针对Neo4j模型/查询性能/配置,需要建议以实现最佳性能。

3
我正在从T-SQL转移我们的关系数据到图形数据库(使用Neo4j)进行GraphDB实验。我们希望处理大量数据,如果我们查询图形结构,则可以获得更好的效益。目前,即使在一些简单的WHERE子句和聚合步骤中,我们也看到了非常低的查询性能。希望能够获得一些建议,以便我们如何实现更好的性能,因为Neo4j声称可以处理十亿级别的节点。以下是我们尝试过的所有内容。
那么,让我描述这些数据: 我们拥有与客户有关的国家(地理)和产品(SKU)在线访问/购买数据。每当客户访问网站时,他的查看/购买情况将作为唯一会话ID的一部分进行跟踪,此ID在30分钟后更改。我们正在尝试通过计算不同的会话ID来准确计算一个人在网站上访问的次数。
我们拥有大约2600万行关于客户访问/购买网站时所做的数据。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次访问。
下面是我们建立的模型: 我们有一个事实节点,每行数据都有一个。 我们创建了不同的地理位置和产品节点,分别为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)

这是模式的快照:The Model's Schema 正如一些人所说,缺少索引可能导致问题,但我需要的所有索引都有,而且还有更多。我认为添加我不经常查询的额外索引不会显著降低性能。
总共有4400万个节点,其中大部分是事实和SessionId节点。有1.31亿个关系。
如果我尝试查询识别属于约20个国家和约20个产品的人的不同访问,需要大约44秒才能获得答案。对于相同的查询(当我在Neo4j中构建索引时),SQL需要大约47秒(未使用索引)。这并不是我希望通过使用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的数据库查询次数:使用模型中的Session节点的基本查询 问题1:是否有方法可以改进这个模型以获得更好的查询性能?例如,我可以通过删除会话节点并仅计算Fact节点上存在的SessionIds来更改上述模型。
(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);

由于Fact和Session之间的节点和关系非常多,因此会出现这种情况。 因此,似乎我更愿意将SessionIds作为Facts节点的属性而受益。
当我使用PROFILE时,结果约为5000万个数据库访问次数: Query on a model where we dont have Session nodes, but which uses SessionId as a property of the Facts in the model 此外,有人能帮助我了解基于节点属性扫描节点变得困难的临界点吗? 当我增加节点拥有的属性数量时,是否有问题?
Q2)如果它需要44秒,我的Neo4j配置是否有问题? 我的Java堆有114GB RAM,但没有SSD。 我没有调整其他配置,并想知道是否可以成为瓶颈,因为我被告知Neo4j可以运行数十亿个节点?
我的机器总RAM:140GB 为Java堆分配的RAM:114GB(据我回忆,从64GB RAM移动到114GB几乎没有性能提高) 页面高速缓存大小:4GB 图形数据库大小:约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);

然而,它提供的性能与Q1中略有改进的查询相当,并记录了相同数量的DBhits。

当我使用PROFILE时,这导致约50M个db hits,与Q1中的查询完全相同: 类似之前的性能

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节点之间创建了一个交叉连接,然后测试每个关系以确定其中的160万种可能的关系组合之一是否存在。我理解得对吗?
我知道这些是长问题和非常长的帖子。但是我已经孜孜不倦地尝试了一个多星期来自己解决这些问题,并在这里分享了一些我的发现。希望社区能够指导我解决这些查询问题。先感谢您阅读该帖子!
编辑-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)

但是,如果我要基于两个字符串属性来测试地理和SKU节点之间的关系,那么我将需要进行测试。(可以说日期可以转换为整数,但仍然存在另一种模型之间的对决)

C) @Tore,感谢您解释并确认了我的对Q4的理解。但是,如果GraphDB执行这样的计算,即连接并比较每个关系与该连接是否相同,它实际上不是以RDBMS应该具有的方式工作吗? 它未能利用应该易于找到直接路径的图形遍历,这对我来说似乎是一种糟糕的实现?


3
您能否提供您的模式,以便我们可以看到唯一约束和索引?此外,在查询中使用PROFILE将产生一个可视化输出,这可能会提供有关如何提高性能的线索。您能否在描述中添加这两个内容? - InverseFalcon
你正在使用哪个版本的Neo4j?堆的大小是114 GB,但页面缓存的大小是多少? - Frank Pavageau
@Inverse:我已经添加了我的SCHEMA和PROFILE结果。 - DK5
@Frank:我在编辑中添加了所有的配置信息。我的页面缓存为4GB,总内存为140GB。我的neo4j版本是3.0.4。 - DK5
我根据描述的补充进行了解释。 - InverseFalcon
3个回答

3
tl,dr: 你几乎肯定缺少一个属性的索引。请仔细检查你的模式中关于geo.Countrysku:SKUSessions.SessionIds的索引。
Q1) 只有一个临界点:如果在查询中引用了任何未索引属性,那么你的查询将大幅减速。未索引属性是为数据存储而设,除非你能严重缩小首先被检查的节点数量,否则不能进行查询。
Q2) 你的瓶颈几乎肯定是缺少索引。请仔细检查你的:schema,确保在查询中访问到的每个属性,在任何标签上,都被索引了。
Q3) 这两个查询几乎完全相同。除非你需要极其敏捷的单个结果,否则来自任何一个查询的效率增益都与查询的其余部分相比微不足道。
Q4) 你的理解基本上是正确的。这个查询会为每个匹配的Geo创建一行记录,然后为这些行中的每一个创建一个匹配的Sku查询,即便两者之间没有潜在的联系。然后过滤掉所有无法存在连接的行。在早期查询中匹配包括中间Fact的模式可以确保这些行从未被创建或跟踪。
编辑:请查看下面InverseFalcon提供的更好的关于如何在图形中对数据进行建模以及是否值得的观点。
编辑2:在看到你的完整查询后,我认为Frank和Inverse所说的点更加相关。如果你需要经常访问单个Geo的最新数据,则图形查询可能更具性能,但是你正在拉取涉及到你的图形很大一部分的报告,而且你的数据结构非常结构化,因此很难打败普通的关系数据库。

嗨@Tore,非常感谢!我添加了:schema截图,并分享了所有索引已经建立。比所需更多的索引已经建立。我还添加了几个PROFILE查询的屏幕截图和从中获得的统计信息。请仔细查看并提供您的反馈。 - DK5

3
The answers from Tore and InverseFalcon already contain a lot of good points, but I'll add a few more points to consider.
Q1
Traversing nodes and relationships is cheap, but not traversing them is even cheaper, especially when it's repeated! The Fact node duplicates information which really seems to belong to the Session node: take the Geography for example, it feels like there should only be one per session (unless knowing a user switched locale is important).
Q2
114 GB is a lot of heap for a JVM. Is this configuration the result of some measurements, or just throwing more memory at the problem? A huge heap has several drawbacks:
  • 较长的GC暂停时间:它们往往随着堆大小呈更糟糕的超线性增长。
  • 增加的内存使用量:如果堆保持在32 GB以下,JVM使用压缩对象指针(oops),一旦超过阈值,内存使用量显著增加。

除非您看到请求消耗大量堆空间,否则应限制堆大小,为页面缓存留更多内存。

Neo4j可以容纳数十亿个节点和关系,但是一次查询涉及的越多,所需时间就越长。如果您只触及局部子图(即使它是一个非常大的子图的一部分),Neo4j比关系型数据库更有效率,因为一旦找到起始节点,遍历到其他节点的关系只需要追踪指针(通常是这样presented),而不是使用索引(无索引邻接)。例如,请参见this SO question


编辑 1

关于内存,考虑到数据库的大小,我建议采用不同的设置:JVM 堆内存较少,但页面缓存更大。比如说,不是使用 114 GB / 4 GB,而改为使用 64 GB / 48 GB。

关于模型,考虑到数据量的大小,我认为将所有内容都转移到关系属性中并不会有所帮助,恰恰相反:对于节点,你只需要进行一次查找,然后再查找与之相关的关系,而对于属性,则需要为每个关系比较其属性。在之前的一个项目中,通过将系统性的属性比较替换为节点查找,然后简单比较 ID,我实际上提高了图遍历的性能。此外,您还创建了多个与 Geography 相关联的关系,这可能会改变某些与 Geography 相关的查询的行为(甚至可能需要重新编写这些查询)。

关于Q4,数据库确实按照您的要求执行:使用WITH创建“屏障”,强制在该点处数据的形状。由于您要求引擎创建GeographySKU节点的笛卡尔积,即使只是用于查找相关的Facts,它也会这样做。Cypher大多是声明式的,但仍有一些方式可以塑造计算发生的方式,并以不同的性能结果为代价:
  • WITH
  • USING INDEX
  • any(...) vs size(filter(...)) > 0
  • 等等。

嗨@Frank,感谢您宝贵的意见 :) 我已经在问题中分享了我的SCHEMA和我的机器以及Neo4j配置。请提供您的意见。 - DK5

3

我觉得你正在尝试同时进行图形建模和关系型数据库建模,这至少会在查询中添加额外的遍历步骤。

虽然我不能保证这样做会带来重大的性能提升,但我建议删除你的:Fact节点,因为它们包含了已经在你的图形中捕获的冗余信息。(假设会话ID不会被重复使用)

只要将没有:Fact作为中心将它们连接起来即可。会话和订单很可能是您的主要节点。

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

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

我们假设会话时间窗口足够小,可以将会话日期视为来自该会话的订单或访问日期相同。如果需要更具体的内容,我们可以在 :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查看您的查询可能会遇到什么问题。
话虽如此,您在这里的用例可能不会比关系数据库获得显着的改进,因为这种数据在关系数据库中既可以很好地建模又可以很好地查询。您是否有任何关于数据的问题,在当前数据库中无法或无法快速获得?
编辑
根据您的编辑,扩展(显示更多细节)PROFILE中的操作也很有帮助,这样您不仅可以看到操作和数据库访问量,还可以了解操作涉及的查询方面。
基于我们在扩展这些操作时发现的内容,我们很可能会看到改善查询性能的机会,因为我猜测购买相关产品的用户数量与一个国家内的总会话数之间存在巨大差异。
我们可以改进的一个可能的领域是建议在查询中使用哪个索引,因为从产品到购买它的用户的会话再到与会话相关的国家遍历,应该比尝试匹配来自给定国家的所有用户的会话更有效。
需要注意的是,当您查询较小的数据子图而不是整个数据集或大块的数据集时,Neo4j的好处才会发挥出来。在示例查询中查看的子图仍然相当大,查看跨整个国家的用户的购买历史记录。那些查询最好使用关系数据库,并且在那个规模下,您正在进行数百万次图遍历,这并不是微不足道的...要查找地理和产品节点之间的连接,它仍然必须执行这些遍历并使用集合操作来仅过滤连接。不过,我想象当询问关于这种规模的数据(在许多不同国家购买的产品)时,这更多是分析操作而不是实时为用户提供服务的操作,因此我想知道这些查询是否存在性能问题。
如果您的查询缩小了所查询的国家,就会开始看到性能的提高。
如果您查询的是单个用户的购买历史记录,则会更好,因此您的查询子图局限于用户。但是,由于您需要的所有数据行都在单个表中,因此在RDBMs中已经很好地对其进行建模。
请记住,Neo4j的优势在于遍历而不是加入,但在您当前的关系型数据库模型中,您没有进行任何加入操作,您所需的一切都在索引行中。对我来说,您的用例是一个跨越庞大子图的查询,而且数据模型实际上比关系型数据库更复杂,并且您提供的查询并没有从这种增加的复杂性中获得太多好处。
当您考虑图形数据库时,真正驱动您决策的应该是您计划在其中进行的查询,以及通常情况下您可能要询问该数据中的关系,并且如果那些问题在当前数据库中难以回答。在我看来,如果您的示例查询代表了您经常进行的查询,那么您当前的数据库处理得很好。如果您提出的问题更难以回答,并且更基于关系(例如基于其他购买或查看这些产品的用户购买内容为用户提供产品建议),则图形数据库解决方案将更有意义,可以用于实时查询,或定期缓存和更新查询结果。
性能改进编辑
对我来说,由于您具有这些:Facts节点,因此似乎您不需要进行许多遍历。但这就像一个关系型数据库一样,因此在这些类型的查询中,关系型数据库执行得更好。
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)

2
使用COUNT(DISTINCT ssn)替代COUNT(DISTINCT ssn.SessionId)可以让计算更加高效。如果节点本身是判别器,则不要访问属性,特别是当该属性为字符串时:与长整型比较相比,比较字符串的性能总是较低的。 - Frank Pavageau
是的,这些节点是不同的,会话ID是整数,因此我们可以进行替换。感谢您指出这一点。我已经在问题中添加了所需的SCHEMA和PROFILE查询截图。请提供您对此的反馈。谢谢! :) - DK5
谢谢,那个编辑解释得非常好!它很好地解释了我们在决定模型时需要牢记的事情,以及我们何时会真正从模型中受益。我会等一下再接受答案,因为我还有其他关于配置和查询性能的问题。关于使用概要文件,这两个报告完全相同的dbhits的查询具有相同的PROFILE结构,并且在每个级别上的行计数估计完全相同吗?我想衡量从我的Q1查询到Q3查询的性能增益。我该如何做? - DK5
@DK5 我在底部添加了一些可能的查询以提高性能。 - InverseFalcon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接