239

I have a simple SQL query in PostgreSQL 8.3 that grabs a bunch of comments. I provide a sorted list of values to the IN construct in the WHERE clause:

SELECT * FROM comments WHERE (comments.id IN (1,3,2,4));

This returns comments in an arbitrary order which in my happens to be ids like 1,2,3,4.

I want the resulting rows sorted like the list in the IN construct: (1,3,2,4).
How to achieve that?

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
nutcracker
  • 2,944
  • 2
  • 17
  • 16

17 Answers17

132

You can do it quite easily with (introduced in PostgreSQL 8.2) VALUES (), ().

Syntax will be like this:

select c.*
from comments c
join (
  values
    (1,1),
    (3,2),
    (2,3),
    (4,4)
) as x (id, ordering) on c.id = x.id
order by x.ordering
  • 3
    @user80168 What if there are thousands values in IN clause? because I have got to do it for thousands records – kamal Oct 20 '16 at 10:35
  • @kamal For that I have used `with ordered_products as (select row_number() OVER (ORDER BY whatever) as reportingorder, id from comments) ... ORDER BY reportingorder`. – Noumenon Mar 24 '19 at 16:27
116

In Postgres 9.4 or later, this is simplest and fastest:

SELECT c.*
FROM   comments c
JOIN   unnest('{1,3,2,4}'::int[]) WITH ORDINALITY t(id, ord) USING (id)
ORDER  BY t.ord;
  • WITH ORDINALITY was introduced with in Postgres 9.4.

  • No need for a subquery, we can use the set-returning function like a table directly. (A.k.a. "table-function".)

  • A string literal to hand in the array instead of an ARRAY constructor may be easier to implement with some clients.

  • For convenience (optionally), copy the column name we are joining to ("id" in the example), so we can join with a short USING clause to only get a single instance of the join column in the result.

  • Works with any input type. If your key column is of type text, provide something like '{foo,bar,baz}'::text[].

Detailed explanation:

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • ODBC Error for me unfortunately: `ODBC ERROR: <4>, Inter code: <7> Native Err#=1 , SQLSTATE=42601, Error_Info='ERROR: syntax error at or near "NULLNULL"; Error while preparing parameters'` – Pipo Aug 08 '20 at 20:52
  • @Pipo: Somehow you managed to concatenate 'NULLNULL', which is obviously not a legal integer value. This is unrelated to my answer. – Erwin Brandstetter Aug 15 '20 at 20:19
  • works on psql cmdline but no with odbc I use, juste added it if anyone has the same error... – Pipo Aug 15 '20 at 20:50
  • 4
    This is the best answer. – Dex Oct 06 '20 at 00:28
  • 4
    This is the way. BTW you can also do `JOIN UNNEST(ARRAY['B','C','A']::text[]) WITH ORDINALITY t(id, ord) USING (id)` in case you want to order according to a list of strings instead of integers. – vreyespue Apr 13 '21 at 09:20
  • Anyone know how to write above query in sequelize raw query ? within the given syntax can't able to pass the value in sequelize ORM. – Raju Sah Sep 01 '22 at 07:15
  • 1
    You can use `LEFT OUTER JOIN` to include records outside of the array. They will be ordered after the array of records. Append other columns to `ORDER BY` to control their order. – ryanb Nov 30 '22 at 20:36
  • How would this be implemented in SQLAlchemy? – mp252 Feb 22 '23 at 16:17
73

Just because it is so difficult to find and it has to be spread: in mySQL this can be done much simpler, but I don't know if it works in other SQL.

SELECT * FROM `comments`
WHERE `comments`.`id` IN ('12','5','3','17')
ORDER BY FIELD(`comments`.`id`,'12','5','3','17')
Menno
  • 641
  • 4
  • 13
das oe
  • 795
  • 5
  • 2
  • 3
    The list of values has to be provided *twice*, in two different ways. Not so simple. The accepted answer only needs it *once* (even if in a more verbose fashion). And it's even simpler with modern Postgres (as demonstrated in newer answers). Also, this question seems to be about Postgres after all. – Erwin Brandstetter Mar 05 '16 at 00:29
  • 10
    `ERROR: cannot pass more than 100 arguments to a function` – brauliobo Jun 22 '16 at 13:15
72

With Postgres 9.4 this can be done a bit shorter:

select c.*
from comments c
join (
  select *
  from unnest(array[43,47,42]) with ordinality
) as x (id, ordering) on c.id = x.id
order by x.ordering;

Or a bit more compact without a derived table:

select c.*
from comments c
  join unnest(array[43,47,42]) with ordinality as x (id, ordering) 
    on c.id = x.id
order by x.ordering

Removing the need to manually assign/maintain a position to each value.

With Postgres 9.6 this can be done using array_position():

with x (id_list) as (
  values (array[42,48,43])
)
select c.*
from comments c, x
where id = any (x.id_list)
order by array_position(x.id_list, c.id);

The CTE is used so that the list of values only needs to be specified once. If that is not important this can also be written as:

select c.*
from comments c
where id in (42,48,43)
order by array_position(array[42,48,43], c.id);
  • This does not repeat the whole `IN` list from the `WHERE` clause again in the `ORDER BY` clause, which makes this the best answer imho... Now only to find something similar for MySQL... – Stijn de Witt Dec 21 '15 at 10:14
  • 1
    My favorite answer but note that array_position does not work with bigint and you'd need to cast: `order by array_position(array[42,48,43], c.id::int);` which, may lead to bugs in some cases. – aaandre Sep 27 '19 at 19:53
  • 3
    @aaandre The following casting is working fine (in Postgres 12 at least) `array_position(array[42, 48, 43]::bigint[], c.id::bigint)`, so no need to truncate `bigint` to `int`. – Vic May 01 '20 at 10:44
  • 1
    If someone consider performance I really do not recommend array_position! I did some tests for 100k rows. `array_position` with TEXT array took about 15min, with INT about 5min and using `join unnest` 1,5min where the query without sorting is taking something above 1min. – klaxon Mar 22 '22 at 11:45
  • I was tempted by the simplicity of `array_position` but also found it slower than `join unnest`. It is about 50% slower in my brief testing on int ids. – ryanb Nov 30 '22 at 20:27
50

I think this way is better :

SELECT * FROM "comments" WHERE ("comments"."id" IN (1,3,2,4))
    ORDER BY  id=1 DESC, id=3 DESC, id=2 DESC, id=4 DESC
R4chi7
  • 853
  • 1
  • 11
  • 36
vantrung -cuncon
  • 10,207
  • 5
  • 47
  • 62
  • 1
    I was able to do this with bound values, i.e.: `... order by id=? desc, id=? desc, id=? desc` and it seems to work fine :-) – KajMagnus Mar 15 '14 at 08:54
  • Works in postgres and seems to be the best solution! – Mike Szyndel Oct 08 '15 at 13:58
  • This solution did the trick for me, but: Did anyone research how this solution is doing performance-wise? It does add multple order by clauses. Therefore it may (i didnt test it yet) get slower exponentially with increasing number of order-ids? Any information on this would be highly appreciated! – Fabian S. Aug 15 '16 at 08:20
  • 1
    ERROR: target lists can have at most 1664 entries -> when you try to run long query... – Fatkhan Fauzi Oct 13 '16 at 00:49
  • @Manngo MS SQL. Can't remember which version. Might have been 2012. – biko May 08 '18 at 09:21
  • I encountered performance issues with this method. I had an order by with around 850 IDs, and Postgres wouldn't even respond back (probably timing out). So be careful if you have a lot of IDs. – Laurent Van Winckel Jul 18 '19 at 10:19
  • `Query 1 ERROR: ERROR: target lists can have at most 1664 entries` – Surya Jan 21 '20 at 06:55
  • Works in PostgreSQL 10.12, and looks like this is the implementation agnostic solution. – Dmitriy Work Jul 13 '20 at 23:34
34

In Postgresql:

select *
from comments
where id in (1,3,2,4)
order by position(id::text in '1,3,2,4')
Dex
  • 12,527
  • 15
  • 69
  • 90
Clodoaldo Neto
  • 118,695
  • 26
  • 233
  • 260
  • 5
    Hum... it bugs if `position(id::text in '123,345,3,678')`. The id `3` will match before the id `345`, dont it? – alanjds Apr 11 '14 at 21:49
  • 6
    I think you are right and would need to have both a start and end delimiter then, maybe like: order by position(','||id::text||',' in ',1,3,2,4,') – Michael Rush Jun 09 '14 at 23:13
  • @MichaelRush This doesn't seem to work 100% either. For example, if there is (11, 1), the 1 will get displayed first. – Dex Oct 06 '20 at 00:08
  • If we want that the result order should be "11, 1, 10, and all other by asc" we can write a such expression: `order by position(','||id::text||',' in ',10,1,11,') desc, id asc` . Pay attention with trick of reverse order in string + desc sort. It is because position of id=2 in this string will cast to 0. – FlameStorm Jun 30 '23 at 19:59
