En mi aplicación Rails, me encontré con un problema un par de veces que me gustaría saber cómo lo resuelven otras personas:
Tengo ciertos registros donde un valor es opcional, por lo que algunos registros tienen un valor y otros son nulos para esa columna.
Si ordeno por esa columna en algunas bases de datos, los nulos se ordenan primero y en algunas bases de datos los nulos se ordenan al final.
Por ejemplo, tengo fotos que pueden o no pertenecer a una colección, es decir, hay algunas fotos donde collection_id=nil
y otras donde, collection_id=1
etc.
Si lo hago Photo.order('collection_id desc)
, en SQLite obtengo los nulos al final, pero en PostgreSQL obtengo los nulos primero.
¿Existe una buena forma estándar de Rails para manejar esto y obtener un rendimiento constante en cualquier base de datos?
where
que no devuelve una matriz, devuelve una ActiveRecord :: Relation y forzar los resultados en una matriz hará que todo lo que espera una ActiveRecord :: Relation estándar falle (como la paginación).No soy un experto en SQL, pero ¿por qué no ordenar primero si algo es nulo y luego ordenarlo por cómo desea ordenarlo?
Photo.order('collection_id IS NULL, collection_id DESC') # Null's last Photo.order('collection_id IS NOT NULL, collection_id DESC') # Null's first
Si solo usa PostgreSQL, también puede hacer esto
Photo.order('collection_id DESC NULLS LAST') #Null's Last Photo.order('collection_id DESC NULLS FIRST') #Null's First
Si desea algo universal (como si estuviera usando la misma consulta en varias bases de datos, puede usar (cortesía de @philT)
Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
fuente
p = Photo.arel_table; Photo.order(p[:collection_id].eq(nil)).order(p[:collection_id].desc)
NULLS LAST
no le permite encadenar.last()
su consulta a partir de Rails 4.2. Publiqué una solución a continuación.IS NULL
poniendo las filas NULL al final? Pensarías que los pondría a ellos primero.A pesar de que estamos en 2017, todavía no existe un consenso sobre si las
NULL
s deben tener prioridad. Sin que sea explícito al respecto, sus resultados variarán según el DBMS.Para ilustrar el problema, compilé una lista de algunos de los casos más populares cuando se trata del desarrollo de Rails:
PostgreSQL
NULL
s tienen el valor más alto.MySQL
NULL
s tienen el valor más bajo.SQLite
NULL
s tienen el valor más bajo.Solución
Desafortunadamente, Rails todavía no ofrece una solución.
Específico de PostgreSQL
Para PostgreSQL, podría usar de manera bastante intuitiva:
Photo.order('collection_id DESC NULLS LAST') # NULLs come last
Específico de MySQL
Para MySQL, puede poner el signo menos al principio, pero esta función parece no estar documentada. Parece funcionar no solo con valores numéricos, sino también con fechas.
Photo.order('-collection_id DESC') # NULLs come last
PostgreSQL y MySQL específicos
Para cubrir ambos, esto parece funcionar:
Photo.order('collection_id IS NULL, collection_id DESC') # NULLs come last
Aún así, este no funciona en SQLite.
Solución universal
Para proporcionar soporte cruzado para todos los DBMS, tendría que escribir una consulta usando
CASE
, ya sugerido por @PhilIT:Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
lo que se traduce en ordenar primero cada uno de los registros primero por
CASE
resultados (en orden ascendente predeterminado, lo que significa que losNULL
valores serán los últimos), segundo porcalculation_id
.fuente
Photo.order('collection_id DESC NULLS LAST')
Sé que es antiguo, pero acabo de encontrar este fragmento y me funciona.
fuente
Ponga el signo menos delante de column_name e invierta la dirección del orden. Funciona en mysql. Más detalles
Product.order('something_date ASC') # NULLS came first Product.order('-something_date DESC') # NULLS came last
fuente
Un poco tarde para el programa, pero hay una forma genérica de SQL para hacerlo. Como de costumbre,
CASE
al rescate.Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
fuente
La forma más sencilla es utilizar:
.order('name nulls first')
fuente
last
parece insertar un DESC errante.Por el bien de la posteridad, quería resaltar un error de ActiveRecord relacionado con
NULLS FIRST
.Si intenta llamar:
Rails intentará llamar
reverse_order.first
, yreverse_order
no es compatible conNULLS LAST
, ya que intenta generar el SQL no válido:PG::SyntaxError: ERROR: syntax error at or near "DESC" LINE 1: ...dents" ORDER BY table_column DESC NULLS LAST DESC LIMIT...
Esto se mencionó hace unos años en algunos problemas de Rails aún abiertos ( uno , dos , tres ). Pude solucionarlo haciendo lo siguiente:
scope :nulls_first, -> { order("table_column IS NOT NULL") } scope :meaningfully_ordered, -> { nulls_first.order("table_column ASC") }
Parece que al encadenar las dos órdenes juntas, se genera SQL válido:
Model Load (12.0ms) SELECT "models".* FROM "models" ORDER BY table_column IS NULL DESC, table_column ASC LIMIT 1
El único inconveniente es que este encadenamiento debe realizarse para cada ámbito.
fuente
En mi caso, necesitaba ordenar las líneas por fecha de inicio y finalización por ASC, pero en algunos casos end_date era nulo y esas líneas deberían estar arriba, usé
@invoice.invoice_lines.order('start_date ASC, end_date ASC NULLS FIRST')
fuente
Parece que tendría que hacerlo en Ruby si desea resultados consistentes en todos los tipos de bases de datos, ya que la base de datos en sí interpreta si los NULLS van o no al principio o al final de la lista.
Photo.all.sort {|a, b| a.collection_id.to_i <=> b.collection_id.to_i}
Pero eso no es muy eficaz.
fuente