Schienen 3: Zufällige Aufzeichnung erhalten

132

Daher habe ich in Rails 2 mehrere Beispiele für das Auffinden eines zufälligen Datensatzes gefunden - die bevorzugte Methode scheint zu sein:

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

Als Neuling bin ich mir nicht sicher, wie dies mit der neuen Find-Syntax in Rails 3 konstruiert werden kann.

Also, was ist der "Rails 3 Way", um einen zufälligen Datensatz zu finden?

Andrew
quelle
9
^^ außer ich suche speziell nach dem Rails 3 optimalen Weg, der den gesamten Zweck der Frage darstellt.
Andrew
Schienen 3 spezifisch ist nur
Abfragekette

Antworten:

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

oder

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

Tatsächlich funktionieren in Rails 3 alle Beispiele. Die Verwendung von Order RANDOMist für große Tische jedoch recht langsam, jedoch eher im SQL-Stil

UPD. Sie können den folgenden Trick für eine indizierte Spalte verwenden (PostgreSQL-Syntax):

select * 
from my_table 
where id >= trunc(
  random() * (select max(id) from my_table) + 1
) 
order by id 
limit 1;
fl00r
quelle
11
Ihr erstes Beispiel funktioniert jedoch nicht in MySQL - die Syntax für MySQL lautet Thing.first (: order => "RAND ()") (eine Gefahr, SQL zu schreiben, anstatt die ActiveRecord-Abstraktionen zu verwenden)
DanSingerman
@ DanSingerman, ja es ist DB spezifisch RAND()oder RANDOM(). Danke
fl00r
Und dies führt nicht zu Problemen, wenn Elemente im Index fehlen? (Wenn etwas in der Mitte des Stapels gelöscht wird, besteht dann die Möglichkeit, dass es angefordert wird?
Victor S
@ VictorS, nein, es wird nicht #offset geht einfach zum nächsten verfügbaren Datensatz. Ich habe es mit Ruby 1.9.2 und Rails 3.1
SooDesuNe
1
@ JohnMerlino, ja 0 ist versetzt, nicht id. Offet 0 bedeutet erster Artikel gemäß Bestellung.
Fl00r
29

Ich arbeite an einem Projekt ( Rails 3.0.15, Ruby 1.9.3-p125-perf ), bei dem sich die Datenbank in localhost befindet und die Benutzertabelle etwas mehr als 100.000 Datensätze enthält .

Verwenden von

Bestellung per RAND ()

ist ziemlich langsam

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

wird

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

und dauert 8 bis 12 Sekunden zu antworten !!

Schienenprotokoll:

Benutzerlast (11030,8 ms) SELECT users. * FROM usersORDER BY RAND () LIMIT 1

von mysql erklären

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

Sie können sehen, dass kein Index verwendet wird ( simply_keys = NULL ), eine temporäre Tabelle erstellt wird und ein zusätzlicher Durchgang erforderlich ist, um den gewünschten Wert abzurufen ( extra = Temporär verwenden; Dateisortierung verwenden ).

Auf der anderen Seite haben wir durch die Aufteilung der Abfrage in zwei Teile und die Verwendung von Ruby eine angemessene Verbesserung der Antwortzeit.

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

(; Null für Konsolengebrauch)

Schienenprotokoll:

Benutzerlast (25,2 ms) SELECT id FROM usersBenutzerlast (0,2 ms) SELECT users. * FROM usersWHERE users. id= 106854 GRENZWERT 1

und mysqls erklären beweist warum:

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

Wir können jetzt nur noch Indizes und den Primärschlüssel verwenden und die Arbeit ungefähr 500 Mal schneller erledigen!

AKTUALISIEREN:

Wie von icantbecool in den Kommentaren hervorgehoben, weist die obige Lösung einen Fehler auf, wenn gelöschte Datensätze in der Tabelle enthalten sind.

Eine Problemumgehung kann sein

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

was zu zwei Abfragen übersetzt

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

und läuft in ca. 500ms.

xlembouras
quelle
Durch Hinzufügen von ".id" nach "last" zu Ihrem zweiten Beispiel wird der Fehler "Modell ohne ID konnte nicht gefunden werden" vermieden. ZB User.find (users.first (Random.rand (users.length)). Last.id)
turing_machine
Warnung! In MySQL RAND(id)wird nicht Ihnen eine andere zufällige Reihenfolge jede Abfrage geben. Verwenden Sie, RAND()wenn Sie eine andere Reihenfolge für jede Abfrage wünschen.
Justin Tanner
Die Datei User.find (users.first (Random.rand (users.length)). Last.id) funktioniert nicht, wenn ein Datensatz gelöscht wurde. [1,2,4,5,] und es könnte möglicherweise die ID 3 auswählen, aber es würde keine aktive Datensatzbeziehung geben.
icantbecool
Außerdem ist users = User.scoped.select (: id); nil ist nicht veraltet. Verwenden Sie stattdessen Folgendes: users = User.where (nil) .select (: id)
icantbecool
Ich glaube, dass die Verwendung von Random.rand (users.length) als erster Parameter ein Fehler ist. Random.rand kann 0 zurückgeben. Wenn 0 als Parameter für first verwendet wird, wird der Grenzwert auf Null gesetzt und dies gibt keine Datensätze zurück. Was man stattdessen verwenden sollte, ist 1 + Random (users.length) unter der Annahme, dass users.length> 0.
SWoo
12

Bei Verwendung von Postgres

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

Bei Verwendung von MySQL

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

In beiden Fällen wählen Sie 5 Datensätze zufällig aus der Benutzertabelle aus. Hier wird die aktuelle SQL-Abfrage in der Konsole angezeigt.

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

Ich habe dafür ein Rails 3-Juwel erstellt, das auf großen Tischen eine bessere Leistung erbringt und es Ihnen ermöglicht, Beziehungen und Bereiche zu verketten:

https://github.com/spilliton/randumb

(Bearbeiten): Das Standardverhalten meines Edelsteins verwendet im Grunde den gleichen Ansatz wie oben, aber Sie haben die Möglichkeit, den alten Weg zu verwenden, wenn Sie möchten :)

