快速从 PostgreSQL 表中获取真正的 RANDOM 行

3 postgresql random select postgresql-performance

我以前总是这样做:

SELECT column FROM table ORDER BY random() LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

对于大表,这令人难以忍受,慢得令人难以置信,以至于在实践中毫无用处。这就是为什么我开始寻找更有效的方法。人们推荐:

SELECT column FROM table TABLESAMPLE BERNOULLI(1) LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

虽然速度很快,但它也提供了毫无价值的随机性。它似乎总是选择相同的该死的记录,所以这也毫无价值。

我也试过:

SELECT column FROM table TABLESAMPLE BERNOULLI(100) LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

它提供了更糟糕的随机性。它每次都选择相同的几条记录。这是完全没有价值的。我需要实际的随机性。

为什么仅选择随机记录显然如此困难?为什么它必须抓取每条记录然后对它们进行排序(在第一种情况下)?为什么“TABLESAMPLE”版本总是抓取相同的愚蠢记录?为什么它们不是随机的?当它一遍又一遍地选择相同的几条记录时,谁会想要使用这个“BERNOULLI”的东西?我不敢相信,经过这么多年,我仍然在询问随机记录……这是最基本的查询之一。

用于从 PG 中的表中抓取随机记录的实际命令是什么,该命令并没有慢到需要几秒钟才能获得一个体面大小的表?

Vér*_*ace 12

有趣的问题 - 有很多可能性/排列(这个答案已经过广泛修订)。

基本上,这个问题可以分为两个主要流。

  • 单条随机记录

  • 多个随机记录(不在问题中 - 请参阅底部的参考和讨论)

已经研究了这个问题,我认为,最快的解决方案,以单个记录的问题是通过tsm_system_rows扩展PostgreSQL的埃文·卡罗尔的提供答案

如果您使用的是二进制发行版,我不确定,但我认为这些contrib模块(其中tsm_system_rows一个)默认可用 - 至少它们适用于我用于测试的EnterpriseDB Windows版本Windows(见下文) . 我的主要测试是在从Linux(make worldmake install-world)上的源代码编译的 12.1 上完成的。

我认为它最适合单记录用例的原因是,提到的有关此扩展的唯一问题是:

与内置的 SYSTEM 采样方法一样,SYSTEM_ROWS 执行块级采样,因此样本不是完全随机的,但可能会受到聚类效应的影响,尤其是在仅请求少量行的情况下。

但是,由于您只对选择 1 行感兴趣,因此块级聚类效应应该不是问题。来自 2ndQuadrant 的这篇文章说明了为什么对于一个记录样本来说这应该不是问题!这是一个主要为小亚问题(看帖子的结尾) -或者,如果你想从一个大表生成的随机记录的大样本(同样,见的讨论tsm_system_rowstsm_system_time下文)。

然后我创建并填充了一个这样的表:

CREATE TABLE rand AS SELECT generate_series(1, 100000000) AS seq, MD5(random()::text);
Run Code Online (Sandbox Code Playgroud)

所以,我现在有一个包含 100,000,000(1 亿)条记录的表。然后我添加了一个PRIMARY KEY

ALTER TABLE rand ADD PRIMARY KEY (seq);
Run Code Online (Sandbox Code Playgroud)

所以,现在到SELECT随机记录:

SELECT LENGTH((seq/100)::TEXT), seq/100::FLOAT, md5 FROM rand TABLESAMPLE SYSTEM_ROWS(1);
Run Code Online (Sandbox Code Playgroud)

请注意,我使用了一个稍微修改过的命令,以便我可以“看到”随机性 - 我还设置了该\timing命令,以便我可以获得经验测量值。

我使用了这个LENGTH()函数,这样我就可以很容易地感知到PRIMARY KEY返回的整数的大小。以下是返回的记录示例:

test=# SELECT LENGTH((seq/100)::TEXT), seq/100::FLOAT, md5 FROM rand TABLESAMPLE SYSTEM_ROWS(1);
 length | ?column?  |               md5                
