Cara terbaik untuk memilih baris acak PostgreSQL

344

Saya ingin pemilihan acak baris di PostgreSQL, saya mencoba ini:

select * from table where random() < 0.01;

Tetapi beberapa yang lain merekomendasikan ini:

select * from table order by random() limit 1000;

Saya punya meja yang sangat besar dengan 500 juta baris, saya ingin cepat.

Pendekatan mana yang lebih baik? Apa perbedaannya? Apa cara terbaik untuk memilih baris acak?

nanounanue
sumber
1
Hai Jack, terima kasih atas tanggapan Anda, waktu pelaksanaannya lebih lambat, tetapi saya ingin tahu mana yang berbeda jika ada ...
nanounanue
Uhhh ... sama-sama. Jadi, sudahkah Anda mencoba membandingkan berbagai pendekatan yang berbeda?
Ada juga cara yang jauh lebih cepat. Itu semua tergantung pada kebutuhan Anda dan apa yang harus Anda kerjakan. Apakah Anda membutuhkan 1000 baris? Apakah tabel memiliki id numerik? Tanpa / sedikit / banyak celah? Seberapa pentingkah kecepatan? Berapa banyak permintaan per unit waktu? Apakah setiap permintaan memerlukan set yang berbeda atau bisakah sama untuk slice waktu yang ditentukan?
Erwin Brandstetter
6
Opsi pertama "(acak () <0,01)" secara matematis salah karena Anda tidak bisa mendapatkan baris dalam respons jika tidak ada angka acak di bawah 0,01, yang dapat terjadi dalam hal apa pun (meskipun kecil kemungkinannya), tidak peduli seberapa besar tabelnya atau lebih tinggi ambangnya. Pilihan kedua selalu benar
Herme
1
Jika Anda ingin memilih hanya satu baris, lihat pertanyaan ini: stackoverflow.com/q/5297396/247696
Flimm

Jawaban:

230

Dengan spesifikasi Anda (ditambah info tambahan di komentar),

  • Anda memiliki kolom ID numerik (angka integer) dengan hanya sedikit (atau sedikit) kesenjangan.
  • Jelas tidak ada atau sedikit operasi penulisan.
  • Kolom ID Anda harus diindeks! Kunci utama berfungsi dengan baik.

Kueri di bawah ini tidak memerlukan pemindaian berurutan dari tabel besar, hanya pemindaian indeks.

Pertama, dapatkan taksiran untuk kueri utama:

SELECT count(*) AS ct              -- optional
     , min(id)  AS min_id
     , max(id)  AS max_id
     , max(id) - min(id) AS id_span
FROM   big;

Bagian hanya mungkin mahal adalah count(*)(untuk meja besar). Dengan spesifikasi di atas, Anda tidak memerlukannya. Perkiraan akan baik-baik saja, tersedia hampir tanpa biaya ( penjelasan terperinci di sini ):

SELECT reltuples AS ct FROM pg_class WHERE oid = 'schema_name.big'::regclass;

Selama cttidak jauh lebih kecil dari itu id_span, kueri akan mengungguli pendekatan lain.

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
  • Hasilkan angka acak di idruang. Anda memiliki "beberapa celah", jadi tambahkan 10% (cukup untuk menutupi dengan mudah) ke jumlah baris yang akan diambil.

  • Masing id- masing dapat dipilih beberapa kali secara kebetulan (meskipun sangat tidak mungkin dengan ruang id besar), jadi kelompokkan angka yang dihasilkan (atau gunakan DISTINCT).

  • Bergabunglah dengan ids ke meja besar. Ini harus sangat cepat dengan indeks di tempat.

  • Akhirnya pangkas kelebihan idyang belum dimakan oleh dupes dan celah. Setiap baris memiliki peluang yang sepenuhnya sama untuk dipetik.

Versi pendek

Anda dapat menyederhanakan pertanyaan ini. CTE dalam kueri di atas hanya untuk tujuan pendidikan:

SELECT *
FROM  (
    SELECT DISTINCT 1 + trunc(random() * 5100000)::integer AS id
    FROM   generate_series(1, 1100) g
    ) r
JOIN   big USING (id)
LIMIT  1000;

Saring dengan rCTE

Terutama jika Anda tidak begitu yakin tentang kesenjangan dan perkiraan.

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
   )
