[英]Best way to select random rows PostgreSQL
我想在 PostgreSQL 中隨機選擇行,我試過這個:
select * from table where random() < 0.01;
但其他一些人建議這樣做:
select * from table order by random() limit 1000;
我有一個非常大的表,有 5 億行,我希望它快。
哪種方法更好? 有什么區別? select 隨機行的最佳方法是什么?
鑒於您的規格(以及評論中的其他信息),
下面的查詢不需要大表的順序掃描,只需要索引掃描。
首先,獲取主查詢的估計值:
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
詳細解釋:
只要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;
特別是如果您對差距和估計不太確定。
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);
與靜態版本的性能大致相同。
有關的:
這對 SQL 注入是安全的。 看:
如果您的要求允許重復調用相同的集合(我們正在談論重復調用),請考慮MATERIALIZED VIEW
。 執行一次上述查詢並將結果寫入表。 用戶以閃電般的速度獲得准隨機選擇。 每隔一段時間或您選擇的事件刷新您的隨機選擇。
TABLESAMPLE SYSTEM (n)
其中n
是百分比。 手冊:
BERNOULLI
和SYSTEM
采樣方法都接受一個參數,該參數是要采樣的表的分數,表示為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);
有關詳細信息,請參閱埃文的答案。
但這仍然不是完全隨機的。
您可以使用檢查和比較兩者的執行計划
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 “大”,如“完整的表不適合內存”。
這很慢,因為它對整個表進行排序以保證每一行都有完全相同的機會被選中。 對於完美的隨機性,全表掃描是不可避免的。
select your_columns from your_table ORDER BY random()
select * from
(select distinct your_columns from your_table) table_alias
ORDER BY random()
這也很慢,因為它必須進行表掃描以確保可能選擇的每一行都有相同的機會被選擇,就在這一刻:
select your_columns from your_table ORDER BY random() limit 1
如果您的表很大,那么上面的表掃描是一個顯示停止器,最多需要 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) 之間的唯一整數重置列。 要獲得隨機行,您“選擇0
和max(id)
之間的隨機整數”並返回 mapper_int 所在的行。 如果該 id 沒有行,因為該行在重新索引后已更改,請選擇另一個隨機行。 如果將一行添加到 big_data.mapper_int 然后用 max(id) + 1 填充它
從 PostgreSQL 9.5 開始,有一種專門用於從表中獲取隨機元素的新語法:
SELECT * FROM mytable TABLESAMPLE SYSTEM (5);
此示例將為您提供mytable
中 5% 的元素。
查看文檔的更多解釋: http ://www.postgresql.org/docs/current/static/sql-select.html
帶有 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 中實現 shuffle 聽起來頗具挑戰。
select * from table order by random() limit 1000;
如果您知道需要多少行,請查看tsm_system_rows
。
模塊提供了表采樣方法SYSTEM_ROWS,可以在SELECT命令的TABLESAMPLE子句中使用。
此表采樣方法接受一個整數參數,該參數是要讀取的最大行數。 生成的樣本將始終包含那么多行,除非表不包含足夠的行,在這種情況下會選擇整個表。 SYSTEM_ROWS 與內置的 SYSTEM 采樣方法一樣,執行塊級采樣,因此樣本不是完全隨機的,而是可能會受到聚類效應的影響,尤其是在僅請求少量行的情況下。
首先安裝擴展
CREATE EXTENSION tsm_system_rows;
然后你的查詢,
SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);
這是一個對我有用的決定。 我想這很容易理解和執行。
SELECT
field_1,
field_2,
field_2,
random() as ordering
FROM
big_table
WHERE
some_conditions
ORDER BY
ordering
LIMIT 1000;
如果您只想要一行,則可以使用從count
派生的計算offset
。
select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));
我的經驗教訓之一:
offset floor(random() * N) limit 1
不比order by random() limit 1
快。
我認為offset
方法會更快,因為它可以節省 Postgres 中的排序時間。 原來不是。
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- id_values
,從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 被添加到id_values
表時執行。
添加一個名為r
的列,類型為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
> 行數或需要選擇多行,則需要再次執行該語句。
我知道我參加聚會有點晚了,但我剛剛發現了這個很棒的工具,叫做pg_sample :
pg_sample
- 從較大的 PostgreSQL 數據庫中提取一個小的樣本數據集,同時保持引用完整性。
我用一個 350M 行的數據庫試過這個,它真的很快,不知道隨機性。
./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db
我認為最好的方法是:
SELECT * FROM tableName ORDER BY random() LIMIT 1
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.