【问题标题】:quick random row selection in PostgresPostgres 中的快速随机行选择
【发布时间】:2011-07-14 22:05:27
【问题描述】:

我在 postgres 中有一个包含数百万行的表。我上网查了一下,发现如下

SELECT myid FROM mytable ORDER BY RANDOM() LIMIT 1;

它有效,但它真的很慢......是否有另一种方法来进行查询,或者直接选择随机行而不读取所有表?顺便说一下,'myid' 是一个整数,但它可以是一个空字段。

【问题讨论】:

标签: postgresql random


【解决方案1】:

您可能想尝试OFFSET,如

SELECT myid FROM mytable OFFSET floor(random() * N) LIMIT 1;

Nmytable 中的行数。你可能需要先做一个SELECT COUNT(*) 来计算N 的值。

更新(Antony Hatchkins)

你必须在这里使用floor

SELECT myid FROM mytable OFFSET floor(random() * N) LIMIT 1;

考虑一个有 2 行的表格; random()*N 生成 0 <= x < 2,例如 SELECT myid FROM mytable OFFSET 1.7 LIMIT 1; 返回 0 行,因为隐式舍入到最接近的 int。

【讨论】:

  • 使使用小于SELECT COUNT(*)的N有意义吗?我的意思是,不要使用表中的所有值而只使用其中的一部分?
  • @Juan 这取决于你的要求。
  • 使用具有不同 N 值的 EXPLAIN SELECT ... 为查询提供相同的成本,那么我想最好选择 N 的最大值。
  • 在下面我的回答中查看错误修复
  • 这有一个错误。它永远不会返回第一行,并且会生成错误 1/COUNT(*),因为它会尝试返回最后一行之后的行。
【解决方案2】:

PostgreSQL 9.5 引入了一种更快的样本选择新方法:TABLESAMPLE

语法是

SELECT * FROM my_table TABLESAMPLE BERNOULLI(percentage);
SELECT * FROM my_table TABLESAMPLE SYSTEM(percentage);

如果您只想选择一行,这不是最佳解决方案,因为您需要知道表的 COUNT 才能计算确切的百分比。

为了避免缓慢的 COUNT 并为从 1 行到数十亿行的表使用快速 TABLESAMPLE,您可以这样做:

 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.000001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.00001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.0001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.001) LIMIT 1;
 ...

这可能看起来不那么优雅,但可能比其他任何答案都快。

要决定是否要使用 BERNULLI 或 SYSTEM,请阅读http://blog.2ndquadrant.com/tablesample-in-postgresql-9-5-2/ 了解不同之处

【讨论】:

  • 这比任何其他答案都更快、更容易——这个应该在顶部。
  • 为什么不能只使用子查询来获取计数? SELECT * FROM my_table TABLESAMPLE SYSTEM(SELECT 1/COUNT(*) FROM my_table) LIMIT 1;?
  • @machineghost "为了避免缓慢的计数..." ...如果您的数据非常小,您可以在合理的时间内进行计数,那就去吧! :-)
  • @machineghost 使用SELECT reltuples FROM pg_class WHERE relname = 'my_table' 进行计数估计。
  • @Hynek-Pichi-Vychodil 非常好的输入!为了确保估计不会过时,最近必须进行 VACUUM ANALYZEd 分析。但是无论如何都应该正确分析一个好的数据库。这一切都取决于具体的用例。通常巨大的桌子不会增长得那么快......谢谢!
【解决方案3】:

我用子查询尝试了这个,它工作得很好。偏移量,至少在 Postgresql v8.4.4 中可以正常工作。

select * from mytable offset random() * (select count(*) from mytable) limit 1 ;

【讨论】:

  • 事实上,v8.4 是必不可少的,它不适用于
  • 在下面我的回答中查看错误修复
【解决方案4】:

你需要使用floor:

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;

【讨论】:

  • 考虑一个有 2 行的表; random()*N 生成 0 SELECT myid FROM mytable OFFSET 1.7 LIMIT 1; 返回 0 行,因为隐式舍入到最接近的 int。
  • 不幸的是,如果您想使用更高的 LIMIT,这不起作用...我需要获取 3 个项目,所以我需要使用 ORDER BY RANDOM() 语法。
  • 三个连续查询仍将比一个order by random() 快,大约为3*O(N) < O(NlogN) - 由于索引,实际数据会略有不同。
  • 我的问题是这 3 个项目必须是不同的,并且 WHERE myid NOT IN (1st-myid)WHERE myid NOT IN (1st-myid, 2nd-myid) 不起作用,因为决定是由偏移量做出的。嗯...我想我可以在第二个和第三个 SELECT 中将 N 减少 1 和 2。
  • 您或任何人能否通过为什么我需要使用floor() 的答案来扩展此答案?它有什么优势?
