首页 > 解决方案 > 使用多个“count distinct”具有巨大的性能影响

问题描述

在 Google Cloud Spanner 上,我们遇到了一些分析大量数据的查询的内存问题

GenerateArrayEvaluator ran out of memory during buffering one value 
(original error message 'GenerateArrayEvaluator ran out of memory. 
Requested: 9 byte(s). Already reserved: 294649856 byte(s). 
Limit: 294649856 byte(s). Details: Cannot reserve 9 bytes to create a new segment 
The operation HashJoinIterator is reserving the most memory (114717769 bytes).'). 
Requested: 0 byte(s). Already reserved: 294649856 byte(s). Limit: 294649856 byte(s).
Max Memory Operation: The operation HashJoinIterator is reserving the most memory (114717769 bytes).

我发现由于某些原因,查询正在执行非常未优化的操作。我能够隔离查询的有罪部分。所以这是重现这种情况的最小查询:

SELECT 
  COUNT(DISTINCT a) a,
  COUNT(DISTINCT b) b
FROM foo
WHERE primary_split_key = "..."

该查询有 2 个子句COUNT(DISTINCT ...),这就是问题所在。它将创建一个map compute操作,该操作将返回的行数乘以COUNT(DISTINCT ...)select 子句中的数量。

换句话说,如果SELECT * FROM foo WHERE primary_split_key = "..."返回 10 行,则 map 计算将生成 20 行 ( 10row * 2countDistinct)。

如果我们有 500k 行和 3 行,count distinct那么它将生成 150 万行。

请参阅 443k 行和 2 的查询说明COUNT(DISTINCT ...)查询说明

所以我们可以说这个查询的扩展性真的很差。

我们正在努力调整它以使其更好地工作。但是,我们想听听 Cloud Spanner 团队的意见:这是 spanner/count distinct 的预期行为,还是您愿意在不久的将来改进的东西?

也总是很高兴听到其他用户体验的替代方案。

编辑:我发现其他一些查询失败并显示相同错误消息的情况。但是,这些查询以这种方式显式使用函数 GENERATE_ARRAY:

SELECT * 
FROM a
JOIN b ON a.id = b.id_a
LEFT JOIN UNNEST(GENERATE_ARRAY(0, a.some_number)) record

出于这个原因,我怀疑 generate_array 未优化或有内存泄漏。

标签: google-cloud-spanner

解决方案


关于内存问题,根据错误消息,它似乎HashJoinIterator是消耗最多内存的那个,它GenerateArrayEvaluator只是由此引起的低内存情况的受害者。您的第二个查询也可能是这种情况。据推测,在您的实际查询中,您有一个哈希连接试图在一个大输入上构建一个哈希表?您是否尝试过使用提示重新执行查询join_method=apply_join

您发布的 count distinct 查询确实对要聚合的数据进行的传递次数与不同聚合的数量(在您的情况下为 2)一样多。虽然计划中显示的 2s 运行时间比我预期的要慢,但运行时间还取决于许多其他因素 - 您的实例的 CPU 利用率和查询的并行度,因此很难对此发表评论。


推荐阅读