Spilliton
quelle
6

Viele der veröffentlichten Antworten funktionieren in ziemlich großen Tabellen (über 1 Million Zeilen) nicht gut. Die zufällige Bestellung dauert schnell einige Sekunden, und das Zählen auf dem Tisch dauert ebenfalls ziemlich lange.

Eine Lösung, die in dieser Situation für mich gut funktioniert, ist die Verwendung RANDOM()mit einer where-Bedingung:

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

In einer Tabelle mit über einer Million Zeilen dauert diese Abfrage im Allgemeinen weniger als 2 ms.

fünfstellig
quelle
Ein weiterer Vorteil Ihrer Lösung ist die Verwendung einer takeFunktion, die eine LIMIT(1)Abfrage ausgibt, jedoch ein einzelnes Element anstelle eines Arrays zurückgibt. So dass wir nicht aufrufen müssenfirst
Piotr Galas
Es scheint mir, dass Datensätze am Anfang der Tabelle eine höhere Wahrscheinlichkeit haben, auf diese Weise ausgewählt zu werden, was möglicherweise nicht das ist, was Sie erreichen möchten.
Gorn
5

Auf geht's

Schienen Weg

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

Verwendung

Model.random #returns single random object

oder der zweite Gedanke ist

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

Verwendung:

Model.random #returns shuffled collection
Tim Kretschmer
quelle
Couldn't find all Users with 'id': (first, {:offset=>1}) (found 0 results, but was looking for 2)
Bruno
Wenn es keine Benutzer gibt und Sie 2 erhalten möchten, erhalten Sie Fehler. Sinn ergeben.
Tim Kretschmer
1
Der zweite Ansatz funktioniert nicht mit Postgres, aber Sie können "RANDOM()"stattdessen verwenden ...
Daniel Richter
4

Das war sehr nützlich für mich, aber ich brauchte etwas mehr Flexibilität, also habe ich Folgendes getan:

Fall 1: Finden einer zufälligen Datensatzquelle : Trevor Turk Site
Fügen Sie diese zum Thing.rb-Modell hinzu

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

dann können Sie in Ihrem Controller so etwas aufrufen

@thing = Thing.random

Fall 2: Finden mehrerer zufälliger Datensätze (keine Wiederholungen) Quelle:
Ich kann mich nicht erinnern, dass ich 10 zufällige Datensätze ohne Wiederholungen suchen musste. Ich fand, dass dies
in Ihrem Controller funktioniert hat :

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

Dabei werden 10 zufällige Datensätze gefunden. Es ist jedoch erwähnenswert, dass eine besonders große Datenbank (Millionen von Datensätzen) nicht ideal ist und die Leistung beeinträchtigt wird. Es wird bis zu ein paar tausend Platten gut abschneiden, was für mich ausreichend war.

Hishalv
quelle
4

Die Ruby-Methode zum zufälligen Auswählen eines Elements aus einer Liste lautet sample. Um ein effizientes samplefür ActiveRecord zu erstellen und basierend auf den vorherigen Antworten, habe ich verwendet:

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

Ich lege dies ein lib/ext/sample.rbund lade es dann mit config/initializers/monkey_patches.rb:

Dir[Rails.root.join('lib/ext/*.rb')].each { |file| require file }
Dan Kohn
quelle
Eigentlich #countwird ein Aufruf an die DB für a COUNT. Wenn der Datensatz bereits geladen ist, ist dies möglicherweise eine schlechte Idee. #sizeStattdessen #countsollte ein Refactor verwendet werden, da er entscheidet, ob er verwendet werden soll oder, falls der Datensatz bereits geladen ist, verwendet werden soll #length.
BenMorganIO
Wechsel von countzu sizebasierend auf Ihrem Feedback. Weitere Infos unter: dev.mensfeld.pl/2014/09/…
Dan Kohn
3

