Carriles: Orden con nulos al final

83

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=nily otras donde, collection_id=1etc.

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?

Andrés
fuente

Respuestas:

0

Agregar matrices juntas preservará el orden:

@nonull = Photo.where("collection_id is not null").order("collection_id desc")
@yesnull = Photo.where("collection_id is null")
@wanted = @nonull+@yesnull

http://www.ruby-doc.org/core/classes/Array.html#M000271

Eric
fuente
2
Bueno, no me encanta esta idea, pero creo que funcionaría. Lamento haberlo dejado abierto tanto tiempo, esperaba que aparecieran otras respuestas. Sin embargo, habiendo pasado más tiempo pensando en ello, creo que esto podría convertirse en un método en el modelo Photo y luego no se sentiría tan mal.
Andrew
Es fácil si usa mysql. Vea mi solución.
Jacob
Bien, debería haber mencionado que la mía era solo la forma más agnóstica que pude encontrar.
Eric
8
Esta es una mala idea ya whereque 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).
Mike Bethany
1
Es cierto, aunque parece que los métodos disponibles se salen de la RA o no son (completamente) portátiles.
Eric
270

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')
Intenciones
fuente
22
+1, mucho mejor que la respuesta aceptada y se puede expresar a través de Arel
m_x
1
¡Guau, este es un comentario antiguo! Así que supongo que lo que intentaba decir era:p = Photo.arel_table; Photo.order(p[:collection_id].eq(nil)).order(p[:collection_id].desc)
m_x
2
Tenga en cuenta que NULLS LASTno le permite encadenar .last()su consulta a partir de Rails 4.2. Publiqué una solución a continuación.
Lanny Bose
1
¿Por qué ordenar IS NULLponiendo las filas NULL al final? Pensarías que los pondría a ellos primero.
jackocnr
2
@jackocr myguess: IS NULL evalúa a 1 si es verdadero, 0 si es falso, ordenado, 0 viene antes de 1 ...
Intentss
37

A pesar de que estamos en 2017, todavía no existe un consenso sobre si las NULLs deben tener prioridad. Sin que sea explícito al respecto, sus resultados variarán según el DBMS.

El estándar no especifica cómo deben ordenarse los NULL en comparación con los valores no NULL, excepto que dos NULL cualesquiera deben considerarse igualmente ordenados, y que los NULL deben ordenarse por encima o por debajo de todos los valores no NULL.

fuente, comparación de la mayoría de DBMS

Para ilustrar el problema, compilé una lista de algunos de los casos más populares cuando se trata del desarrollo de Rails:

PostgreSQL

NULLs tienen el valor más alto.

De forma predeterminada, los valores nulos se ordenan como si fueran más grandes que cualquier valor no nulo.

fuente: documentación de PostgreSQL

MySQL

NULLs tienen el valor más bajo.

Al hacer un ORDER BY, los valores NULL se presentan primero si hace ORDER BY ... ASC y por último si hace ORDER BY ... DESC.

fuente: documentación de MySQL

SQLite

NULLs tienen el valor más bajo.

Una fila con un valor NULL es más alta que las filas con valores regulares en orden ascendente y se invierte en orden descendente.

fuente

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 CASEresultados (en orden ascendente predeterminado, lo que significa que los NULLvalores serán los últimos), segundo por calculation_id.

Adam Sibik
fuente
14
Photo.order('collection_id DESC NULLS LAST')

Sé que es antiguo, pero acabo de encontrar este fragmento y me funciona.

Thomas Yancey
fuente
No sé en qué versión de Ruby / Rails funcionó esto, pero para ruby ​​2.5 y rails 5 no parece funcionar.
Tasos Anesiadis
13

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
raymondralibi
fuente
10

Un poco tarde para el programa, pero hay una forma genérica de SQL para hacerlo. Como de costumbre, CASEal rescate.

Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
PhilT
fuente
6

La forma más sencilla es utilizar:

.order('name nulls first')

Jean-Etienne Durand
fuente
3
Esto es lo mejor para Postgres
Sergii Mostovyi
1
Esta es una buena solución, pero tenga en cuenta que ActiveRecord lastparece insertar un DESC errante.
sirvine
6

Por el bien de la posteridad, quería resaltar un error de ActiveRecord relacionado con NULLS FIRST .

Si intenta llamar:

Model.scope_with_nulls_first.last

Rails intentará llamar reverse_order.first, y reverse_orderno es compatible con NULLS 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.

Lanny Bose
fuente
2

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')

Dmitriy Gusev
fuente
-3

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.

jaredonline
fuente