pemilihan baris acak cepat di Postgres


98

Saya memiliki tabel di postgres yang berisi beberapa juta baris. Saya telah memeriksanya di internet dan saya menemukan yang berikut ini

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

Ini berfungsi, tetapi sangat lambat ... apakah ada cara lain untuk membuat kueri itu, atau cara langsung untuk memilih baris acak tanpa membaca semua tabel? Ngomong-ngomong, 'myid' adalah bilangan bulat tetapi dapat menjadi bidang kosong.


1
Jika Anda ingin memilih beberapa baris acak, lihat pertanyaan ini: stackoverflow.com/q/8674718/247696
Flimm

Jawaban:


99

Anda mungkin ingin bereksperimen dengan OFFSET, seperti pada

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

Ini Nadalah jumlah baris dalam mytable. Anda mungkin perlu melakukan a terlebih dahulu SELECT COUNT(*)untuk mengetahui nilai N.

Pembaruan (oleh Antony Hatchkins)

Anda harus menggunakan di floorsini:

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

Pertimbangkan tabel 2 baris; random()*Nmenghasilkan 0 <= x < 2dan misalnya SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;mengembalikan 0 baris karena pembulatan implisit ke int terdekat.


masuk akal untuk menggunakan N kurang dari SELECT COUNT(*)?, maksud saya, tidak menggunakan semua nilai dalam tabel tetapi hanya sebagian?
Juan

@Juan Itu tergantung pada kebutuhan Anda.
NPE

menggunakan EXPLAIN SELECT ...dengan nilai N yang berbeda memberikan biaya yang sama untuk kueri, maka saya kira lebih baik menggunakan nilai maksimum N.
Juan

3
lihat perbaikan bug dalam jawaban saya di bawah
Antony Hatchkins

2
Ini memiliki kesalahan satu per satu. Ini tidak akan pernah mengembalikan baris pertama dan akan menghasilkan kesalahan 1 / COUNT (*) karena akan mencoba mengembalikan baris setelah baris terakhir.
Ian

62

PostgreSQL 9.5 memperkenalkan pendekatan baru untuk pemilihan sampel yang jauh lebih cepat: TABLESAMPLE

Sintaksnya adalah

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

Ini bukan solusi optimal jika Anda hanya ingin satu baris dipilih, karena Anda perlu mengetahui JUMLAH tabel untuk menghitung persentase yang tepat.

Untuk menghindari HITUNGAN lambat dan menggunakan TABLESAMPLE cepat untuk tabel dari 1 baris hingga miliaran baris, Anda dapat melakukan:

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

Ini mungkin tidak terlihat begitu elegan, tetapi mungkin lebih cepat daripada jawaban lainnya.

Untuk memutuskan apakah Anda ingin menggunakan BERNULLI atau SYSTEM, baca perbedaannya di http://blog.2ndquadrant.com/tablesample-in-postgresql-9-5-2/


2
Ini jauh lebih cepat dan lebih mudah daripada jawaban lainnya - yang ini seharusnya ada di atas.
Hayden Schiff

1
Mengapa Anda tidak bisa menggunakan subquery saja untuk menghitungnya? SELECT * FROM my_table TABLESAMPLE SYSTEM(SELECT 1/COUNT(*) FROM my_table) LIMIT 1;?
machineghost

2
@machineghost "Untuk menghindari HITUNGAN lambat ..." ... Jika data Anda sangat kecil, sehingga Anda dapat menghitung dalam waktu yang wajar, lakukanlah! :-)
alfonx

2
@machineghost Gunakan SELECT reltuples FROM pg_class WHERE relname = 'my_table'untuk estimasi hitungan.
Hynek -Pichi- Vychodil

@ Hynek-Pichi-Vychodil masukan yang sangat bagus! Untuk memastikan bahwa estimasi tidak ketinggalan jaman, itu harus dilakukan ANALYZEd VACUUM baru-baru ini .. tetapi database yang baik harus dianalisis dengan benar .. Dan itu semua tergantung pada kasus penggunaan tertentu. Biasanya meja besar tidak tumbuh begitu cepat ... Terima kasih!
alfonx

34

Saya mencoba ini dengan subquery dan berhasil dengan baik. Offset, setidaknya di Postgresql v8.4.4 berfungsi dengan baik.

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

Faktanya, v8.4 sangat penting agar ini berfungsi, tidak berfungsi untuk <= 8.3.
Antony Hatchkins

1
lihat perbaikan bug dalam jawaban saya di bawah
Antony Hatchkins

32

Anda perlu menggunakan floor:

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

Pertimbangkan tabel 2 baris; random()*Nmenghasilkan 0 <= x <2 dan misalnya SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;mengembalikan 0 baris karena pembulatan implisit ke int terdekat.
Antony Hatchkins

Sayangnya ini tidak berfungsi jika Anda ingin menggunakan LIMIT yang lebih tinggi ... Saya perlu mendapatkan 3 item jadi saya perlu menggunakan sintaks ORDER BY RANDOM ().
Alexis Wilke

1
Tiga query berturut-turut masih akan lebih cepat dari satu order by random(), kira-kira 3*O(N) < O(NlogN)angka realife akan sedikit berbeda karena indeks.
Antony Hatchkins