31

Another way to do it in Postgres would be to use the idx function.

SELECT *
FROM comments
ORDER BY idx(array[1,3,2,4], comments.id)

Don't forget to create the idx function first, as described here: http://wiki.postgresql.org/wiki/Array_Index

That1Guy
  • 7,075
  • 4
  • 47
  • 59
Carl Mercier
  • 1,231
  • 1
  • 12
  • 10
  • 12
    This function is now available in an extension that comes with PostgreSQL: http://www.postgresql.org/docs/9.2/static/intarray.html Install it with `CREATE EXTENSION intarray;`. – Alex Kahn Aug 28 '14 at 16:32
  • 1
    Just piling on further, for Amazon RDS users, the ROR migration function `enable_extension` will let you activate this so long as your app user is a member of the `rds_superuser` group. – Dave S. Aug 06 '15 at 15:39
  • in PG 9.6.2 PG::UndefinedFunction: ERROR: function idx(integer[], integer) does not exist – Yakob Ubaidi Apr 13 '17 at 09:18
  • Thank you, best answer when combined with @AlexKahn's comment – Andrew Nov 09 '17 at 14:37
3

On researching this some more I found this solution:

SELECT * FROM "comments" WHERE ("comments"."id" IN (1,3,2,4)) 
ORDER BY CASE "comments"."id"
WHEN 1 THEN 1
WHEN 3 THEN 2
WHEN 2 THEN 3
WHEN 4 THEN 4
END

However this seems rather verbose and might have performance issues with large datasets. Can anyone comment on these issues?

nutcracker
  • 2,944
  • 2
  • 17
  • 16
  • 8
    Sure, I can comment on them. There are things SQL is good at, and things it is not good at. SQL is not good at this. Just sort the results in whatever language you're making the queries from; it will save you much wailing and gnashing of teeth. SQL is a set-oriented language, and sets are not ordered collections. – kquinn May 15 '09 at 00:24
  • Hmmm ... Is that based on personal experience and testing? My tested experience is that this is a quite effective technique for ordering. (However, the accepted answer is better overall because it eliminates the "IN (...)" clause). Remember that for any reasonable result set size, deriving the set should be the expensive part. Once it's down to several hundred records or less, sorting is trivial. – dkretz Jun 05 '09 at 15:52
  • What if there are thousands values in `IN` clause? because I have got to do it for thousands records. – kamal Oct 20 '16 at 10:34
2

To do this, I think you should probably have an additional "ORDER" table which defines the mapping of IDs to order (effectively doing what your response to your own question said), which you can then use as an additional column on your select which you can then sort on.

In that way, you explicitly describe the ordering you desire in the database, where it should be.

Paul Sonier
  • 38,903
  • 3
  • 77
  • 117
  • This seems like the right way to do it. However I'd like to create that ordering table on the fly. I've suggested using a constant table in one of the answers. Is this going to be performant when I'm dealing with hundreds or thousands of comments? – nutcracker May 15 '09 at 01:14
2

sans SEQUENCE, works only on 8.4:

select * from comments c
join 
(
    select id, row_number() over() as id_sorter  
    from (select unnest(ARRAY[1,3,2,4]) as id) as y
) x on x.id = c.id
order by x.id_sorter
Michael Buen
  • 38,643
  • 9
  • 94
  • 118
1
SELECT * FROM "comments" JOIN (
  SELECT 1 as "id",1 as "order" UNION ALL 
  SELECT 3,2 UNION ALL SELECT 2,3 UNION ALL SELECT 4,4
) j ON "comments"."id" = j."id" ORDER BY j.ORDER

or if you prefer evil over good:

SELECT * FROM "comments" WHERE ("comments"."id" IN (1,3,2,4))
ORDER BY POSITION(','+"comments"."id"+',' IN ',1,3,2,4,')
Hafthor
  • 16,358
  • 9
  • 56
  • 65
