Es un patrón bastante común, querer obtener uno (o más) objetos aleatorios de una tabla (modelo en Rails). Ayer mismo lo estuvimos hablando con Jaime en Twitter.
El primer impulso:
item = Model.find(:first, :order => "RAND()")
Y funcionar funciona, pero a nada que la tabla sea un poco grande, estás crujiendo la base de datos, básicamente porque cualquier consulta con ORDER BY que no pueda usar un índice (y obviamente esta no puede) lo hace. Le obligas a ordenar toda la tabla (en La Coctelera hay tablas con más de dos millones de filas) para darte una sola fila.
Este truquito lo recuerdo de mis tiempos PHPeros: miras el número de filas (n), generas un número aleatorio entre 0 y n-1, y usas ese offset para hacer una consulta sin ORDER BY (a.k.a. muy rápida) y con ese OFFSET. En Rails:
n = Model.count o = rand(n - 1) item = Model.find(:first, :limit => 1, :offset => o)
Si quieres más de uno, pues repetir. Muchos tienen que ser para que te salga más caro hacerlo así que con el RAND().
Como es un patrón relativamente común y es un poco feo andar haciendo esas cosas en los controladores, en un ratito que tuve ayer hice un mini-plugin para hacer este tipo de consultas (la verdad es que es tan mínimo que más que para un plugin daba para un pastie y gracias, pero los pasties no se testean y yo sin Caseratests no como
). Y nada, aquí está.

Muy buena, porras. Dani y yo estuvimos peleando el otro día por lo mismo.
En iwanna hicimos algo parecido pero para rellenar un array de n elementos, pero vamos, ya te imaginarás cómo fue.
Genial...
Había probado esto con :limit => 15 para coger 15 aleatorios pero claro, no es lo mismo, pq coges una franja consecutiva, no aleatoria. O sea que lo suyo es hacer 15 aleatorios individuales.
bufff, que placer da leer unos tests y poder entender como funciona algo
Aaaay! Que con las prisas no había leído que te has currado un plugin! Nuevamente gracias!
Las veces que he hecho un pequeño plugin como este, siempre me queda la siguiente duda:
¿Se notará alguna carencia en el rendimiento, si llenamos nuestra aplicación Rails de pequeños plugins como este, en lugar de dejarlo en una pequeña función en APP/lib ?
¿Alguna idea?
No es por meter cizaña, pero... ¿te has fijado que se pueden repetir elementos al seleccionar aleatoriamente y no controlar cuáles habías seleccionado antes?
Notese, que n = Model.count también ataca a la base de datos, y una consulta de este tipo en mysql/innodb en una tabla grande puede llevar minutos, con lo cual el problema de crujir la base de datos no esta resuelto. Aunque es verdad que un select count id siempre será menor en términos de complejidad a un order by (O(n) frente a como mínimo O(n ln(n)), no recuerdo exactamente la complejidad de algoritmos de ordenacion en bases de datos), creo que sigue sin ser una solución óptima.
La solución que propone Sergio debería de ser capaz de obtener el número de filas de la tabla en O(1), y de eso debería de encargarse rails o el sgbd, otra solución que se me ocurre es introducir desnormalizacion añadiendo un campo con el numero de elementos de la tabla.
@blat gracias majete ;)
@jaime sí, eso es lo que hace el plugin, pilla el parámetro mit pero por debajo los coge de uno en uno
@david con mocha es lo primerito que hago y creo que mola, de shoulda ya llevo un tiempito enamorado
otra vez @jaime jajaja qué bien escribo que los lectores se me saltan aquello de lo que va el post, voy a probar a ponerle un blink o algo :D
@gaizka pues yo entiendo que no, quiero decir, los plugins tampoco son "magia" es código ruby que se carga y ya está, no debería ser diferente a nivel rendimiento, consumo de memoria, etc. Pero vamos no tengo pruebas =;-) Lo que sí que es cierto es que instalar mogollón de plugins pequeñitos genera un problema de carga... en la cabeza del programador. Si te pasas es verdad que estás metiendo ahí una complejidad a nivel de búsqueda de errores, manejo de actualizaciones, etc. que probablemente no merezca la pena según los casos.
otra vez @blat sí que me había fijado sí, lo dejamos en los TODOs del plugin. en teoría es posible por ejemplo, según los vas sacando, ir almacenando en un array sus ids y asegurarte que no los vuelves a sacar con un id NOT IN (x, y). No sé si esto te haría perder parte de la mejora de rendimiento pero entiendo que al ser la clave primaria no. Quién sabe, habrá que probarlo =;-)
@therobot ZZZzzz No, en serio =:-D Ya sabes que yo controlo poco a tan bajo nivel (alguno se despollará pero para mí eso es bajo nivel), pero aunque no sea óptima siempre será mejor, métele un ORDER BY RAND() a esa tabla y puedes flipar, puedes escuchar la explosión del CPD desde tu silla =:-D Algo es algo. En cualquier caso, ahora que lo has dicho, sí recuerdo que alguna vez alguien me ha comentado el problema de los count en innodb, a mí (sólo por intuición) me sigue flipando.
no es por meter el dedo en la llaga pero he encontrado otro problema, el count se hace sin restricciones sobre toda la tabla con lo que el random puede darnos un offset superior al número de elementos que nos devuelve la consulta.
Um, cierto en algún caso. Está probado y funciona bien hacer esto:
user.posts.random(:first)
Gracias a la "magia" de Rails tanto el count como el find conservan el scope de la relación.
Pero efectivamente si le pasas directamente un :conditions, pues no:
Post.random(:first, :conditions => { :user_id => user.id })
Aunque creo que el arreglo es sencillo: bastará con pasarle ese :conditions también al count, esta misma tarde que tendré un rato, lo hago (si algún githubero se me adelanta que avise =;-) ).
Te referías a eso, ¿no David?
Por cierto, ni dedos ni llagas, el software libre funciona así y es guay =;-)
Si no lo hubiera publicado me hubiera pasado por alto ese error así que para eso se publica.
Nunca me habia parado a pensar como funciona el RAND() de mysql por debajo pero esa misma sugerencia la vi el otro dia en las transparencias de Obie Fernandez "The Worst Rails Code You've Ever Seen" de la RailsConf
Fixed!