Masalah saya adalah bahwa 3 item harus berbeda dan WHERE myid NOT IN (1st-myid)dan WHERE myid NOT IN (1st-myid, 2nd-myid)tidak akan berfungsi karena keputusan dibuat oleh OFFSET. Hmmm ... Saya kira saya bisa mengurangi N sebesar 1 dan 2 di SELECT kedua dan ketiga.
Alexis Wilke

Bisakah Anda atau siapa pun memperluas jawaban ini dengan jawaban mengapa saya perlu menggunakan floor()? Keuntungan apa yang ditawarkannya?
ADTC

14

Lihat tautan ini untuk beberapa opsi berbeda. http://www.depesz.com/index.php/2007/09/16/my-thoughts-on-getting-random-row/

Memperbarui: (A. Hatchkins)

Rangkuman artikel (sangat) panjang itu adalah sebagai berikut.

Penulis mendaftar empat pendekatan:

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

2) ORDER BY id where id>=random()*N LIMIT 1- tidak seragam jika ada celah

3) kolom acak - perlu diperbarui sesekali

4) agregat acak khusus - metode licik, bisa lambat: random () perlu dibuat N kali

dan menyarankan untuk meningkatkan metode # 2 dengan menggunakan

5) ORDER BY id where id=random()*N LIMIT 1 dengan permintaan berikutnya jika hasilnya kosong.


Saya bertanya-tanya mengapa mereka tidak meliput OFFSET? Menggunakan ORDER tidak mungkin dilakukan hanya untuk mendapatkan baris acak. Untungnya, OFFSET tercakup dengan baik dalam jawabannya.
androidguy

4

Cara termudah dan tercepat untuk mengambil baris acak adalah dengan menggunakan tsm_system_rowsekstensi:

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;

Kemudian Anda dapat memilih jumlah baris yang Anda inginkan:

SELECT myid  FROM mytable TABLESAMPLE SYSTEM_ROWS(1);

Ini tersedia dengan PostgreSQL 9.5 dan yang lebih baru.

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


1
Peringatan yang adil, ini tidak sepenuhnya acak. Pada tabel yang lebih kecil, saya selalu mengembalikan baris pertama secara berurutan.
Ben Aubin

1
ya, ini dijelaskan dengan jelas dalam dokumentasi (tautan di atas): «Seperti metode pengambilan sampel SISTEM bawaan, SYSTEM_ROWS melakukan pengambilan sampel tingkat blok, sehingga sampel tidak sepenuhnya acak tetapi mungkin terkena efek pengelompokan, terutama jika hanya kecil jumlah baris diminta. ». Jika Anda memiliki kumpulan data kecil, ORDER BY random() LIMIT 1;seharusnya cukup cepat.
daamien

Saya melihat bahwa. Hanya ingin menjelaskan kepada siapa saja yang tidak mengeklik tautan atau jika tautan mati di masa mendatang.
Ben Aubin

1
Juga perlu dicatat bahwa ini hanya akan berfungsi untuk memilih baris acak dari tabel dan KEMUDIAN pemfilteran, sebagai lawan / dibandingkan dengan menjalankan kueri dan kemudian memilih satu atau beberapa catatan secara acak.
nomen

3

Saya telah menemukan solusi yang sangat cepat tanpa TABLESAMPLE. Jauh lebih cepat dari OFFSET random()*N LIMIT 1. Itu bahkan tidak membutuhkan hitungan tabel.

Idenya adalah membuat indeks ekspresi dengan data acak tetapi dapat diprediksi, misalnya md5(primary key).

Berikut adalah pengujian dengan sampel data 1 juta baris:

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;

Hasil:

 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

Query ini terkadang (dengan probabilitas sekitar 1 / Number_of_rows) mengembalikan 0 baris, sehingga perlu diperiksa dan dijalankan ulang. Probabilitas juga tidak persis sama - beberapa baris lebih mungkin daripada yang lain.

Untuk perbandingan:

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

Hasil sangat bervariasi, tetapi bisa sangat buruk:

 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
Cepat ya. Benar-benar acak, tidak. Nilai md5 yang kebetulan menjadi nilai lebih besar berikutnya setelah nilai lain yang ada memiliki peluang yang sangat kecil untuk dipilih, sedangkan nilai setelah celah besar di ruang bilangan memiliki peluang yang jauh lebih besar (lebih besar dengan jumlah nilai yang mungkin di antaranya) . Distribusi yang dihasilkan tidak acak.
Erwin Brandstetter

sangat menarik, dapatkah itu bekerja dalam kasus penggunaan dari pertanyaan seperti lotere: permintaan harus melihat ke semua tiket yang tersedia dan secara acak hanya mengembalikan SATU tiket. juga dapatkah saya menggunakan kunci pesimis (pilih ... untuk pembaruan) dengan teknik Anda?
Mathieu

Untuk semua lotere yang terkait, Anda harus benar-benar menggunakan pengambilan sampel acak yang adil dan aman secara kriptografis - misalnya, pilih nomor acak antara 1 dan maks (id) hingga Anda menemukan id yang ada. Metode dari jawaban ini tidak adil dan juga tidak aman - cepat. Dapat digunakan untuk hal-hal seperti 'dapatkan 1% baris secara acak untuk menguji sesuatu', atau 'tampilkan 5 entri acak'.
Tometzky
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.