选择随机行PostgreSQL的最佳方法


344

我想要在PostgreSQL中随机选择行,我尝试了以下方法:

select * from table where random() < 0.01;

但是其他一些建议:

select * from table order by random() limit 1000;

我有一个很大的表,有5亿行,我希望它能很快。

哪种方法更好?有什么区别?选择随机行的最佳方法是什么?


1
嗨,杰克,谢谢您的回应,执行时间依次缩短了,但我想知道有什么区别……
nanounanue 2011年

嗯...不客气。因此,您是否尝试过对不同方法进行基准测试?

也有很多更快的方式。这完全取决于您的要求以及必须使用的工具。您是否需要1000行?该表格是否有数字ID?没有/很少/很多差距?速度有多重要?每个时间单位有多少个请求?是否每个请求都需要不同的集合,或者在定义的时间段内它们可以相同?
欧文·布兰德斯特

6
第一个选项“(random()<0.01)”在数学上是不正确的,因为如果随机数不低于0.01,您将无法获得任何行的响应,无论表多大,这种情况在任何情况下都可能发生(尽管可能性较小)或更高的阈值。第二个选择永远是对的
荷米

1
如果您只想选择一行,请参阅以下问题:stackoverflow.com/q/5297396/247696
Flimm,

Answers:


230

根据您的要求(加上注释中的其他信息),

  • 您有一个数字ID列(整数),并且只有很少(或很少有)间隙。
  • 显然没有或只有很少的写操作。
  • 您的ID列必须建立索引!主键很好用。

下面的查询不需要大表的顺序扫描,只需要索引扫描。

首先,获取主要查询的估算值:

SELECT count(*) AS ct              -- optional
     , min(id)  AS min_id
     , max(id)  AS max_id
     , max(id) - min(id) AS id_span
FROM   big;

唯一可能昂贵的部分是count(*)(用于大型桌子)。鉴于上述规格,您不需要它。估算就可以了,几乎可以免费获得(此处有详细说明):

SELECT reltuples AS ct FROM pg_class WHERE oid = 'schema_name.big'::regclass;

只要ct不是太大小于id_span,查询会优于其他方法。

WITH params AS (
    SELECT 1       AS min_id           -- minimum id <= current min id
         , 5100000 AS id_span          -- rounded up. (max_id - min_id + buffer)
    )
SELECT *
FROM  (
    SELECT p.min_id + trunc(random() * p.id_span)::integer AS id
    FROM   params p
          ,generate_series(1, 1100) g  -- 1000 + buffer
    GROUP  BY 1                        -- trim duplicates
    ) r
JOIN   big USING (id)
LIMIT  1000;                           -- trim surplus
  • id空间中生成随机数。您的差距很小,因此要检索的行数增加10%(足以轻松覆盖空白)。

  • 每个都id可以被偶然选择多次(尽管ID空间很大,这是不太可能的),因此请对生成的数字进行分组(或使用DISTINCT)。

  • ids 加入大表。有了适当的索引,这应该非常快。

  • 最后,修剪id未被骗子和缺口吃掉的剩余食物。每行都有完全相等的机会被选中。

简洁版本

您可以简化此查询。上面查询中的CTE仅用于教育目的:

SELECT *
FROM  (
    SELECT DISTINCT 1 + trunc(random() * 5100000)::integer AS id
    FROM   generate_series(1, 1100) g
    ) r
JOIN   big USING (id)
LIMIT  1000;

使用rCTE进行优化

特别是如果您不确定差距和估算值。

WITH RECURSIVE random_pick AS (
   SELECT *
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   generate_series(1, 1030)  -- 1000 + few percent - adapt to your needs
      LIMIT  1030                      -- hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss

   UNION                               -- eliminate dupe
   SELECT b.*
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   random_pick r             -- plus 3 percent - adapt to your needs
      LIMIT  999                       -- less than 1000, hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss
   )
SELECT *
FROM   random_pick
LIMIT  1000;  -- actual limit

