ChatGPT解决这个技术问题 Extra ChatGPT

选择随机行PostgreSQL的最佳方法

我想在 PostgreSQL 中随机选择行,我试过这个:

select * from table where random() < 0.01;

但其他一些人建议这样做:

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

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

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

嗨,杰克,感谢您的回复,执行时间按顺序变慢了,但我想知道如果有的话,哪个是不同的......
呃……不客气。那么,您是否尝试过对不同方法进行基准测试?
还有更快的方法。这一切都取决于您的要求以及您必须使用的内容。您是否需要正好 1000 行?表是否有数字 ID?没有/很少/很多差距?速度有多重要?每个时间单位有多少请求?每个请求是否需要不同的集合,或者在定义的时间片内它们是否相同?
第一个选项“(random() < 0.01)”在数学上是不正确的,因为如果没有随机数低于 0.01,您将不会得到任何响应行,这在任何情况下都可能发生(尽管可能性较小),无论表有多大或更高的阈值。第二个选项永远是对的
如果您只想选择一行,请参阅此问题:stackoverflow.com/q/5297396/247696

E
Erwin Brandstetter

快捷方式

鉴于您的规格(以及评论中的其他信息),

您有一个数字 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 / relpages * (pg_relation_size(oid) / 8192))::bigint AS ct
FROM   pg_class
WHERE  oid = 'big'::regclass;  -- your table name

详细解释:

在 PostgreSQL 中发现表的行数的快速方法

只要 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)。

将 id 加入到大表中。有了索引,这应该非常快。

最后修剪没有被骗子和差距吃掉的多余的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
   )
TABLE  random_pick
LIMIT  1000;  -- actual limit

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

rCTE 中的 UNION 消除了重复项。

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

此查询经过精心起草以使用可用索引,生成实际随机行并且在我们达到限制之前不会停止(除非递归运行枯竭)。如果你要重写它,这里有很多陷阱。

包装成函数

对于具有不同参数的同一张表重复使用:

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big
  LANGUAGE plpgsql VOLATILE ROWS 1000 AS
$func$
DECLARE
   _surplus  int := _limit * _gaps;
   _estimate int := (           -- get current estimate from system
      SELECT (reltuples / relpages * (pg_relation_size(oid) / 8192))::bigint
      FROM   pg_class
      WHERE  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
   )
   TABLE  random_pick
   LIMIT  _limit;
END
$func$;

称呼:

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

通用函数

我们可以使这个通用方法适用于具有唯一整数列(通常是 PK)的任何表:将表作为多态类型和(可选)PK 列的名称传递并使用 EXECUTE

CREATE OR REPLACE FUNCTION f_random_sample(_tbl_type anyelement
                                         , _id text = 'id'
                                         , _limit int = 1000
                                         , _gaps real = 1.03)
  RETURNS SETOF anyelement
  LANGUAGE plpgsql VOLATILE ROWS 1000 AS
$func$
DECLARE
   -- safe syntax with schema & quotes where needed
   _tbl text := pg_typeof(_tbl_type)::text;
   _estimate int := (SELECT (reltuples / relpages
                          * (pg_relation_size(oid) / 8192))::bigint
                     FROM   pg_class  -- get current estimate from system
                     WHERE  oid = _tbl::regclass);
BEGIN
   RETURN QUERY EXECUTE format(
   $$
   WITH RECURSIVE random_pick AS (
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * $1)::int
         FROM   generate_series(1, $2) g
         LIMIT  $2                 -- hint for query planner
         ) r(%2$I)
      JOIN   %1$s USING (%2$I)     -- eliminate misses

      UNION                        -- eliminate dupes
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * $1)::int
         FROM   random_pick        -- just to make it recursive
         LIMIT  $3                 -- hint for query planner
         ) r(%2$I)
      JOIN   %1$s USING (%2$I)     -- eliminate misses
   )
   TABLE  random_pick
   LIMIT  $3;
   $$
 , _tbl, _id
   )
   USING _estimate              -- $1
       , (_limit * _gaps)::int  -- $2 ("surplus")
       , _limit                 -- $3
   ;
