Rel 3: Dapatkan Rekam Acak

132

Jadi, saya telah menemukan beberapa contoh untuk menemukan catatan acak di Rails 2 - metode yang disukai tampaknya:

Thing.find :first, :offset => rand(Thing.count)

Menjadi seorang pemula, saya tidak yakin bagaimana ini bisa dibangun menggunakan sintaks find baru di Rails 3.

Jadi, apa "Rails 3 Way" untuk menemukan catatan acak?

Andrew
sumber
9
^^ kecuali saya secara khusus mencari Rails 3 cara optimal, yang merupakan seluruh tujuan pertanyaan.
Andrew
rails 3 spesifik hanya rantai permintaan :)
fl00r

Jawaban:

216
Thing.first(:order => "RANDOM()") # For MySQL :order => "RAND()", - thanx, @DanSingerman
# Rails 3
Thing.order("RANDOM()").first

atau

Thing.first(:offset => rand(Thing.count))
# Rails 3
Thing.offset(rand(Thing.count)).first

Sebenarnya, dalam Rails 3 semua contoh akan berfungsi. Tetapi menggunakan pesanan RANDOMcukup lambat untuk meja besar tetapi lebih gaya sql

UPD. Anda dapat menggunakan trik berikut pada kolom yang diindeks (sintaks PostgreSQL):

