鉴于您的规格(以及 cmets 中的其他信息),
- 您有一个数字 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(*)(用于大桌子)。鉴于上述规格,您不需要它。估计就可以了,几乎免费提供 (detailed explanation here):
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 到大桌子。有了索引,这应该非常快。
-
最后修剪没有被骗子和差距吃掉的多余ids。每一行都有完全平等的机会被选中。
短版
您可以简化此查询。上述查询中的 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 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
)
TABLE random_pick
LIMIT _limit;
END
$func$;
呼叫:
SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);
您甚至可以使这个泛型适用于任何表:将 PK 列的名称和表作为多态类型并使用 EXECUTE ...但这超出了本问题的范围。见:
可能的替代方案
如果您的要求允许重复调用的相同集(我们正在谈论重复调用),我会考虑物化视图。执行一次上述查询并将结果写入表。用户以闪电般的速度获得准随机选择。每隔一段时间或您选择的事件刷新您的随机选择。
n 是一个百分比。 The manual:
BERNOULLI 和 SYSTEM 采样方法均接受单个
参数是要采样的表的分数,表示为
0 到 100 之间的百分比。这个参数可以是任何real-valued 表达式。
我的大胆强调。它非常快,但结果并非完全随机。再看手册:
SYSTEM 方法明显快于BERNOULLI 方法
当指定小采样百分比时,它可能会返回一个
由于聚类效应,表的随机样本较少。
返回的行数可以变化很大。对于我们的示例,要获得大约 1000 行:
SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);
相关:
或安装附加模块 tsm_system_rows 以准确获取请求的行数(如果足够的话)并允许更方便的语法:
SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);
详情请见Evan's answer。
但这仍然不是完全随机的。