END
$func$;

使用默认值调用(重要!):

SELECT * FROM f_random_sample(null::big);  --!

或者更具体地说:

SELECT * FROM f_random_sample(null::"my_TABLE", 'oDD ID', 666, 1.15);

与静态版本的性能大致相同。

有关的:

重构 PL/pgSQL 函数以返回各种 SELECT 查询的输出 - “各种完整表类型”一章

从 PostgreSQL 函数返回 SETOF 行

执行格式()中整数变量的格式说明符?

在触发器函数中插入动态表名

这对 SQL 注入是安全的。看:

表名作为 PostgreSQL 函数参数

Postgres 函数中的 SQL 注入与准备好的查询

可能的替代方案

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

Postgres 9.5 引入 TABLESAMPLE SYSTEM (n)

其中 n 是百分比。 The manual:

BERNOULLI 和 SYSTEM 采样方法都接受一个参数,该参数是要采样的表的分数,表示为 0 到 100 之间的百分比。这个参数可以是任何实值表达式。

大胆强调我的。它非常快,但结果并不是完全随机的。再看说明书:

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

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

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

有关的:

在 PostgreSQL 中发现表的行数的快速方法

安装附加模块 tsm_system_rows 以准确获取请求的行数(如果有足够的)并允许使用更方便的语法:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

有关详细信息,请参阅 Evan's answer

但这仍然不是完全随机的。


t 表在哪里定义?它应该 r 而不是 t 吗?
@LucM:在这里定义:JOIN bigtbl t,是 JOIN bigtbl AS t 的缩写。 tbigtbltable alias。它的目的是缩短语法,但在这种特殊情况下不需要它。我在我的答案中简化了查询并添加了一个简单的版本。
generate_series(1,1100) 的值范围的目的是什么?
@Awesome-o:目标是检索 1000 行,我从额外的 10% 开始以补偿一些空白或(不太可能但可能)重复的随机数......解释在我的答案中。
Erwin,我发布了您的“可能替代方案”的变体:stackoverflow.com/a/23634212/430128。会对你的想法感兴趣。
C
Community

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

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 返回相当多的命中。

Postgres 中的快速随机行选择

如何从 postgreSQL 表中检索随机数据行?

postgres:从表中获取随机条目 - 太慢了

以及 depez 的链接文章概述了更多方法:

http://www.depesz.com/index.php/2007/09/16/my-thoughts-on-getting-random-row/

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


关于编写用于订购的临时文件的要点。这确实是一个很大的打击。我想我们可以先做random() < 0.02,然后随机播放该列表,然后再limit 1000!这种排序在几千行上会更便宜(哈哈)。
“select * from table where random() < 0.05 limit 500;”是 postgresql 更简单的方法之一。我们在我们的一个项目中使用了这一点,我们需要选择 5% 的结果,并且一次处理不超过 500 行。
为什么你会考虑使用 O(n) 完全扫描来检索 500m 行表上的样本?在大桌子上速度慢得离谱,完全没有必要。
E
Eric Leschinski

postgresql order by random(),以随机顺序选择行:

这很慢,因为它对整个表进行排序以保证每一行都有完全相同的机会被选中。对于完美的随机性,全表扫描是不可避免的。

select your_columns from your_table ORDER BY random()

postgresql order by random() 具有不同的:

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

postgresql order by 随机限制一行:

这也很慢,因为它必须进行表扫描以确保可能选择的每一行都有相同的机会被选择,就在这一刻:

select your_columns from your_table ORDER BY random() limit 1

恒定时间使用周期表扫描选择随机 N 行:

如果您的表很大,那么上面的表扫描是一个显示停止器,最多需要 5 分钟才能完成。

为了加快速度,您可以安排在幕后进行夜间表扫描重新索引,这将保证以 O(1) 恒定时间速度进行完美随机选择,除了在夜间重新索引表扫描期间,它必须等待维护完成您可能会收到另一个随机行。