【解决方案5】:

查看此链接以了解一些不同的选项。 http://www.depesz.com/index.php/2007/09/16/my-thoughts-on-getting-random-row/

更新: (A.Hatchkins)

(很)长的文章总结如下。

作者列举了四种方法:

1) ORDER BY random() LIMIT 1; -- 慢

2) ORDER BY id where id>=random()*N LIMIT 1 -- 如果有差距则不均匀

3) 随机列——需要不时更新

4) 自定义random aggregate -- 狡猾的方法,可能很慢:random() 需要生成 N 次

并建议通过使用改进方法#2

5) ORDER BY id where id=random()*N LIMIT 1 如果结果为空,则进行后续请求。

【讨论】:

  • 我想知道他们为什么不涵盖 OFFSET?使用 ORDER 只是为了获得随机行是不可能的。幸运的是,答案很好地涵盖了 OFFSET。
【解决方案6】:

获取随机行最简单、最快的方法是使用tsm_system_rows 扩展:

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;

然后你可以选择你想要的确切行数:

SELECT myid  FROM mytable TABLESAMPLE SYSTEM_ROWS(1);

这适用于 PostgreSQL 9.5 及更高版本。

见:https://www.postgresql.org/docs/current/static/tsm-system-rows.html

【讨论】:

  • 公平警告,这不是完全随机的。在较小的表上,我总是让它按顺序返回第一行。
  • 是的,这在文档中有清楚的解释(上面的链接):« 与内置的 SYSTEM 采样方法一样,SYSTEM_ROWS 执行块级采样,因此样本不是完全随机的,但可能会受到集群效应,尤其是在只请求少量行的情况下。 » .如果您有一个小数据集,ORDER BY random() LIMIT 1; 应该足够快。
  • 我看到了。只是想向不点击链接的任何人说明,或者该链接将来是否会失效。
  • 另外值得注意的是,这仅适用于从表中选择随机行并进行过滤,而不是/与运行查询然后随机选择一个或一些记录相比。
【解决方案7】:

我想出了一个没有TABLESAMPLE 的非常快速的解决方案。比OFFSET random()*N LIMIT 1 快得多。它甚至不需要表数。

这个想法是用随机但可预测的数据创建一个表达式索引,例如md5(primary key)

这是一个包含 1M 行样本数据的测试:

create table randtest (id serial primary key, data int not null);

insert into randtest (data) select (random()*1000000)::int from generate_series(1,1000000);

create index randtest_md5_id_idx on randtest (md5(id::text));

explain analyze
select * from randtest where md5(id::text)>md5(random()::text)
order by md5(id::text) limit 1;

结果:

 Limit  (cost=0.42..0.68 rows=1 width=8) (actual time=6.219..6.220 rows=1 loops=1)
   ->  Index Scan using randtest_md5_id_idx on randtest  (cost=0.42..84040.42 rows=333333 width=8) (actual time=6.217..6.217 rows=1 loops=1)
         Filter: (md5((id)::text) > md5((random())::text))
         Rows Removed by Filter: 1831
 Total runtime: 6.245 ms

此查询有时(以大约 1/Number_of_rows 的概率)返回 0 行,因此需要检查并重新运行。概率也不完全相同 - 有些行比其他行更有可能。

比较:

explain analyze SELECT id FROM randtest OFFSET random()*1000000 LIMIT 1;

结果差异很大,但可能非常糟糕:

 Limit  (cost=1442.50..1442.51 rows=1 width=4) (actual time=179.183..179.184 rows=1 loops=1)
   ->  Seq Scan on randtest  (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.016..134.835 rows=915702 loops=1)
 Total runtime: 179.211 ms
(3 rows)

【讨论】:

  • 快,是的。真正随机的,没有。恰好是另一个现有值之后的下一个更大值的 md5 值被选择的机会非常小,而在数字空间中有很大差距之后的值有更大的机会(更大的可能值之间的数量) .结果分布不是随机的。
  • 非常有趣,它能否在类似彩票的查询的用例中工作:查询必须查看所有可用的票并随机返回一张票。我也可以在您的技术中使用悲观锁(选择...进行更新)吗?
  • 对于任何与彩票相关的事情,您都应该真正使用公平且加密安全的随机抽样——例如选择一个介于 1 和 max(id) 之间的随机数,直到找到现有的 id。这个答案的方法既不公平也不安全 - 它很快。可用于“获取随机 1% 的行以测试某项内容”或“显示随机 5 个条目”等内容。
猜你喜欢
  • 2016-04-27
  • 1970-01-01
  • 2010-09-17
  • 2011-05-18
  • 1970-01-01
相关资源
最近更新 更多