--------+-----------+----------------------------------
      6 | 970749.61 | bf18719016ff4f5d16ed54c5f4679e20
(1 row)

Time: 30.606 ms
test=# SELECT LENGTH((seq/100)::TEXT), seq/100::FLOAT, md5 FROM rand TABLESAMPLE SYSTEM_ROWS(1);
 length | ?column?  |               md5                
--------+-----------+----------------------------------
      6 | 512101.21 | d27fbeea30b79d3e4eacdfea7a62b8ac
(1 row)

Time: 0.556 ms
test=# SELECT LENGTH((seq/100)::TEXT), seq/100::FLOAT, md5 FROM rand TABLESAMPLE SYSTEM_ROWS(1);
 length | ?column?  |               md5                
--------+-----------+----------------------------------
      6 | 666476.41 | c7c0c34d59229bdc42d91d0d4d9d1403
(1 row)

Time: 0.650 ms
test=# SELECT LENGTH((seq/100)::TEXT), seq/100::FLOAT, md5 FROM rand TABLESAMPLE SYSTEM_ROWS(1);
 length | ?column? |               md5                
--------+----------+----------------------------------
      5 | 49152.01 | 0a2ff4da00a2b81697e7e465bd67d85c
(1 row)

Time: 0.593 ms
test=# SELECT LENGTH((seq/100)::TEXT), seq/100::FLOAT, md5 FROM rand TABLESAMPLE SYSTEM_ROWS(1);
 length | ?column? |               md5                
--------+----------+----------------------------------
      5 | 18061.21 | ee46adc96a6f8264a5c6614f8463667d
(1 row)

Time: 0.616 ms
test=# SELECT LENGTH((seq/100)::TEXT), seq/100::FLOAT, md5 FROM rand TABLESAMPLE SYSTEM_ROWS(1);
 length | ?column?  |               md5                
--------+-----------+----------------------------------
      6 | 691962.01 | 4bac0d051490c47716f860f8afb8b24a
(1 row)

Time: 0.743 ms
Run Code Online (Sandbox Code Playgroud)

因此,如您所见,该LENGTH()函数在大多数情况下返回 6 - 这是可以预期的,因为大多数记录将在 10,000,000 和 100,000,000 之间,但有一些显示值为 5(也已看到值 3 & 4 - 数据未显示)。

现在,注意时间。第一个为 30 毫秒 (ms),但其余为亚毫秒(约 0.6 - 0.7ms)。大多数随机样本在这个亚毫秒范围内返回,但是,在 25 - 30 毫秒内返回结果(平均 3 分之 1 或 4)。

有时,这种多毫秒的结果可能会连续出现两次甚至三次,但正如我所说,大多数结果(大约 66 - 75%)都是亚毫秒级的。没有为我的解决方案,我所看到的响应时间已经超过75ms的。

在我的研究中,我还发现了tsm_system_time类似于tsm_system_rows. 现在,我还对这个扩展进行了如下基准测试:

SELECT LENGTH((seq/100)::TEXT), seq/100::FLOAT, md5 FROM rand TABLESAMPLE SYSTEM_TIME(0.001) LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

请注意,时间量是 1/1000 毫秒,即微秒 - 如果输入任何低于此值的数字,则不会返回任何记录。然而,有趣的是,即使是这个微小的量子也总是返回 120 行。

为什么它的 120 比我的工资等级高一点——PostgreSQL 页面大小是 8192(默认值)

test=# SELECT current_setting('block_size');
 current_setting 
-----------------
 8192
(1 row)
Run Code Online (Sandbox Code Playgroud)

并且file system block size是4096

[pol@UNKNOWN inst]$blockdev --getbsz /dev/mapper/fedora_localhost--live-home 
4096
Run Code Online (Sandbox Code Playgroud)