SELECT *
FROM   random_pick
LIMIT  1000;  -- actual limit

Kami dapat bekerja dengan surplus yang lebih kecil dalam kueri basis. Jika ada terlalu banyak celah sehingga kami tidak menemukan cukup baris di iterasi pertama, rCTE terus beralih dengan istilah rekursif. Kita masih membutuhkan celah yang relatif sedikit di ruang ID atau rekursi bisa mengering sebelum batas tercapai - atau kita harus mulai dengan buffer yang cukup besar yang menentang tujuan mengoptimalkan kinerja.

Duplikat dihilangkan oleh UNIONdi rCTE.

Bagian luar LIMITmembuat CTE berhenti segera setelah kami memiliki cukup baris.

Permintaan ini dirancang dengan hati-hati untuk menggunakan indeks yang tersedia, menghasilkan baris yang benar-benar acak dan tidak berhenti sampai kami memenuhi batas (kecuali rekursi menjadi kering). Ada sejumlah jebakan di sini jika Anda ingin menulis ulang.

Bungkus ke dalam fungsi

Untuk penggunaan berulang dengan berbagai parameter:

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big 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
   )
   SELECT *
   FROM   random_pick
   LIMIT  _limit;
END
$func$  LANGUAGE plpgsql VOLATILE ROWS 1000;

Panggilan:

SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);

Anda bahkan dapat membuat generik ini berfungsi untuk tabel apa pun: Ambil nama kolom PK dan tabel sebagai tipe polimorfik dan gunakan EXECUTE... Tapi itu di luar cakupan pertanyaan ini. Lihat:

Alternatif yang mungkin

JIKA persyaratan Anda memungkinkan set yang sama untuk panggilan berulang (dan kita berbicara tentang panggilan berulang), saya akan mempertimbangkan pandangan terwujud . Jalankan query di atas satu kali dan tulis hasilnya ke sebuah tabel. Pengguna mendapatkan pilihan acak semu dengan kecepatan tinggi. Segarkan pilihan acak Anda pada interval atau acara yang Anda pilih.

Postgres 9.5 memperkenalkan TABLESAMPLE SYSTEM (n)

Dimana npersentasenya. Manual:

Metode BERNOULLIdan SYSTEMsampling masing-masing menerima argumen tunggal yang merupakan sebagian kecil dari tabel untuk sampel, dinyatakan sebagai persentase antara 0 dan 100 . Argumen ini bisa berupa realekspresi bernilai apa pun .

Penekanan berani saya. Ini sangat cepat , tetapi hasilnya tidak sepenuhnya acak . Manual lagi:

The SYSTEMMetode secara signifikan lebih cepat daripada BERNOULLImetode ketika persentase contoh kecil yang ditentukan, tetapi mungkin kembali sampel kurang-acak meja sebagai akibat dari efek klastering.

Jumlah baris yang dikembalikan dapat sangat bervariasi. Sebagai contoh kami, untuk mendapatkan sekitar 1000 baris:

SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);

Terkait:

Atau instal modul tambahan tsm_system_rows untuk mendapatkan jumlah baris yang diminta secara tepat (jika ada cukup) dan memungkinkan sintaks yang lebih nyaman:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Lihat jawaban Evan untuk detailnya.

Tapi itu masih belum sepenuhnya acak.

Erwin Brandstetter
sumber
Di mana didefinisikan tabel t ? Haruskah r bukan t ?
Luc M
1
@ LUCM: Ini didefinisikan di sini:, JOIN bigtbl tyang merupakan kependekan dari JOIN bigtbl AS t. tadalah alias tabel untuk bigtbl. Tujuannya adalah untuk mempersingkat sintaks tetapi tidak diperlukan dalam kasus khusus ini. Saya menyederhanakan kueri dalam jawaban saya dan menambahkan versi sederhana.
Erwin Brandstetter
Apa tujuan dari rentang nilai dari generate_series (1,1100)?
Hebat-o
@ Keren-o: Tujuannya adalah untuk mengambil 1000 baris, saya mulai dengan tambahan 10% untuk mengkompensasi beberapa celah atau (tidak mungkin tetapi mungkin) duplikat angka acak ... penjelasannya ada dalam jawaban saya.
Erwin Brandstetter
Erwin, saya memposting variasi "Kemungkinan pilihan" Anda: stackoverflow.com/a/23634212/430128 . Akan tertarik dengan pemikiran Anda.
Raman
100