Funktioniert in Rails 5 und ist DB-unabhängig:

Dies in Ihrem Controller:

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

Sie können dies natürlich in ein Problem bringen, wie hier gezeigt .

App / Modelle / Anliegen / 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

dann...

app / models / book.rb.

class Book < ActiveRecord::Base
  include Randomable
end

Dann können Sie einfach verwenden, indem Sie:

Books.random

oder

Books.random(3)
richardun
quelle
Dies erfordert immer nachfolgende Datensätze, die zumindest dokumentiert werden müssen (da dies möglicherweise nicht den Wünschen des Benutzers entspricht).
Gorn
2

Sie können sample () in ActiveRecord verwenden

Z.B

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

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

Trond
quelle
33
Dies ist eine sehr schlechte Abfrage, wenn Sie über eine große Anzahl von Datensätzen verfügen, da die Datenbank ALLE Datensätze auswählt und Rails fünf Datensätze daraus auswählt - massiv verschwenderisch.
DaveStephens
5
sampleist nicht in ActiveRecord, Beispiel ist in Array. api.rubyonrails.org/classes/Array.html#method-i-sample
Frans
3
Dies ist eine teure Methode, um eine zufällige Aufzeichnung zu erhalten, insbesondere von einem großen Tisch. Rails lädt ein Objekt für jeden Datensatz aus Ihrer Tabelle in den Speicher. Wenn Sie einen Beweis benötigen, führen Sie 'Rails Console' aus, versuchen Sie 'SomeModelFromYourApp.find (: all) .sample (5)' und sehen Sie sich das erzeugte SQL an.
Eliot Sykes
1
Siehe meine Antwort, die diese teure Antwort in eine optimierte Schönheit verwandelt, um mehrere zufällige Datensätze zu erhalten.
Arcolye
1

Bei Verwendung von Oracle

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

Ausgabe

SELECT * FROM users ORDER BY DBMS_RANDOM.VALUE WHERE ROWNUM <= 10
Marcelo Österreich
quelle
1

Empfehlen Sie dieses Juwel dringend für zufällige Datensätze, das speziell für Tabellen mit vielen Datenzeilen entwickelt wurde:

https://github.com/haopingfan/quick_random_records

Alle anderen Antworten funktionieren mit einer großen Datenbank schlecht, mit Ausnahme dieses Edelsteins:

  1. quick_random_records kosten nur 4.6mstotal.

Geben Sie hier die Bildbeschreibung ein

  1. die akzeptierte Antwort User.order('RAND()').limit(10)Kosten 733.0ms.

Geben Sie hier die Bildbeschreibung ein

  1. Der offsetAnsatz kostete 245.4mstotal.

Geben Sie hier die Bildbeschreibung ein

  1. die User.all.sample(10)Anflugkosten 573.4ms.

Geben Sie hier die Bildbeschreibung ein

Hinweis: Meine Tabelle hat nur 120.000 Benutzer. Je mehr Datensätze Sie haben, desto größer wird der Leistungsunterschied sein.


AKTUALISIEREN:

Führen Sie eine Tabelle mit 550.000 Zeilen durch

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

Geben Sie hier die Bildbeschreibung ein

  1. gem: quick_random_recordskostet nur 6.4mstotal

Geben Sie hier die Bildbeschreibung ein

Derek Fan
quelle
-2

Eine sehr einfache Möglichkeit, mehrere zufällige Datensätze aus der Tabelle abzurufen. Dies macht 2 billige Anfragen.

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

Sie können die "3" in die Anzahl der gewünschten zufälligen Datensätze ändern.

Arcolye
quelle
1
Nein, der Teil Model.pluck (: id) .sample (3) ist nicht billig. Es wird das ID-Feld für jedes Element in der Tabelle gelesen.
Maximiliano Guzman
Gibt es einen schnelleren datenbankunabhängigen Weg?
Arcolye
-5

Ich bin gerade auf dieses Problem gestoßen, als ich eine kleine Anwendung entwickelt habe, in der ich eine zufällige Frage aus meiner Datenbank auswählen wollte. Ich benutzte:

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

Und es funktioniert gut für mich. Ich kann nicht darüber sprechen, wie die Leistung für größere DBs ist, da dies nur eine kleine Anwendung ist.

Rails_Newbie
quelle
Ja, hier werden nur alle Ihre Datensätze abgerufen und Ruby-Array-Methoden verwendet. Der Nachteil dabei ist natürlich, dass Sie alle Ihre Datensätze in den Speicher laden, sie dann nach dem Zufallsprinzip neu anordnen und dann das zweite Element im neu geordneten Array auswählen. Das könnte definitiv ein Gedächtnisfresser sein, wenn Sie mit einem großen Datensatz zu tun haben. Nebenbei bemerkt, warum nicht das erste Element greifen? (dh shuffle[0])
Andrew
muss gemischt werden [0]
Marcelo Austria