Rails 3: รับบันทึกแบบสุ่ม


132

ดังนั้นฉันได้พบหลายตัวอย่างในการค้นหาเร็กคอร์ดแบบสุ่มใน Rails 2 - วิธีที่ต้องการน่าจะเป็น:

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

เนื่องจากเป็นมือใหม่ฉันไม่แน่ใจว่าจะสร้างสิ่งนี้ได้อย่างไรโดยใช้ไวยากรณ์การค้นหาใหม่ใน Rails 3

แล้ว "Rails 3 Way" ในการค้นหาระเบียนแบบสุ่มคืออะไร?



9
^^ ยกเว้นผมโดยเฉพาะที่กำลังมองหาทางรถไฟ 3 ทางที่ดีที่สุดซึ่งเป็นวัตถุประสงค์ทั้งหมดของคำถาม
Andrew

ราง 3 ที่เฉพาะเจาะจงเป็นเพียงโซ่แบบสอบถาม :)
fl00r

คำตอบ:


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

หรือ

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

จริงๆแล้วใน Rails 3 ทุกตัวอย่างจะใช้ได้ แต่การใช้คำสั่งRANDOMค่อนข้างช้าสำหรับโต๊ะขนาดใหญ่ แต่มีลักษณะ sql มากกว่า

UPD. คุณสามารถใช้เคล็ดลับต่อไปนี้ในคอลัมน์ที่จัดทำดัชนี (ไวยากรณ์ PostgreSQL):

select * 
from my_table 
where id >= trunc(
  random() * (select max(id) from my_table) + 1
) 
order by id 
limit 1;

11
ตัวอย่างแรกของคุณใช้ไม่ได้ใน MySQL แม้ว่าไวยากรณ์สำหรับ MySQL คือ Thing.first (: order => "RAND ()") (อันตรายจากการเขียน SQL แทนที่จะใช้ ActiveRecord abstractions)
DanSingerman

@ DanSingerman ใช่มันเป็น DB เฉพาะRAND()หรือRANDOM(). ขอบคุณ
fl00r

