【问题标题】:Simulating MySQL's ORDER BY FIELD() in Postgresql在 Postgresql 中模拟 MySQL 的 ORDER BY FIELD()
【发布时间】:2010-11-21 12:44:50
【问题描述】:

第一次尝试 PostgreSQL,来自 MySQL。在我们的 Rails 应用程序中,我们有几个使用 SQL 的位置,如下所示:

SELECT * FROM `currency_codes` ORDER BY FIELD(code, 'GBP', 'EUR', 'BBD', 'AUD', 'CAD', 'USD') DESC, name ASC

很快就发现 PostgreSQL 不支持/不允许这样做。

有谁知道如何在 PostgreSQL 中模拟这种行为,还是我们必须在代码中进行排序?

【问题讨论】:

  • 解释您想要实现的目标可能很有用。很难想象 - 不是每个人都知道 MySQL :)
  • 好点depesz!基本上自定义排序顺序是我们所追求的。 FIELD 函数允许您创建自定义集来进行排序。
  • 现在在 Rails 中你可以考虑 #in_order_of github.com/rails/rails/pull/42061

标签: mysql postgresql


【解决方案1】:

ilgam's answer 自 Rails 6.1 起将无法使用,将引发 ActiveRecord::UnknownAttributeReference 错误:https://api.rubyonrails.org/classes/ActiveRecord/UnknownAttributeReference.html

推荐的方法是使用 Arel 而不是原始 SQL。

除了ilgam's answer,这里是Rails 6.1的解决方案:

def self.order_by_ids(ids)
  t = User.arel_table
  condition = Arel::Nodes::Case.new(t[:id])
  ids.each_with_index do |id, index|
    condition.when(id).then(index)
  end
  order(condition)
end

【讨论】:

  • 完美。刚刚添加了“返回自我,除非 ids.any?”一开始
  • 实际上“返回所有,除非 ids.any?” (都保留作用域链)
【解决方案2】:

也可以使用数组unnestWITH ORDINALITY 功能进行此排序:

--- Table and data setup ...
CREATE TABLE currency_codes (
     code text null,
     name text
);
INSERT INTO currency_codes
  (code)
VALUES
  ('USD'), ('BBD'), ('GBP'), ('EUR'), ('AUD'), ('CAD'), ('AUD'), ('AUD');

-- ...and the Query
SELECT
  c.*
FROM
  currency_codes c
JOIN
  unnest('{"GBP", "EUR", "BBD", "AUD", "CAD", "USD"}'::text[])
  WITH ORDINALITY t(code, ord)
  USING (code)
ORDER BY t.ord DESC, c.name ASC;