Anda dapat memeriksa dan membandingkan rencana eksekusi keduanya dengan menggunakan

EXPLAIN select * from table where random() < 0.01;
EXPLAIN select * from table order by random() limit 1000;

Tes cepat pada tabel besar 1 menunjukkan, bahwa yang ORDER BYpertama mengurutkan tabel lengkap dan kemudian mengambil 1000 item pertama. Mengurutkan tabel besar tidak hanya membaca tabel itu tetapi juga melibatkan membaca dan menulis file sementara. The where random() < 0.1hanya scan tabel lengkap sekali.

Untuk tabel besar, ini mungkin bukan yang Anda inginkan karena bahkan satu pemindaian tabel lengkap mungkin butuh waktu lama.

Proposal ketiga adalah

select * from table where random() < 0.01 limit 1000;

Yang ini menghentikan pemindaian tabel segera setelah 1000 baris telah ditemukan dan karenanya kembali lebih cepat. Tentu saja ini sedikit mengurangi keacakannya, tetapi mungkin ini cukup baik untuk kasus Anda.

Sunting: Selain pertimbangan ini, Anda dapat memeriksa pertanyaan yang sudah diajukan untuk ini. Menggunakan kueri [postgresql] randommenghasilkan beberapa klik.

Dan artikel terkait depez yang menguraikan beberapa pendekatan lagi:


1 "besar" seperti pada "tabel lengkap tidak akan masuk ke dalam memori".

AH
sumber
1
Poin bagus tentang menulis file sementara untuk melakukan pemesanan. Itu memang sukses besar. Saya kira kita bisa melakukan random() < 0.02dan mengocok daftar itu, lalu limit 1000! Pengurutan akan lebih murah pada beberapa ribu baris (lol).
Donald Miner
"Pilih * dari tabel di mana acak () <0,05 membatasi 500;" adalah salah satu metode yang lebih mudah untuk postgresql. Kami menggunakan ini di salah satu proyek kami di mana kami perlu memilih 5% dari hasil dan tidak lebih dari 500 baris pada satu waktu untuk diproses.
tgharold
Mengapa Anda pernah mempertimbangkan pemindaian penuh O (n) untuk mengambil sampel pada tabel baris 500 m? Itu sangat lambat di meja besar dan sama sekali tidak perlu.
mafu
75

urutan postgresql secara acak (), pilih baris dalam urutan acak:

select your_columns from your_table ORDER BY random()

pesanan postgresql secara acak () dengan yang berbeda:

select * from 
  (select distinct your_columns from your_table) table_alias
ORDER BY random()

pesanan postgresql dengan batas acak satu baris:

select your_columns from your_table ORDER BY random() limit 1
Eric Leschinski
sumber
1
select your_columns from your_table ORDER BY random() limit 1mengambil ~ 2 menit untuk exec pada baris 45mil
Nguyen
apakah ada cara untuk mempercepat ini?
CpILL
43

Dimulai dengan PostgreSQL 9.5, ada sintaks baru yang didedikasikan untuk mendapatkan elemen acak dari tabel:

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

Contoh ini akan memberi Anda 5% elemen dari mytable.

Lihat penjelasan lebih lanjut di posting blog ini: http://www.postgresql.org/docs/current/static/sql-select.html

Mickaël Le Baillif
sumber
3
Catatan penting dari dokumen: "Metode SISTEM melakukan pengambilan sampel tingkat blok dengan setiap blok memiliki peluang tertentu untuk dipilih; semua baris di setiap blok yang dipilih dikembalikan. Metode SISTEM secara signifikan lebih cepat daripada metode BERNOULLI ketika persentase sampel kecil ditentukan, tetapi mungkin mengembalikan sampel tabel yang kurang acak sebagai hasil dari efek pengelompokan. "
Tim
1
Apakah ada cara untuk menentukan jumlah baris dan bukan persentase?
Flimm
4
Anda dapat menggunakan TABLESAMPLE SYSTEM_ROWS(400)untuk mendapatkan sampel 400 baris acak. Anda perlu mengaktifkan built-in tsm_system_rowsekstensi untuk menggunakan pernyataan ini.
Mickaël Le Baillif
sayangnya tidak berfungsi dengan delete.
Gadelkareem
27