และสิ่งนี้จะไม่สร้างปัญหาหากมีรายการที่ขาดหายไปจากดัชนี? (ถ้าสิ่งที่อยู่ตรงกลางของสแต็กถูกลบจะมีโอกาสขอได้ไหม
Victor S

@VictorS ไม่มันจะไม่ #offset เพียงแค่ไปที่ระเบียนถัดไป ฉันทดสอบกับ Ruby 1.9.2 และ Rails 3.1
SooDesuNe

1
@JohnMerlino ใช่ 0 ถูกชดเชยไม่ใช่ id Offet 0 หมายถึงรายการแรกตามคำสั่งซื้อ
fl00r

29

ฉันกำลังทำงานในโปรเจ็กต์ ( Rails 3.0.15, Ruby 1.9.3-p125-perf ) โดยที่ db อยู่ในlocalhostและตารางผู้ใช้มีเร็กคอร์ดมากกว่า100Kเล็กน้อย

การใช้

สั่งซื้อโดย RAND ()

ค่อนข้างช้า

User.order ("RAND (id)") ก่อน

กลายเป็น

SELECT users. * FROM usersORDER BY RAND (id) LIMIT 1

และใช้เวลา8ถึง12 วินาทีในการตอบกลับ !!

บันทึกราง:

User Load (11030.8ms) SELECT users. * FROM usersORDER BY RAND () LIMIT 1

จากคำอธิบายของ mysql

+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
| 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 |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+

คุณจะเห็นว่าไม่มีการใช้ดัชนี ( possible_keys = NULL ) ตารางชั่วคราวจะถูกสร้างขึ้นและต้องใช้รหัสผ่านพิเศษเพื่อดึงค่าที่ต้องการ ( extra = ใช้ชั่วคราวการใช้ filesort )

ในทางกลับกันการแบ่งคำค้นหาออกเป็นสองส่วนและใช้ Ruby เรามีเวลาตอบสนองที่ดีขึ้นตามสมควร

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

(ไม่มีสำหรับการใช้คอนโซล)

บันทึกราง:

โหลดผู้ใช้ (25.2ms) ID เลือกจากusersผู้ใช้ไฟฟ้า (0.2ms) SELECT users. * FROM WHEREusers = 106854 LIMIT 1usersid

และ mysql อธิบายว่าทำไม:

+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
| 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 |       |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

ตอนนี้เราสามารถใช้ดัชนีและคีย์หลักเท่านั้นและทำงานได้เร็วขึ้นประมาณ 500 เท่า!

อัพเดท:

ตามที่ icantbecool ชี้ให้เห็นในความคิดเห็นวิธีแก้ปัญหาข้างต้นมีข้อบกพร่องหากมีการลบบันทึกในตาราง

วิธีแก้ปัญหาที่สามารถทำได้

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

ซึ่งแปลเป็นสองแบบสอบถาม

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

และทำงานในประมาณ 500ms


การเพิ่ม ".id" หลัง "สุดท้าย" ในตัวอย่างที่สองจะหลีกเลี่ยงข้อผิดพลาด "ไม่พบโมเดลที่ไม่มี ID" เช่น User.find (users.first (Random.rand (users.length)) last.id)
turing_machine

คำเตือน! ใน MySQL RAND(id)จะไม่ให้คำสั่งสุ่มที่แตกต่างกันทุกแบบสอบถาม ใช้RAND()ถ้าต้องการลำดับที่แตกต่างกันแต่ละแบบสอบถาม
Justin Tanner

User.find (users.first (Random.rand (users.length)). last.id) จะไม่ทำงานหากมีการลบบันทึก [1,2,4,5,] และอาจเลือก id ของ 3 ได้ แต่จะไม่มีความสัมพันธ์ของเร็กคอร์ดที่ใช้งานอยู่
icantbecool

นอกจากนี้ users = User.scoped.select (: id); ไม่มีการเลิกใช้งาน ใช้สิ่งนี้แทน: users = User.where (nil) .select (: id)
icantbecool

ฉันเชื่อว่าการใช้ Random.rand (users.length) เป็นพารามิเตอร์แรกเป็นข้อบกพร่อง Random.rand สามารถคืนค่า 0 เมื่อใช้ 0 เป็นพารามิเตอร์เป็นอันดับแรกขีด จำกัด จะถูกตั้งค่าเป็นศูนย์และจะไม่ส่งคืนระเบียน สิ่งที่ควรใช้แทนคือ 1 + Random (users.length) สมมติว่า users.length> 0.
SWoo

12

หากใช้ Postgres

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

หากใช้ MySQL

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

ในทั้งสองกรณีคุณจะเลือก 5 ระเบียนแบบสุ่มจากตารางผู้ใช้ นี่คือแบบสอบถาม SQL จริงที่แสดงในคอนโซล

SELECT * FROM users ORDER BY RANDOM() LIMIT 5

11

ฉันสร้างราง 3 อัญมณีเพื่อทำสิ่งนี้ซึ่งทำงานได้ดีขึ้นบนโต๊ะขนาดใหญ่และช่วยให้คุณเชื่อมโยงความสัมพันธ์และขอบเขต:

https://github.com/spilliton/randumb

(แก้ไข): พฤติกรรมเริ่มต้นของอัญมณีของฉันโดยพื้นฐานแล้วใช้แนวทางเดียวกับข้างต้นในตอนนี้ แต่คุณมีตัวเลือกที่จะใช้วิธีเก่าหากคุณต้องการ :)


6

คำตอบจำนวนมากที่โพสต์ไม่สามารถทำงานได้ดีในตารางที่ค่อนข้างใหญ่ (1 ล้านแถว) การสั่งซื้อแบบสุ่มใช้เวลาไม่กี่วินาทีอย่างรวดเร็วและการนับจำนวนบนโต๊ะก็ใช้เวลาค่อนข้างนานเช่นกัน

วิธีแก้ปัญหาที่ใช้ได้ดีสำหรับฉันในสถานการณ์นี้คือใช้RANDOM()กับเงื่อนไขที่:

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

บนตารางที่มีแถวมากกว่าล้านแถวโดยทั่วไปการสืบค้นนี้จะใช้เวลาน้อยกว่า 2ms


ข้อดีอีกอย่างของโซลูชันของคุณคือใช้takeฟังก์ชันที่ให้LIMIT(1)แบบสอบถาม แต่ส่งคืนองค์ประกอบเดียวแทนอาร์เรย์ ดังนั้นเราจึงไม่จำเป็นต้องเรียกfirst
Piotr Galas

สำหรับฉันแล้วดูเหมือนว่าระเบียนที่จุดเริ่มต้นของตารางจะมีความน่าจะเป็นสูงกว่าที่เลือกด้วยวิธีนี้ซึ่งอาจไม่ใช่สิ่งที่คุณต้องการบรรลุ
เข้า

5

ไปเลย

ทางรถไฟ

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

การใช้งาน

Model.random #returns single random object

หรือความคิดที่สองคือ

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

การใช้งาน:

Model.random #returns shuffled collection

Couldn't find all Users with 'id': (first, {:offset=>1}) (found 0 results, but was looking for 2)
Bruno