【讨论】:

    【解决方案3】:
    SELECT * FROM (VALUES ('foo'), ('bar'), ('baz'), ('egg'), ('lol')) t1(name)
    ORDER BY ARRAY_POSITION(ARRAY['foo', 'baz', 'egg', 'bar'], name)
    

    这个怎么样?以上一次获取如下:

    foo
    baz
    egg
    bar
    lol
    

    正如你已经知道的那样,如果一个元素不在数组中,那么它会回到后面。

    【讨论】:

    • [42883] 错误:函数 array_position(text[], uuid) 不存在 提示:没有函数匹配给定的名称和参数类型。您可能需要添加显式类型转换。位置:240
    • @AkimKelar 是的,正如提示所说,您需要在 ARRAY_POSITION 的后一个参数中添加显式类型转换。
    • 我知道这是一个 Rails 问题,但对于使用 Django 的任何人,there is an elegant way to implement this.
    【解决方案4】:

    在mysql中排序:

    > ids = [11,31,29]
    => [11, 31, 29]
    > User.where(id: ids).order("field(id, #{ids.join(',')})")
    

    在 postgres 中:

    def self.order_by_ids(ids)
      order_by = ["CASE"]
      ids.each_with_index do |id, index|
        order_by << "WHEN id='#{id}' THEN #{index}"
      end
      order_by << "END"
      order(order_by.join(" "))
    end
    
    User.where(id: [3,2,1]).order_by_ids([3,2,1]).map(&:id) 
    #=> [3,2,1]
    

    【讨论】:

    【解决方案5】:

    使用此函数创建迁移

    CREATE OR REPLACE FUNCTION field(anyelement, VARIADIC anyarray) RETURNS bigint AS $$
      SELECT n FROM (
        SELECT row_number() OVER () AS n, x FROM unnest($2) x)
          numbered WHERE numbered.x = $1;
    $$ LANGUAGE SQL IMMUTABLE STRICT;
    

    那就这样做吧

    sequence = [2,4,1,5]
    Model.order("field(id,#{sequence.join(',')})")
    

    瞧!

    【讨论】:

      【解决方案6】:

      当我回答 here 时,我刚刚发布了一个 gem (order_as_specified),它允许您像这样进行本机 SQL 排序:

      CurrencyCode.order_as_specified(code: ['GBP', 'EUR', 'BBD', 'AUD', 'CAD', 'USD'])
      

      它返回一个 ActiveRecord 关系,因此可以与其他方法链接,它适用于我测试过的每个 RDBMS。

      【讨论】:

        【解决方案7】:

        如果您经常运行此操作,请添加一个新列和一个预插入/更新触发器。然后根据此触发器设置新列中的值并按此字段排序。您甚至可以在此字段上添加索引。

        【讨论】:

        • 触发器不好,嗯?尽可能避免!
        • @WillSheppard 这是错误的。它们不能盲目地经常使用,但你不能说“触发器不好”
        【解决方案8】:

        只需定义FIELD 函数并使用它。这很容易实现。以下应该在 8.4 中工作,因为它具有 unnest 和像 row_number 这样的窗口函数:

        CREATE OR REPLACE FUNCTION field(text, VARIADIC text[]) RETURNS bigint AS $$
        SELECT n FROM (
            SELECT row_number() OVER () AS n, x FROM unnest($2) x
        ) numbered WHERE numbered.x = $1;
        $$ LANGUAGE 'SQL' IMMUTABLE STRICT;
        

        您还可以定义另一个带有签名的副本:

        CREATE OR REPLACE FUNCTION field(anyelement, VARIADIC anyarray) RETURNS bigint AS $$
        

        如果您想为任何数据类型支持field(),则使用相同的正文。

        【讨论】:

        • 我认为这是最好的答案,但是当我尝试创建函数时,pgadmin 说 - “在声明返回 bigint 的函数中返回类型不匹配,函数的最终语句必须是选择/插入”的想法?
        • @chrismarx Pg 版本? Select version()
        • 也 unnest 似乎不是注册函数,“PostgreSQL 9.3.4 on x86_64-apple-darwin13.1.0,由 Apple LLVM 版本 5.1 (clang-503.0.38) 编译(基于LLVM 3.4svn),64 位"
        • 这听起来很无聊。 unnest 自 8.4 以来一直存在...请显示 \df unnest
        【解决方案9】:

        其实 postgres 8.1 的版本是另一个优势。

        调用 postgres 函数时,您不能向其传递超过 100 个参数,因此您最多可以对 99 个元素进行排序。

        使用使用数组作为第二个参数的函数,而不是使用可变参数,只需删除此限制。

        【讨论】:

          【解决方案10】:

          你可以这样做……

          SELECT 
             ..., code
          FROM 
             tablename
          ORDER BY 
             CASE 
                WHEN code='GBP' THEN 1
                WHEN code='EUR' THEN 2
                WHEN code='BBD' THEN 3
                ELSE 4
             END
          

          但是为什么要将这些硬编码到查询中——支持表不是更合适吗?

          --

          编辑:按照 cmets 翻转它

          【讨论】:

          • @gahooa,我认为您对“代码”的理解是颠倒的——代码是三个 alpha 缩写,OP 希望以非 alpha 方式对其进行排序。
          • 我希望我能理解为什么 SQL 是这样的,我们正在努力重构,但我承认我现在正在寻找快速修复
          【解决方案11】:

          更新,充实了@Tometzky 的绝妙建议。

          这应该在 pg 8.4 下给你一个 MySQL FIELD()-alike 函数:

          -- SELECT FIELD(varnames, 'foo', 'bar', 'baz')
          CREATE FUNCTION field(anyelement, VARIADIC anyarray) RETURNS numeric AS $$
            SELECT
              COALESCE(
               ( SELECT i FROM generate_subscripts($2, 1) gs(i)
                 WHERE $2[i] = $1 ),
               0);
          $$ LANGUAGE SQL STABLE
          

          Mea culpa,但我现在无法在 8.4 上验证上述内容;但是,我可以向后工作到适用于我面前的 8.1 实例的“道德”等效版本:

          -- SELECT FIELD(varname, ARRAY['foo', 'bar', 'baz'])
          CREATE OR REPLACE FUNCTION field(anyelement, anyarray) RETURNS numeric AS $$
            SELECT
              COALESCE((SELECT i
                        FROM generate_series(1, array_upper($2, 1)) gs(i)
                        WHERE $2[i] = $1),
                       0);
          $$ LANGUAGE SQL STABLE
          

          更尴尬的是,您仍然可以便携地使用(可能派生的)货币代码排名表,如下所示:

          pg=> select cc.* from currency_codes cc
               left join
                 (select 'GBP' as code, 0 as rank union all
                  select 'EUR', 1 union all
                  select 'BBD', 2 union all
                  select 'AUD', 3 union all
                  select 'CAD', 4 union all
                  select 'USD', 5) cc_weights
               on cc.code = cc_weights.code
               order by rank desc, name asc;
           code |           name
          ------+---------------------------
           USD  | USA bits
           CAD  | Canadian maple tokens
           AUD  | Australian diwallarangoos
           BBD  | Barbadian tridents
           EUR  | Euro chits
           GBP  | British haypennies
          (6 rows)
          

          【讨论】:

          • 第一个在 postgres 11 上运行良好,在将返回类型更改为“整数”之后。按“外国”数据排序非常方便。
          【解决方案12】:

          这是我认为最简单的方法:

          create temporary table test (id serial, field text);
          insert into test(field) values
            ('GBP'), ('EUR'), ('BBD'), ('AUD'), ('CAD'), ('USD'),
            ('GBP'), ('EUR'), ('BBD'), ('AUD'), ('CAD'), ('USD');
          select * from test
          order by field!='GBP', field!='EUR', field!='BBD',
            field!='AUD', field!='CAD', field!='USD';
           id | field 
          ----+-------
            1 | GBP
            7 | GBP
            2 | EUR
            8 | EUR
            3 | BBD
            9 | BBD
            4 | AUD
           10 | AUD
            5 | CAD
           11 | CAD
            6 | USD
           12 | USD
          (12 rows)
          

          在 PostgreSQL 8.4 中,您还可以使用 function with variable number of arguments(可变函数)来移植 field 函数。

          【讨论】:

          • +1 for order-by-bang 和VARIADIC 建议,我将尝试实施。
          【解决方案13】:

          啊,gahooa 这么近:

          SELECT * FROM currency_codes
            ORDER BY
            CASE
              WHEN code='USD' THEN 1
              WHEN code='CAD' THEN 2
              WHEN code='AUD' THEN 3
              WHEN code='BBD' THEN 4
              WHEN code='EUR' THEN 5
              WHEN code='GBP' THEN 6
              ELSE 7
            END,name;
          

          【讨论】:

          • 当您使用 DISTINCT 时,此方法不起作用。对于这种情况还有其他想法吗?
          • 我不确定为什么会这样,但我想出了一个替代方案。如果您希望结果按 j、a、k、e 的顺序排列,那么您order by id=e, id=k, id=a, id=j
          • ELSE 不应该为 0 吗?这就是 FIELD 返回不匹配的内容。
          • @Corey 也许您可以将 DISTINCT 的结果用另一个实现上述功能的 SELECT 包装起来?
          猜你喜欢
          • 2012-04-12
          • 1970-01-01
          • 2020-08-25
          • 2013-11-10
          • 2011-11-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多