select * 
from my_table 
where id >= trunc(
  random() * (select max(id) from my_table) + 1
) 
order by id 
limit 1;
fl00r
sumber
11
Contoh pertama Anda tidak akan bekerja di MySQL - sintaks untuk MySQL adalah Thing.first (: order => "RAND ()") (bahaya menulis SQL daripada menggunakan abstraksi ActiveRecord)
DanSingerman
@ DanSingerman, ya itu spesifik DB RAND()atau RANDOM(). Terima kasih
fl00r
Dan ini tidak akan membuat masalah jika ada item yang hilang dari indeks? (jika sesuatu di tengah tumpukan dihapus, apakah ada kemungkinan akan diminta?
Victor S
@ Viktor, tidak itu tidak akan #offset hanya pergi ke catatan yang tersedia berikutnya. Saya mengujinya dengan Ruby 1.9.2 dan Rails 3.1
SooDesuNe
1
@ JohnMerlino, ya 0 diimbangi, bukan id. Offet 0 berarti item pertama sesuai pesanan.
fl00r
29

Saya sedang mengerjakan sebuah proyek ( Rails 3.0.15, ruby ​​1.9.3-p125-perf ) di mana db berada di localhost dan tabel pengguna memiliki catatan lebih dari 100 ribu .

Menggunakan

dipesan oleh RAND ()

cukup lambat

User.order ("RAND (id)"). Pertama

menjadi

PILIH users. * DARI usersPESANAN OLEH RAND (id) LIMIT 1

dan membutuhkan 8 hingga 12 detik untuk merespons !!

Log rel:

Beban Pengguna (11030.8ms) PILIH users. * DARI usersORDER OLEH RAND () LIMIT 1

dari mysql jelaskan

+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra                           |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
|  1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 110165 | Using temporary; Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+

Anda dapat melihat bahwa tidak ada indeks yang digunakan ( possible_keys = NULL ), sebuah tabel sementara dibuat dan pass tambahan diperlukan untuk mengambil nilai yang diinginkan ( ekstra = Menggunakan sementara; Menggunakan filesort ).

Di sisi lain, dengan memisahkan kueri dalam dua bagian dan menggunakan Ruby, kami memiliki peningkatan yang wajar dalam waktu respons.

users = User.scoped.select(:id);nil
User.find( users.first( Random.rand( users.length )).last )

(; nihil untuk penggunaan konsol)

Log rel:

User Load (25.2ms) SELECT id FROM usersUser Load (0.2ms) SELECT users. * DARI usersMANA users. id= 106854 BATAS 1

dan mysql menjelaskan mengapa:

+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
| id | select_type | table | type  | possible_keys | key                      | key_len | ref  | rows   | Extra       |
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
|  1 | SIMPLE      | users | index | NULL          | index_users_on_user_type | 2       | NULL | 110165 | Using index |
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+

+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | users | const | PRIMARY       | PRIMARY | 4       | const |    1 |       |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

kita sekarang dapat menggunakan hanya indeks dan kunci utama dan melakukan pekerjaan sekitar 500 kali lebih cepat!

MEMPERBARUI:

seperti yang ditunjukkan oleh icantbecool dalam komentar solusi di atas memiliki kelemahan jika ada catatan yang dihapus dalam tabel.

Solusi yang bisa dilakukan

users_count = User.count
User.scoped.limit(1).offset(rand(users_count)).first

yang diterjemahkan menjadi dua pertanyaan

SELECT COUNT(*) FROM `users`
SELECT `users`.* FROM `users` LIMIT 1 OFFSET 148794

dan berjalan sekitar 500ms.

xlembouras
sumber
menambahkan ".id" setelah "terakhir" ke contoh kedua Anda akan menghindari kesalahan "tidak dapat menemukan Model tanpa ID". Contoh: User.find (users.first (Random.rand (users.length)). Last.id)
turing_machine
Peringatan! Di MySQL TIDAKRAND(id) akan memberi Anda urutan acak yang berbeda setiap permintaan. Gunakan jika ingin urutan berbeda setiap kueri. RAND()
Justin Tanner
User.find (users.first (Random.rand (users.length)). Last.id) tidak akan berfungsi jika ada catatan yang dihapus. [1,2,4,5,] dan ini berpotensi memilih id 3, tetapi tidak akan ada relasi rekaman aktif.
icantbecool
Juga, users = User.scoped.select (: id); nihil tidak ditinggalkan. Gunakan ini sebagai gantinya: users = User.where (nil) .select (: id)
icantbecool
Saya percaya menggunakan Random.rand (users.length) sebagai parameter untuk pertama adalah bug. Random.rand dapat mengembalikan 0. Ketika 0 digunakan sebagai parameter untuk pertama kali, batasnya ditetapkan ke nol dan ini tidak mengembalikan catatan. Yang harus digunakan adalah 1 + Acak (users.length) dengan asumsi users.length> 0.
SWoo
12

Jika menggunakan Postgres

User.limit(5).order("RANDOM()")

Jika menggunakan MySQL

User.limit(5).order("RAND()")

Dalam kedua kasus Anda memilih 5 catatan secara acak dari tabel Pengguna. Berikut adalah query SQL aktual yang ditampilkan di konsol.

SELECT * FROM users ORDER BY RANDOM() LIMIT 5
icantbecool
sumber
11

Saya membuat permata 3 rails untuk melakukan ini yang berkinerja lebih baik di meja besar dan memungkinkan Anda untuk menjalin hubungan dan cakupan:

https://github.com/spilliton/randumb

(edit): Perilaku default permata saya pada dasarnya menggunakan pendekatan yang sama seperti di atas sekarang, tetapi Anda memiliki opsi untuk menggunakan cara lama jika mau :)

tumpahan
sumber
6

Banyak jawaban yang diposting sebenarnya tidak berkinerja baik di tabel yang agak besar (1+ juta baris). Pemesanan acak dengan cepat membutuhkan beberapa detik, dan melakukan penghitungan di atas meja juga memakan waktu yang cukup lama.

Solusi yang berfungsi baik bagi saya dalam situasi ini adalah digunakan RANDOM()dengan kondisi di mana:

Thing.where('RANDOM() >= 0.9').take

Di atas meja dengan lebih dari satu juta baris, kueri ini biasanya memakan waktu kurang dari 2ms.

fivedigit
sumber
Kelebihan lain dari solusi Anda adalah penggunaan takefungsi yang memberikan LIMIT(1)kueri tetapi mengembalikan elemen tunggal, bukan array. Jadi kita tidak perlu memanggilfirst
Piotr Galas
Sepertinya saya bahwa catatan di awal tabel memiliki kemungkinan lebih tinggi untuk dipilih dengan cara ini, yang mungkin bukan yang ingin Anda capai.
gorn
5

Kita mulai

cara rel

#in your initializer
module ActiveRecord
  class Base
    def self.random
      if (c = count) != 0
        find(:first, :offset =>rand(c))
      end
    end
  end
end

pemakaian

