【问题标题】:Return random, no-repeating rows from a table - PostgreSQL从表中返回随机的、不重复的行 - PostgreSQL
【发布时间】:2014-09-11 04:25:36
【问题描述】:

我有一个有趣的问题,我正在寻找一些指导。 我有一个包含数千行的表“图像”。 我希望能够返回一组随机行,一次限制为 50 个。

客户端,我有一个初始的 GetImages() 方法,它最初会返回 50 个“随机”图像(如果有那么多的话)。当用户滚动浏览它们并达到某个数字(大约 40+)时,另一个函数会触发 - GetMoreImages()。

问题是我不确定如何检索更多图像而不会有返回相同结果的风险。

例如,如果总共有 60 张图像,我希望 GetMoreImages() 调用只返回剩余的 10 张图像。

我觉得我还应该提到我的 Id 表是不连续的,因为我使用的是 Instagram 方法 (http://instagram-engineering.tumblr.com/post/10853187575/sharding-ids-at-instagram) 这让我在每行 id 之间存在潜在的巨大差距。

我可以尝试的一种方法是传入我已经拥有的图像的所有 id,但如果用户滚动浏览数千张图像,这将变得笨拙。

我猜另一种方法是在每个用户的应用服务器上存储一组缓存的“随机”值,但我也不太喜欢这个想法。

如果有任何最佳实践可以指导我实现,我们将不胜感激。

【问题讨论】:

    标签: sql postgresql unique session-state random-sample


    【解决方案1】:

    您可以通过以下查询获得随机图像:

    select *
    from images
    order by random()
    limit 50;
    

    我不是 100% 相信以下方法会起作用,但它可能会起作用。你想要的是一个随机数生成器,它可以重现相同的值。为此,请使用setseed()。所以,你可以这样做:

    with t as (
          select setseed(1)
         )
    select *
    from images cross join t
    order by random()
    limit 50;
    

    那么你可以得到后续的值:

    with t as (
          select setseed(1)
         ) 
    select *
    from images cross join t
    order by random()
    limit 50;
    

    问题在于random() 在后续调用中是否以完全相同的顺序被调用。您可以通过以下方式强制执行此操作:

    with t as (
          select setseed(1)
         ),
         i as (
          select i.*, random() as rand
          from images i cross join t
         )
    select *
    from i
    order by i.rand
    limit 50;
    

    但是,这仍然假定对同一个表的多次调用将以相同的顺序进行。 然后,您可以使用 limit 10 offset 50 等运行相同的查询。

    您可以使用计数器、与当前日期时间相关的函数或仅使用随机数生成器来更改每次调用的种子值。

    编辑:

    我通常的做法是使用伪随机数生成器。我只是取相对较大的素数,做一些算术并使用那个值。

    通过更改方程式中的值,您可以根据需要调整参数。例如,我记得 8,191 和 131,071 是素数(因为它们是梅森素数)。所以,我可能会这样处理:

    select i.*
    from images i
    order by mod(i.id * 8191 + 1, 131071)
    limit 50 offset xxx;
    

    您可以调整“+1”来创建不同的序列。这不是真正的“随机”,它取决于id 是一个整数类型,但它避免了随机数生成器方法的不稳定性。这仍在执行order by,因此它可能效率低下,具体取决于您的数据大小。

    【讨论】:

    • 最终查询看起来没问题。前两个不会产生稳定的结果。请注意,最终查询每次都会强制读取整个表并对其进行排序,因此对于大表来说效率会非常低。
    • ... 但是,ORDER BY random() 也是如此。所以没关系。
    • 这实际上是一个很难有效解决的问题。您对我的回答的想法将不胜感激。
    • @CraigRinger 。 . .这是一个有趣的讨论。但是,OP 实际上并没有提到性能是一个问题(并且他/她可能已经在前 50 行使用“随机”order by)。对于缩放,这是一个问题。一些数据库内置了对随机样本的支持。
    • setseed(1) 示例不起作用(在 PG 9.1 中),因为 CTE 未引用,因此已优化;您必须在 i 的定义中引用 t,例如from images i cross join t.
    【解决方案2】:

    客户端状态跟踪

    如果您在这个问题上退后一步,您会发现它从根本上是一个难题,迫使您在效率与正确性之间进行权衡。

    为什么?

    因为要提供您想要的非重复属性,同时向每个用户返回一组不同的随机图像,您需要以某种方式跟踪每个用户的可见/不可见图像

    对于很多客户来说,这是很多状态。

    如果您将状态推送到客户端并保留他们随每个请求发送并附加到的已查看图像列表,这会将状态跟踪负载推送到客户端,但这会使您的查询变得笨拙 - 您可能会想要在VALUES 列表上进行反连接以排除看到的图像,因为NOT IN 在规模上会变得低效。此外,还有客户端必须发送到服务器、服务器必须处理等的所有额外数据。

    Gordon 的解决方案是一个变体,它通过强制进行稳定的随机排序来简化客户端状态,因此客户端状态只是“我看到了多少图像”而不是“我看到了哪些图像”。缺点是顺序是稳定的 - 如果客户端再次请求它,它将从同一随机集的开头开始,而不是不同的。

    如果您不将状态推送到客户端,则服务器必须知道每个客户端看到了哪些图像。有很多方法可以做到这一点,但它们都需要跟踪一堆客户端状态并有效地使该状态过期。选项包括:

    • CREATE TABLE AS SELECT ... 当您第一次收到请求时。然后从该表返回结果。简单且对后续请求非常有效,但对于第一个请求非常慢。不需要您保持事务或会话打开。浪费大量存储空间并要求您使副本过期。这不是一个好方法。

    • 使用WITH HOLD 游标,或使用带有打开事务的常规游标。可以产生相当快的第一个结果,并且存储效率相当高 - 尽管它有时会消耗 很多 的临时存储空间。但是,要求您保持会话打开并与特定客户端关联,因此它不会针对大量客户端进行扩展。还要求您使旧会话过期。

    • 发送由客户端在第一次请求时生成的随机值作为 Gordon 方法中的随机种子。因为他的方法需要全表扫描和排序,我不建议这样做,但它至少可以解决每个客户端/新请求重复相同随机值的问题。您将从客户端发送“offset=50&&seed=1231”。

    • 使用客户端会话表。使用常用方法(cookie、URL 会话 ID 等)跟踪应用程序中的 HTTP 会话,并将它们与数据库或其他地方的状态相关联。客户端只是将会话 ID 提供给服务器,服务器在其本地存储中查找客户端的会话数据,以了解客户端所看到的内容。有了这个,您可以使用NOT IN 列表或针对 ID 列表的VALUES 列表的左反连接,而无需向/从客户端发送 ID。

    所以。很多选择。我确定我也没有全部列出。

    就我个人而言,我会直接使用客户端的 HTTP 会话,或者存储我在客户端首次请求一组新的随机图像时生成的随机请求 ID。我会将看到的图像列表存储在服务器端会话缓存中,这将是一个包含(sessionid, imageid) 对的UNLOGGED 表,或者如果使用后一种方法,则为(requestid, imageid)。在生成随机集时,我会对会话表进行左反连接以排除看到的图像。

    获取随机行

    哈,你还没说完呢。

    ORDER BY random() 的幼稚方法会进行全表扫描和排序。这对于一张大桌子来说真的很痛苦。

    如果 PostgreSQL 提供了一种从表中读取随机行的方法,只需选择一个表页并从中读取一行,那就太好了。不幸的是,它甚至不是可重复的版本。

    由于您的 ID 很稀疏,因此您无法轻松生成一组随机 ID 以供选择。

    Picking random rows from a table turns out to be a hard problem。同样,您可以选择简化问题以换取减少随机性,例如:

    • 在数据中随机选取一个按 ID 排序的连续行块OFFSET

    • CREATE UNLOGGED TABLE AS SELECT ... 缓存数据的随机副本。要么创建一堆并在客户端的第一个请求进入时随机选择其中一个,要么创建一个并定期重新创建。

    • ...可能更多

    简化问题

    那么,最初是什么让这变得如此困难?

    • 非重复结果
    • 随机行
    • 每个客户或请求的唯一性

    我们可以做些什么来使这更容易?

    • 放宽对随机性的要求。例如在随机偏移处选择连续行。
    • 删除不重复图像的要求或将其放宽为(例如)仅最后两组图像不应重复。
    • 放宽对每个客户端或每个请求唯一性的要求。对所有客户端使用相同的随机化并将其缓存。

    另一个有用的技巧可能是在客户端之间共享状态。如果您不想重复发送给一个客户的图像,但您不在乎给定的客户可能永远不会看到图像,您可以有效地跟踪看到的图像,客户在一起。例如,您可能有一个 WITH HOLD 游标池,您可以通过在客户端的 HTTP 会话中存储映射来将其分配给客户端。每组客户端都从特定游标中获取结果。当一个客户端从游标读取结果块时,同一池中的其他客户端将不会在此会话中看到这些结果。所以这种方法只有在你有一个“非常大”的图像集时才有效,即客户端实际上不会在一个浏览会话中用完。

    同样,您可能有一个缓存的UNLOGGED 随机数据表池。当客户端发送他们的第一个请求时,您使用他们的 HTTP 会话 ID 将它们分配给其中一个表 - 通过散列分桶或通过存储映射。然后,您可以从该表返回结果以供后续请求。

    呸。哇。那变得有点长了。我希望它有一些意义,并有一些有用的想法。

    【讨论】:

      【解决方案3】:

      底部的表格定义。对于一组极其快速的随机唯一 ID:

      with r as (
          select distinct
              ceil(
                  random() *
                  (select max(image_id) from image)
              )::int as image_id
          from generate_series(1, 200)
          limit 100
      )
      select image_id
      from image inner join r using (image_id)
      limit 50
      

      由于主键有间隙,因此需要将表连接到超过 50 个随机生成的 id 以确保至少有 50 个。还有多少将取决于 PK 的“间隙”。在示例表中,每 5 个缺少 2 个。

      要获得不同的随机生成的 id,还需要生成多于(在上面的示例中)要加入的 100 个。还有多少取决于桌子有多大。对于一个非常大的桌子来说,再多几个就足够了。

      即使上面的数字被夸大了,对性能的影响也可以忽略不计。为了让它不返回看起来像的图像,我将创建一个 seem_images 表,其中包含 session_id(或 user_id 用于更长的到期时间)和 image_id 并在每个 GetImages 处插入。无需额外功能GetMoreImages

      with r as (
          select distinct
              ceil(
                  random() *
                  (select max(image_id) from image)
              )::int as image_id
          from generate_series(1, 200)
          limit 100
      ), t as (
          select image_id, image
          from image inner join r using (image_id)
          where not exists (
              select 1
              from seem_image
              where image_id = image.image_id and session_id = 1
          )
          limit 50
      ), i as (
          insert into seem_image (image_id, session_id)
          select image_id, 1
          from t
      )
      select * from t;
      

      上面的查询只会返回不看起来的图像。对于样本 300 万行 image 表,它非常快。对于长图像浏览和会话,有必要从上面的100200 更改为适当的数字。过期会话应定期从seem_image 表中删除,具体取决于会话过期时间,以避免它变得太大。

      示例image(以整数作为主键,有间隙)和seem_image

      create table image (
          image_id integer primary key,
          image bytea
      );
      insert into image (image_id, image)
      select image_id, image
      from
          generate_series(1, 5000000) g (image_id)
          cross join
          (values (decode(rpad('', 1024 * 100, 'F'), 'hex'))) i (image)
      where mod (image_id, 5) not in (0, 1)
      ;
      analyze image;
      
      create table seem_image (
          session_id integer,
          image_id integer,
          primary key (image_id, session_id)
      );
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-10
        • 1970-01-01
        • 2020-08-31
        • 2020-12-19
        • 2017-09-07
        • 2021-12-26
        相关资源
        最近更新 更多