Rails: заказ с последними нулями

83

В моем приложении Rails я пару раз сталкивался с проблемой, которую мне хотелось бы знать, как ее решают другие:

У меня есть определенные записи, в которых значение не является обязательным, поэтому некоторые записи имеют значение, а некоторые - null для этого столбца.

Если я упорядочу по этому столбцу в некоторых базах данных, сначала сортируются нули, а в некоторых базах данных - последним.

Например, у меня есть фотографии, которые могут принадлежать или не принадлежать к Коллекции, т.е. есть фотографии где- collection_id=nilто, где-то collection_id=1и т.

Если я это сделаю, Photo.order('collection_id desc)то в SQLite я получаю нули последними, но в PostgreSQL я сначала получаю нули.

Есть ли хороший стандартный способ Rails справиться с этим и добиться стабильной производительности в любой базе данных?

Андрей
источник

Ответы:

0

Добавление массивов вместе сохранит порядок:

@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
Что ж, мне не нравится эта идея, но я думаю, что это сработает. Извините, что оставил его открытым так долго, я надеялся, что появятся другие ответы. Потратив некоторое время на размышления об этом, я думаю, что это можно было бы превратить в метод на модели Photo, и тогда это было бы не так уж плохо.
Эндрю
Это просто, если вы используете mysql. Смотрите мое решение.
Джейкоб
Правильно, я должен был упомянуть, что мой был только самым агностическим способом, который я мог найти.
Эрик
8
Это плохая идея, поскольку whereон не возвращает массив, он возвращает ActiveRecord :: Relation, а принудительное занесение результатов в массив приведет к сбою всего, что ожидает стандартного ActiveRecord :: Relation (например, разбиения на страницы).
Майк Бетани
1
Верно, хотя кажется, что доступные методы либо выходят за рамки AR, либо (полностью) не переносимы.
Эрик
273

Я не эксперт в SQL, но почему бы просто не отсортировать, если что-то сначала имеет значение null, а затем отсортировать по тому, как вы хотели его отсортировать.

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

Если вы используете только PostgreSQL, вы также можете сделать это

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

Если вам нужно что-то универсальное (например, вы используете один и тот же запрос в нескольких базах данных, вы можете использовать (любезно предоставлено @philT)

Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
Намерения
источник
22
+1, намного лучше, чем принятый ответ, и может быть выражен через Arel
m_x
1
вау, это старый комментарий! Итак, я предполагаю, что я пытался сказать:p = Photo.arel_table; Photo.order(p[:collection_id].eq(nil)).order(p[:collection_id].desc)
m_x
2
Обратите внимание, что в Rails 4.2 NULLS LASTэто не позволяет вам подключаться .last()к вашему запросу. Я опубликовал обходной путь ниже.
Лэнни Боз,
1
Почему при упорядочивании IS NULLстроки NULL помещаются в последнюю очередь? Можно подумать, это поставит их на первое место.
jackocnr
2
@jackocr myguess: IS NULL оценивается как 1, если истина, 0, если ложь, отсортировано, 0 предшествует 1 ...
Intentss
37

Несмотря на то, что сейчас 2017 год, до сих пор нет единого мнения о том, NULLдолжны ли s иметь приоритет. Если вы не говорите об этом явно, ваши результаты будут зависеть от СУБД.

Стандарт не определяет, как должны быть упорядочены значения NULL по сравнению со значениями, отличными от NULL, за исключением того, что любые два значения NULL должны считаться одинаково упорядоченными и что значения NULL должны сортироваться либо выше, либо ниже всех значений, отличных от NULL.

источник, сравнение большинства СУБД

Чтобы проиллюстрировать проблему, я составил список из нескольких наиболее популярных случаев, когда дело доходит до разработки на Rails:

PostgreSQL

NULLs имеют наибольшее значение.

По умолчанию значения NULL сортируются так, как если бы они были больше любого ненулевого значения.

источник: документация PostgreSQL

MySQL

NULLs имеют наименьшее значение.

При выполнении ORDER BY значения NULL отображаются первыми, если вы выполняете ORDER BY ... ASC, и последними, если вы выполняете ORDER BY ... DESC.

источник: документация MySQL

SQLite

NULLs имеют наименьшее значение.

Строка со значением NULL в порядке возрастания выше, чем строки с обычными значениями, а в порядке убывания она перевёрнута.

источник

Решение

К сожалению, сам Rails пока не предлагает решения для этого.

Специфика PostgreSQL

Для PostgreSQL вы можете довольно интуитивно использовать:

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

Специфичный для MySQL

Для MySQL вы можете поставить знак минус вперед, но эта функция, похоже, недокументирована. Оказывается, работает не только с числовыми значениями, но и с датами.

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

Только для PostgreSQL и MySQL

Чтобы охватить их обоих, это, похоже, работает:

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

Тем не менее, это не работает в SQLite.

Универсальное решение

Чтобы обеспечить кросс-поддержку для всех СУБД, вам нужно будет написать запрос, используя CASEуже предложенный @PhilIT:

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

что переводится как первая сортировка каждой записи сначала по CASEрезультатам (по умолчанию в порядке возрастания, что означает, что NULLзначения будут последними), а затем по calculation_id.

Адам Сибик
источник
14
Photo.order('collection_id DESC NULLS LAST')

Я знаю, что это старый, но я только что нашел этот фрагмент, и он мне подходит.

Томас Янси
источник
Я не знаю, в какой версии Ruby / Rails это работало, но для Ruby 2.5 и rails 5, похоже, не работает.
Tasos Anesiadis
13

Поместите знак минус перед column_name и измените направление порядка. Работает на mysql. Подробнее

Product.order('something_date ASC') # NULLS came first
Product.order('-something_date DESC') # NULLS came last
Раймондралиби
источник
10

Немного поздно на шоу, но есть общий способ SQL. Как обычно, CASEна помощь.

Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
PhilT
источник
6

Самый простой способ - использовать:

.order('name nulls first')

Жан-Этьен Дюран
источник
3
Это лучше всего для
Постгреса
1
Это хорошее решение, но имейте в виду, что ActiveRecord, lastкажется, вставляет ошибочный DESC.
sirvine
6

Ради потомков я хотел выделить ошибку ActiveRecord, связанную с NULLS FIRST .

Если вы попытаетесь позвонить:

Model.scope_with_nulls_first.last

Rails попытается вызвать reverse_order.first, reverse_orderно несовместимо с NULLS LAST, поскольку пытается сгенерировать недопустимый SQL:

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

На это ссылались несколько лет назад в некоторых все еще нерешенных выпусках Rails ( один , два , три ). Я смог обойти это, выполнив следующие действия:

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

Похоже, что путем объединения двух заказов вместе создается действительный SQL:

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

Единственным недостатком является то, что эту цепочку нужно выполнять для каждой области.

Лэнни Боз
источник
2

В моем случае мне нужны были строки сортировки по дате начала и окончания по ASC, но в некоторых случаях end_date был нулевым, и эти строки должны быть выше, я использовал

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

Дмитрий Гусев
источник
-3

Похоже, вам придется сделать это в Ruby, если вы хотите получить согласованные результаты для разных типов баз данных, поскольку сама база данных интерпретирует, идут ли NULL в начале или в конце списка.

Photo.all.sort {|a, b| a.collection_id.to_i <=> b.collection_id.to_i}

Но это не очень эффективно.

Джаредонлайн
источник