Model.random #returns single random object

atau pikiran kedua adalah

module ActiveRecord
  class Base
    def self.random
      order("RAND()")
    end
  end
end

pemakaian:

Model.random #returns shuffled collection
Tim Kretschmer
sumber
Couldn't find all Users with 'id': (first, {:offset=>1}) (found 0 results, but was looking for 2)
Bruno
jika tidak ada pengguna dan Anda ingin mendapatkan 2, maka Anda mendapatkan kesalahan. masuk akal.
Tim Kretschmer
1
Pendekatan kedua tidak akan bekerja dengan postgres, tetapi Anda dapat menggunakan "RANDOM()"sebagai gantinya ...
Daniel Richter
4

Ini sangat berguna bagi saya namun saya membutuhkan sedikit lebih banyak fleksibilitas, jadi inilah yang saya lakukan:

Case1: Menemukan satu sumber rekaman acak : situs trevor turk
Tambahkan ini ke model Thing.rb

def self.random
    ids = connection.select_all("SELECT id FROM things")
    find(ids[rand(ids.length)]["id"].to_i) unless ids.blank?
end

maka di controller Anda, Anda dapat memanggil sesuatu seperti ini

@thing = Thing.random

Case2: Menemukan beberapa catatan acak (tanpa pengulangan) sumber: tidak dapat mengingat
saya perlu menemukan 10 catatan acak tanpa pengulangan jadi inilah yang saya temukan bekerja
pada controller Anda:

thing_ids = Thing.find( :all, :select => 'id' ).map( &:id )
@things = Thing.find( (1..10).map { thing_ids.delete_at( thing_ids.size * rand ) } )

Ini akan menemukan 10 catatan acak, namun perlu disebutkan bahwa jika database sangat besar (jutaan catatan), ini tidak akan ideal, dan kinerja akan terhambat. Apakah akan tampil dengan baik hingga beberapa ribu catatan yang cukup bagi saya.

Hishalv
sumber
4

Metode Ruby untuk memilih item secara acak dari daftar adalah sample. Ingin membuat efisien sampleuntuk ActiveRecord, dan berdasarkan jawaban sebelumnya, saya menggunakan:

module ActiveRecord
  class Base
    def self.sample
      offset(rand(size)).first
    end
  end
end

Saya memasukkan ini ke dalam lib/ext/sample.rbdan kemudian memuatnya dengan ini di config/initializers/monkey_patches.rb:

Dir[Rails.root.join('lib/ext/*.rb')].each { |file| require file }
Dan Kohn
sumber
Sebenarnya, #countakan melakukan panggilan ke DB untuk a COUNT. Jika catatan sudah dimuat, ini mungkin ide yang buruk. Sebuah refactor akan digunakan #sizesebagai gantinya karena itu akan memutuskan apakah #countharus digunakan, atau, jika catatan sudah dimuat, untuk digunakan #length.
BenMorganIO
Beralih dari countmenjadi sizeberdasarkan umpan balik Anda. Info lebih lanjut di: dev.mensfeld.pl/2014/09/...
Dan Kohn
3

Bekerja di Rails 5 dan DB agnostik:

Ini di controller Anda:

@quotes = Quote.offset(rand(Quote.count - 3)).limit(3)

Anda dapat, tentu saja, menaruh kekhawatiran ini seperti yang ditunjukkan di sini .

aplikasi / model / masalah / randomable.rb

module Randomable
  extend ActiveSupport::Concern

  class_methods do
    def random(the_count = 1)
      records = offset(rand(count - the_count)).limit(the_count)
      the_count == 1 ? records.first : records
    end
  end
end

kemudian...

app / model / book.rb

class Book < ActiveRecord::Base
  include Randomable
end

Maka Anda dapat menggunakannya hanya dengan melakukan:

Books.random

atau

Books.random(3)
richardun
sumber
Ini selalu mengambil catatan berikutnya, yang perlu setidaknya didokumentasikan (karena mungkin bukan yang diinginkan pengguna).
gorn
2

Anda dapat menggunakan sampel () di ActiveRecord

Misalnya

def get_random_things_for_home_page
  find(:all).sample(5)
end