记录应该是(1 INTEGER(4 字节)+ 1 UUID(16 字节))(= 20 字节)+seq字段上的索引(大小?)。4096/120 = 34.1333... - 我几乎不认为这个表的每个索引条目需要 14 个字节 - 所以 120 来自哪里,我不确定。

我不太确定该LIMIT子句是否总是返回页面或块的第一个元组 - 从而在等式中引入非随机性元素。

tsm_system_time查询的性能与tsm_system_rows扩展的性能相同(AFAICS - 数据未显示)。关于不确定是否存在由这些扩展如何选择它们的第一条记录引入的非随机性元素的相同警告也适用于tsm_system_rows查询。请参阅下面这两种方法的(所谓的)随机性的讨论和基准测试。

关于性能,仅供参考,我使用的是带有 1TB HDD(旋转锈蚀)和 8GB DDR3 RAM 运行 Fedora 31 的 Dell Studio 1557。这是一台10年的机器!

我也在一台机器(Packard Bell,EasyNote TM - 也有 10 年历史,运行 Windows 2019 Server 的 8GB DDR3 RAM)上做了同样的事情,我有一个 SSD(SSD 无论如何都不是顶级的!)和响应时间通常(奇怪的是)稍高(~ 1.3 ms),但峰值较少,并且这些值较低(~ 5 - 7 ms)。

2019 Server 的后台很可能有很多东西在运行——但如果你有一台配备了不错 SSD 的现代笔记本电脑,你没有理由不期望亚毫秒级的响应时间是理所当然的!

所有测试均使用 PostgreSQL 12.1 运行。

为了检查这两种方法的真正“随机性”,我创建了下表:

CREATE TABLE rand_samp 
(
  seq INT, 
  md5 TEXT
);
Run Code Online (Sandbox Code Playgroud)

然后运行(每次 3 次):

DO
$$
DECLARE 
  i RECORD;
BEGIN
  FOR i IN 1..10000 LOOP
    INSERT INTO rand_samp (seq, md5)
    SELECT seq, md5 FROM rand TABLESAMPLE SYSTEM_ROWS(1);  
  END LOOP;
END;
$$
;
Run Code Online (Sandbox Code Playgroud)

并且还使用(在上述函数的内循环中)

SELECT seq, md5 FROM rand TABLESAMPLE SYSTEM_TIME(0.001) LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

然后每次运行后,我查询我的rand_samp表:

SELECT 
  seq, COUNT(seq) 
FROM rand_samp 
GROUP BY seq 
HAVING COUNT(seq) > 1;
Run Code Online (Sandbox Code Playgroud)

并得到以下计数:

对于TABLESAMPLE SYSTEM_ROWS,我得到了 258、63、44 个欺骗,所有计数均为 2。对于TABLESAMPLE SYSTEM_TIME,我得到了 46、54 和 62,同样都是计数为 2。

现在,我的统计数据有点生疏,但是从 100M 记录表的随机样本中,从 10,000 条样本中(rand表中记录数的十分之一),我希望有几个重复-也许不时,但没有像我所得到的数字。此外,如果存在真正的随机性,我也希望(少量)3 和 4。

我用 100,000 次运行进行了两次测试,第一次运行时TABLESAMPLE SYSTEM_ROWS获得了 5540 个重复(约 200 个,3 个重复,6 个,4 个重复),第二次运行 5465 个(约 200 个,3 个,6 个,4 个)。然而,有趣的查询是这样的:

SELECT COUNT(s.seq)
FROM rand_samp s
WHERE s.seq IN (SELECT sb.seq FROM rand_samp_bis sb);
Run Code Online (Sandbox Code Playgroud)

其中I在100,000两个试验相对于彼此进行比较愚弄-答案是高达11250(> 10%)由相同的-这对于第一千(1/1000)的样品是WAY得多将下降到机会!

结果 100,000 次运行SYSTEM_TIME- 5467 次重复,第一组 215 次,3 次,9 次,4 次,第二组 5472、210 (3) 和 12 (4) 次。匹配记录的数量为 11,328(再次 > 10%)。