--Create a demo table with lots of random nonuniform data, big_data 
--is your huge table you want to get random rows from in constant time. 
drop table if exists big_data;  
CREATE TABLE big_data (id serial unique, some_data text );  
CREATE INDEX ON big_data (id);  
--Fill it with a million rows which simulates your beautiful data:  
INSERT INTO big_data (some_data) SELECT md5(random()::text) AS some_data
FROM generate_series(1,10000000);
 
--This delete statement puts holes in your index
--making it NONuniformly distributed  
DELETE FROM big_data WHERE id IN (2, 4, 6, 7, 8); 
 
 
--Do the nightly maintenance task on a schedule at 1AM.
drop table if exists big_data_mapper; 
CREATE TABLE big_data_mapper (id serial, big_data_id int); 
CREATE INDEX ON big_data_mapper (id); 
CREATE INDEX ON big_data_mapper (big_data_id); 
INSERT INTO big_data_mapper(big_data_id) SELECT id FROM big_data ORDER BY id;
 
--We have to use a function because the big_data_mapper might be out-of-date
--in between nightly tasks, so to solve the problem of a missing row, 
--you try again until you succeed.  In the event the big_data_mapper 
--is broken, it tries 25 times then gives up and returns -1. 
CREATE or replace FUNCTION get_random_big_data_id()  
RETURNS int language plpgsql AS $$ 
declare  
    response int; 
BEGIN
    --Loop is required because big_data_mapper could be old
    --Keep rolling the dice until you find one that hits.
    for counter in 1..25 loop
        SELECT big_data_id 
        FROM big_data_mapper OFFSET floor(random() * ( 
            select max(id) biggest_value from big_data_mapper 
            )
        ) LIMIT 1 into response;
        if response is not null then
            return response;
        end if;
    end loop;
    return -1;
END;  
$$; 
 
--get a random big_data id in constant time: 
select get_random_big_data_id(); 
 
--Get 1 random row from big_data table in constant time: 
select * from big_data where id in ( 
    select get_random_big_data_id() from big_data limit 1 
); 
┌─────────┬──────────────────────────────────┐ 
│   id    │            some_data             │ 
├─────────┼──────────────────────────────────┤ 
│ 8732674 │ f8d75be30eff0a973923c413eaf57ac0 │ 
└─────────┴──────────────────────────────────┘ 

--Get 4 random rows from big_data in constant time: 
select * from big_data where id in ( 
    select get_random_big_data_id() from big_data limit 3 
);
┌─────────┬──────────────────────────────────┐ 
│   id    │            some_data             │ 
├─────────┼──────────────────────────────────┤ 
│ 2722848 │ fab6a7d76d9637af89b155f2e614fc96 │ 
│ 8732674 │ f8d75be30eff0a973923c413eaf57ac0 │ 
│ 9475611 │ 36ac3eeb6b3e171cacd475e7f9dade56 │ 
└─────────┴──────────────────────────────────┘ 

--Test what happens when big_data_mapper stops receiving 
--nightly reindexing.
delete from big_data_mapper where 1=1; 
select get_random_big_data_id();   --It tries 25 times, and returns -1
                                   --which means wait N minutes and try again.

改编自:https://www.gab.lc/articles/bigdata_postgresql_order_by_random

或者,如果上述所有工作太多。

对于恒定时间选择随机行,一个更简单的“nuff”解决方案是在您的大表上创建一个名为 big_data 的新列。mapper_int 使用唯一索引使其不为空。每天晚上用 1 到 max(n) 之间的唯一整数重置列。要获得随机行,您“在 0max(id) 之间选择一个随机整数”并返回 mapper_int 所在的行。如果该 id 没有行,因为该行在重新索引后已更改,请选择另一个随机行。如果将一行添加到 big_data.mapper_int 然后用 max(id) + 1 填充它


select your_columns from your_table ORDER BY random() limit 1 在 4500 万行上执行大约需要 2 分钟
有没有办法加快速度?
K
Kevin Languasco

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

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