Sumber: http://thinkingeek.com/2011/07/04/easily-select-random-records-rails/

Trond
sumber
33
Ini adalah permintaan yang sangat buruk untuk digunakan jika Anda memiliki sejumlah besar catatan, karena DB akan memilih SEMUA catatan, kemudian Rails akan mengambil lima catatan dari itu - boros secara besar-besaran.
DaveStephens
5
sampletidak ada dalam ActiveRecord, sampel ada di Array. api.rubyonrails.org/classes/Array.html#method-i-sample
Frans
3
Ini adalah cara yang mahal untuk mendapatkan catatan acak, terutama dari tabel besar. Rails akan memuat objek untuk setiap record dari tabel Anda ke dalam memori. Jika Anda perlu bukti, jalankan 'rails console', coba 'SomeModelFromYourApp.find (: all) .sample (5)' dan lihat SQL yang dihasilkan.
Eliot Sykes
1
Lihat jawaban saya, yang mengubah jawaban mahal ini menjadi kecantikan ramping untuk mendapatkan beberapa catatan acak.
Arcolye
1

Jika menggunakan Oracle

User.limit(10).order("DBMS_RANDOM.VALUE")

Keluaran

SELECT * FROM users ORDER BY DBMS_RANDOM.VALUE WHERE ROWNUM <= 10
Marcelo Austria
sumber
1

Sangat merekomendasikan permata ini untuk catatan acak, yang dirancang khusus untuk tabel dengan banyak baris data:

https://github.com/haopingfan/quick_random_records

Semua jawaban lain berkinerja buruk dengan basis data besar, kecuali permata ini:

  1. quick_random_records hanya membutuhkan biaya 4.6mstotal.

masukkan deskripsi gambar di sini

  1. User.order('RAND()').limit(10)biaya jawaban yang diterima 733.0ms.

masukkan deskripsi gambar di sini

  1. yang offsetpendekatan biaya 245.4mstotal.

masukkan deskripsi gambar di sini

  1. yang User.all.sample(10)pendekatan biaya 573.4ms.

masukkan deskripsi gambar di sini

Catatan: Meja saya hanya memiliki 120.000 pengguna. Semakin banyak catatan yang Anda miliki, semakin besar perbedaan kinerja yang akan terjadi.


MEMPERBARUI:

Tampil di atas meja dengan 550.000 baris

  1. Model.where(id: Model.pluck(:id).sample(10)) biaya 1384.0ms

masukkan deskripsi gambar di sini

  1. gem: quick_random_recordshanya biaya 6.4mstotal

masukkan deskripsi gambar di sini

Derek Fan
sumber
-2

Cara yang sangat mudah untuk mendapatkan beberapa catatan acak dari tabel. Ini membuat 2 pertanyaan murah.

Model.where(id: Model.pluck(:id).sample(3))

Anda dapat mengubah "3" menjadi jumlah catatan acak yang Anda inginkan.

Arcolye
sumber
1
tidak, bagian Model.pluck (: id) .sample (3) tidak murah. Ini akan membaca bidang id untuk setiap elemen dalam tabel.
Maximiliano Guzman
Apakah ada cara agnostik database yang lebih cepat?
Arcolye
-5

Saya baru saja mengalami masalah ini mengembangkan aplikasi kecil di mana saya ingin memilih pertanyaan acak dari DB saya. Saya menggunakan:

@question1 = Question.where(:lesson_id => params[:lesson_id]).shuffle[1]

Dan itu bekerja dengan baik untuk saya. Saya tidak dapat berbicara tentang bagaimana kinerja untuk DB yang lebih besar karena ini hanya aplikasi kecil.

rails_newbie
sumber
Ya, ini hanya mendapatkan semua catatan Anda dan menggunakan metode array ruby ​​pada mereka. Kekurangannya tentu saja itu berarti memuat semua catatan Anda ke dalam memori, kemudian menyusun ulang secara acak, lalu mengambil item kedua dalam array yang dipesan ulang. Itu pasti bisa menjadi babi memori jika Anda berurusan dengan dataset besar. Selain kecil, mengapa tidak mengambil elemen pertama? (mis. shuffle[0])
Andrew
must be shuffle [0]
Marcelo Austria