显然(很多)非随机行为正在发生。我会把它留给 OP 来决定速度/随机权衡是否值得!

其他答案的基准。

我决定对其他提议的解决方案进行基准测试 - 使用我上面的 1 亿条记录表。我运行了所有测试 5 次 - 在任何一系列测试开始时忽略任何异常值以消除缓存/任何影响。所有异常值都高于下面报告的值。

我正在使用带有 HDD 的机器 - 稍后将使用 SSD 机器进行测试。该.mmm报道手段毫秒-任何答案,但我自己不显著。

Daniel Vérité的回答:

SELECT * FROM
  (SELECT seq FROM rand TABLESAMPLE BERNOULLI(1)) AS s
 ORDER BY RANDOM() LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

跑了 5 次 - 所有时间都超过一分钟 - 通常是 01:00.mmm(1 次在 01:05.mmm)。

典型运行:

test=# SELECT * FROM
  (SELECT seq FROM rand TABLESAMPLE BERNOULLI(1)) AS s
 ORDER BY RANDOM() LIMIT 1;
   seq   
---------
 9529212
(1 row)

Time: 60789.988 ms (01:00.790)
Run Code Online (Sandbox Code Playgroud)

斯瓦夫的回答:

SELECT md5 FROM rand OFFSET (
    SELECT floor(random() * (SELECT count(seq) from rand))::int
) limit 1;
Run Code Online (Sandbox Code Playgroud)

跑了 5 次 - 所有时间都超过一分钟 - 从 01:03 到 01:29

典型运行:

test=# SELECT md5 FROM rand OFFSET (
    SELECT floor(random() * (SELECT count(seq) from rand))::int
) limit 1;
               md5                
----------------------------------
 8004dfdfbaa9ac94243c33e9753e1f77
(1 row)

Time: 68558.096 ms (01:08.558)
Run Code Online (Sandbox Code Playgroud)

科林哈特的回答:

select * from rand where seq >= (
  select random()*(max(seq)-min(seq)) + min(seq) from rand
)
order by seq
limit 1;
Run Code Online (Sandbox Code Playgroud)

跑了 5 次 - 时间在 00:06.mmm 和 00:14.mmm 之间变化(其余最佳!)

典型运行:

test=# select * from rand where seq >= (
  select random()*(max(seq)-min(seq)) + min(seq) from rand
)
order by seq
limit 1;
   seq    |               md5                
----------+----------------------------------
 29277339 | 2b27c594f65659c832f8a609c8cf8e78
(1 row)

Time: 6944.771 ms (00:06.945)
Run Code Online (Sandbox Code Playgroud)

Colin 't Hart的第二个答案(由我改编):

WITH min_max AS MATERIALIZED -- or NOT, doesn't appear to make a difference
(
  SELECT MIN(seq) AS min_s, MAX(seq) AS max_s, (MAX(seq) - MIN(seq)) - MIN(seq) AS diff_s
  FROM rand
),
other  AS MATERIALIZED
(
  SELECT FLOOR(RANDOM() * (SELECT diff_s FROM min_max))::INT AS seq_val
)
SELECT seq, md5 
FROM rand
WHERE seq = (SELECT seq_val FROM other);
Run Code Online (Sandbox Code Playgroud)

响应时间在 ~ 30 - 45ms 之间,这些时间的两侧都有奇怪的异常值 - 它甚至有时会下降到 1.xxx ms。我只能说它似乎比SYSTEM_TIMESYSTEM_ROWS方法中的任何一个都更一致。

然而,这种方法存在一个主要问题。如果一个人为随机性选择的基础字段是稀疏的,那么这个方法不会一直返回一个值 - 这可能会可能不会被 OP 接受?您可以执行以下操作(查询结束):

SELECT seq, md5 
FROM rand
WHERE seq >= (SELECT seq_val FROM other)
LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