หากไม่มีผู้ใช้และคุณต้องการรับ 2 แสดงว่าคุณได้รับข้อผิดพลาด เข้าท่า.
Tim Kretschmer

1
แนวทางที่สองจะใช้ไม่ได้กับ postgres แต่คุณสามารถใช้"RANDOM()"แทนได้ ...
Daniel Richter

4

สิ่งนี้มีประโยชน์มากสำหรับฉัน แต่ฉันต้องการความยืดหยุ่นมากกว่านี้เล็กน้อยดังนั้นนี่คือสิ่งที่ฉันทำ:

Case1: การค้นหาแหล่งที่มาของเร็กคอร์ดแบบสุ่มหนึ่งรายการ: trevor turk site
เพิ่มสิ่งนี้ในโมเดล 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

จากนั้นในคอนโทรลเลอร์ของคุณคุณสามารถเรียกสิ่งนี้ได้

@thing = Thing.random

Case2: การค้นหาระเบียนแบบสุ่มหลายรายการ (ไม่ซ้ำ) แหล่งที่มา: จำไม่ได้ว่า
ฉันต้องการค้นหาระเบียนสุ่ม 10 รายการโดยไม่มีการทำซ้ำดังนั้นนี่คือสิ่งที่ฉันพบ
ในคอนโทรลเลอร์ของคุณ:

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

สิ่งนี้จะพบระเบียนแบบสุ่ม 10 รายการอย่างไรก็ตามควรกล่าวถึงว่าหากฐานข้อมูลมีขนาดใหญ่เป็นพิเศษ (นับล้านระเบียน) สิ่งนี้จะไม่เหมาะและประสิทธิภาพจะถูกขัดขวาง จะทำงานได้ดีมากถึงสองสามพันบันทึกซึ่งเพียงพอสำหรับฉัน


4

sampleวิธีทับทิมสำหรับการสุ่มหยิบจากรายการคือ ต้องการสร้างประสิทธิภาพsampleสำหรับ ActiveRecord และจากคำตอบก่อนหน้านี้ฉันใช้:

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

ฉันใส่สิ่งนี้lib/ext/sample.rbแล้วโหลดด้วยสิ่งนี้ในconfig/initializers/monkey_patches.rb:

Dir[Rails.root.join('lib/ext/*.rb')].each { |file| require file }

จริงๆแล้ว#countจะโทรไปที่ DB สำหรับไฟล์COUNT. หากโหลดบันทึกไว้แล้วอาจเป็นความคิดที่ไม่ดี refactor จะใช้#sizeแทนเพราะมันจะตัดสินใจว่าควรจะใช้หรือหากบันทึกที่มีการโหลดแล้วกับการใช้งาน#count #length
BenMorganIO

เปลี่ยนจากcountเป็นsizeตามความคิดเห็นของคุณ ข้อมูลเพิ่มเติมได้ที่: dev.mensfeld.pl/2014/09/…
Dan Kohn

3

ทำงานใน Rails 5 และ DB ไม่เชื่อเรื่องพระเจ้า:

สิ่งนี้ในตัวควบคุมของคุณ:

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

คุณสามารถของหลักสูตรใส่นี้ในความกังวลดังที่แสดงไว้ที่นี่

แอป / รุ่น / ข้อกังวล / 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

แล้ว ...

แอพ / รุ่น / book.rb

class Book < ActiveRecord::Base
  include Randomable
end

จากนั้นคุณสามารถใช้งานได้ง่ายๆโดยทำ:

Books.random

หรือ

Books.random(3)

สิ่งนี้จะใช้บันทึกที่ตามมาเสมอซึ่งต้องมีการจัดทำเป็นเอกสารเป็นอย่างน้อย (เนื่องจากอาจไม่ใช่สิ่งที่ผู้ใช้ต้องการ)
gorn

2

คุณสามารถใช้ sample () ใน ActiveRecord

เช่น

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

ที่มา: http://thinkingeek.com/2011/07/04/easily-select-random-records-rails/


33
นี่เป็นแบบสอบถามที่ไม่ดีอย่างยิ่งที่จะใช้หากคุณมีระเบียนจำนวนมากเนื่องจาก DB จะเลือกระเบียนทั้งหมดจากนั้น Rails จะเลือกระเบียนห้ารายการจากนั้นซึ่งเป็นการสิ้นเปลืองอย่างมาก
DaveStephens

5
sampleไม่ได้อยู่ใน ActiveRecord ตัวอย่างอยู่ใน Array api.rubyonrails.org/classes/Array.html#method-i-sample
Frans

