Rails:最后以空值排序
在我的 Rails 应用程序中,我遇到了几次问题,我想知道其他人如何解决:
我有某些记录,其中值是可选的,因此有些记录具有值,有些记录对该列为空。
如果我在某些数据库上按该列排序,则空值首先排序,而在某些数据库上,空值最后排序。
例如,我的照片可能属于也可能不属于某个集合,即有些照片 collection_id=nil
,有些照片 collection_id=1
等。
如果我这样做Photo.order('collection_id desc)
然后在 SQLite 上我最后得到空值,但在 PostgreSQL 上我首先得到空值。
有没有一种好的、标准的 Rails 方法来处理这个问题并在任何数据库中获得一致的性能?
In my Rails app I've run into an issue a couple times that I'd like to know how other people solve:
I have certain records where a value is optional, so some records have a value and some are null for that column.
If I order by that column on some databases the nulls sort first and on some databases the nulls sort last.
For instance, I have Photos which may or may not belong to a Collection, ie there are some Photos where collection_id=nil
and some where collection_id=1
etc.
If I do Photo.order('collection_id desc)
then on SQLite I get the nulls last but on PostgreSQL I get the nulls first.
Is there a nice, standard Rails way to handle this and get consistent performance across any database?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(12)
尽管现在已经是 2017 年了,但对于
NULL
是否应该优先尚未达成共识。如果您没有明确说明,您的结果将根据 DBMS 的不同而有所不同。为了说明这个问题,我编制了一个列表在 Rails 开发中,有几个最常见的情况:
PostgreSQL
NULL
具有最高的值。MySQL
NULL
s 具有最低值。SQLite
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 中不起作用。
通用解决方案
要为所有 DBMS 提供交叉支持,您必须使用
CASE
编写查询,@PhilIT 已经建议:Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
转换为首先按
CASE
结果对每条记录进行排序(默认升序,这意味着NULL
值将是最后一个),第二个是calculation_id
。Even though it's 2017 now, there is still yet to be a consensus on whether
NULL
s should take precedence. Without you being explicit about it, your results are going to vary depending on the DBMS.To illustrate the problem, I compiled a list of a few most popular cases when it comes to Rails development:
PostgreSQL
NULL
s have the highest value.MySQL
NULL
s have the lowest value.SQLite
NULL
s have the lowest value.Solution
Unfortunately, Rails itself doesn't provide a solution for it yet.
PostgreSQL specific
For PostgreSQL you could quite intuitively use:
Photo.order('collection_id DESC NULLS LAST') # NULLs come last
MySQL specific
For MySQL, you could put the minus sign upfront, yet this feature seems to be undocumented. Appears to work not only with numerical values, but with dates as well.
Photo.order('-collection_id DESC') # NULLs come last
PostgreSQL and MySQL specific
To cover both of them, this appears to work:
Photo.order('collection_id IS NULL, collection_id DESC') # NULLs come last
Still, this one does not work in SQLite.
Universal solution
To provide cross-support for all DBMSs you'd have to write a query using
CASE
, already suggested by @PhilIT:Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
which translates to first sorting each of the records first by
CASE
results (by default ascending order, which meansNULL
values will be the last ones), second bycalculation_id
.在column_name前面加上减号并反转顺序方向。它适用于 mysql。 更多详细信息
Put minus sign in front of column_name and reverse the order direction. It works on mysql. More details
我知道这是旧的,但我刚刚找到了这个片段,它对我有用。
I know this is an old one but I just found this snippet and it works for me.
虽然有点晚了,但有一个通用的 SQL 方法可以做到这一点。像往常一样,
CASE
来救援。Bit late to the show but there is a generic SQL way to do it. As usual,
CASE
to the rescue.Rails 6.1 向 Arel for PostgreSQL 添加了 nulls_first 和 nulls_last 方法。
示例:
来源:https ://www.bigbinary.com/blog/rails-6-1-adds-nulls-first-and-nulls-last-to-arel
Rails 6.1 adds nulls_first and nulls_last methods to Arel for PostgreSQL.
Example:
Source: https://www.bigbinary.com/blog/rails-6-1-adds-nulls-first-and-nulls-last-to-arel
最简单的方法是使用:
.order('name nulls first')
The easiest way is to use:
.order('name nulls first')
为了后代的利益,我想强调与
NULLS FIRST
相关的 ActiveRecord 错误。如果您尝试调用:
Rails 将尝试调用
reverse_order.first
,并且reverse_order
与NULLS LAST
不兼容,因为它尝试生成无效的 SQL:几年前在一些仍然悬而未决的 Rails 问题中引用了这一点(一个, 两个,
看来,通过将两个订单链接在一起,可以生成有效的 SQL:
唯一的缺点是必须针对每个范围完成此链接。
For posterity's sake, I wanted to highlight an ActiveRecord error relating to
NULLS FIRST
.If you try to call:
Rails will attempt to call
reverse_order.first
, andreverse_order
is not compatible withNULLS LAST
, as it tries to generate the invalid SQL:This was referenced a few years ago in some still-open Rails issues (one, two, three). I was able to work around it by doing the following:
It appears that by chaining the two orders together, valid SQL gets generated:
The only downside is that this chaining has to be done for each scope.
以下是一些 Rails 6 解决方案。
@Adam Sibik 的答案很好地总结了各种数据库系统之间的差异。
但不幸的是,一些提出的解决方案,包括“通用解决方案”和“PostgreSQL 和 MySQL 特定的”,将不再适用于 Rails 6 (ActiveRecord 6),因为这是因为
order()
规范发生变化,不接受某些原始 SQL(我确认“PostgreSQL 特定”解决方案在 Rails 6.1.4 中仍然有效)。有关此更改的背景,请参阅例如"Rails 6.1 中 SQL 注入的更新”贾斯汀。
为了避免这个问题,您可以使用
来封装 SQL 语句Arel.sql
如下,其中 NULL 位于最后,假设您 100% 确定您给出的 SQL 语句是安全的。仅供参考,如果您想按布尔列(例如
is_ok
)按 [TRUE、FALSE、NULL] 的顺序排序,而不管数据库系统如何,则以下任一方法都可以:(注意,SQLite 没有 Boolean 类型,因此前者可能更安全,尽管这并不重要,因为 Rails 应该保证该值为 0 或 1(或 NULL)。)
Here are some Rails 6 solutions.
The answer by @Adam Sibik is a great summary about the difference between various database systems.
Unfortunately, though, some of the presented solutions, including "Universal solution" and "PostgreSQL and MySQL specific", would not work any more with Rails 6 (ActiveRecord 6) as a result of its changed specification of
order()
not accepting some raw SQLs (I confirm the "PostgreSQL specific" solution still works as of Rails 6.1.4). For the background of this change, see, for example,"Updates for SQL Injection in Rails 6.1" by Justin.
To circumvent the problem, you can wrap around the SQL statements with
Arel.sql
as follows, where NULLs come last, providing you are 100% sure the SQL statements you give are safe.Just for reference, if you want to sort by a Boolean column (
is_ok
, as an example) in the order of [TRUE, FALSE, NULL] regardless of the database systems, either of these should work:(n.b., SQLite does not have the Boolean type and so the former may be safer arguably, though it should not matter because Rails should guarantee the value is either 0 or 1 (or NULL).)
就我而言,我需要按 ASC 按开始日期和结束日期对行进行排序,但在少数情况下 end_date 为空,并且该行应该在上面,我使用
@invoice.invoice_lines.order('start_date ASC, end_date ASC NULLS FIRST ')
In my case I needed sort lines by start and end date by ASC, but in few cases end_date was null and that lines should be in above, I used
@invoice.invoice_lines.order('start_date ASC, end_date ASC NULLS FIRST')
将数组添加在一起将保留顺序:
http://www.ruby-doc .org/core/classes/Array.html#M000271
Adding arrays together will preserve order:
http://www.ruby-doc.org/core/classes/Array.html#M000271
如果您希望跨数据库类型获得一致的结果,似乎您必须在 Ruby 中执行此操作,因为数据库本身会解释 NULL 是否位于列表的前端或末尾。
但这效率不高。
It seems like you'd have to do it in Ruby if you want consistent results across database types, as the database itself interprets whether or not the NULLS go at the front or end of the list.
But that is not very efficient.
我不是 SQL 专家,但为什么不先按是否为 null 进行排序,然后再按您想要的方式对其进行排序。
如果您只使用 PostgreSQL,您也可以这样做
如果您想要通用的东西(比如您在多个数据库中使用相同的查询,您可以使用 (由@philT提供)
I'm no expert at SQL, but why not just sort by if something is null first then sort by how you wanted to sort it.
If you are only using PostgreSQL, you can also do this
If you want something universal (like you're using the same query across several databases, you can use (courtesy of @philT)