(注意>=LIMIT 1)。这可能非常有效,(1.xxx 毫秒),但似乎不仅仅是seq =...公式的变化 - 但是一旦缓存似乎被预热,它通常会给出约 1.5毫秒的响应时间。

该解决方案的另一个优点是,它并不需要它,根据上下文的任何特殊扩展(顾问没有被允许安装“特殊”的工具,DBA规则...)可能无法使用。

关于上述解决方案的一个真正奇怪的事情是,如果::INTCAST 被删除,查询需要大约 1 分钟。即使FLOOR函数应该返回一个INTEGER. 我只是通过运行发现这是一个问题EXPLAIN (ANALYZE BUFFERS)

随着 ::INT

   CTE other
     ->  Result  (cost=0.02..0.04 rows=1 width=4) (actual time=38.906..38.907 rows=1 loops=1)
           Buffers: shared hit=1 read=9
           InitPlan 4 (returns $3)
             ->  CTE Scan on min_max  (cost=0.00..0.02 rows=1 width=4) (actual time=38.900..38.902 rows=1 loops=1)
                   Buffers: shared hit=1 read=9
   InitPlan 6 (returns $5)
     ->  CTE Scan on other  (cost=0.00..0.02 rows=1 width=4) (actual time=38.909..38.910 rows=1 loops=1)
           Buffers: shared hit=1 read=9
 Planning Time: 0.329 ms
 Execution Time: 68.449 ms
(31 rows)

Time: 99.708 ms
test=#
Run Code Online (Sandbox Code Playgroud)

没有 ::INT

   CTE other
     ->  Result  (cost=0.02..0.04 rows=1 width=8) (actual time=0.082..0.082 rows=1 loops=1)
           Buffers: shared hit=10
           InitPlan 4 (returns $3)
             ->  CTE Scan on min_max  (cost=0.00..0.02 rows=1 width=4) (actual time=0.076..0.077 rows=1 loops=1)
                   Buffers: shared hit=10
   InitPlan 6 (returns $5)
     ->  CTE Scan on other  (cost=0.00..0.02 rows=1 width=8) (actual time=0.085..0.085 rows=1 loops=1)
           Buffers: shared hit=10
   ->  Parallel Seq Scan on rand  (cost=0.00..1458334.00 rows=208333 width=37) (actual time=52644.672..60025.906 rows=0 loops=3)
         Filter: ((seq)::double precision = $5)
         Rows Removed by Filter: 33333333
         Buffers: shared hit=14469 read=818865
 Planning Time: 0.378 ms
 Execution Time: 60259.401 ms
(37 rows)

Time: 60289.827 ms (01:00.290)
test=#
Run Code Online (Sandbox Code Playgroud)

注意(没有::INT

   ->  Parallel Seq Scan on rand  (cost=0.00..1458334.00 rows=208333 width=37) (actual time=52644.672..60025.906 rows=0 loops=3)
         Filter: ((seq)::double precision = $5)
Run Code Online (Sandbox Code Playgroud)

Parallel Seq Scan(成本高),filter on (seq)::double

为什么要加倍??)。

Buffers: shared hit=14469 read=818865
Run Code Online (Sandbox Code Playgroud)

与(与::INT)相比

Buffers: shared hit=1 read=9
Run Code Online (Sandbox Code Playgroud)

最后,我自己的答案再次(同一台机器,时间和缓存):

(鉴于上面执行的基准测试,这现在是多余的)。

再次运行我自己的基准测试 15 次 - 通常时间是亚毫秒,偶尔(大约 3/4 中的 1 次)运行大约需要。25 毫秒。

典型运行:

test=# SELECT LENGTH((seq/100)::TEXT), seq/100::FLOAT, md5 FROM rand TABLESAMPLE SYSTEM_ROWS(1);
 length | ?column?  |               md5                
--------+-----------+----------------------------------
      6 | 401578.81 | 30ff7ecfedea088dab75932f0b1ea872
