lựa chọn hàng ngẫu nhiên nhanh chóng trong Postgres


98

Tôi có một bảng trong postgres chứa vài triệu hàng. Tôi đã kiểm tra trên internet và tôi thấy những điều sau

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

Nó hoạt động, nhưng nó thực sự chậm ... có cách nào khác để thực hiện truy vấn đó hay cách trực tiếp để chọn một hàng ngẫu nhiên mà không cần đọc tất cả bảng không? Nhân tiện, 'myid' là một số nguyên nhưng nó có thể là một trường trống.


1
Nếu bạn muốn chọn nhiều hàng ngẫu nhiên, hãy xem câu hỏi này: stackoverflow.com/q/8674718/247696
Flimm,

Câu trả lời:


99

Bạn có thể muốn thử nghiệm OFFSET, như trong

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

Nsố hàng trong mytable. Trước tiên, bạn có thể cần phải làm một SELECT COUNT(*)để tìm ra giá trị của N.

Cập nhật (bởi Antony Hatchkins)

Bạn phải sử dụng floorở đây:

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

Hãy xem xét một bảng gồm 2 hàng; random()*Ntạo 0 <= x < 2và ví dụ SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;trả về 0 hàng vì làm tròn ẩn đến số nguyên gần nhất.


làm cho việc sử dụng N nhỏ hơn SELECT COUNT(*)?, Ý tôi là, không sử dụng tất cả các giá trị trong bảng mà chỉ sử dụng một phần của chúng?
Juan

@Juan Điều đó tùy thuộc vào yêu cầu của bạn.
NPE

sử dụng EXPLAIN SELECT ...với giá trị khác nhau của N cung cấp cho các chi phí tương tự cho các truy vấn, sau đó tôi đoán là tốt hơn để đi cho giá trị lớn nhất của N.
Juan

3
thấy một bugfix trong câu trả lời của tôi dưới đây
Antony Hatchkins

2
Điều này có một lỗi. Nó sẽ không bao giờ trả về hàng đầu tiên và sẽ tạo ra lỗi 1 / COUNT (*) vì nó sẽ cố gắng trả về hàng sau hàng cuối cùng.
Ian

62

PostgreSQL 9.5 đã giới thiệu một cách tiếp cận mới để chọn mẫu nhanh hơn nhiều: TABLESAMPLE

Cú pháp là

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

Đây không phải là giải pháp tối ưu nếu bạn chỉ muốn chọn một hàng, vì bạn cần biết ĐẾM của bảng để tính toán tỷ lệ phần trăm chính xác.

Để tránh COUNT chậm và sử dụng TABLESAMPLE nhanh cho các bảng từ 1 hàng đến hàng tỷ hàng, bạn có thể làm:

 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;
 ...

Câu trả lời này có vẻ không quá thanh lịch, nhưng có lẽ nhanh hơn bất kỳ câu trả lời nào khác.

Để quyết định xem bạn có muốn sử dụng BERNULLI oder SYSTEM hay không, hãy đọc về sự khác biệt tại http://blog.2ndquadrant.com/tablesample-in-postgresql-9-5-2/


2
Câu trả lời này nhanh hơn và dễ dàng hơn nhiều so với bất kỳ câu trả lời nào khác - câu trả lời này phải ở trên cùng.
Hayden Schiff

1
Tại sao bạn không thể chỉ sử dụng một truy vấn con để lấy số lượng? SELECT * FROM my_table TABLESAMPLE SYSTEM(SELECT 1/COUNT(*) FROM my_table) LIMIT 1;?
machineghost

2
@machineghost "Để tránh COUNT chậm ..." ... Nếu dữ liệu của bạn quá nhỏ để bạn có thể đếm trong thời gian hợp lý, hãy bắt đầu! :-)
alfonx

2
@machineghost Sử dụng SELECT reltuples FROM pg_class WHERE relname = 'my_table'để ước tính số lượng.
Hynek -Pichi- Vychodil

@ Hynek-Pichi-Vychodil đầu vào rất tốt! Để đảm bảo rằng ước tính không bị lỗi thời, gần đây nó phải sử dụng VACUUM ANALYZEd .. nhưng dù sao thì một cơ sở dữ liệu tốt cũng nên được phân tích đúng cách .. Và tất cả phụ thuộc vào trường hợp sử dụng cụ thể. Thông thường các bảng lớn không phát triển quá nhanh ... Cảm ơn!
alfonx

34

Tôi đã thử điều này với một truy vấn con và nó hoạt động tốt. Bù lại, ít nhất trong Postgresql v8.4.4 hoạt động tốt.

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

Trên thực tế, v8.4 là cần thiết để điều này hoạt động, không hoạt động đối với <= 8.3.
Antony Hatchkins

1
thấy một bugfix trong câu trả lời của tôi dưới đây
Antony Hatchkins

32

Bạn cần sử dụng floor:

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

Hãy xem xét một bảng gồm 2 hàng; random()*Ntạo ra 0 <= x <2 và ví dụ SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;trả về 0 hàng vì làm tròn ẩn đến int gần nhất.
Antony Hatchkins

Rất tiếc, điều này không hiệu quả nếu bạn muốn sử dụng LIMIT cao hơn ... Tôi cần lấy 3 mục nên tôi cần sử dụng cú pháp ORDER BY RANDOM ().
Alexis Wilke,

1
Ba truy vấn liên tiếp sẽ vẫn nhanh hơn một order by random(), xấp xỉ 3*O(N) < O(NlogN)- các số liệu trong đời thực sẽ hơi khác một chút do các chỉ số.
Antony Hatchkins