Yang dengan ORDER BY akan menjadi yang lebih lambat.

select * from table where random() < 0.01;pergi merekam dengan catatan, dan memutuskan untuk secara acak memfilternya atau tidak. Ini akan menjadi O(N)karena hanya perlu memeriksa setiap catatan sekali.

select * from table order by random() limit 1000;akan mengurutkan seluruh tabel, lalu memilih 1000 yang pertama. Selain sihir voodoo di belakang layar, urutannya adalah O(N * log N).

Kelemahan dari yang random() < 0.01satu adalah bahwa Anda akan mendapatkan sejumlah variabel catatan keluaran.


Catatan, ada cara yang lebih baik untuk mengacak satu set data daripada mengurutkan secara acak: The Fisher-Yates Shuffle , yang berjalan di O(N). Menerapkan shuffle dalam SQL sepertinya cukup menantang.

Donald Miner
sumber
3
Tidak ada alasan Anda tidak dapat menambahkan Batas 1 ke akhir contoh pertama Anda. Satu-satunya masalah adalah ada potensi bahwa Anda tidak akan mendapatkan catatan kembali, jadi Anda harus mempertimbangkan itu dalam kode Anda.
Relequestual
Masalahnya dengan Fisher-Yates adalah Anda harus memiliki seluruh dataset dalam memori untuk memilihnya. Tidak layak untuk dataset yang sangat besar :(
CpILL
16

Ini keputusan yang cocok untuk saya. Saya kira itu sangat sederhana untuk dipahami dan dieksekusi.

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;
Bogdan Surai
sumber
6
Saya pikir solusi ini berfungsi seperti ORDER BY random()yang berfungsi tetapi mungkin tidak efisien ketika bekerja dengan meja besar.
Anh Cao
15
select * from table order by random() limit 1000;

Jika Anda tahu berapa banyak baris yang Anda inginkan, periksa tsm_system_rows.

tsm_system_rows

modul menyediakan metode sampling tabel SYSTEM_ROWS, yang dapat digunakan dalam klausa TABLESAMPLE dari perintah SELECT.

Metode pengambilan sampel tabel ini menerima argumen integer tunggal yang merupakan jumlah maksimum baris untuk dibaca. Sampel yang dihasilkan akan selalu mengandung banyak baris, kecuali jika tabel tidak mengandung cukup banyak baris, dalam hal ini seluruh tabel dipilih. Seperti metode pengambilan sampel SISTEM bawaan, SYSTEM_ROWS melakukan pengambilan sampel tingkat blok, sehingga sampel tidak sepenuhnya acak tetapi dapat dikenakan efek pengelompokan, terutama jika hanya sejumlah kecil baris yang diminta.

Pertama instal ekstensi

CREATE EXTENSION tsm_system_rows;

Lalu pertanyaan Anda,

SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);
Evan Carroll
sumber
2
Saya menambahkan tautan ke jawaban Anda yang ditambahkan, ini merupakan peningkatan penting dibandingkan SYSTEMmetode bawaan.
Erwin Brandstetter
Saya baru saja menjawab pertanyaan di sini (catatan tunggal acak) di mana saya melakukan benchmarking dan pengujian yang cukup besar tsm_system_rowsdan tsm_system_timeekstensi. Sejauh yang saya bisa lihat, mereka sebenarnya tidak berguna untuk apa pun kecuali pemilihan baris acak yang minimal . Saya akan berterima kasih jika Anda dapat melihat dan mengomentari validitas atau analisis saya.
Jejak
6

Jika Anda ingin hanya satu baris, Anda dapat menggunakan offsetturunan terhitung count.

select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));
Nelo Mitranim
sumber
2

Variasi tampilan terwujud "Alternatif yang memungkinkan" yang diuraikan oleh Erwin Brandstetter dimungkinkan.

Katakan, misalnya, bahwa Anda tidak ingin duplikat dalam nilai acak yang dikembalikan. Jadi, Anda perlu menetapkan nilai boolean pada tabel utama yang berisi set nilai Anda (non-acak).

Dengan asumsi ini adalah tabel input:

id_values  id  |   used
           ----+--------
           1   |   FALSE
           2   |   FALSE
           3   |   FALSE
           4   |   FALSE
           5   |   FALSE
           ...

Isi ID_VALUEStabel sesuai kebutuhan. Kemudian, seperti dijelaskan oleh Erwin, buat tampilan terwujud yang mengacak ID_VALUEStabel sekali:

CREATE MATERIALIZED VIEW id_values_randomized AS
  SELECT id
  FROM id_values
  ORDER BY random();

Perhatikan bahwa tampilan terwujud tidak mengandung kolom yang digunakan, karena ini akan dengan cepat menjadi usang. Tampilan juga tidak perlu mengandung kolom lain yang mungkin ada diid_values tabel.

Untuk mendapatkan (dan "mengonsumsi") nilai acak, gunakan UPDATE-RETURNING on id_values, pilih id_valuesdari id_values_randomizeddengan join, dan terapkan kriteria yang diinginkan untuk mendapatkan hanya kemungkinan yang relevan. Sebagai contoh:

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;

Ubah LIMITseperlunya - jika Anda hanya perlu satu nilai acak pada satu waktu, ubah LIMITke 1.

Dengan indeks yang tepat aktif id_values, saya percaya UPDATE-RETURNING harus dijalankan dengan sangat cepat dengan sedikit beban. Ini mengembalikan nilai acak dengan satu perjalanan pulang pergi database. Kriteria untuk baris "yang memenuhi syarat" bisa serumit yang dipersyaratkan. Baris baru dapat ditambahkan ke id_valuestabel kapan saja, dan mereka akan dapat diakses ke aplikasi segera setelah tampilan terwujud di-refresh (yang kemungkinan dapat dijalankan pada waktu yang tidak sibuk). Pembuatan dan penyegaran tampilan terwujud akan lambat, tetapi hanya perlu dijalankan ketika id baru ditambahkan ke id_valuestabel.

Raman
sumber
sangat menarik. Apakah itu berfungsi jika saya tidak hanya perlu memilih tetapi juga memperbarui menggunakan select..for memperbarui dengan pg_try_advisory_xact_lock? (Yaitu saya perlu banyak membaca bersamaan dan menulis)
Mathieu
1

Satu pelajaran dari pengalaman saya:

offset floor(random() * N) limit 1tidak lebih cepat dari order by random() limit 1.

Saya pikir offsetpendekatannya akan lebih cepat karena harus menghemat waktu penyortiran Postgres. Ternyata tidak.

pengguna10375
sumber
0

Tambahkan kolom yang disebut rdengan tipe serial. Indeksr .

Asumsikan kita memiliki 200.000 baris, kita akan menghasilkan angka acak n, di mana 0 <n <<= 200, 000.

Pilih baris dengan r > n, urutkan ASCdan pilih yang terkecil.

Kode:

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

Kode ini jelas. Subquery di tengah digunakan untuk dengan cepat memperkirakan jumlah baris tabel dari https://stackoverflow.com/a/7945274/1271094 .

Di level aplikasi Anda perlu menjalankan pernyataan lagi jika n> jumlah baris atau perlu memilih beberapa baris.

MK Yung
sumber
Saya suka ini karena pendek dan elegan :) Dan saya bahkan menemukan cara untuk memperbaikinya: EXPLAIN ANALYZE memberitahu saya bahwa seperti ini, indeks PKEY tidak akan digunakan karena acak () mengembalikan dua kali lipat, sedangkan PKEY membutuhkan BIGINT.
fxtentacle
pilih * dari YOUR_TABLE di mana r> (pilih (pilih reltuples :: bigint AS estimasi dari pg_class mana oid = 'public.YOUR_TABLE' :: regclass) * acak ()) :: pesanan BIGINT dengan batas r asc (1);
fxtentacle
0

Saya tahu saya sedikit terlambat ke pesta, tetapi saya baru saja menemukan alat yang luar biasa ini disebut pg_sample :

pg_sample - ekstrak dataset sampel kecil dari database PostgreSQL yang lebih besar sambil mempertahankan integritas referensial.

Saya mencoba ini dengan database 350M baris dan itu sangat cepat, tidak tahu tentang keacakan .

./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db
Daniel Gerber
sumber