web-dev-qa-db-fra.com

Rails: Ordre avec nulls last

Dans mon application Rails, j'ai rencontré plusieurs fois un problème que j'aimerais savoir comment les autres résolvent:

J'ai certains enregistrements où une valeur est facultative, donc certains enregistrements ont une valeur et certains sont nuls pour cette colonne.

Si je commande par cette colonne sur certaines bases de données, les nulls trient en premier et sur certaines bases de données, les nulls trient en dernier.

Par exemple, j'ai des photos qui peuvent appartenir ou non à une collection, c'est-à-dire qu'il y a des photos où collection_id=nil Et d'autres où collection_id=1 Etc.

Si je fais Photo.order('collection_id desc) alors sur SQLite j'obtiens les nuls en dernier mais sur PostgreSQL j'obtiens les nuls en premier.

Existe-t-il un moyen standard Rails pour gérer cela et obtenir des performances cohérentes dans n'importe quelle base de données?

75
Andrew

Ajouter des tableaux ensemble préservera l'ordre:

@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

2
Eric

Je ne suis pas un expert en SQL, mais pourquoi ne pas simplement trier par si quelque chose est nul d'abord, puis trier par la façon dont vous vouliez le trier.

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 vous utilisez uniquement PostgreSQL, vous pouvez également le faire

Photo.order('collection_id DESC NULLS LAST')  #Null's Last
Photo.order('collection_id DESC NULLS FIRST') #Null's First

Si vous voulez quelque chose d'universel (comme si vous utilisez la même requête sur plusieurs bases de données, vous pouvez l'utiliser (gracieuseté de @philT)

Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
238
Intentss

Même si c'est maintenant 2017, il n'y a toujours pas de consensus sur la question de savoir si NULLs doit avoir la priorité. Sans que vous soyez explicite à ce sujet, vos résultats vont varier en fonction du SGBD.

La norme ne spécifie pas comment les NULL doivent être ordonnés par rapport aux valeurs non NULL, sauf que deux NULL doivent être considérés de manière égale et que les NULL doivent trier au-dessus ou au-dessous de toutes les valeurs non NULL.

source, comparaison de la plupart des SGBD

Pour illustrer le problème, j'ai compilé une liste de quelques cas les plus populaires en matière de développement Rails:

PostgreSQL

NULLs ont la valeur la plus élevée.

Par défaut, les valeurs nulles sont triées comme si elles étaient supérieures à toute valeur non nulle.

source: documentation PostgreSQL

MySQL

NULLs ont la valeur la plus basse.

Lorsque vous effectuez un ORDER BY, les valeurs NULL sont présentées en premier si vous effectuez ORDER BY ... ASC et en dernier si vous effectuez ORDER BY ... DESC.

source: documentation MySQL

SQLite

NULLs ont la valeur la plus basse.

Une ligne avec une valeur NULL est supérieure aux lignes avec des valeurs régulières dans l'ordre croissant et elle est inversée pour l'ordre décroissant.

source

Solution

Malheureusement, Rails lui-même ne fournit pas encore de solution pour cela.

Spécifique à PostgreSQL

Pour PostgreSQL, vous pouvez utiliser de manière assez intuitive:

Photo.order('collection_id DESC NULLS LAST') # NULLs come last

Spécifique à MySQL

Pour MySQL, vous pouvez mettre le signe moins à l'avance, mais cette fonctionnalité semble être non documentée. Semble fonctionner non seulement avec des valeurs numériques, mais aussi avec des dates.

Photo.order('-collection_id DESC') # NULLs come last

Spécifique à PostgreSQL et MySQL

Pour couvrir les deux, cela semble fonctionner:

Photo.order('collection_id IS NULL, collection_id DESC') # NULLs come last

Pourtant, celui-ci ne fonctionne pas dans SQLite.

Solution universelle

Pour fournir une prise en charge croisée de tous les SGBD, vous devez écrire une requête à l'aide de CASE, déjà suggérée par @PhilIT:

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

ce qui se traduit par un premier tri de chacun des enregistrements d'abord par CASE résultats (par ordre croissant par défaut, ce qui signifie que NULL les valeurs seront les dernières), ensuite par calculation_id.

31
Adam Sibik

Mettez signe moins devant nom_colonne et inversez le sens de l'ordre. Cela fonctionne sur mysql. Plus de détails

Product.order('something_date ASC') # NULLS came first
Product.order('-something_date DESC') # NULLS came last
12
raymondralibi
Photo.order('collection_id DESC NULLS LAST')

Je sais que c'est un ancien mais je viens de trouver cet extrait et cela fonctionne pour moi.

12
Thomas Yancey

Un peu tard pour le spectacle, mais il existe un moyen générique SQL de le faire. Comme d'habitude, CASE à la rescousse.

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

Pour la postérité, je voulais souligner une erreur ActiveRecord relative à NULLS FIRST.

Si vous essayez d'appeler:

Model.scope_with_nulls_first.last

Rails tentera d'appeler reverse_order.first, et reverse_order n'est pas compatible avec NULLS LAST, car il essaie de générer le SQL non valide:

PG::SyntaxError: ERROR:  syntax error at or near "DESC"
LINE 1: ...dents"  ORDER BY table_column DESC NULLS LAST DESC LIMIT...

Cela a été référencé il y a quelques années dans certains problèmes encore ouverts Rails ( one , two , three J'ai pu contourner ce problème en procédant comme suit:

  scope :nulls_first, -> { order("table_column IS NOT NULL") }
  scope :meaningfully_ordered, -> { nulls_first.order("table_column ASC") }

Il semble qu'en chaînant les deux commandes ensemble, un SQL valide soit généré:

Model Load (12.0ms)  SELECT  "models".* FROM "models"  ORDER BY table_column IS NULL DESC, table_column ASC LIMIT 1

Le seul inconvénient est que ce chaînage doit être fait pour chaque portée.

6
Lanny Bose

La façon la plus simple est d'utiliser:

.order('name nulls first')

5
Jean-Etienne Durand

Dans mon cas, j'avais besoin de trier les lignes par date de début et de fin par ASC, mais dans quelques cas, end_date était nul et que les lignes devraient être au-dessus, j'ai utilisé

@invoice.invoice_lines.order('start_date ASC, end_date ASC NULLS FIRST')

1
Dmitriy Gusev