Vấn đề của tôi là 3 mục cần phải khác biệt và a WHERE myid NOT IN (1st-myid)WHERE myid NOT IN (1st-myid, 2nd-myid)sẽ không hoạt động vì quyết định được đưa ra bởi OFFSET. Hmmm ... Tôi đoán tôi có thể giảm N đi 1 và 2 trong lần CHỌN thứ hai và thứ ba.
Alexis Wilke,

Bạn hoặc bất cứ ai có thể mở rộng câu trả lời này với câu trả lời cho lý do tại sao tôi cần sử dụng floor()? Nó mang lại lợi thế gì?
ADTC

14

Kiểm tra liên kết này để biết một số tùy chọn khác nhau. http://www.depesz.com/index.php/2007/09/16/my-thoughts-on-getting-random-row/

Cập nhật: (A.Hatchkins)

Tóm tắt (rất) dài của bài báo như sau.

Tác giả liệt kê bốn cách tiếp cận:

1) ORDER BY random() LIMIT 1; - chậm

2) ORDER BY id where id>=random()*N LIMIT 1- không đồng nhất nếu có khoảng trống

3) cột ngẫu nhiên - cần được cập nhật mọi lúc

4) tổng hợp ngẫu nhiên tùy chỉnh - phương pháp tinh ranh, có thể chậm: random () cần được tạo N lần

và đề xuất cải thiện phương pháp số 2 bằng cách sử dụng

5) ORDER BY id where id=random()*N LIMIT 1 với các truy vấn tiếp theo nếu kết quả là trống.


Tôi tự hỏi tại sao họ không bao gồm OFFSET? Sử dụng ORDER không phải là câu hỏi chỉ để lấy một hàng ngẫu nhiên. May mắn thay, OFFSET được bao gồm trong các câu trả lời.
androidguy

4

Cách dễ nhất và nhanh nhất để tìm nạp hàng ngẫu nhiên là sử dụng tsm_system_rowstiện ích mở rộng:

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;

Sau đó, bạn có thể chọn số hàng chính xác mà bạn muốn:

SELECT myid  FROM mytable TABLESAMPLE SYSTEM_ROWS(1);

Điều này có sẵn với PostgreSQL 9.5 trở lên.

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


1
Cảnh báo công bằng, điều này không hoàn toàn ngẫu nhiên. Trên các bảng nhỏ hơn, tôi luôn trả về các hàng đầu tiên theo thứ tự.
Ben Aubin

1
vâng, điều này được giải thích rõ ràng trong tài liệu (liên kết ở trên): «Giống như phương pháp lấy mẫu HỆ THỐNG tích hợp sẵn, SYSTEM_ROWS thực hiện lấy mẫu cấp khối, do đó mẫu không hoàn toàn ngẫu nhiên nhưng có thể chịu hiệu ứng phân cụm, đặc biệt nếu chỉ là một số hàng được yêu cầu. ». Nếu bạn có một tập dữ liệu nhỏ, ORDER BY random() LIMIT 1;phải đủ nhanh.
daamien

Tôi đã thấy điều đó. Chỉ muốn nói rõ với bất kỳ ai không nhấp vào liên kết hoặc nếu liên kết sẽ chết trong tương lai.
Ben Aubin

1
Cũng cần lưu ý rằng điều này sẽ chỉ hoạt động để chọn các hàng ngẫu nhiên ra khỏi bảng và lọc THEN, ngược lại / so với việc chạy một truy vấn và sau đó chọn một hoặc một số bản ghi ngẫu nhiên.
nomen

3

Tôi đã nghĩ ra một giải pháp rất nhanh mà không cần TABLESAMPLE. Nhanh hơn nhiều so với OFFSET random()*N LIMIT 1. Nó thậm chí không yêu cầu đếm bảng.

Ý tưởng là tạo một chỉ mục biểu thức với dữ liệu ngẫu nhiên nhưng có thể dự đoán được, chẳng hạn md5(primary key).

Đây là một thử nghiệm với dữ liệu mẫu 1 triệu hàng:

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;

Kết quả:

 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

Truy vấn này đôi khi có thể (với xác suất khoảng 1 / Number_of_rows) trả về 0 hàng, vì vậy nó cần được kiểm tra và chạy lại. Ngoài ra, các xác suất không hoàn toàn giống nhau - một số hàng có nhiều xác suất hơn những hàng khác.

Để so sánh:

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

Kết quả rất khác nhau, nhưng có thể khá tệ:

 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)

2
Nhanh chóng, có. Thực sự là ngẫu nhiên, không. Giá trị md5 xảy ra là giá trị lớn hơn tiếp theo sau một giá trị hiện có khác có cơ hội được chọn rất nhỏ, trong khi các giá trị sau khoảng cách lớn trong không gian số có cơ hội lớn hơn nhiều (lớn hơn bởi số giá trị có thể ở giữa) . Phân phối kết quả không phải là ngẫu nhiên.
Erwin Brandstetter

rất thú vị, nó có thể hoạt động theo cách sử dụng của một truy vấn giống như xổ số: truy vấn phải xem xét tất cả các vé có sẵn và chỉ trả lại ngẫu nhiên MỘT vé duy nhất. Tôi cũng có thể sử dụng một khóa bi quan (chọn ... để cập nhật) với kỹ thuật của bạn?
Mathieu

Đối với bất kỳ thứ gì liên quan đến xổ số, bạn thực sự nên sử dụng lấy mẫu ngẫu nhiên công bằng và an toàn bằng mật mã - ví dụ: chọn một số ngẫu nhiên từ 1 đến tối đa (id) cho đến khi bạn tìm thấy id hiện có. Phương pháp từ câu trả lời này không công bằng cũng không an toàn - nó nhanh chóng. Có thể sử dụng cho những thứ như "lấy ngẫu nhiên 1% hàng để kiểm tra thứ gì đó" hoặc "hiển thị 5 mục nhập ngẫu nhiên".
Tometzky
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.