0
select * from comments where comments.id in 
(select unnest(ids) from bbs where id=19795) 
order by array_position((select ids from bbs where id=19795),comments.id)

here, [bbs] is the main table that has a field called ids, and, ids is the array that store the comments.id .

passed in postgresql 9.6

0

Lets get a visual impression about what was already said. For example you have a table with some tasks:

SELECT a.id,a.status,a.description FROM minicloud_tasks as a ORDER BY random();

 id |   status   |   description    
----+------------+------------------
  4 | processing | work on postgres
  6 | deleted    | need some rest
  3 | pending    | garden party
  5 | completed  | work on html

And you want to order the list of tasks by its status. The status is a list of string values:

(processing, pending,  completed, deleted)

The trick is to give each status value an interger and order the list numerical:

SELECT a.id,a.status,a.description FROM minicloud_tasks AS a
  JOIN (
    VALUES ('processing', 1), ('pending', 2), ('completed', 3), ('deleted', 4)
  ) AS b (status, id) ON (a.status = b.status)
  ORDER BY b.id ASC;

Which leads to:

 id |   status   |   description    
----+------------+------------------
  4 | processing | work on postgres
  3 | pending    | garden party
  5 | completed  | work on html
  6 | deleted    | need some rest

Credit @user80168

Manuel
  • 337
  • 2
  • 6
0
create sequence serial start 1;

select * from comments c
join (select unnest(ARRAY[1,3,2,4]) as id, nextval('serial') as id_sorter) x
on x.id = c.id
order by x.id_sorter;

drop sequence serial;

[EDIT]

unnest is not yet built-in in 8.3, but you can create one yourself(the beauty of any*):

create function unnest(anyarray) returns setof anyelement
language sql as
$$
    select $1[i] from generate_series(array_lower($1,1),array_upper($1,1)) i;
$$;

that function can work in any type:

select unnest(array['John','Paul','George','Ringo']) as beatle
select unnest(array[1,3,2,4]) as id
Michael Buen
  • 38,643
  • 9
  • 94
  • 118
  • Thanks Michael but the unnest function doesn't seem to exist for my PSQL and I can't find any mention of it in the docs either. Is it 8.4 only? – nutcracker May 15 '09 at 01:26
  • unnest is not yet built-in in 8.3, but you can implement one yourself. see the code above – Michael Buen May 15 '09 at 02:09
0

And here's another solution that works and uses a constant table (http://www.postgresql.org/docs/8.3/interactive/sql-values.html):

SELECT * FROM comments AS c,
(VALUES (1,1),(3,2),(2,3),(4,4) ) AS t (ord_id,ord)
WHERE (c.id IN (1,3,2,4)) AND (c.id = t.ord_id)
ORDER BY ord

But again I'm not sure that this is performant.

I've got a bunch of answers now. Can I get some voting and comments so I know which is the winner!

Thanks All :-)

nutcracker
  • 2,944
  • 2
  • 17
  • 16
  • 1
    your answer is almost the same with depesz, just remove the c.ID IN (1,3,2,4). anyway his is better, he uses JOIN, as much as possible use the ANSI SQL way of joining, don't use table comma table. i should have read your answer carefully, i'm having a hard time figuring out how to alias the two columns, first i tried this: (values(1,1) as x(id,sort_order), (3,2), (2,3), (4,4)) as y. but to no avail :-D your answer could have provided me a clue if i've read it carefully :-) – Michael Buen May 15 '09 at 09:44
0

Slight improvement over the version that uses a sequence I think:

CREATE OR REPLACE FUNCTION in_sort(anyarray, out id anyelement, out ordinal int)
LANGUAGE SQL AS
$$
    SELECT $1[i], i FROM generate_series(array_lower($1,1),array_upper($1,1)) i;
$$;

SELECT 
    * 
FROM 
    comments c
    INNER JOIN (SELECT * FROM in_sort(ARRAY[1,3,2,4])) AS in_sort
        USING (id)
ORDER BY in_sort.ordinal;
-4

I agree with all other posters that say "don't do that" or "SQL isn't good at that". If you want to sort by some facet of comments then add another integer column to one of your tables to hold your sort criteria and sort by that value. eg "ORDER BY comments.sort DESC " If you want to sort these in a different order every time then... SQL won't be for you in this case.

Trey
  • 11,032
  • 1
  • 23
  • 21