Publicidad:
La Coctelera

Sugerencia de presentación

He nacido para vago

13 Junio 2008

Obtener una fila aleatoria de una tabla en Rails

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á.

servido por porras 13 comentarios compártelo

13 comentarios · Escribe aquí tu comentario

blat

blat dijo

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.

13 Junio 2008 | 10:58

Jaime Iniesta

Jaime Iniesta dijo

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.

13 Junio 2008 | 11:47

David Calavera

David Calavera dijo

bufff, que placer da leer unos tests y poder entender como funciona algo

13 Junio 2008 | 12:21

Jaime Iniesta

Jaime Iniesta dijo

Aaaay! Que con las prisas no había leído que te has currado un plugin! Nuevamente gracias!

13 Junio 2008 | 12:24

Gaizka

Gaizka dijo

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?

13 Junio 2008 | 12:27

blat

blat dijo

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?

13 Junio 2008 | 12:27

The Robot

The Robot dijo

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.

13 Junio 2008 | 12:44

porras

porras dijo

@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.

13 Junio 2008 | 01:01

David Calavera

David Calavera dijo

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.

13 Junio 2008 | 02:14

porras

porras dijo

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?

13 Junio 2008 | 02:28

porras

porras dijo

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.

13 Junio 2008 | 02:29

Victor

Victor dijo

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

13 Junio 2008 | 02:49

Escribe tu comentario


Sobre mí

Avatar de porras

Sugerencia de presentación

ver perfil »
contacto »

Me llamo Sergio Gil Pérez de la Manga, y mi madre se cabrea si escribo mi nombre con un sólo apellido. Vivo, trabajo y hago casi todo lo demás en Madrid.

Trabajo como programador porque es lo más parecido que he encontrado a no trabajar. Sobre todo si lo haces bien. Y en eso estoy, en hacerlo cada vez mejor para trabajar cada vez menos. Alguno lo llamaría vagancia, y yo ahí no me meto.

Algunas de las herramientas que en este momento me llevan al Nirvana de no dar un palo al agua son Ruby, Ruby on Rails, Textmate, cualquier sabor de Unix (en este momento principalmente MacOSX pero también Ubuntu Linux) y sus herramientas, o Rake. En ocasiones hablo de ellas aquí, pienso que a alguien le pueden servir y que no puedo ser el único al que no le gusta trabajar.

Y como no sólo de tecnología vive el hombre (bueno, el hombre no sé, pero desde luego yo no), por aquí aparece de vez en cuando la punta del iceberg de mis pequeños pensamientos; al menos la parte de ellos que no cabe en Twitter.

Bienvenidos todos.

Y ahora: ¿Y tú?

Fotos

porras todavía no ha subido ninguna foto.

¡Anímale a hacerlo!

Buscar

suscríbete

Selecciona el agregador que utilices para suscribirte a este blog (también puedes obtener la URL de los feeds):

¿Qué es esto?

Crea tu blog gratis en La Coctelera