知识库

理解零行聚合

在某些情况下,Cypher 中的聚合可能很棘手。尤其是在执行 MATCH 后立即进行聚合且没有匹配项,或者在过滤操作过滤掉所有结果后进行聚合时。在某些情况下,在这些情况下使用聚合的查询可能不会产生任何结果,这可能会让一些用户感到意外。

当所有先前结果都被过滤掉时,可以在零行上成功执行聚合,但此行为的一些方面以及使用上的限制需要理解,以便正确执行这些聚合并获得正确的结果。

关于行、运算符和聚合的回顾

Cypher 中的运算符生成行,并且它们也按行执行。当没有更多行时(由于 MATCH 未找到匹配项或 WHERE 子句进行过滤),则没有剩余内容可供执行,并且剩余的操作将变为无操作。

一个主要的例外是聚合函数,最常见的是count()collect()

这些函数是允许的,因为存在我们可能希望统计发生 0 次的情况,因此 0 的count()是有意义的。

MATCH (person:Person)
WHERE person.nonExistentProperty = 123
// after the WHERE, there are 0 rows
RETURN count(person) as count
// but when we apply the count(), we get a single row with 0

同样,我们可能希望从匹配的路径中collect()某些内容,而没有路径时,空列表是有意义的。

MATCH (person:Person)
WHERE person.nonExistentProperty = 123
// after the WHERE, there are 0 rows
RETURN collect(person) as people
// but when we apply the collect(), we get a single row with []

这两者的使用都具有 0 行的输入并产生包含 0 的计数或空列表的单行的输出。现在我们有了可以处理的行,查询中的后续操作可以执行(如果我们使用 WITH 而不是 RETURN)。

其他聚合函数在 0 行上运行时可以产生类似的结果。

sum()产生 0 的输出,stDev()stDevP()产生 0.0 的输出,而avg()min()max()percentileDisc()percentileCont()产生null

当存在非聚合项时,无法在 0 行上聚合

聚合仅与执行聚合时存在的非聚合项相关。

当仅存在聚合时,聚合的上下文是相对于所有行。

当存在任何其他非聚合项时,聚合将相对于这些项的组合(即分组键)。

例如,在以下查询片段中,count()聚合是相对于一个人的,它是参与电影的人数。

MATCH (person:Person)-[:ACTED_IN]->(movie:Movie)
WITH person, count(movie) as actedMovieCount
...

但是,这确实需要至少一行才能使聚合工作(我们相对于某些内容进行聚合)。

但在我们减少到 0 行的情况下,没有分组键(至少没有一个数据),因此 Cypher 不允许聚合发生,并且行保持为 0。

让我们考虑一个我们使用:Movie:Person 节点的情况。:Movie 节点具有标题,但:Person 节点没有。

让我们先看看这个查询

MATCH (person:Person)
WHERE EXISTS(person.title)
WITH count(person) as personCount
MATCH (movie:Movie)
WHERE EXISTS(movie.title)
RETURN personCount, count(movie) as movieCount

这会返回 0 的personCount和 38 的movieCount

以下是事件顺序

  1. 匹配了:Person 节点,但没有一个具有title 属性。行变为 0。

  2. 接下来我们计算person 节点的count(),并且由于只有一个聚合项,因此这会发出一个包含 0 的personCount的单行,这是正确的。

  3. 由于我们有一行可以操作,因此可以进行下一个 MATCH,并且由于所有电影都具有title 属性,因此我们得到所有电影的计数,相对于之前的personCount条目。我们得到了预期的结果。

但是如果我们改变此查询的顺序呢?然后会发生什么?

MATCH (movie:Movie)
WHERE EXISTS(movie.title)
WITH count(movie) as movieCount
MATCH (person:Person)
WHERE EXISTS(person.title)
WITH movieCount, count(person) as personCount
RETURN personCount, movieCount

我们交换了顺序,因此我们首先匹配并聚合电影,然后相对于之前的 movieCount 匹配并聚合人员。

我们的结果是

(无变化,无记录)