(1 row)

Time: 0.708 ms
Run Code Online (Sandbox Code Playgroud)

因此,看起来我的解决方案的最坏时间比其他人的答案(Colin't Hart)中最快的时间快约 200 倍。

我的分析是没有完美的解决方案,但最好的解决方案似乎是对 Colin 't Hart 解决方案的改编。

最后,下面显示了与将此解决方案用于多条记录相关的问题的图形演示 - 抽取 25 条记录(执行多次 - 显示典型运行)的样本。

tsm_system_rows方法将产生25个顺序记录。这可能适用于某些目的,其中随机样本是多个连续记录的事实不是问题,但绝对值得牢记。

test=# SELECT LENGTH((seq/100)::TEXT), seq/100::FLOAT, md5 FROM rand TABLESAMPLE SYSTEM_ROWS(25);
 length | ?column?  |               md5                
--------+-----------+----------------------------------
      6 | 763140.01 | 7e84b36ab30d3d2038ebd832c241b54d
      6 | 763140.02 | a976e258f6047df18e8ed0559ff48c36
--
--    SEQUENTIAL values of seq!
--
      6 | 763140.23 | ad4f1b4362187d6a300aaa9aaef30170
      6 | 763140.24 | 0c7fcc4f07d27fbcece68b2503f28d88
      6 | 763140.25 | 64d4507b18b5481a724d8a5bb6ac59c8
(25 rows)
Run Code Online (Sandbox Code Playgroud)

时间:29.348 毫秒

类似的情况适用于该SYSTEM_TIME方法的情况。如上所述,即使最短时间为 1?s,它也会提供 120 条记录。与 一样SYSTEM_ROWS,这些给出 的顺序值PRIMARY KEY

test=# SELECT seq, md5 FROM rand TABLESAMPLE SYSTEM_TIME(0.001);
Run Code Online (Sandbox Code Playgroud)

返回:

   seq    |               md5                
----------+----------------------------------
 42392881 | e92f15cba600f0c7aa16db98c0183828
 42392882 | 93db51ea870e15202144d11810c8f40c
 42392883 | 7357bf0cf1fa23ab726e642832bb87b0
 42392884 | 1f5ce45fb17c8ba19b391f9b9c835242
 42392885 | f9922b502d4fd9ee84a904ac44d4e560
 ...
 ...  115 sequential values snipped for brevity!
Run Code Online (Sandbox Code Playgroud)

我们的姊妹网站 StackOverflow 处理了这个问题here。(又一次) Erwin Brandstetterhere和 Evan Carroll提供了很好的答案here。整个线程值得详细阅读 - 因为random(单调增加/减少,Pseudorandom number generators......)和sampling(有或没有替换......)有不同的定义。


Dan*_*ité 5

你的错误是总是取样本的第一行。

取随机行:

SELECT * FROM
  (SELECT column FROM table TABLESAMPLE BERNOULLI(1)) AS s
 ORDER BY RANDOM() LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

样本的内容是随机的,但样本中的顺序不是随机的。由于采样会进行表扫描,因此它往往会按照表的顺序生成行。如果您查看一个新创建的、排序完美的表,这一点是显而易见的:

create table a as select * from generate_series(1,1000000) as i;

select * from a tablesample bernoulli(1) limit 10;
  i   
------
  248
  394
  463
  557
  686
  918
  933
 1104
 1124
 1336
(10 rows)
Run Code Online (Sandbox Code Playgroud)

将 LIMIT 直接应用于样本往往会产生始终较小的值,从表的开头按照其在磁盘上的顺序。使用 LIMIT 1 时情况会更糟。

现在将其与正确的方法进行比较:

select * from (select * from a tablesample bernoulli(1) ) s order by random() limit 10;
   i    
--------
 622931
 864123
 817263
 729949
 748422
 127263
 322338
 900781
  49371
 616774
Run Code Online (Sandbox Code Playgroud)