3
นี่เป็นวิธีที่มีราคาแพงในการรับระเบียนแบบสุ่มโดยเฉพาะจากตารางขนาดใหญ่ Rails จะโหลดวัตถุสำหรับทุกระเบียนจากตารางของคุณลงในหน่วยความจำ หากคุณต้องการหลักฐานให้เรียกใช้ 'คอนโซลราง' ลอง 'SomeModelFromYourApp.find (: all) .sample (5)' และดู SQL ที่สร้างขึ้น
Eliot Sykes

1
ดูคำตอบของฉันซึ่งจะเปลี่ยนคำตอบราคาแพงนี้ให้กลายเป็นความสวยงามที่คล่องตัวในการรับระเบียนแบบสุ่มหลายรายการ
Arcolye


1

แนะนำอัญมณีนี้เป็นอย่างยิ่งสำหรับการบันทึกแบบสุ่มซึ่งออกแบบมาเป็นพิเศษสำหรับตารางที่มีแถวข้อมูลจำนวนมาก:

https://github.com/haopingfan/quick_random_records

คำตอบอื่น ๆ ทั้งหมดทำงานได้ไม่ดีกับฐานข้อมูลขนาดใหญ่ยกเว้นอัญมณีนี้:

  1. quick_random_records มีค่าใช้จ่าย4.6msทั้งหมดเท่านั้น

ป้อนคำอธิบายภาพที่นี่

  1. คำตอบที่ได้รับการยอมรับค่าใช้จ่ายUser.order('RAND()').limit(10)733.0ms

ป้อนคำอธิบายภาพที่นี่

  1. offsetค่าใช้จ่ายวิธีการ245.4msทั้งหมด

ป้อนคำอธิบายภาพที่นี่

  1. ค่าใช้จ่ายวิธีUser.all.sample(10)573.4ms

ป้อนคำอธิบายภาพที่นี่

หมายเหตุ: โต๊ะของฉันมีผู้ใช้เพียง 120,000 คน ยิ่งคุณมีบันทึกมากเท่าไหร่ประสิทธิภาพก็จะยิ่งแตกต่างกันมากเท่านั้น


อัพเดท:

ดำเนินการบนโต๊ะด้วย 550,000 แถว

  1. Model.where(id: Model.pluck(:id).sample(10)) ค่าใช้จ่าย 1384.0ms

ป้อนคำอธิบายภาพที่นี่

  1. gem: quick_random_recordsเพียงค่าใช้จ่าย6.4msทั้งหมด

ป้อนคำอธิบายภาพที่นี่


-2

วิธีที่ง่ายมากในการรับระเบียนแบบสุ่มหลายรายการจากตาราง สิ่งนี้ทำให้ 2 แบบสอบถามราคาถูก

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

คุณสามารถเปลี่ยน "3" เป็นจำนวนระเบียนแบบสุ่มที่คุณต้องการ


1
ไม่ส่วน Model.pluck (: id) ตัวอย่าง (3) ไม่ถูก มันจะอ่านฟิลด์ id สำหรับทุกองค์ประกอบในตาราง
Maximiliano Guzman

มีวิธีที่ไม่เชื่อเรื่องพระเจ้าฐานข้อมูลที่เร็วกว่านี้หรือไม่?
Arcolye

-5

ฉันเพิ่งพบปัญหานี้ในการพัฒนาแอปพลิเคชันขนาดเล็กที่ฉันต้องการเลือกคำถามแบบสุ่มจากฐานข้อมูลของฉัน ฉันใช้:

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

และมันก็ใช้ได้ดีสำหรับฉัน ฉันไม่สามารถพูดได้เกี่ยวกับประสิทธิภาพของฐานข้อมูลขนาดใหญ่เนื่องจากนี่เป็นเพียงแอปพลิเคชันขนาดเล็ก


ใช่นี่เป็นเพียงการรับบันทึกทั้งหมดของคุณและใช้วิธีการอาร์เรย์ทับทิมกับพวกเขา แน่นอนว่าข้อเสียเปรียบนั้นหมายถึงการโหลดบันทึกทั้งหมดของคุณลงในหน่วยความจำจากนั้นจัดเรียงใหม่แบบสุ่มจากนั้นจึงคว้ารายการที่สองในอาร์เรย์ที่เรียงลำดับใหม่ นั่นอาจเป็นหน่วยความจำอย่างแน่นอนหากคุณกำลังจัดการกับชุดข้อมูลขนาดใหญ่ กันเล็กน้อยทำไมไม่คว้าองค์ประกอบแรก (เช่นshuffle[0])
แอนดรู

ต้องสับเปลี่ยน [0]
Marcelo Austria
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.