此示例将为您提供来自 mytable 的 5% 的元素。

查看文档的更多解释:http://www.postgresql.org/docs/current/static/sql-select.html


来自文档的重要说明:“SYSTEM 方法对每个块进行块级采样,每个块具有被选中的指定机会;返回每个选定块中的所有行。当采样百分比较小时,SYSTEM 方法比 BERNOULLI 方法快得多已指定,但由于聚类效应,它可能会返回表的随机样本较少。”
有没有办法指定行数而不是百分比?
您可以使用 TABLESAMPLE SYSTEM_ROWS(400) 获取 400 个随机行的样本。您需要启用 built-in tsm_system_rows extension 才能使用此语句。
R
Rob Bednark

带有 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 的缺点是您将获得可变数量的输出记录。

请注意,有一种比随机排序更好的方法来打乱一组数据:The Fisher-Yates Shuffle,它在 O(N) 中运行。不过,在 SQL 中实现 shuffle 听起来颇具挑战。


不过,没有理由不能在第一个示例的末尾添加限制 1。唯一的问题是您可能无法取回任何记录,因此您必须在代码中考虑这一点。
Fisher-Yates 的问题在于您需要将整个数据集保存在内存中才能从中进行选择。对于非常大的数据集不可行:(
E
Evan Carroll
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);

我添加了指向您添加的答案的链接,这是对内置 SYSTEM 方法的显着改进。
我刚刚回答了一个问题 here(随机单条记录),在此期间我执行了大量 benchmarking and testingtsm_system_rowstsm_system_time 扩展。据我所知,除了绝对minimal 选择随机行之外,它们几乎没有任何用处。如果您能快速浏览并评论我分析的有效性或其他方面,我将不胜感激。
B
Bogdan Surai

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

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

我认为此解决方案与 ORDER BY random() 一样有效,但在处理大表时可能效率不高。
N
Nelo Mitranim

如果您只需要一行,则可以使用从 count 派生的计算得到的 offset

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

u
user10375

我的经验教训之一:

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

我认为 offset 方法会更快,因为它可以节省在 Postgres 中排序的时间。原来不是。


你能解释一下为什么吗?
因为当您请求偏移量时,它仍会计算直到您的偏移量的行。例如,如果您要求偏移量 100 限制 20,它将处理前 120 行的查询,然后丢弃前 100 行并返回最后 20 行。因此,使用偏移量不会保存任何内容。
C
Community

物化视图“可能的替代方案”outlined by 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 表中的其他列。

为了获得(和“使用”)随机值,请在 id_values 上使用 UPDATE-RETURNING,从 id_values_randomized 中选择 id_values 并进行连接,并应用所需的标准以仅获得相关的可能性。例如:

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 - 如果您一次只需要一个随机值,请将 LIMIT 更改为 1

使用 id_values 上的正确索引,我相信 UPDATE-RETURNING 应该在很少负载的情况下非常快速地执行。它通过一次数据库往返返回随机值。 “合格”行的标准可以根据需要复杂。可以随时将新行添加到 id_values 表中,一旦物化视图刷新(可能在非高峰时间运行),应用程序就可以访问它们。物化视图的创建和刷新会很慢,但只需要在向 id_values 表添加新 id 时执行。


很有意思。如果我不仅需要选择而且还需要使用 select..for 使用 pg_try_advisory_xact_lock 进行更新,那会起作用吗? (即我需要许多并发读写)
C
Community

添加类型为 serial 的名为 r 的列。索引 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() 返回一个双精度值,而 PKEY 需要一个 BIGINT。
select * from YOUR_TABLE where r > ( select ( select reltuples::bigint AS 估计 from pg_class where oid = 'public.YOUR_TABLE'::regclass) * random() )::BIGINT order by r asc limit(1);
F
Flimm

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

pg_sample - 从较大的 PostgreSQL 数据库中提取一个小的样本数据集,同时保持引用完整性。

我用一个 350M 行的数据库尝试了这个,它真的很快,不知道随机性。

./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db