【问题标题】:How to guarantee that at least N rows are returned by recursive CTE in Postgres如何保证Postgres中的递归CTE至少返回N行
【发布时间】:2016-07-07 06:17:52
【问题描述】:

在 Postgres 中描述 SELECT TOP ... 查询的大多数资源都说您应该改用 LIMIT,如果您需要按某种顺序选择顶部元素,则可能使用 ORDER BY 子句。

如果您需要从没有排序的递归查询中选择前 N 个元素,并且查询可能返回少于 N 行而不进行递归(因此 TOP 部分是否需要确保结果集至少 N 行,而LIMIT 可以允许更少的行)?

我的具体用例是对dynamic SQL pattern for selecting a random subsample of a table 的修改。

这是我修改的link to the sql source。最简单的方法是查看那里定义的最终函数_random_select。它非常接近上述链接,但已被修改为在输入表和输出结果集中是多态的,并且正确地考虑了只返回输入表中已经存在的列的需要(另一个动态 SQL破解以从最终结果集中排除临时 row_number 结果)。

这是一个令人眼花缭乱的东西,但它是我所拥有的最接近可重现示例的东西。如果您使用_random_select 并尝试从大于 4500 行的表中获取大约 4500 行的内容,您开始看到较小的结果集的可能性很高,并且随着您增加所需样本的大小,它只会变得更糟(因为随着您所需的样本变大,重复出现的情况会变得更糟)。

请注意,在我的修改中,我没有使用此链接中的 _gaps 技巧,这意味着如果某个索引列中存在间隙,则过度采样以抵消采样效率低下的问题。那部分与这个问题无关,在我的例子中,我使用row_number 来确保有一个整数列没有可能的间隙。

CTE 是递归的,以确保如果 CTE 的第一个非递归部分没有给您足够的行(因为 UNION 删除了重复项),那么它将通过另一轮返回CTE 的递归调用,并继续处理更多结果,直到你得到足够的结果。

在链接的示例中,使用了LIMIT,但我发现这不起作用。该方法返回的结果较少,因为LIMIT 只是最多N 行 保证。

您如何获得至少 N 行的保证?选择TOP N 行似乎是执行此操作的自然方式(因此递归 CTE 必须继续运行,直到获得足够的行以满足TOP 条件),但这在 Postgres 中不可用。

【问题讨论】:

  • 说,您的目标是前 5 个类别,但您的商店只有 3 个。额外的 2 个类别应该显示什么——“未来类别 1”和“未来类别 2”?
  • 这是一个递归 CTE,所以这实际上是不可能的。它会生成随机行,并且可以在需要时继续这样做。查询在到达已经少于表中可用行数的样本之前就被缩短了。
  • TOP 是 SQL Server、Sybase 和其他一些数据库使用的关键字。它的工作原理与 Postgres 中的 LIMIT 几乎相同。我不知道有任何数据库会具有您想要的行为(如果您从只能提供少于 N 行的数据流中请求 N 行,那么所需的行为是它应该“挂起”)。也许,你最好专注于你的实际问题,而不是你的解决方案。如果您想从表中选择一个随机子样本,请询问有关该问题的问题,而不是有关 LIMIT 的问题。 TOP/LIMIT 只限制结果集,从不扩展它。
  • 接下来的问题是如何继续从递归 CTE 请求行,直到返回最小的行集。我正在处理的示例问题是随机抽样而不重复,但它可能是您需要来自无限数据流的 N 行的任何问题。什么时候可以提早返回的规则是什么?人们会认为没有明确停止标准的递归 CTE 将继续产生结果,直到LIMIT 停止它。但是,在加入递归调用之前,可能会返回中间结果。它是如何工作的?
  • 这个问题的一个更简单的版本可能是生成一个包含前 N 个斐波那契数列的列,方法是将第 i 步的计算与第 i+1 步的递归调用合并,然后允许LIMIT成为递归终止的机制。 This link 讨论了它,即使与像 Haskell 列表这样的惰性流进行了相同的比较。但是通过LIMIT(或其他东西)将其扩展为使用更通用的“拉到足够”,这正是问题所在。

标签: sql postgresql common-table-expression recursive-query sql-limit


【解决方案1】:

可以使用Set Returning Functions (SRF) 生成已知数量的行。此外,OUTER 连接保证,连接的一侧将全部返回。

在这种情况下,我假设FULL JOINgenerate_series(1, 100) 之间(如果您的目标是至少 100 行)应该可以解决问题。事实上,LEFT 加入也可以解决问题,但它也可以过滤掉实际需要的额外行,因此我会选择FULL

附:如果你能展示你的代码,提供一些例子会更容易。

【讨论】:

  • 我确实链接到我上面的代码。我想你误会了。 generate_series 不是连接的一部分,而是一个与generate_series 具有相同size 的随机整数列用于连接。该问题与该联接无关。问题是必须删除重复的结果,这就是UNION 所做的。但是查询优化器看着它并说“啊哈,在我用UNION 删除重复但之前我为递归调用而烦恼之后,较小的非重复值集已经小于或等于LIMIT 的大小,所以就退货吧,不要试图获得更多。”
  • 然而,希望递归 CTE 被视为随机行的 infinite 流,唯一能防止它在无限递归中永远挂起的方法是LIMIT 的最外层使用 ... 如果它是这样工作的,那么 LIMIT 将足以确保至少返回所需的行数。不幸的是,优化器似乎试图更聪明,并在进入下一轮递归之前检查递归 CTE 的部分结果集是否已经具有令人满意的行数。任何低于限制的东西都会显示正常。
