65

Just trying out PostgreSQL for the first time, coming from MySQL. In our Rails application we have a couple of locations with SQL like so:

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

It didn't take long to discover that this is not supported/allowed in PostgreSQL.

Does anyone know how to simulate this behaviour in PostgreSQL or do we have to pull sorting out into the code?

3
  • 3
    It might be useful to explain what you want to achieve. As hard to imagine as it is - not everybody knows MySQL :) Commented Aug 21, 2009 at 9:14
  • Great point depesz! Basically a custom sort order is what we are after. The FIELD function allows you to create custom set to do your sorting with. Commented Aug 21, 2009 at 11:47
  • Now in Rails you may consider #in_order_of github.com/rails/rails/pull/42061 Commented Aug 9, 2021 at 11:43

15 Answers 15

81

Ah, gahooa was so close:

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;
Sign up to request clarification or add additional context in comments.

5 Comments

This method does not work when you are using DISTINCT. Any other ideas for this scenario?
I'm not sure why this works, but I figured out an alternative. If you want results in order by j, a, k, e, then you order by id=e, id=k, id=a, id=j.
This works because each expression returns a 0 or 1, and the default sort order for each is asc, so all rows where id=e get put last, then the id!=e set is sorted by id=k and so on. If you want to keep the priority order in "writer's priority order", you can say id=j desc, id=k desc...
Shouldn't the ELSE be 0? That's what FIELD returns for no match.
@Corey Perhaps you can wrap results from your DISTINCT with another SELECT which implements the above?
33

sort in mysql:

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

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

4 Comments

Gold. Railsy way.
order_by_ids won't work on Rails 6.1, it will raise ActiveRecord::UnknownAttributeReference error api.rubyonrails.org/classes/ActiveRecord/…
For Rails 6.1 and above, you need to use Arel instead. In case you are looking for a solution for Rails 6.1, see stackoverflow.com/a/66517571/7313509
Anyone able to provide a Laravel version?
15

Update, fleshing out terrific suggestion by @Tometzky.

This ought to give you a MySQL FIELD()-alike function under pg 8.4:

-- 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, but I cannot verify the above on 8.4 right now; however, I can work backwards to a "morally" equivalent version that works on the 8.1 instance in front of me:

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

More awkwardly, you still can portably use a (possibly derived) table of currency code rankings, like so:

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)

1 Comment

The first one works great on postgres 11, after changing the return type to 'integer'. Very handy for ordering by 'foreign' data.
13

This is I think the simplest way:

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)

In PostgreSQL 8.4 you can also use a function with variable number of arguments (variadic function) to port field function.

1 Comment

+1 for order-by-bang, and for VARIADIC suggestion, which I'll try to implement.
12
SELECT * FROM (VALUES ('foo'), ('bar'), ('baz'), ('egg'), ('lol')) t1(name)
ORDER BY ARRAY_POSITION(ARRAY['foo', 'baz', 'egg', 'bar'], name)

How about this? Above one fetches as below:

foo
baz
egg
bar
lol

As you already get it, if an element isn't in the array then it goes to the back. Note that you can also use the same technique if you need an IN operator with a custom sort order:

SELECT * FROM table
WHERE id IN (17, 5, 11)
ORDER BY ARRAY_POSITION(ARRAY[17, 5, 11], id);

3 Comments

[42883] ERROR: function array_position(text[], uuid) does not exist Hint: No function matches the given name and argument types. You might need to add explicit type casts. Position: 240
@AkimKelar yes, as the hint says, you need to add explicit type cast to the latter argument in ARRAY_POSITION.
I know this is a Rails question but for anyone using Django, there is an elegant way to implement this.
6

ilgam's answer won't work since Rails 6.1, an ActiveRecord::UnknownAttributeReference error will be raised: https://api.rubyonrails.org/classes/ActiveRecord/UnknownAttributeReference.html

The recommended way is to use Arel instead of raw SQL.

In addition to ilgam's answer, here is the solution for 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

2 Comments

Perfect. Just added "return self unless ids.any?" at beginning
Actually "return all unless ids.any?" (all preserve the scope chaining)
5

For folks coming here later, Rails 7 added in_order_of, so you can do:

CurrencyCode.in_order_of(:code, 'GBP', 'EUR', 'BBD', 'AUD', 'CAD', 'USD')

See also: https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-in_order_of

1 Comment

Worked like a charm! Needs more upvotes. And to confirm, you can chain this with other methods. I just used it like this: query.reorder(nil).in_order_of(:current_status, ordered_statuses).order(starts_on: :asc)
4

Actually the version for postgres 8.1 as another advantage.

When calling a postgres function you cannot pass more than 100 parameters to it, so your ordering can be done at maximum on 99 elements.

Using the function using an array as second argument instead of having a variadic argument just remove this limit.

Comments

3

You can do this...

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

But why are you hardcoding these into the query -- wouldn't a supporting table be more appropriate?

--

Edit: flipped it around as per comments

2 Comments

@gahooa, I think you've the sense of "code" reversed -- the code is the three alpha abbreviation, which the OP desires to sort in a non-alpha fashion.
I wish I could take credit for why the SQL is the way it is, we are working on a refactor, but I admit I am looking for the quick fix right now
2

Just define the FIELD function and use it. It's easy enough to implement. The following should work in 8.4, as it has unnest and window functions like 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;

You can also define another copy with the signature:

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

and the same body if you want to support field() for any data type.

4 Comments

I think this is the best answer, but when I try to create the function, pgadmin says - "return type mismatch in function declared to return bigint, Function's final statement must be select/insert" ideas?
@chrismarx Pg version? Select version()
also unnest doesn't seem to be a registered function, "PostgreSQL 9.3.4 on x86_64-apple-darwin13.1.0, compiled by Apple LLVM version 5.1 (clang-503.0.38) (based on LLVM 3.4svn), 64-bit"
That sounds pretty borked. unnest has been around since 8.4... Show \df unnest please
1

If you'll run this often, add a new column and a pre-insert/update trigger. Then you set the value in the new column based on this trigger and order by this field. You can even add an index on this field.

2 Comments

Triggers are bad, mmkay? Avoid if at all possible!
@WillSheppard That is wrong. They can't be used brainlessly and often, but you simply can't say "Triggers are bad"
1

Create a migration with this function

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;

Then just do this

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

voila!

Comments

0

As I answered here, I just released a gem (order_as_specified) that allows you to do native SQL ordering like this:

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

It returns an ActiveRecord relation, and thus can be chained with other methods, and it's worked with every RDBMS I've tested.

Comments

0

It's also possible to do this ordering using the array unnest together WITH ORDINALITY functionality:

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

Comments

0

For my needs I have created function order_by_field that returns key from array.

First parameter is table field; Second list of values by comma;

CREATE OR REPLACE FUNCTION order_by_field(field integer, str_list text)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
declare
   val int;
     array_list int[];
begin

    array_list = string_to_array(str_list,',');
    
    SELECT i FROM generate_subscripts(array_list, 1) gs(i) WHERE array_list[i] = field   
  into val;
      
  return val;
end;
$function$
;

And example useage:

select * from app_fields where id in (1,2,3) order by order_by_field(id, '3,2,1')

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.