那么这里发生了什么?

  1. 匹配了:Movie 节点,并且它们都具有title 属性。行变为 38,每个电影一行。

  2. 接下来我们计算movie 节点的count(),这会给我们一个包含 38 的movieCount的单行。

  3. 我们的下一个 MATCH 是到具有title 属性的:Person 节点。不存在这样的节点,因此我们的单行被过滤掉。我们没有行!我们的movieCount数据消失了(它存储在该单行中),因此我们无法引用它!

  4. 我们尝试进行聚合,获取人员的count(),但我们有一个分组键movieCount。我们之前可能在此处拥有的一切数据都消失了,在该行被过滤掉时丢失了。我们无法执行此聚合,因为我们没有可用于分组键的内容(请注意,没有分组键与具有null 值的分组键不同)。我们无法运行聚合。我们不输出任何行。

使用 OPTIONAL MATCH 或模式推导避免问题

当我们在不存在行时使用分组键进行聚合时,就会出现问题,因此我们可以通过避免转到 0 行来避免问题。

一种方法是在我们知道可能没有匹配项但仍希望继续查询时使用 OPTIONAL MATCH。这不会过滤掉行,并且我们的聚合将发出预期的结果。

如果我们从扩展中收集结果,则可以使用模式推导作为快捷方式,因为如果推导中的模式不存在,我们将获得一个空集合,同样不会过滤掉行。

当我们有 0 行时,为什么不将分组键默认为 null?

对于一些人来说,当我们有分组键但 0 行时无法聚合的想法并不正确。一个常见的建议是:为什么不允许聚合(与我们没有分组键时相同)并将分组键值设置为 null?

简短的答案是在这些情况下将范围内的数据的更改为 null 会导致意外且严重错误的结果,尤其是在允许查询继续在此错误数据上执行时。生成的查询结果可能不合理。

例如,假设我们正在使用早期查询的变体将一些计数存储在节点中以便以后快速访问。我们将修复使用的属性(person.name 而不是person.title),但让我们在将:Person 节点添加到我们的图之前运行它,我们只有 movie 节点

MATCH (movie:Movie)
WHERE EXISTS(movie.title)
WITH count(movie) as movieCount
MATCH (person:Person)
WHERE EXISTS(person.name)
WITH movieCount, count(person) as personCount
MERGE (count:CountTracker)
SET count.personCount = personCount, count.movieCount = movieCount
RETURN personCount, movieCount

现在我们从我们已经介绍的内容中知道,当我们匹配到:Person 节点时,我们的行将变为 0,因为图中还没有任何节点,并且因此,我们计算count(person)的聚合将失败,并且我们将获得 0 行,并且查询中的任何进一步操作都将无法执行(没有行可供执行)。

但如果 Cypher 而是将分组键置空并允许查询继续呢?那么 movieCount 将变为 null,而 personCount 将变为 0。无论 personCount 之前是什么值(假设它之前存在任何值),都将被移除,因为将属性设置为 null 等同于将其移除。

如果这是一个更复杂的查询,请考虑将您知道不可能为 null 的值突然更改为 null 的影响。对该属性的使用可能会产生完全意想不到的结果。如果将属性设置为现在为 null 的值,您可能会最终擦除属性。您可能依赖于对该值的比较,而现在由于它是 null,比较的结果将为 null(在 Cypher 中,与 null 的不等式结果为 null),并且可能会根据您使用该结果值的目的进一步传播。

值得庆幸的是,当将属性与 null 进行比较时,您不会在 MATCH 或 WHERE 中遇到麻烦,因为我们要求在此检查中使用 IS NULLIS NOT NULL,使用属性值与 null 的常规相等性将始终失败。

但是,应该清楚的是,将分组键设置为 null 会产生负面和意外的后果,尤其是在使用这些值写入图形时。如果我们不返回并检查输出,则可能会将错误数据写入图形,而谁知道何时会检测到这些错误数据。

出于这些原因,我们认为在这些情况下保持 0 行比突然意外地更改变量值并让查询在不那么正常的状态下继续更正确。