【解决方案2】:

这对于评论来说太长了,但可以提供有关我现有查询的信息。从documentation on recursive query evaluation 开始,递归查询将采取的步骤是:

计算非递归项。对于 UNION(但不是 UNION ALL),丢弃重复的行。在递归查询的结果中包含所有剩余的行,并将它们放在临时工作表中。

只要工作表不为空,重复这些步骤:

一个。评估递归项,将工作表的当前内容替换为递归自引用。对于 UNION(但不是 UNION ALL),丢弃重复的行和与任何先前结果行重复的行。在递归查询的结果中包含所有剩余的行,并将它们放在一个临时中间表中。

b.用中间表的内容替换工作表的内容,然后清空中间表。

所以我对 cme​​ts 的预感(在尝试使用 UNION ALL 之后)大部分都在正确的轨道上。

正如文档所述,这实际上只是一种 iteration,它重用了先前的非递归结果部分来代替在递归部分。

所以它更像是一个不断缩小的过程,其中用于替换递归名称的“工作表”仅包含最近递归轮次中的特定结果子集,与之前的结果不重复.

这是一个例子。假设我们在表中有 5000 行,我们想要对 3000 个唯一行进行采样,一次对 1000 个(可能不是唯一的)样本进行递归采样。

我们做了第一批 1000 个,删除了重复项,因此对于这些大数字(N=5000,m = 1000,k=1,重新排列术语以避免溢出),我们最终得到了大约 818 个非重复值based on the binomial distribution

这些 818 成为工作表,这个结果集被替换为我们下一次遍历的递归项。我们绘制了另一组大约 818 个唯一行,但是在与工作表中的原始 818 行进行比较时,必须删除重复行 (UNION)。两个不同的 818 随机抽取将有很大的重叠(平均大约 150 个),因此所有这些都被丢弃,并且任何 new 唯一的行仍然成为 new 工作表。

因此,您将在第一次抽签时添加大约 818 个独特样本,然后工作台缩小,第二次抽签时您将添加大约 650 个,工作台缩小......然后您继续这样做,直到您达到所需的总样本总数(在这种情况下为 3000 个)工作表最终为空。

一旦工作表足够小,其中的所有内容很有可能在下一次抽签 1000 中被复制,此时工作表为空,递归终止。

对于绘制 3000 个样本,您可能可以对这个工作表进行足够多次的更新。但是,当您从 3000 接近表的总大小 5000 时,概率很快缩小到几乎为零。

因此,这不是优化器问题导致较小的结果集短路,实际上是 Postgres 中实现“递归”的特定方式的问题——它实际上不是递归,而是在集合上运行的简单迭代当前工作表和最近的递归查询结果集之间的差异。对于像这样的随机抽样,该工作表会随着每次迭代而迅速变小,直到由于选择重复项的可能性很高而极有可能是空的。

【讨论】:

  • 所以,归结为在 Postgres 中如何使用UNION(不是全部)进行递归查询。很高兴看到您深入了解并在答案中将其拼写出来。我从来没有仔细研究过它,知道它很有趣。从您到目前为止所写的内容来看,在我看来,对于从表中选择子样本这一特定任务,您的递归方法并不真正合适。我会尝试看看其他(非递归)方法。
【解决方案3】:

你的评价很中肯。 my referenced answer 中的递归查询仅比原始简单查询更灵活一些。它仍然需要 ID 空间中相对较少的间隙和远小于表大小的样本大小才能可靠。

虽然我们在简单查询中需要一个舒适的盈余(“限制 + 缓冲区”)来涵盖缺失和重复的最坏情况,但我们可以使用通常足够的较小盈余 - 因为我们有一个安全网如果我们在第一遍中未达到限制,将填写递归查询。

无论哪种方式,该技术旨在快速从大表中随机选择一个小项。

该技术没有意义对于具有太多差距或(您的焦点)样本量太接近总表的情况size - 这样递归项可能会在达到限制之前耗尽。对于这种情况,一个普通的旧:

SELECT *   -- or DISTINCT * to fold duplicates like UNION does
FROM   TABLE
ORDER  BY random()
LIMIT  n;

.. 更高效:无论如何你都会阅读大部分表格。

【讨论】:

  • 虽然这个问题恰好是为随机抽样问题服务的,但真正的问题是如何创建一个充当惰性无限流的递归查询,这样只有使用LIMIT 会导致查询终止并产生结果集。似乎递归 CTE 通常无法做到这一点(我猜如果您使用 UNION ALL,您可能会得到一个无限查询with duplicates,这可能很有用),因为它重新使用最新的唯一结果集。也许递归函数是在 Postgres 中执行此操作的正确方法。
  • 您认为临时索引数组(或临时表)可以在这里提供帮助吗?不会有间隙,并且结构长度可用于在递归循环中一个接一个地获取唯一元素。该结构需要在此循环期间移出选定的元素(或虚拟调整大小并转换为不变的结构)......我不知道这是否可行或根据需要执行,但你能看到这个工作吗?
  • @PinnyM:一旦您可以无间隙地工作,解决方案将非常快速和简单。在并发负载下新添加的行可能会出现并发症......也许问一个新的 question 与您的想法的详细信息?评论不是新方面的地方。您可以随时链接到这个以获取上下文。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-26
  • 1970-01-01
  • 2021-12-24
  • 2023-04-06
  • 2017-08-31
  • 1970-01-01
相关资源
最近更新 更多