【问题标题】:Rails: Order with nulls lastRails:最后以空值排序
【发布时间】:2011-08-15 03:14:39
【问题描述】:

在我的 Rails 应用程序中,我遇到过几次问题,我想知道其他人是如何解决的:

我有一些记录的值是可选的,所以一些记录有一个值,而一些记录对于该列是空的。

如果我在某些数据库上按该列排序,则空值首先排序,而在某些数据库中,空值最后排序。

例如,我的照片可能属于也可能不属于某个收藏,即有一些照片在collection_id=nil 和一些在collection_id=1 等位置。

如果我执行Photo.order('collection_id desc),那么在 SQLite 上我最后得到空值,但在 PostgreSQL 上我首先得到空值。

有没有一种很好的标准 Rails 方法来处理这个问题并在任何数据库中获得一致的性能?

【问题讨论】:

    标签: ruby-on-rails ruby-on-rails-3 sorting activerecord null


    【解决方案1】:

    我不是 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')
    

    【讨论】:

    • +1,比公认的答案好很多,可以通过Arel来表达
    • 哇,这是一条旧评论!所以我想我想说的是:p = Photo.arel_table; Photo.order(p[:collection_id].eq(nil)).order(p[:collection_id].desc)
    • 请注意,从 Rails 4.2 开始,NULLS LAST 不允许您将 .last() 链接到查询上。我在下面发布了一个解决方法。
    • 为什么按IS NULL 排序将NULL 行放在最后?你会认为它会把它们放在第一位。
    • @jackocr myguess: IS NULL 如果为真,则为 1,如果为假,则为 0,已排序,0 在 1 之前...
    【解决方案2】:

    尽管现在是 2017 年,但对于 NULLs 是否应该优先考虑,还没有达成共识。如果您没有明确说明,您的结果将因 DBMS 而异。

    该标准没有指定 NULL 与非 NULL 值相比应如何排序,但任何两个 NULL 都应被视为同等排序,并且 NULL 应在所有非 NULL 值的上方或下方排序。

    source, comparison of most DBMSs

    为了说明问题,我整理了几个 Rails 开发中最流行的案例:

    PostgreSQL

    NULLs 的值最高。

    默认情况下,空值排序为大于任何非空值。

    source: PostgreSQL documentation

    MySQL

    NULLs 的值最低。

    在执行 ORDER BY 时,如果执行 ORDER BY ... ASC,则首先显示 NULL 值,如果执行 ORDER BY ... DESC,则最后显示 NULL 值。

    source: MySQL documentation

    SQLite

    NULLs 的值最低。

    NULL值的行升序高于常规值的行,降序倒序。

    source

    解决方案

    不幸的是,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 提供交叉支持,您必须使用 @PhilIT 建议的 CASE 编写查询:

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

    这意味着首先按CASE结果对每条记录进行排序(默认升序,这意味着NULL的值将是最后一个),其次是calculation_id

    【讨论】:

    • ActiveRecord 6 似乎不再允许原始 sql,如上面的“通用解决方案”中所示。我很想知道如何在 Rails 6 中做到这一点。
    • @LesNightingill 没错。总之,使用Arel.sql来规避问题。我刚刚为 Rails 6 添加了答案。
    【解决方案3】:
    Photo.order('collection_id DESC NULLS LAST')
    

    我知道这是一个旧的,但我刚刚找到了这个 sn-p,它对我有用。

    【讨论】:

    • 我不知道这在哪个版本的 Ruby/Rails 中有效,但对于 ruby​​ 2.5 和 rails 5 似乎不起作用。
    【解决方案4】:

    在column_name前面加上减号,颠倒顺序。它适用于mysql。 More details

    Product.order('something_date ASC') # NULLS came first
    Product.order('-something_date DESC') # NULLS came last
    

    【讨论】:

      【解决方案5】:

      演出有点晚了,但有一种通用的 SQL 方法可以做到这一点。像往常一样,CASE 来救援。

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

      【讨论】:

        【解决方案6】:

        最简单的方法是使用:

        .order('name nulls first')

        【讨论】:

        • 这对 Postgres 来说是最好的
        • 这是一个很好的解决方案,但要注意 ActiveRecord last 似乎插入了错误的 DESC。
        【解决方案7】:

        为了子孙后代,我想强调一个与 NULLS FIRST 相关的 ActiveRecord 错误。

        如果您尝试调用:

        Model.scope_with_nulls_first.last
        

        Rails 会尝试调用reverse_order.first,而reverse_orderNULLS LAST 不兼容,因为它会尝试生成无效的SQL:

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

        几年前在一些尚未解决的 Rails 问题中(onetwothree)引用了这一点。我能够通过执行以下操作来解决它:

          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
        

        唯一的缺点是必须为 each 范围完成此链接。

        【讨论】:

          【解决方案8】:

          这里有一些 Rails 6 解决方案。

          @Adam Sibik 的The answer 很好地总结了各种数据库系统之间的差异。

          不幸的是,一些提出的解决方案,包括“通用解决方案”和“PostgreSQL 和 MySQL 特定”,不再适用于 Rails 6(ActiveRecord 6) 由于 order() 的更改规范不接受某些原始 SQL(我确认“PostgreSQL 特定”解决方案在 Rails 6.1.4 中仍然有效)。有关此更改的背景,请参见,例如,
          贾斯汀的“Updates for SQL Injection in Rails 6.1”。

          为了避免该问题,您可以使用Arel.sql 将 SQL 语句包裹起来,如下所示,其中 NULL 位于最后,假设您 100% 确定您给出的 SQL 语句是安全的。

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

          仅供参考,如果您想按 [TRUE, FALSE, NULL] 的顺序按布尔列(例如is_ok)排序,而不管数据库系统如何,这些都应该可以工作:

          Photo.order(Arel.sql('CASE WHEN is_ok IS NULL THEN 1 ELSE 0 END, is_ok DESC'))
          Photo.order(Arel.sql('CASE WHEN is_ok IS NULL THEN 1 WHEN is_ok IS TRUE THEN -1 ELSE 0 END'))
          

          (注意,SQLite 没有 Boolean 类型,因此可以说前者可能更安全,尽管这无关紧要,因为 Rails 应该保证该值为 0 或 1(或 NULL)。)

          【讨论】:

            【解决方案9】:

            Rails 6.1 为 Arel for PostgreSQL 添加了 nulls_firstnulls_last 方法。

            例子:

            User.order(User.arel_table[:login_count].desc.nulls_last)
            

            来源:https://www.bigbinary.com/blog/rails-6-1-adds-nulls-first-and-nulls-last-to-arel

            【讨论】:

              【解决方案10】:

              在我的情况下,我需要按 ASC 的开始日期和结束日期对行进行排序,但在少数情况下 end_date 为空并且该行应该在上面,我使用了

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

              【讨论】:

                【解决方案11】:

                将数组添加在一起将保持顺序:

                @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

                【讨论】:

                • 好吧,我不喜欢这个主意,但我认为这会奏效。很抱歉让它打开这么久,我希望会出现其他一些答案。不过花了一些时间思考之后,我认为这可以在 Photo 模型上做成一个方法,然后感觉不会太糟糕。
                • 如果你使用 mysql,这很容易。查看我的解决方案。
                • 对,我应该提到我的方法是我能找到的最不可知论的方法。
                • 这是一个坏主意,因为where 不返回数组,它返回一个 ActiveRecord::Relation 并将结果强制放入一个数组将导致所有期望标准 ActiveRecord::Relation失败(如分页)。
                • 没错,尽管看起来可用的方法要么脱离了 AR,要么不(完全)可移植。
                【解决方案12】:

                如果您想要跨数据库类型获得一致的结果,您似乎必须在 Ruby 中执行此操作,因为数据库本身会解释 NULLS 是位于列表的前面还是末尾。

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

                但这不是很有效。

                【讨论】:

                  猜你喜欢
                  • 2021-12-24
                  • 1970-01-01
                  • 1970-01-01
                  • 2010-12-02
                  • 2011-09-25
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-11-02
                  相关资源
                  最近更新 更多