我们可以在基本查询中使用较小的盈余。如果间隙太多,那么在第一次迭代中我们找不到足够的行,则rCTE会继续使用递归项进行迭代。我们仍然需要在ID空间中保持相对较少的间隙,否则在达到限制之前递归可能会枯竭-否则我们必须从足够大的缓冲区开始,这不利于优化性能。

重复项UNION在rCTE中被消除。

LIMIT一旦我们有足够的行,外部将使CTE停止。

该查询经过精心设计,可以使用可用的索引,生成实际上是随机的行,并且直到我们达到限制后才会停止(除非递归运行干了)。如果要重写它,这里有很多陷阱。

包装功能

重复使用不同的参数:

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big AS
$func$
DECLARE
   _surplus  int := _limit * _gaps;
   _estimate int := (           -- get current estimate from system
      SELECT c.reltuples * _gaps
      FROM   pg_class c
      WHERE  c.oid = 'big'::regclass);
BEGIN

   RETURN QUERY
   WITH RECURSIVE random_pick AS (
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   generate_series(1, _surplus) g
         LIMIT  _surplus           -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses

      UNION                        -- eliminate dupes
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   random_pick        -- just to make it recursive
         LIMIT  _limit             -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses
   )
   SELECT *
   FROM   random_pick
   LIMIT  _limit;
END
$func$  LANGUAGE plpgsql VOLATILE ROWS 1000;

呼叫:

SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);

您甚至可以使此泛型适用于任何表:将PK列和表的名称作为多态类型并使用EXECUTE...但这超出了此问题的范围。看到:

可能的选择

如果您的要求允许重复调用使用相同的集合(并且我们正在谈论重复调用),我将考虑实例化视图。一次执行上述查询,然后将结果写入表中。用户以闪电般的速度获得准随机选择。在您选择的时间间隔或事件中刷新您的随机选择。

Postgres 9.5简介 TABLESAMPLE SYSTEM (n)

n百分比在哪里。手册:

BERNOULLISYSTEM采样方法的每一个接受单个参数,它是表格样本的分数,表示为 0到100之间的百分比。此参数可以是任何real值的表达式。

大胆强调我的。这是非常快的,但结果并非完全是随机的。再次手册:

SYSTEM方法明显快于BERNOULLI指定较小采样百分比时的方法,但是由于聚类效果,它可能会返回表中的随机性较低的样本。

返回的行数可以有很大的不同。对于我们的示例,要获得大约 1000行:

SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);

有关:

安装其他模块tsm_system_rows以获得确切的请求行数(如果有的话),并允许使用更方便的语法:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

有关详细信息,请参见Evan的答案

但这还不是完全随机的。


t表在哪里定义?应该用r代替t吗?
Luc M

1
@LucM:它在这里定义:JOIN bigtbl t,是的缩写JOIN bigtbl AS tt表别名bigtbl。其目的是缩短语法,但在此特定情况下不需要。我在答案中简化了查询,并添加了一个简单的版本。
Erwin Brandstetter'1

generate_series(1,1100)中的值范围的目的是什么?
Awesome-o

@ Awesome-o:目标是检索1000行,我从额外的10%开始,以补偿一些差距或(不太可能,但可能)重复的随机数...解释在我的答案中。
Erwin Brandstetter,2014年

欧文(Erwin),我发布了您的“可能的替代方案”的变体:stackoverflow.com/a/23634212/430128。会对您的想法感兴趣。
拉曼

100

您可以使用以下方法检查和比较两者的执行计划

EXPLAIN select * from table where random() < 0.01;
EXPLAIN select * from table order by random() limit 1000;

对大型表1进行的快速测试显示,ORDER BY首先对整个表进行排序,然后再选择前1000个项目。对大型表进行排序不仅可以读取该表,还可以读取和写入临时文件。该where random() < 0.1只扫描整个表一次。

对于大型表,这可能不是您想要的,因为即使是一次完整的表扫描也可能需要很长时间。

第三个建议是

select * from table where random() < 0.01 limit 1000;

一旦找到1000行,该表就会停止表扫描,因此会更快返回。当然,这会降低随机性,但是对于您的情况,这也许就足够了。

编辑:除了这些注意事项,您可能会为此查看已问到的问题。使用查询会[postgresql] random返回很多匹配。

