在Postgresql中模拟MySQL的ORDER BY FIELD()

时间:2009-08-21 01:07:04

标签: mysql ruby-on-rails postgresql

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

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

没过多久就发现PostgreSQL不支持/不允许这样做。

有没有人知道如何在PostgreSQL中模拟这种行为,还是我们必须将整理到代码中?

13 个答案:

答案 0 :(得分:56)

啊,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;

答案 1 :(得分:24)

在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]

答案 2 :(得分:12)

更新,充实了@Tometzky的精彩建议。

这应该在pg 8.4:

下给你一个类似MySQL FIELD()的函数
-- 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)

答案 3 :(得分:11)

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

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函数。

答案 4 :(得分:4)

实际上,postgres 8.1的版本是另一个优势。

当调用postgres函数时,你不能传递超过100个参数,所以你的排序最多可以在99个元素上完成。

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

答案 5 :(得分:2)

只需定义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()任何数据类型,请使用相同的正文。

答案 6 :(得分:2)

使用此功能创建迁移

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(',')})")

瞧!

答案 7 :(得分:2)

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

答案 8 :(得分:1)

你可以这样做......

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

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

-

编辑:根据评论将其翻转

答案 9 :(得分:1)

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

答案 10 :(得分:0)

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

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

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

答案 11 :(得分:0)

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

如您所知,如果元素不在数组中,那么它将返回到后面。

答案 12 :(得分:0)

还可以使用数组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;