Depez的链接文章概述了其他几种方法:


1 “大”,如“完整表将不适合内存”。


1
关于编写用于执行排序的临时文件的要点。这确实是一个很大的打击。我想我们可以这样做random() < 0.02,然后再整理那个列表,然后limit 1000!这种排序在几千行上会更便宜(lol)。
唐纳德·麦纳

“从表中选择*,其中random()<0.05限制为500;” 是postgresql的较简单方法之一。我们在我们的一个项目中利用了这一点,我们需要选择5%的结果,一次最多只能选择500行。
tgharold 2014年

为什么在世界上您会考虑使用O(n)全面扫描来检索500m行表上的样本?在大型表上它的速度很慢,而且完全没有必要。
mafu

74

按random()进行PostgreSQL排序,以随机顺序选择行:

select your_columns from your_table ORDER BY random()

通过random()进行postgresql排序,具有以下不同:

select * from 
  (select distinct your_columns from your_table) table_alias
ORDER BY random()

postgresql命令按随机数限制一行:

select your_columns from your_table ORDER BY random() limit 1

1
select your_columns from your_table ORDER BY random() limit 1拿〜2分钟EXEC上45mil行

有什么办法可以加快速度吗?
CpILL

43

从PostgreSQL 9.5开始,有一种新语法专门用于从表中获取随机元素:

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

本示例将为您提供的5%的元素mytable

请参阅此博客文章的更多说明:http : //www.postgresql.org/docs/current/static/sql-select.html


3
docs中的重要说明:“ SYSTEM方法执行块级采样,每个块都有指定的机会被选中;返回每个选中块中的所有行。当采样率较小时,SYSTEM方法比BERNOULLI方法快得多是指定的,但是由于聚类效果,它可能会返回表的随机性较低。”
蒂姆(Tim)

1
有没有办法指定行数而不是百分比?
Flimm

4
您可以TABLESAMPLE SYSTEM_ROWS(400)用来获取400个随机行的样本。您需要启用内置tsm_system_rows扩展才能使用此语句。
米卡·勒·百利夫(MickaëlLe Baillif)'18年

不幸的是,不适用于删除。
加德尔卡列姆

27

带有ORDER BY的将是速度较慢的一个。

select * from table where random() < 0.01;逐条记录,并决定是否对其进行随机过滤。这是O(N)因为它只需要检查每个记录一次。

select * from table order by random() limit 1000;将对整个表进行排序,然后选择第一个1000。除了幕后的任何伏都教徒魔法外,顺序是O(N * log N)

缺点random() < 0.01是您将获得可变数量的输出记录。


请注意,与随机排序相比,有一种改组数据的更好的方法:Fisher-Yates Shuffle,在中运行O(N)。但是,在SQL中实现混洗听起来确实是个挑战。


3
不过,没有理由不能在第一个示例的末尾添加限制1。唯一的问题是您有可能无法收回任何记录,因此您必须在代码中加以考虑。
Rerequestual'5

Fisher-Yates的问题在于您需要将整个数据集存储在内存中以便从中进行选择。对于大型数据集不可行:(
CpILL

16

这是一个对我有用的决定。我想这很容易理解和执行。

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;

6
我认为该解决方案可以正常工作ORDER BY random(),但在处理大型表时可能效率不高。
Anh Cao

15
select * from table order by random() limit 1000;

如果您知道要多少行,请签出tsm_system_rows

tsm_system_rows

模块提供表采样方法SYSTEM_ROWS,可在SELECT命令的TABLESAMPLE子句中使用。

此表采样方法接受单个整数参数,该参数是要读取的最大行数。除非表没有足够的行,否则结果样本将始终恰好包含那么多的行,在这种情况下,将选择整个表。像内置的SYSTEM采样方法一样,SYSTEM_ROWS执行块级采样,因此采样不是完全随机的,但是可能会受到聚类的影响,尤其是在仅要求少量行的情况下。

首先安装扩展

CREATE EXTENSION tsm_system_rows;

然后查询

SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);

2
我在您添加的答案中添加了链接,这是对内置SYSTEM方法的显着改进。
Erwin Brandstetter,2016年

我刚刚在这里回答了一个问题(随机单条记录),在此期间,我对和扩展进行了大量的基准测试。据我所知,它们几乎是无用的,只是对随机行的选择极少。如果您能快速浏览一下并评论我分析的有效性或其他方式,将不胜感激。tsm_system_rowstsm_system_time
Vérace

6

如果只需要一行,则可以使用offset从导出的计算值count

select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));

2

Erwin Brandstetter概述的物化视图“可能的选择”的变体是可能的。

例如,假设您不希望重复的随机值重复。因此,您将需要在包含您的(非随机)值集的主表上设置一个布尔值。

假设这是输入表:

id_values  id  |   used
           ----+--------
           1   |   FALSE
           2   |   FALSE
           3   |   FALSE
           4   |   FALSE
           5   |   FALSE
           ...

ID_VALUES根据需要填充表。然后,按照Erwin的描述,创建一个实例化视图,该视图将ID_VALUES表随机化一次:

CREATE MATERIALIZED VIEW id_values_randomized AS
  SELECT id
  FROM id_values
  ORDER BY random();

请注意,实例化视图不包含已使用的列,因为这将很快过时。视图也不需要包含表中可能存在的其他列id_values

为了获得(并“消费”)随机值,请在上使用UPDATE-RETURNING id_valuesid_valuesid_values_randomized连接中选择,然后应用所需的条件仅获得相关的可能性。例如:

UPDATE id_values
SET used = TRUE
WHERE id_values.id IN 
  (SELECT i.id
    FROM id_values_randomized r INNER JOIN id_values i ON i.id = r.id
    WHERE (NOT i.used)
    LIMIT 5)
RETURNING id;

LIMIT根据需要进行更改-如果一次只需要一个随机值,请更改LIMIT1

启用正确的索引后id_values,我相信UPDATE-RETURNING应该在很少的负载下非常快速地执行。它通过一次数据库往返返回随机值。“合格”行的条件可以根据要求复杂。可以id_values在任何时候将新行添加到表中,并且一旦物化视图刷新(它们很可能在非高峰时间运行),它们将可供应用程序访问。物化视图的创建和刷新将很慢,但是仅当将新ID添加到id_values表中时才需要执行该视图。


很有意思。如果我不仅需要选择,还需要使用select..for pg_try_advisory_xact_lock进行更新,那是否可行?(即我需要许多并发读取和写入)
Mathieu 2015年

1

根据我的经验教训:

offset floor(random() * N) limit 1不比快order by random() limit 1

我认为该offset方法会更快,因为它可以节省在Postgres中进行排序的时间。事实并非如此。


0

添加名为rtype 的列serial。索引r

假设我们有200,000行,我们将生成一个随机数n,其中0 n<<== 200,000。

用选择行r > n,对行进行排序,ASC然后选择最小的行。

码:

select * from YOUR_TABLE 
where r > (
    select (
        select reltuples::bigint AS estimate
        from   pg_class
        where  oid = 'public.YOUR_TABLE'::regclass) * random()
    )
order by r asc limit(1);

该代码是不言自明的。中间的子查询用于从https://stackoverflow.com/a/7945274/1271094快速估算表行数。

在应用程序级别,如果n>行数或需要选择多行,则需要再次执行该语句。


我喜欢它,因为它简短而优雅:)我什至找到了一种改进的方法:EXPLAIN ANALYZE告诉我,这样,将不会使用PKEY索引,因为random()返回一个double,而PKEY需要一个BIGINT。
fxtentacle

选择*从YOUR_TABLE其中r>(选择(从pg_class选择reltuples :: bigint AS估计,oid ='public.YOUR_TABLE':: regclass)* random()):: BIGINT按r asc limit(1);
fxtentacle

0

我知道我参加聚会有点晚了,但是我才发现这个很棒的工具pg_sample

pg_sample -从较大的PostgreSQL数据库中提取少量样本数据集,同时保持参照完整性。

我用一个3.5亿行的数据库进行了尝试,它确实非常快,不了解随机性

./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.